From f6a421fd723cc5ff52fa91087e238d138b128ac2 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Mon, 14 Oct 2024 15:12:35 +0200 Subject: [PATCH 1/3] Implement entity history tables for each entity --- .../dynamic/codegen/src/db/Entities.res.hbs | 174 ++++++++++++++++++ .../static/codegen/src/db/Migrations.res | 9 +- 2 files changed, 182 insertions(+), 1 deletion(-) diff --git a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs index 8cbc763b3..a64aeaa17 100644 --- a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs @@ -23,6 +23,171 @@ let isIndex = true @genType type whereOperations<'entity, 'fieldType> = {eq: 'fieldType => promise>} +module EntityHistory = { + type historyFields = { + chain_id: int, + block_timestamp: int, + block_number: int, + log_index: int, + } + + type historyRow<'entiy> = { + current: historyFields, + previous: option, + entityData: 'entiy, + } + + type t<'entity> = { + table: table, + createInsertFnQuery: string, + // insertFn: historyRow<'entity> => promise, + } + + type entityInternal + + external castInternal: t<'entity> => t = "%identity" + + let fromTable = (table: table): t<'entity> => { + let entity_history_block_timestamp = "entity_history_block_timestamp" + let entity_history_chain_id = "entity_history_chain_id" + let entity_history_block_number = "entity_history_block_number" + let entity_history_log_index = "entity_history_log_index" + + //NB: Ordered by hirarchy of event ordering + let currentChangeFieldNames = [ + entity_history_block_timestamp, + entity_history_chain_id, + entity_history_block_number, + entity_history_log_index, + ] + + let currentHistoryFields = + currentChangeFieldNames->Belt.Array.map(fieldName => + mkField(fieldName, Integer, ~isPrimaryKey) + ) + + let previousChangeFieldNames = + currentChangeFieldNames->Belt.Array.map(fieldName => "previous_" ++ fieldName) + + let previousHistoryFields = + previousChangeFieldNames->Belt.Array.map(fieldName => + mkField(fieldName, Integer, ~isNullable) + ) + + let entity_id = "entity_id" + + let dataFields = table.fields->Belt.Array.keepMap(field => + switch field { + | Field(field) => + switch field.fieldName { + //entity_id is not nullable and should be part of the pk + | "id" => {...field, fieldName: entity_id, isPrimaryKey: true}->Field->Some + //db_write_timestamp can be removed for this. TODO: remove this when we depracate + //automatic db_write_timestamp creation + | "db_write_timestamp" => None + | _ => + { + ...field, + isNullable: true, //All entity fields are nullable in the case + isIndex: false, //No need to index any additional entity data fields in entity history + } + ->Field + ->Some + } + + | DerivedFrom(_) => None + } + ) + + let dataFieldNames = dataFields->Belt.Array.map(field => field->getFieldName) + + let originTableName = table.tableName + let historyTableName = originTableName ++ "_history" + //ignore composite indices + let table = mkTable( + historyTableName, + ~fields=Belt.Array.concatMany([currentHistoryFields, previousHistoryFields, dataFields]), + ) + + let createInsertFnQuery = { + let insertFnName = `insert_${table.tableName}` + let historRowArg = "history_row" + + let previousHistoryFieldsAreNullStr = + previousChangeFieldNames + ->Belt.Array.map(fieldName => `${historRowArg}.${fieldName} IS NULL`) + ->Js.Array2.joinWith(" OR ") + + let currentChangeFieldNamesCommaSeparated = currentChangeFieldNames->Js.Array2.joinWith(", ") + + let dataFieldNamesDoubleQuoted = dataFieldNames->Belt.Array.map(fieldName => `"${fieldName}"`) + let dataFieldNamesCommaSeparated = dataFieldNamesDoubleQuoted->Js.Array2.joinWith(", ") + + let allFieldNames = Belt.Array.concatMany([ + currentChangeFieldNames, + previousChangeFieldNames, + dataFieldNamesDoubleQuoted, + ]) + + `CREATE OR REPLACE FUNCTION ${insertFnName}(${historRowArg}) + RETURNS void AS $$ + DECLARE + v_previous_record RECORD; + v_origin_record RECORD; + BEGIN + -- Check if previous values are not provided + IF ${previousHistoryFieldsAreNullStr} THEN + -- Find the most recent record for the same entity_type and entity_id + SELECT ${currentChangeFieldNamesCommaSeparated} INTO v_previous_record + FROM ${historyTableName} + WHERE ${entity_id} = ${historRowArg}.${entity_id} + ORDER BY ${currentChangeFieldNames + ->Belt.Array.map(fieldName => fieldName ++ " DESC") + ->Js.Array2.joinWith(", ")} + LIMIT 1; + + -- If a previous record exists, use its values + IF FOUND THEN + ${Belt.Array.zip(currentChangeFieldNames, previousChangeFieldNames) + ->Belt.Array.map(((currentFieldName, previousFieldName)) => { + `${historRowArg}.${previousFieldName} := v_previous_record.${currentFieldName};` + }) + ->Js.Array2.joinWith(" ")} + ElSE + -- Check if a value for the entity_id exists in the origin table and if so, insert a history row for it. + SELECT ${dataFieldNamesCommaSeparated} FROM ${originTableName} WHERE id = ${historRowArg}.${entity_id} INTO v_origin_record; + IF FOUND THEN + INSERT INTO ${historyTableName} (${currentChangeFieldNamesCommaSeparated}, ${dataFieldNamesCommaSeparated}) + -- SET the current change data fields to 0 since we don't know what they were + -- and it doesn't matter provided they are less than any new values + VALUES (${currentChangeFieldNames + ->Belt.Array.map(_ => "0") + ->Js.Array2.joinWith(", ")}, ${dataFieldNames + ->Belt.Array.map(fieldName => `v_origin_record."${fieldName}"`) + ->Js.Array2.joinWith(", ")}); + + ${previousChangeFieldNames + ->Belt.Array.map(previousFieldName => { + `${historRowArg}.${previousFieldName} := 0;` + }) + ->Js.Array2.joinWith(" ")} + END IF; + END IF; + END IF; + + INSERT INTO ${historyTableName} (${allFieldNames->Js.Array2.joinWith(", ")}) + VALUES (${allFieldNames + ->Belt.Array.map(fieldName => `${historRowArg}.${fieldName}`) + ->Js.Array2.joinWith(", ")}); + END; + $$ LANGUAGE plpgsql; + ` + } + + {table, createInsertFnQuery} + } +} + {{#each entities as |entity|}} module {{entity.name.capitalized}} = { let key = "{{entity.name.original}}" @@ -86,6 +251,8 @@ module {{entity.name.capitalized}} = { ], {{/if}} ) + + let entityHistory = table->EntityHistory.fromTable } {{/each}} @@ -109,6 +276,13 @@ let allTables: array = [ {{entity.name.capitalized}}.table, {{/each}} ] + +let allEntityHistory: array> = [ +{{#each entities as |entity|}} + {{entity.name.capitalized}}.entityHistory->EntityHistory.castInternal, +{{/each}} +] + let schema = Schema.make(allTables) @get diff --git a/codegenerator/cli/templates/static/codegen/src/db/Migrations.res b/codegenerator/cli/templates/static/codegen/src/db/Migrations.res index 793671823..3c0302c91 100644 --- a/codegenerator/cli/templates/static/codegen/src/db/Migrations.res +++ b/codegenerator/cli/templates/static/codegen/src/db/Migrations.res @@ -446,8 +446,9 @@ let runUpMigrations = async (~shouldExit) => { ) }) + let allEntityHistoryTables = Entities.allEntityHistory->Belt.Array.map(table => table.table) //Create all tables with indices - await [TablesStatic.allTables, Entities.allTables] + await [TablesStatic.allTables, Entities.allTables, allEntityHistoryTables] ->Belt.Array.concatMany ->awaitEach(async table => { await creatTableIfNotExists(DbFunctions.sql, table)->handleFailure( @@ -458,6 +459,12 @@ let runUpMigrations = async (~shouldExit) => { ) }) + await Entities.allEntityHistory->awaitEach(async entityHistory => { + await sql + ->Postgres.unsafe(entityHistory.createInsertFnQuery) + ->handleFailure(~msg=`EE800: Error creating ${entityHistory.table.tableName} insert function`) + }) + //Create extra entity history tables await EntityHistory.createEntityHistoryTableFunctions()->handleFailure( ~msg=`EE800: Error creating entity history table`, From 33f80de64f4ede803141bc9f79353dc020cce4d8 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Mon, 14 Oct 2024 18:04:55 +0200 Subject: [PATCH 2/3] Wip implement per table insert history item function with tests --- .../dynamic/codegen/src/db/Entities.res.hbs | 26 +-- .../test/lib_tests/EntityHistory_test.res | 149 ++++++++++++++++++ 2 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 scenarios/test_codegen/test/lib_tests/EntityHistory_test.res diff --git a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs index a64aeaa17..335ab5616 100644 --- a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs @@ -74,14 +74,14 @@ module EntityHistory = { mkField(fieldName, Integer, ~isNullable) ) - let entity_id = "entity_id" + let id = "id" let dataFields = table.fields->Belt.Array.keepMap(field => switch field { | Field(field) => switch field.fieldName { - //entity_id is not nullable and should be part of the pk - | "id" => {...field, fieldName: entity_id, isPrimaryKey: true}->Field->Some + //id is not nullable and should be part of the pk + | "id" => {...field, fieldName: id, isPrimaryKey: true}->Field->Some //db_write_timestamp can be removed for this. TODO: remove this when we depracate //automatic db_write_timestamp creation | "db_write_timestamp" => None @@ -110,8 +110,10 @@ module EntityHistory = { ) let createInsertFnQuery = { - let insertFnName = `insert_${table.tableName}` + let insertFnName = `"insert_${table.tableName}"` let historRowArg = "history_row" + let historyTablePath = `"public"."${historyTableName}"` + let originTablePath = `"public"."${originTableName}"` let previousHistoryFieldsAreNullStr = previousChangeFieldNames @@ -129,7 +131,7 @@ module EntityHistory = { dataFieldNamesDoubleQuoted, ]) - `CREATE OR REPLACE FUNCTION ${insertFnName}(${historRowArg}) + `CREATE OR REPLACE FUNCTION ${insertFnName}(${historRowArg} ${historyTablePath}) RETURNS void AS $$ DECLARE v_previous_record RECORD; @@ -137,10 +139,10 @@ module EntityHistory = { BEGIN -- Check if previous values are not provided IF ${previousHistoryFieldsAreNullStr} THEN - -- Find the most recent record for the same entity_type and entity_id + -- Find the most recent record for the same id SELECT ${currentChangeFieldNamesCommaSeparated} INTO v_previous_record - FROM ${historyTableName} - WHERE ${entity_id} = ${historRowArg}.${entity_id} + FROM ${historyTablePath} + WHERE ${id} = ${historRowArg}.${id} ORDER BY ${currentChangeFieldNames ->Belt.Array.map(fieldName => fieldName ++ " DESC") ->Js.Array2.joinWith(", ")} @@ -154,10 +156,10 @@ module EntityHistory = { }) ->Js.Array2.joinWith(" ")} ElSE - -- Check if a value for the entity_id exists in the origin table and if so, insert a history row for it. - SELECT ${dataFieldNamesCommaSeparated} FROM ${originTableName} WHERE id = ${historRowArg}.${entity_id} INTO v_origin_record; + -- Check if a value for the id exists in the origin table and if so, insert a history row for it. + SELECT ${dataFieldNamesCommaSeparated} FROM ${originTablePath} WHERE id = ${historRowArg}.${id} INTO v_origin_record; IF FOUND THEN - INSERT INTO ${historyTableName} (${currentChangeFieldNamesCommaSeparated}, ${dataFieldNamesCommaSeparated}) + INSERT INTO ${historyTablePath} (${currentChangeFieldNamesCommaSeparated}, ${dataFieldNamesCommaSeparated}) -- SET the current change data fields to 0 since we don't know what they were -- and it doesn't matter provided they are less than any new values VALUES (${currentChangeFieldNames @@ -175,7 +177,7 @@ module EntityHistory = { END IF; END IF; - INSERT INTO ${historyTableName} (${allFieldNames->Js.Array2.joinWith(", ")}) + INSERT INTO ${historyTablePath} (${allFieldNames->Js.Array2.joinWith(", ")}) VALUES (${allFieldNames ->Belt.Array.map(fieldName => `${historRowArg}.${fieldName}`) ->Js.Array2.joinWith(", ")}); diff --git a/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res new file mode 100644 index 000000000..32ac2deda --- /dev/null +++ b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res @@ -0,0 +1,149 @@ +open RescriptMocha + +type testEntity = { + id: string, + fieldA: int, + fieldB: option, +} + +let testEntitySchema: S.t = S.schema(s => { + id: s.matches(S.string), + fieldA: s.matches(S.int), + fieldB: s.matches(S.option(S.string)), +}) + +let testEntityRowsSchema = S.array(testEntitySchema) + +let mockEntityTable = Table.mkTable( + "TestEntity", + ~fields=[ + Table.mkField("id", Text, ~isPrimaryKey=true), + Table.mkField("fieldA", Integer), + Table.mkField("fieldB", Text, ~isNullable=true), + ], +) +let mockEntityHistory = Entities.EntityHistory.fromTable(mockEntityTable) + +let batchSetMockEntity = Table.PostgresInterop.makeBatchSetFn( + ~table=mockEntityTable, + ~rowsSchema=testEntityRowsSchema, +) + +let getAllMockEntity = sql => + sql + ->Postgres.unsafe(`SELECT * FROM "public"."${mockEntityTable.tableName}"`) + ->Promise.thenResolve(json => json->S.parseOrRaiseWith(testEntityRowsSchema)) + +let getAllMockEntityHistory = sql => + sql->Postgres.unsafe(`SELECT * FROM "public"."${mockEntityHistory.table.tableName}"`) + +describe_only("Entity History Codegen", () => { + it("Creates an insert function", () => { + let expected = `CREATE OR REPLACE FUNCTION "insert_TestEntity_history"(history_row "public"."TestEntity_history") + RETURNS void AS $$ + DECLARE + v_previous_record RECORD; + v_origin_record RECORD; + BEGIN + -- Check if previous values are not provided + IF history_row.previous_entity_history_block_timestamp IS NULL OR history_row.previous_entity_history_chain_id IS NULL OR history_row.previous_entity_history_block_number IS NULL OR history_row.previous_entity_history_log_index IS NULL THEN + -- Find the most recent record for the same id + SELECT entity_history_block_timestamp, entity_history_chain_id, entity_history_block_number, entity_history_log_index INTO v_previous_record + FROM "public"."TestEntity_history" + WHERE id = history_row.id + ORDER BY entity_history_block_timestamp DESC, entity_history_chain_id DESC, entity_history_block_number DESC, entity_history_log_index DESC + LIMIT 1; + + -- If a previous record exists, use its values + IF FOUND THEN + history_row.previous_entity_history_block_timestamp := v_previous_record.entity_history_block_timestamp; history_row.previous_entity_history_chain_id := v_previous_record.entity_history_chain_id; history_row.previous_entity_history_block_number := v_previous_record.entity_history_block_number; history_row.previous_entity_history_log_index := v_previous_record.entity_history_log_index; + ElSE + -- Check if a value for the id exists in the origin table and if so, insert a history row for it. + SELECT "id", "fieldA", "fieldB" FROM "public"."TestEntity" WHERE id = history_row.id INTO v_origin_record; + IF FOUND THEN + INSERT INTO "public"."TestEntity_history" (entity_history_block_timestamp, entity_history_chain_id, entity_history_block_number, entity_history_log_index, "id", "fieldA", "fieldB") + -- SET the current change data fields to 0 since we don't know what they were + -- and it doesn't matter provided they are less than any new values + VALUES (0, 0, 0, 0, v_origin_record."id", v_origin_record."fieldA", v_origin_record."fieldB"); + + history_row.previous_entity_history_block_timestamp := 0; history_row.previous_entity_history_chain_id := 0; history_row.previous_entity_history_block_number := 0; history_row.previous_entity_history_log_index := 0; + END IF; + END IF; + END IF; + + INSERT INTO "public"."TestEntity_history" (entity_history_block_timestamp, entity_history_chain_id, entity_history_block_number, entity_history_log_index, previous_entity_history_block_timestamp, previous_entity_history_chain_id, previous_entity_history_block_number, previous_entity_history_log_index, "id", "fieldA", "fieldB") + VALUES (history_row.entity_history_block_timestamp, history_row.entity_history_chain_id, history_row.entity_history_block_number, history_row.entity_history_log_index, history_row.previous_entity_history_block_timestamp, history_row.previous_entity_history_chain_id, history_row.previous_entity_history_block_number, history_row.previous_entity_history_log_index, history_row."id", history_row."fieldA", history_row."fieldB"); + END; + $$ LANGUAGE plpgsql; + ` + + Assert.equal(expected, mockEntityHistory.createInsertFnQuery) + }) + + Async.it("Creating tables and functions works", async () => { + let _ = await Migrations.runDownMigrations(~shouldExit=false) + let _resA = await Migrations.creatTableIfNotExists(DbFunctions.sql, mockEntityTable) + let _resB = await Migrations.creatTableIfNotExists(DbFunctions.sql, mockEntityHistory.table) + let _createFn = await DbFunctions.sql->Postgres.unsafe(mockEntityHistory.createInsertFnQuery) + + // let res = await DbFunctions.sql->Postgres.unsafe(``) + let mockEntity = {id: "1", fieldA: 1, fieldB: Some("test")} + await DbFunctions.sql->batchSetMockEntity([mockEntity]) + let afterInsert = await DbFunctions.sql->getAllMockEntity + Assert.deepEqual(afterInsert, [mockEntity]) + + let chainId = 137 + let blockNumber = 123456 + let blockTimestamp = blockNumber * 15 + let logIndex = 1 + + let entityHistoryItem = { + "entity_id": "1", + "fieldA": 2, + "fieldB": Some("test2"), + "entity_history_chain_id": chainId, + "entity_history_block_number": blockNumber, + "entity_history_block_timestamp": blockTimestamp, + "entity_history_log_index": logIndex, + } + + //TODO: this should be created in the entity history module + let query = `(sql, args) => sql\`select "insert_TestEntity_history"(ROW(\${args.entity_history_block_timestamp}, \${args.entity_history_chain_id}, \${args.entity_history_block_number}, \${args.entity_history_log_index}, \${args.previous_entity_history_block_timestamp}, \${args.previous_entity_history_chain_id}, \${args.previous_entity_history_block_number}, \${args.previous_entity_history_log_index}, \${args.entity_id}, \${args.fieldA}, \${args.fieldB}));\`` + + let call: (Postgres.sql, 'a) => promise = Table.PostgresInterop.eval(query) + + let _callRes = await DbFunctions.sql->call(entityHistoryItem) + + let expectedResult = [ + { + "entity_history_block_timestamp": 0, + "entity_history_chain_id": 0, + "entity_history_block_number": 0, + "entity_history_log_index": 0, + "previous_entity_history_block_timestamp": Js.Nullable.Null, + "previous_entity_history_chain_id": Js.Nullable.Null, + "previous_entity_history_block_number": Js.Nullable.Null, + "previous_entity_history_log_index": Js.Nullable.Null, + "id": "1", + "fieldA": 1, + "fieldB": "test", + }, + { + "entity_history_block_timestamp": blockTimestamp, + "entity_history_chain_id": chainId, + "entity_history_block_number": blockNumber, + "entity_history_log_index": logIndex, + "previous_entity_history_block_timestamp": Js.Nullable.Value(0), + "previous_entity_history_chain_id": Js.Nullable.Value(0), + "previous_entity_history_block_number": Js.Nullable.Value(0), + "previous_entity_history_log_index": Js.Nullable.Value(0), + "id": "1", + "fieldA": 2, + "fieldB": "test2", + }, + ] + + let currentHistoryItems = await DbFunctions.sql->getAllMockEntityHistory + Assert.deepEqual(currentHistoryItems, expectedResult) + }) +}) From 3ae5772b1ed82eb870ca3e44049b01813ed406b8 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Tue, 15 Oct 2024 14:53:45 +0200 Subject: [PATCH 3/3] Add entity history schema creator --- .../dynamic/codegen/src/db/Entities.res.hbs | 81 +++++++++++++++++-- .../test/lib_tests/EntityHistory_test.res | 75 ++++++++++++++++- 2 files changed, 148 insertions(+), 8 deletions(-) diff --git a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs index 335ab5616..a77a4c086 100644 --- a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs @@ -24,19 +24,86 @@ let isIndex = true type whereOperations<'entity, 'fieldType> = {eq: 'fieldType => promise>} module EntityHistory = { - type historyFields = { - chain_id: int, - block_timestamp: int, - block_number: int, - log_index: int, + type historyFieldsGeneral<'a> = { + chain_id: 'a, + block_timestamp: 'a, + block_number: 'a, + log_index: 'a, } - type historyRow<'entiy> = { + type historyFields = historyFieldsGeneral + + type historyRow<'entity> = { current: historyFields, previous: option, - entityData: 'entiy, + entityData: 'entity, } + type previousHistoryFields = historyFieldsGeneral> + + let previousHistoryFieldsSchema = S.object(s => { + chain_id: s.field("previous_entity_history_chain_id", S.null(S.int)), + block_timestamp: s.field("previous_entity_history_block_timestamp", S.null(S.int)), + block_number: s.field("previous_entity_history_block_number", S.null(S.int)), + log_index: s.field("previous_entity_history_log_index", S.null(S.int)), + }) + + let currentHistoryFieldsSchema = S.object(s => { + chain_id: s.field("entity_history_chain_id", S.int), + block_timestamp: s.field("entity_history_block_timestamp", S.int), + block_number: s.field("entity_history_block_number", S.int), + log_index: s.field("entity_history_log_index", S.int), + }) + + let makeHistoryRowSchema: S.t<'entity> => S.t> = entitySchema => + S.object(s => { + { + "current": s.flatten(currentHistoryFieldsSchema), + "previous": s.flatten(previousHistoryFieldsSchema), + "entityData": s.flatten(entitySchema), + } + })->S.transform(s => { + parser: v => { + current: v["current"], + previous: switch v["previous"] { + | { + chain_id: Some(chain_id), + block_timestamp: Some(block_timestamp), + block_number: Some(block_number), + log_index: Some(log_index), + } => + Some({ + chain_id, + block_timestamp, + block_number, + log_index, + }) + | {chain_id: None, block_timestamp: None, block_number: None, log_index: None} => None + | _ => s.fail("Unexpected mix of null and non-null values in previous history fields") + }, + entityData: v["entityData"], + }, + serializer: v => + { + "current": v.current, + "entityData": v.entityData, + "previous": switch v.previous { + | Some({chain_id, block_timestamp, block_number, log_index}) => { + chain_id: Some(chain_id), + block_timestamp: Some(block_timestamp), + block_number: Some(block_number), + log_index: Some(log_index), + } + | None => { + chain_id: None, + block_timestamp: None, + block_number: None, + log_index: None, + } + }, + }, + }) + type t<'entity> = { table: table, createInsertFnQuery: string, diff --git a/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res index 32ac2deda..20c2cad4b 100644 --- a/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res +++ b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res @@ -14,6 +14,9 @@ let testEntitySchema: S.t = S.schema(s => { let testEntityRowsSchema = S.array(testEntitySchema) +type testEntityHistory = Entities.EntityHistory.historyRow +let testEntityHistorySchema = Entities.EntityHistory.makeHistoryRowSchema(testEntitySchema) + let mockEntityTable = Table.mkTable( "TestEntity", ~fields=[ @@ -37,7 +40,77 @@ let getAllMockEntity = sql => let getAllMockEntityHistory = sql => sql->Postgres.unsafe(`SELECT * FROM "public"."${mockEntityHistory.table.tableName}"`) -describe_only("Entity History Codegen", () => { +describe("Entity history serde", () => { + it("serializes and deserializes correctly", () => { + let history: testEntityHistory = { + current: { + chain_id: 1, + block_number: 2, + block_timestamp: 3, + log_index: 4, + }, + previous: None, + entityData: {id: "1", fieldA: 1, fieldB: Some("test")}, + } + + let serializedHistory = history->S.serializeOrRaiseWith(testEntityHistorySchema) + let expected = %raw(`{ + "entity_history_block_timestamp": 3, + "entity_history_chain_id": 1, + "entity_history_block_number": 2, + "entity_history_log_index": 4, + "previous_entity_history_block_timestamp": null, + "previous_entity_history_chain_id": null, + "previous_entity_history_block_number": null, + "previous_entity_history_log_index": null, + "id": "1", + "fieldA": 1, + "fieldB": "test" + }`) + + Assert.deepEqual(serializedHistory, expected) + let deserializedHistory = serializedHistory->S.parseOrRaiseWith(testEntityHistorySchema) + Assert.deepEqual(deserializedHistory, history) + }) + + it("serializes and deserializes correctly with previous history", () => { + let history: testEntityHistory = { + current: { + chain_id: 1, + block_number: 2, + block_timestamp: 3, + log_index: 4, + }, + previous: Some({ + chain_id: 5, + block_number: 6, + block_timestamp: 7, + log_index: 8, + }), //previous + entityData: {id: "1", fieldA: 1, fieldB: Some("test")}, + } + let serializedHistory = history->S.serializeOrRaiseWith(testEntityHistorySchema) + let expected = %raw(`{ + "entity_history_block_timestamp": 3, + "entity_history_chain_id": 1, + "entity_history_block_number": 2, + "entity_history_log_index": 4, + "previous_entity_history_block_timestamp": 7, + "previous_entity_history_chain_id": 5, + "previous_entity_history_block_number": 6, + "previous_entity_history_log_index": 8, + "id": "1", + "fieldA": 1, + "fieldB": "test" + }`) + + Assert.deepEqual(serializedHistory, expected) + let deserializedHistory = serializedHistory->S.parseOrRaiseWith(testEntityHistorySchema) + Assert.deepEqual(deserializedHistory, history) + }) +}) + +describe("Entity History Codegen", () => { it("Creates an insert function", () => { let expected = `CREATE OR REPLACE FUNCTION "insert_TestEntity_history"(history_row "public"."TestEntity_history") RETURNS void AS $$