From cc96152e56386caa49f068b149ca9db4be6d4ef0 Mon Sep 17 00:00:00 2001 From: Jono Prest <65739024+JonoPrest@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:09:11 +0200 Subject: [PATCH] [2] Add eval function for inserting entity history (#319) * Add eval function for inserting entity history * Fix spelling of variable --- .../dynamic/codegen/src/db/Entities.res.hbs | 71 ++++++++++++------- .../test/lib_tests/EntityHistory_test.res | 53 +++++++++----- 2 files changed, 80 insertions(+), 44 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 a77a4c086..aab779c35 100644 --- a/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs +++ b/codegenerator/cli/templates/dynamic/codegen/src/db/Entities.res.hbs @@ -107,14 +107,20 @@ module EntityHistory = { type t<'entity> = { table: table, createInsertFnQuery: string, - // insertFn: historyRow<'entity> => promise, + schema: S.t>, + insertFn: (Postgres.sql, Js.Json.t) => promise, + } + + let insertRow = (self: t<'entity>, ~sql, ~historyRow: historyRow<'entity>) => { + let row = historyRow->S.serializeOrRaiseWith(self.schema) + self.insertFn(sql, row) } type entityInternal external castInternal: t<'entity> => t = "%identity" - let fromTable = (table: table): t<'entity> => { + let fromTable = (table: table, ~schema: S.t<'entity>): 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" @@ -176,29 +182,30 @@ module EntityHistory = { ~fields=Belt.Array.concatMany([currentHistoryFields, previousHistoryFields, dataFields]), ) - let createInsertFnQuery = { - let insertFnName = `"insert_${table.tableName}"` - let historRowArg = "history_row" - let historyTablePath = `"public"."${historyTableName}"` - let originTablePath = `"public"."${originTableName}"` + let insertFnName = `"insert_${table.tableName}"` + let historyRowArg = "history_row" + let historyTablePath = `"public"."${historyTableName}"` + let originTablePath = `"public"."${originTableName}"` - let previousHistoryFieldsAreNullStr = - previousChangeFieldNames - ->Belt.Array.map(fieldName => `${historRowArg}.${fieldName} IS NULL`) - ->Js.Array2.joinWith(" OR ") + let previousHistoryFieldsAreNullStr = + previousChangeFieldNames + ->Belt.Array.map(fieldName => `${historyRowArg}.${fieldName} IS NULL`) + ->Js.Array2.joinWith(" OR ") - let currentChangeFieldNamesCommaSeparated = currentChangeFieldNames->Js.Array2.joinWith(", ") + let currentChangeFieldNamesCommaSeparated = currentChangeFieldNames->Js.Array2.joinWith(", ") - let dataFieldNamesDoubleQuoted = dataFieldNames->Belt.Array.map(fieldName => `"${fieldName}"`) - let dataFieldNamesCommaSeparated = dataFieldNamesDoubleQuoted->Js.Array2.joinWith(", ") + let dataFieldNamesDoubleQuoted = dataFieldNames->Belt.Array.map(fieldName => `"${fieldName}"`) + let dataFieldNamesCommaSeparated = dataFieldNamesDoubleQuoted->Js.Array2.joinWith(", ") - let allFieldNames = Belt.Array.concatMany([ + let allFieldNamesDoubleQuoted = + Belt.Array.concatMany([ currentChangeFieldNames, previousChangeFieldNames, - dataFieldNamesDoubleQuoted, - ]) + dataFieldNames, + ])->Belt.Array.map(fieldName => `"${fieldName}"`) - `CREATE OR REPLACE FUNCTION ${insertFnName}(${historRowArg} ${historyTablePath}) + let createInsertFnQuery = { + `CREATE OR REPLACE FUNCTION ${insertFnName}(${historyRowArg} ${historyTablePath}) RETURNS void AS $$ DECLARE v_previous_record RECORD; @@ -209,7 +216,7 @@ module EntityHistory = { -- Find the most recent record for the same id SELECT ${currentChangeFieldNamesCommaSeparated} INTO v_previous_record FROM ${historyTablePath} - WHERE ${id} = ${historRowArg}.${id} + WHERE ${id} = ${historyRowArg}.${id} ORDER BY ${currentChangeFieldNames ->Belt.Array.map(fieldName => fieldName ++ " DESC") ->Js.Array2.joinWith(", ")} @@ -219,12 +226,12 @@ module EntityHistory = { IF FOUND THEN ${Belt.Array.zip(currentChangeFieldNames, previousChangeFieldNames) ->Belt.Array.map(((currentFieldName, previousFieldName)) => { - `${historRowArg}.${previousFieldName} := v_previous_record.${currentFieldName};` + `${historyRowArg}.${previousFieldName} := v_previous_record.${currentFieldName};` }) ->Js.Array2.joinWith(" ")} ElSE -- 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; + SELECT ${dataFieldNamesCommaSeparated} FROM ${originTablePath} WHERE id = ${historyRowArg}.${id} INTO v_origin_record; IF FOUND THEN INSERT INTO ${historyTablePath} (${currentChangeFieldNamesCommaSeparated}, ${dataFieldNamesCommaSeparated}) -- SET the current change data fields to 0 since we don't know what they were @@ -237,23 +244,33 @@ module EntityHistory = { ${previousChangeFieldNames ->Belt.Array.map(previousFieldName => { - `${historRowArg}.${previousFieldName} := 0;` + `${historyRowArg}.${previousFieldName} := 0;` }) ->Js.Array2.joinWith(" ")} END IF; END IF; END IF; - INSERT INTO ${historyTablePath} (${allFieldNames->Js.Array2.joinWith(", ")}) - VALUES (${allFieldNames - ->Belt.Array.map(fieldName => `${historRowArg}.${fieldName}`) + INSERT INTO ${historyTablePath} (${allFieldNamesDoubleQuoted->Js.Array2.joinWith(", ")}) + VALUES (${allFieldNamesDoubleQuoted + ->Belt.Array.map(fieldName => `${historyRowArg}.${fieldName}`) ->Js.Array2.joinWith(", ")}); END; $$ LANGUAGE plpgsql; ` } - {table, createInsertFnQuery} + let insertFnString = `(sql, rowArgs) => + sql\`select ${insertFnName}(ROW(${allFieldNamesDoubleQuoted + ->Belt.Array.map(fieldNameDoubleQuoted => `\${rowArgs[${fieldNameDoubleQuoted}]\}`) + ->Js.Array2.joinWith(", ")}));\`` + + let insertFn: (Postgres.sql, Js.Json.t) => promise = + insertFnString->Table.PostgresInterop.eval + + let schema = makeHistoryRowSchema(schema) + + {table, createInsertFnQuery, schema, insertFn} } } @@ -321,7 +338,7 @@ module {{entity.name.capitalized}} = { {{/if}} ) - let entityHistory = table->EntityHistory.fromTable + let entityHistory = table->EntityHistory.fromTable(~schema) } {{/each}} diff --git a/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res index 20c2cad4b..f09bd0461 100644 --- a/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res +++ b/scenarios/test_codegen/test/lib_tests/EntityHistory_test.res @@ -1,5 +1,8 @@ open RescriptMocha +//unsafe polymorphic toString binding for any type +@send external toStringUnsafe: 'a => string = "toString" + type testEntity = { id: string, fieldA: int, @@ -25,7 +28,8 @@ let mockEntityTable = Table.mkTable( Table.mkField("fieldB", Text, ~isNullable=true), ], ) -let mockEntityHistory = Entities.EntityHistory.fromTable(mockEntityTable) + +let mockEntityHistory = mockEntityTable->Entities.EntityHistory.fromTable(~schema=testEntitySchema) let batchSetMockEntity = Table.PostgresInterop.makeBatchSetFn( ~table=mockEntityTable, @@ -111,7 +115,7 @@ describe("Entity history serde", () => { }) describe("Entity History Codegen", () => { - it("Creates an insert function", () => { + it("Creates a postgres insert function", () => { let expected = `CREATE OR REPLACE FUNCTION "insert_TestEntity_history"(history_row "public"."TestEntity_history") RETURNS void AS $$ DECLARE @@ -144,8 +148,8 @@ describe("Entity History Codegen", () => { 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"); + 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; ` @@ -153,6 +157,15 @@ describe("Entity History Codegen", () => { Assert.equal(expected, mockEntityHistory.createInsertFnQuery) }) + it("Creates a js insert function", () => { + let insertFnString = mockEntityHistory.insertFn->toStringUnsafe + + let expected = `(sql, rowArgs) => + sql\`select "insert_TestEntity_history"(ROW(\${rowArgs["entity_history_block_timestamp"]}, \${rowArgs["entity_history_chain_id"]}, \${rowArgs["entity_history_block_number"]}, \${rowArgs["entity_history_log_index"]}, \${rowArgs["previous_entity_history_block_timestamp"]}, \${rowArgs["previous_entity_history_chain_id"]}, \${rowArgs["previous_entity_history_block_number"]}, \${rowArgs["previous_entity_history_log_index"]}, \${rowArgs["id"]}, \${rowArgs["fieldA"]}, \${rowArgs["fieldB"]}));\`` + + Assert.equal(expected, insertFnString) + }) + Async.it("Creating tables and functions works", async () => { let _ = await Migrations.runDownMigrations(~shouldExit=false) let _resA = await Migrations.creatTableIfNotExists(DbFunctions.sql, mockEntityTable) @@ -170,22 +183,28 @@ describe("Entity History Codegen", () => { 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, + let entityHistoryItem: Entities.EntityHistory.historyRow = { + current: { + chain_id: chainId, + block_timestamp: blockTimestamp, + block_number: blockNumber, + log_index: logIndex, + }, + previous: None, + entityData: { + id: "1", + fieldA: 2, + fieldB: Some("test2"), + }, } - //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 mockEntityHistory->Entities.EntityHistory.insertRow( + ~sql=DbFunctions.sql, + ~historyRow=entityHistoryItem, + ) - let _callRes = await DbFunctions.sql->call(entityHistoryItem) + // let _callRes = await DbFunctions.sql->call(entityHistoryItem) let expectedResult = [ {