From 7c1ba722dcd553d6d45fa0c855bb0d01070c3efa Mon Sep 17 00:00:00 2001 From: Ulad Kasach Date: Sun, 1 Dec 2024 16:49:53 -0800 Subject: [PATCH] feat(refs): support direct declarations of Refs --- package-lock.json | 164 +++++++----- package.json | 4 +- .../objects/SqlSchemaReferenceMetadata.ts | 12 + .../src/domain/objects/CarriageCargo.ts | 35 +++ .../src/domain/objects/index.ts | 1 + .../readConfig.integration.test.ts.snap | 60 +++++ ...atasFromConfigCriteria.integration.test.ts | 6 +- .../getAllPathsMatchingGlobs.test.ts | 2 +- .../getConfig/readConfig.integration.test.ts | 2 +- ...sForDomainObjects.integration.test.ts.snap | 248 ++++++++++++++++++ ...CastMethodCodeForDomainObject.test.ts.snap | 17 ++ ...xpressionForSqlSchemaProperty.test.ts.snap | 7 + ...efineDaoFindByMethodCodeForDomainObject.ts | 10 +- ...oUtilCastMethodCodeForDomainObject.test.ts | 56 ++++ ...ineDaoUtilCastMethodCodeForDomainObject.ts | 25 ++ ...tExpressionForDomainObjectProperty.test.ts | 59 +++++ ...nInputExpressionForDomainObjectProperty.ts | 38 ++- ...nputExpressionForSqlSchemaProperty.test.ts | 39 +++ ...ueryInputExpressionForSqlSchemaProperty.ts | 33 ++- ...lectExpressionForSqlSchemaProperty.test.ts | 47 ++++ ...erySelectExpressionForSqlSchemaProperty.ts | 36 ++- ...erenceAvailableProvisionOrder.test.ts.snap | 4 + ...sForDomainObjects.integration.test.ts.snap | 14 + ...sForDomainObjects.integration.test.ts.snap | 37 +++ ...aGeneratorCodeForDomainObject.test.ts.snap | 17 ++ ...SchemaGeneratorCodeForDomainObject.test.ts | 137 +++++++--- ...psForDomainObject.integration.test.ts.snap | 164 ++++++++++++ ...emaPropertyForDomainObjectProperty.test.ts | 36 +++ ...qlSchemaPropertyForDomainObjectProperty.ts | 4 + ...lSchemaReferenceForDomainObjectProperty.ts | 44 ++-- ...ineSqlSchemaRelationshipForDomainObject.ts | 2 +- 31 files changed, 1220 insertions(+), 140 deletions(-) create mode 100644 src/logic/__test_assets__/exampleProject/src/domain/objects/CarriageCargo.ts diff --git a/package-lock.json b/package-lock.json index 4fe67c9..2eee7bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,13 +10,13 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@ehmpathy/error-fns": "1.3.1", + "@ehmpathy/error-fns": "1.3.7", "@oclif/core": "3.26.6", "@oclif/plugin-help": "6.0.22", "chalk": "2.4.2", "change-case": "4.1.1", "domain-objects": "0.22.1", - "domain-objects-metadata": "0.7.4", + "domain-objects-metadata": "0.7.5", "fast-glob": "3.2.2", "joi": "17.4.0", "lodash.omit": "4.5.0", @@ -2321,25 +2321,17 @@ } }, "node_modules/@ehmpathy/error-fns": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", - "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.7.tgz", + "integrity": "sha512-Lm3WXBMFkNDh9RU21zNTyziUARh7e/FZCcxGUefz7Cx/4yJwBJ9oBxI1JNh8cI9q5HPxAwVkEM8DDORhNM67Sg==", "hasInstallScript": true, "dependencies": { - "type-fns": "0.9.0" + "type-fns": "1.17.0" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/@ehmpathy/error-fns/node_modules/type-fns": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", - "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -8013,6 +8005,28 @@ "declapract": "0.11.5" } }, + "node_modules/declapract-typescript-ehmpathy/node_modules/@ehmpathy/error-fns": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", + "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "type-fns": "0.9.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/declapract-typescript-ehmpathy/node_modules/type-fns": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", + "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/declapract/node_modules/@ehmpathy/error-fns": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.0.2.tgz", @@ -9126,12 +9140,12 @@ } }, "node_modules/domain-objects-metadata": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/domain-objects-metadata/-/domain-objects-metadata-0.7.4.tgz", - "integrity": "sha512-YgnXsdds1LQAnXTbvYSMJExLb+ooTY9mHUWX49t1+o6MaSRGJdW4t+QX9sITfS/a64+0jVwWkjDykVF6XsU0fA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/domain-objects-metadata/-/domain-objects-metadata-0.7.5.tgz", + "integrity": "sha512-lwKU9Jwdn63JIjwH4vXS+VSl8GIdLf75DY7UBm/y+HNZ7k8QyXL5/SAo/FL1VT6ZT0UXBd9L9Kfnf8jxDRQ7ZA==", "hasInstallScript": true, "dependencies": { - "@ehmpathy/error-fns": "1.0.2", + "@ehmpathy/error-fns": "^1.3.7", "domain-objects": "^0.22.1", "joi": "17.4.0", "type-fns": "^1.16.0", @@ -9142,26 +9156,6 @@ "node": ">=8.0.0" } }, - "node_modules/domain-objects-metadata/node_modules/@ehmpathy/error-fns": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.0.2.tgz", - "integrity": "sha512-v3aJIqUvD9a3drx1pyS8La+9u9WTTvNE35NksiD4Oo3VanNe8Rmue/atRHPg4nNYQ/xPv4+RoqC+OBj6cAY8VA==", - "hasInstallScript": true, - "dependencies": { - "type-fns": "0.9.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/domain-objects-metadata/node_modules/@ehmpathy/error-fns/node_modules/type-fns": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", - "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/domain-objects-metadata/node_modules/typescript": { "version": "3.9.10", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", @@ -20468,6 +20462,28 @@ "node": ">=8.0.0" } }, + "node_modules/test-fns/node_modules/@ehmpathy/error-fns": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", + "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "type-fns": "0.9.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/test-fns/node_modules/type-fns": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", + "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -24813,18 +24829,11 @@ } }, "@ehmpathy/error-fns": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", - "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.7.tgz", + "integrity": "sha512-Lm3WXBMFkNDh9RU21zNTyziUARh7e/FZCcxGUefz7Cx/4yJwBJ9oBxI1JNh8cI9q5HPxAwVkEM8DDORhNM67Sg==", "requires": { - "type-fns": "0.9.0" - }, - "dependencies": { - "type-fns": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", - "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==" - } + "type-fns": "1.17.0" } }, "@eslint-community/eslint-utils": { @@ -29911,6 +29920,23 @@ "domain-objects": "0.22.1", "expect": "29.4.2", "flat": "5.0.2" + }, + "dependencies": { + "@ehmpathy/error-fns": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", + "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "dev": true, + "requires": { + "type-fns": "0.9.0" + } + }, + "type-fns": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", + "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", + "dev": true + } } }, "decompress-response": { @@ -30163,11 +30189,11 @@ } }, "domain-objects-metadata": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/domain-objects-metadata/-/domain-objects-metadata-0.7.4.tgz", - "integrity": "sha512-YgnXsdds1LQAnXTbvYSMJExLb+ooTY9mHUWX49t1+o6MaSRGJdW4t+QX9sITfS/a64+0jVwWkjDykVF6XsU0fA==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/domain-objects-metadata/-/domain-objects-metadata-0.7.5.tgz", + "integrity": "sha512-lwKU9Jwdn63JIjwH4vXS+VSl8GIdLf75DY7UBm/y+HNZ7k8QyXL5/SAo/FL1VT6ZT0UXBd9L9Kfnf8jxDRQ7ZA==", "requires": { - "@ehmpathy/error-fns": "1.0.2", + "@ehmpathy/error-fns": "^1.3.7", "domain-objects": "^0.22.1", "joi": "17.4.0", "type-fns": "^1.16.0", @@ -30175,21 +30201,6 @@ "uuid": "9.0.0" }, "dependencies": { - "@ehmpathy/error-fns": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.0.2.tgz", - "integrity": "sha512-v3aJIqUvD9a3drx1pyS8La+9u9WTTvNE35NksiD4Oo3VanNe8Rmue/atRHPg4nNYQ/xPv4+RoqC+OBj6cAY8VA==", - "requires": { - "type-fns": "0.9.0" - }, - "dependencies": { - "type-fns": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", - "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==" - } - } - }, "typescript": { "version": "3.9.10", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", @@ -38886,6 +38897,23 @@ "dev": true, "requires": { "@ehmpathy/error-fns": "1.3.1" + }, + "dependencies": { + "@ehmpathy/error-fns": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ehmpathy/error-fns/-/error-fns-1.3.1.tgz", + "integrity": "sha512-WSQQ95mAVU9JammqFeEfeyf23YeVIRSnCtH1TZurlgCpFuilui8hId77cfq6RlaOkPZd5zdFEs6pjTTDXLchqA==", + "dev": true, + "requires": { + "type-fns": "0.9.0" + } + }, + "type-fns": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/type-fns/-/type-fns-0.9.0.tgz", + "integrity": "sha512-ndhY4JBIbKix0LuGA5smh/XhFFnbeudnih++WxVoGTfdrITsZe/s3qje9GZNdWwsO+YWGyQkNXwAjnWyM/dipw==", + "dev": true + } } }, "text-extensions": { diff --git a/package.json b/package.json index 8d19120..b21fbef 100644 --- a/package.json +++ b/package.json @@ -76,13 +76,13 @@ "postinstall": "[ -d .git ] && npx husky install || exit 0" }, "dependencies": { - "@ehmpathy/error-fns": "1.3.1", + "@ehmpathy/error-fns": "1.3.7", "@oclif/core": "3.26.6", "@oclif/plugin-help": "6.0.22", "chalk": "2.4.2", "change-case": "4.1.1", "domain-objects": "0.22.1", - "domain-objects-metadata": "0.7.4", + "domain-objects-metadata": "0.7.5", "fast-glob": "3.2.2", "joi": "17.4.0", "lodash.omit": "4.5.0", diff --git a/src/domain/objects/SqlSchemaReferenceMetadata.ts b/src/domain/objects/SqlSchemaReferenceMetadata.ts index aaebbe2..db23620 100644 --- a/src/domain/objects/SqlSchemaReferenceMetadata.ts +++ b/src/domain/objects/SqlSchemaReferenceMetadata.ts @@ -3,7 +3,19 @@ import { DomainObjectReferenceMetadata } from 'domain-objects-metadata'; import Joi from 'joi'; export enum SqlSchemaReferenceMethod { + /** + * .what = when a domain literal is directly nested in another domain object + */ DIRECT_BY_NESTING = 'DIRECT_BY_NESTING', + + /** + * .what = when a domain object is explicitly referenced, via Ref<>, in another domain object + */ + DIRECT_BY_DECLARATION = 'DIRECT_BY_DECLARATION', + + /** + * .what = when a domain object is implicitly referenced, via uuid, in another domain object + */ IMPLICIT_BY_UUID = 'IMPLICIT_BY_UUID', } diff --git a/src/logic/__test_assets__/exampleProject/src/domain/objects/CarriageCargo.ts b/src/logic/__test_assets__/exampleProject/src/domain/objects/CarriageCargo.ts new file mode 100644 index 0000000..15c1c6b --- /dev/null +++ b/src/logic/__test_assets__/exampleProject/src/domain/objects/CarriageCargo.ts @@ -0,0 +1,35 @@ +import { DomainEntity, Ref } from 'domain-objects'; +import { Carriage } from './Carriage'; + +/** + * .what = cargo which has been allocated to a carriage at a given point in time + */ +export interface CarriageCargo { + id?: number; + uuid?: string; + + /** + * the itinerary during which it is allocated + */ + itineraryUuid: string; // todo: ref to an itinerary once we declare it + + /** + * the carriage it has been allocated to + */ + carriageRef: Ref; + + /** + * the slot that it has been allocated to on the carriage + */ + slot: number; + + /** + * the cargo that has been allocated + */ + cargoExid: null | string; +} +export class CarriageCargo extends DomainEntity implements CarriageCargo { + public static primary = ['uuid'] as const; + public static unique = ['itineraryUuid', 'carriageRef', 'slot'] as const; + public static updatable = ['cargoExid']; +} diff --git a/src/logic/__test_assets__/exampleProject/src/domain/objects/index.ts b/src/logic/__test_assets__/exampleProject/src/domain/objects/index.ts index c5c5140..30779fd 100644 --- a/src/logic/__test_assets__/exampleProject/src/domain/objects/index.ts +++ b/src/logic/__test_assets__/exampleProject/src/domain/objects/index.ts @@ -1,4 +1,5 @@ export * from './AsyncTaskPredictStationCongestion' +export * from './CarriageCargo'; export * from './Carriage'; export * from './Certificate'; export * from './Engineer'; diff --git a/src/logic/config/getConfig/__snapshots__/readConfig.integration.test.ts.snap b/src/logic/config/getConfig/__snapshots__/readConfig.integration.test.ts.snap index 57db650..96f8b48 100644 --- a/src/logic/config/getConfig/__snapshots__/readConfig.integration.test.ts.snap +++ b/src/logic/config/getConfig/__snapshots__/readConfig.integration.test.ts.snap @@ -125,6 +125,66 @@ exports[`readConfig should be able to read the example config provisioned in __t }, }, }, + DomainObjectMetadata { + "decorations": { + "alias": null, + "primary": [ + "uuid", + ], + "unique": [ + "itineraryUuid", + "carriageRef", + "slot", + ], + "updatable": [ + "cargoExid", + ], + }, + "extends": "DomainEntity", + "name": "CarriageCargo", + "properties": { + "cargoExid": DomainObjectPropertyMetadata { + "name": "cargoExid", + "nullable": true, + "required": true, + "type": "STRING", + }, + "carriageRef": DomainObjectPropertyMetadata { + "name": "carriageRef", + "nullable": false, + "of": DomainObjectReferenceMetadata { + "extends": "DomainEntity", + "name": "Carriage", + }, + "required": true, + "type": "REFERENCE", + }, + "id": DomainObjectPropertyMetadata { + "name": "id", + "nullable": false, + "required": false, + "type": "NUMBER", + }, + "itineraryUuid": DomainObjectPropertyMetadata { + "name": "itineraryUuid", + "nullable": false, + "required": true, + "type": "STRING", + }, + "slot": DomainObjectPropertyMetadata { + "name": "slot", + "nullable": false, + "required": true, + "type": "NUMBER", + }, + "uuid": DomainObjectPropertyMetadata { + "name": "uuid", + "nullable": false, + "required": false, + "type": "STRING", + }, + }, + }, DomainObjectMetadata { "decorations": { "alias": null, diff --git a/src/logic/config/getConfig/extractDomainObjectMetadatasFromConfigCriteria.integration.test.ts b/src/logic/config/getConfig/extractDomainObjectMetadatasFromConfigCriteria.integration.test.ts index fd81c48..2fb7751 100644 --- a/src/logic/config/getConfig/extractDomainObjectMetadatasFromConfigCriteria.integration.test.ts +++ b/src/logic/config/getConfig/extractDomainObjectMetadatasFromConfigCriteria.integration.test.ts @@ -10,7 +10,7 @@ describe('extractDomainObjectMetadatasFromConfigCriteria', () => { exclude: null, }); // console.log(JSON.stringify(metadatas, null, 2)); - expect(metadatas.length).toEqual(12); + expect(metadatas.length).toEqual(13); }); it('should find all of the domain objects findable by all search paths in a directory, with no dupes', async () => { const metadatas = await extractDomainObjectMetadatasFromConfigCriteria({ @@ -27,7 +27,7 @@ describe('extractDomainObjectMetadatasFromConfigCriteria', () => { exclude: null, }); // console.log(JSON.stringify(metadatas, null, 2)); - expect(metadatas.length).toEqual(12); + expect(metadatas.length).toEqual(13); }); it('should only find the domain objects imported from the files in the search paths', async () => { const metadatas = await extractDomainObjectMetadatasFromConfigCriteria({ @@ -55,7 +55,7 @@ describe('extractDomainObjectMetadatasFromConfigCriteria', () => { exclude: ['TrainLocatedEvent'], }); // console.log(JSON.stringify(metadatas, null, 2)); - expect(metadatas.length).toEqual(11); + expect(metadatas.length).toEqual(12); }); it('should only keep the one named in the `include` list, if provided', async () => { const metadatas = await extractDomainObjectMetadatasFromConfigCriteria({ diff --git a/src/logic/config/getConfig/getAllPathsMatchingGlobs.test.ts b/src/logic/config/getConfig/getAllPathsMatchingGlobs.test.ts index 7b4e7f7..b7533c0 100644 --- a/src/logic/config/getConfig/getAllPathsMatchingGlobs.test.ts +++ b/src/logic/config/getConfig/getAllPathsMatchingGlobs.test.ts @@ -16,7 +16,7 @@ describe('getAllPathsMatchingGlobs', () => { expect(files).toContain('src/domain/objects/Train.ts'); expect(files).toContain('src/domain/objects/TrainLocatedEvent.ts'); expect(files).toContain('src/domain/objects/Station.ts'); - expect(files.length).toEqual(14); + expect(files.length).toEqual(15); }); it('should return paths that match each glob', async () => { const files = await getAllPathsMatchingGlobs({ diff --git a/src/logic/config/getConfig/readConfig.integration.test.ts b/src/logic/config/getConfig/readConfig.integration.test.ts index a3819c6..7f91c45 100644 --- a/src/logic/config/getConfig/readConfig.integration.test.ts +++ b/src/logic/config/getConfig/readConfig.integration.test.ts @@ -37,7 +37,7 @@ describe('readConfig', () => { }, }, }); - expect(config.for.objects.length).toEqual(11); + expect(config.for.objects.length).toEqual(12); expect(config).toMatchSnapshot({ rootDir: expect.anything() }); // to log an example of the output; ignore the rootDir, to make it machine independent }); }); diff --git a/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoCodeFilesForDomainObjects.integration.test.ts.snap b/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoCodeFilesForDomainObjects.integration.test.ts.snap index a26803d..290d648 100644 --- a/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoCodeFilesForDomainObjects.integration.test.ts.snap +++ b/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoCodeFilesForDomainObjects.integration.test.ts.snap @@ -460,6 +460,254 @@ export const upsert = async ( GeneratedCodeFile { "content": "import { withExpectOutput } from 'procedure-fns'; +import { findById } from './findById'; +import { findByUnique } from './findByUnique'; +import { findByUuid } from './findByUuid'; +import { findByRef } from './findByRef'; +import { upsert } from './upsert'; + +export const carriageCargoDao = { + findById: withExpectOutput(findById), + findByUnique: withExpectOutput(findByUnique), + findByUuid: withExpectOutput(findByUuid), + findByRef: withExpectOutput(findByRef), + upsert, +}; + +// include an alias, for improved ease of access via autocomplete +export const daoCarriageCargo = carriageCargoDao;", + "relpath": "carriageCargoDao/index.ts", + }, + GeneratedCodeFile { + "content": "import { HasMetadata } from 'type-fns'; + +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { SqlQueryFindCarriageByIdOutput, SqlQueryFindCarriageCargoByIdOutput } from '$PATH_TO_GENERATED_SQL_TYPES'; +import { castFromDatabaseObject as castCarriageFromDatabaseObject } from '../carriageDao/castFromDatabaseObject'; + +export const castFromDatabaseObject = ( + dbObject: SqlQueryFindCarriageCargoByIdOutput, +): HasMetadata => + new CarriageCargo({ + id: dbObject.id, + uuid: dbObject.uuid, + itineraryUuid: dbObject.itinerary_uuid, + carriageRef: { uuid: dbObject.carriage_uuid }, + slot: dbObject.slot, + cargoExid: dbObject.cargo_exid, + }) as HasMetadata;", + "relpath": "carriageCargoDao/castFromDatabaseObject.ts", + }, + GeneratedCodeFile { + "content": "import { HasMetadata } from 'type-fns'; + +import { DatabaseConnection } from '$PATH_TO_DATABASE_CONNECTION'; +import { log } from '$PATH_TO_LOG_OBJECT'; +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { sqlQueryFindCarriageCargoById } from '$PATH_TO_GENERATED_SQL_QUERY_FUNCTIONS'; +import { castFromDatabaseObject } from './castFromDatabaseObject'; + +export const sql = \` + -- query_name = find_carriage_cargo_by_id + SELECT + carriage_cargo.id, + carriage_cargo.uuid, + carriage_cargo.itinerary_uuid, + carriage_cargo.carriage_uuid, + carriage_cargo.slot, + carriage_cargo.cargo_exid + FROM view_carriage_cargo_hydrated AS carriage_cargo + WHERE carriage_cargo.id = :id; +\`; + +export const findById = async ( + { + id, + }: { + id: number; + }, + context: { dbConnection: DatabaseConnection }, +): Promise | null> => { + const results = await sqlQueryFindCarriageCargoById({ + dbExecute: context.dbConnection.query, + logDebug: log.debug, + input: { + id, + }, + }); + const [dbObject, ...moreDbObjects] = results; + if (moreDbObjects.length) throw new Error('expected only one db object for this query'); + if (!dbObject) return null; + return castFromDatabaseObject(dbObject); +};", + "relpath": "carriageCargoDao/findById.ts", + }, + GeneratedCodeFile { + "content": "import { HasMetadata } from 'type-fns'; + +import { DatabaseConnection } from '$PATH_TO_DATABASE_CONNECTION'; +import { log } from '$PATH_TO_LOG_OBJECT'; +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { sqlQueryFindCarriageCargoByUnique } from '$PATH_TO_GENERATED_SQL_QUERY_FUNCTIONS'; +import { castFromDatabaseObject } from './castFromDatabaseObject'; + +export const sql = \` + -- query_name = find_carriage_cargo_by_unique + SELECT + carriage_cargo.id, + carriage_cargo.uuid, + carriage_cargo.itinerary_uuid, + carriage_cargo.carriage_uuid, + carriage_cargo.slot, + carriage_cargo.cargo_exid + FROM view_carriage_cargo_hydrated AS carriage_cargo + JOIN view_carriage_cargo_current ON carriage_cargo.id = view_carriage_cargo_current.id + WHERE 1=1 + AND carriage_cargo.itinerary_uuid = :itineraryUuid + AND view_carriage_cargo_current.carriage_id = (SELECT id FROM carriage WHERE carriage.uuid = :carriageUuid) + AND carriage_cargo.slot = :slot; +\`; + +export const findByUnique = async ( + { + itineraryUuid, + carriageRef, + slot, + }: { + itineraryUuid: string; + carriageRef: Carriage; + slot: number; + }, + context: { dbConnection: DatabaseConnection }, +): Promise | null> => { + const results = await sqlQueryFindCarriageCargoByUnique({ + dbExecute: context.dbConnection.query, + logDebug: log.debug, + input: { + itineraryUuid, + carriageUuid: isPrimaryKeyRef({ of: Carriage })(carriageRef) ? carriageRef.uuid : (await carriageDao.findByRef({ ref: carriageRef }, context).expect('isPresent')).uuid, + slot, + }, + }); + const [dbObject, ...moreDbObjects] = results; + if (moreDbObjects.length) throw new Error('expected only one db object for this query'); + if (!dbObject) return null; + return castFromDatabaseObject(dbObject); +};", + "relpath": "carriageCargoDao/findByUnique.ts", + }, + GeneratedCodeFile { + "content": "import { HasMetadata } from 'type-fns'; + +import { DatabaseConnection } from '$PATH_TO_DATABASE_CONNECTION'; +import { log } from '$PATH_TO_LOG_OBJECT'; +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { sqlQueryFindCarriageCargoByUuid } from '$PATH_TO_GENERATED_SQL_QUERY_FUNCTIONS'; +import { castFromDatabaseObject } from './castFromDatabaseObject'; + +export const sql = \` + -- query_name = find_carriage_cargo_by_uuid + SELECT + carriage_cargo.id, + carriage_cargo.uuid, + carriage_cargo.itinerary_uuid, + carriage_cargo.carriage_uuid, + carriage_cargo.slot, + carriage_cargo.cargo_exid + FROM view_carriage_cargo_hydrated AS carriage_cargo + WHERE carriage_cargo.uuid = :uuid; +\`; + +export const findByUuid = async ( + { + uuid, + }: { + uuid: string; + }, + context: { dbConnection: DatabaseConnection }, +): Promise | null> => { + const results = await sqlQueryFindCarriageCargoByUuid({ + dbExecute: context.dbConnection.query, + logDebug: log.debug, + input: { + uuid, + }, + }); + const [dbObject, ...moreDbObjects] = results; + if (moreDbObjects.length) throw new Error('expected only one db object for this query'); + if (!dbObject) return null; + return castFromDatabaseObject(dbObject); +};", + "relpath": "carriageCargoDao/findByUuid.ts", + }, + GeneratedCodeFile { + "content": "import { UnexpectedCodePathError } from '@ehmpathy/error-fns'; +import { Ref, isPrimaryKeyRef, isUniqueKeyRef } from 'domain-objects'; +import { HasMetadata } from 'type-fns'; + +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { DatabaseConnection } from '$PATH_TO_DATABASE_CONNECTION'; +import { findByUnique } from './findByUnique'; +import { findByUuid } from './findByUuid'; + +export const findByRef = async ( + input: { ref: Ref }, + context: { dbConnection: DatabaseConnection }, +): Promise | null> => { + if (isPrimaryKeyRef({ of: CarriageCargo })(input.ref)) + return await findByUuid(input.ref, context); + if (isUniqueKeyRef({ of: CarriageCargo })(input.ref)) + return await findByUnique(input.ref, context); + throw new UnexpectedCodePathError('invalid ref for CarriageCargo', { input }); +};", + "relpath": "carriageCargoDao/findByRef.ts", + }, + GeneratedCodeFile { + "content": "import { HasMetadata } from 'type-fns'; + +import { DatabaseConnection } from '$PATH_TO_DATABASE_CONNECTION'; +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { log } from '$PATH_TO_LOG_OBJECT'; +import { sqlQueryUpsertCarriageCargo } from '$PATH_TO_GENERATED_SQL_QUERY_FUNCTIONS'; + +export const sql = \` + -- query_name = upsert_carriage_cargo + SELECT + dgv.id, dgv.uuid + FROM upsert_carriage_cargo( + :itineraryUuid, + (SELECT id FROM carriage WHERE carriage.uuid = :carriageUuid), + :slot, + :cargoExid + ) as dgv; +\`; + +export const upsert = async ( + { + carriageCargo, + }: { + carriageCargo: CarriageCargo; + }, + context: { dbConnection: DatabaseConnection }, +): Promise> => { + const results = await sqlQueryUpsertCarriageCargo({ + dbExecute: context.dbConnection.query, + logDebug: log.debug, + input: { + itineraryUuid: carriageCargo.itineraryUuid, + carriageUuid: isPrimaryKeyRef({ of: Carriage })(carriageCargo.carriageRef) ? carriageCargo.carriageRef.uuid : (await carriageDao.findByRef({ ref: carriageCargo.carriageRef }, context).expect('isPresent')).uuid, + slot: carriageCargo.slot, + cargoExid: carriageCargo.cargoExid, + }, + }); + const { id, uuid } = results[0]!; // grab the db generated values + return new CarriageCargo({ ...carriageCargo, id, uuid }) as HasMetadata; +};", + "relpath": "carriageCargoDao/upsert.ts", + }, + GeneratedCodeFile { + "content": "import { withExpectOutput } from 'procedure-fns'; + import { findById } from './findById'; import { findByUnique } from './findByUnique'; import { upsert } from './upsert'; diff --git a/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoUtilCastMethodCodeForDomainObject.test.ts.snap b/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoUtilCastMethodCodeForDomainObject.test.ts.snap index 30618c3..6362afe 100644 --- a/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoUtilCastMethodCodeForDomainObject.test.ts.snap +++ b/src/logic/define/databaseAccessObjects/__snapshots__/defineDaoUtilCastMethodCodeForDomainObject.test.ts.snap @@ -1,5 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`defineDaoUtilCastMethodCodeForDomainObject should look correct for a domain entity which directly declares a reference to another 1`] = ` +"import { HasMetadata } from 'type-fns'; + +import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'; +import { SqlQueryFindCarriageByIdOutput, SqlQueryFindCarriageCargoByIdOutput } from '$PATH_TO_GENERATED_SQL_TYPES'; +import { castFromDatabaseObject as castCarriageFromDatabaseObject } from '../carriageDao/castFromDatabaseObject'; + +export const castFromDatabaseObject = ( + dbObject: SqlQueryFindCarriageCargoByIdOutput, +): HasMetadata => + new CarriageCargo({ + id: dbObject.id, + uuid: dbObject.uuid, + carriageRef: { uuid: dbObject.carriage_uuid }, + }) as HasMetadata;" +`; + exports[`defineDaoUtilCastMethodCodeForDomainObject should look correct for a domain event with a static referenced array 1`] = ` "import { HasMetadata } from 'type-fns'; diff --git a/src/logic/define/databaseAccessObjects/__snapshots__/defineQuerySelectExpressionForSqlSchemaProperty.test.ts.snap b/src/logic/define/databaseAccessObjects/__snapshots__/defineQuerySelectExpressionForSqlSchemaProperty.test.ts.snap index d223c1d..1a82dc3 100644 --- a/src/logic/define/databaseAccessObjects/__snapshots__/defineQuerySelectExpressionForSqlSchemaProperty.test.ts.snap +++ b/src/logic/define/databaseAccessObjects/__snapshots__/defineQuerySelectExpressionForSqlSchemaProperty.test.ts.snap @@ -1,5 +1,12 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`defineQuerySelectExpressionForSqlSchemaProperty reference: direct_by_declaration should define the select expression correctly for a solo DIRECT_BY_DECLARATION reference 1`] = ` +"( + SELECT carriage.uuid + FROM carriage WHERE carriage.id = carriage_cargo.carriage_id + ) AS carriage_uuid" +`; + exports[`defineQuerySelectExpressionForSqlSchemaProperty reference: direct_by_nesting should define the select expression correctly for a solo DIRECT_BY_NESTING reference 1`] = ` "( SELECT json_build_object( diff --git a/src/logic/define/databaseAccessObjects/defineDaoFindByMethodCodeForDomainObject.ts b/src/logic/define/databaseAccessObjects/defineDaoFindByMethodCodeForDomainObject.ts index 2d73128..e697370 100644 --- a/src/logic/define/databaseAccessObjects/defineDaoFindByMethodCodeForDomainObject.ts +++ b/src/logic/define/databaseAccessObjects/defineDaoFindByMethodCodeForDomainObject.ts @@ -368,7 +368,15 @@ export const sql = \` .map(({ domainObject: domainObjectProperty }) => !domainObjectProperty ? null - : `${sqlSchemaName}.${snakeCase(domainObjectProperty.name)}`, + : `${sqlSchemaName}.${ + isDomainObjectReferenceProperty(domainObjectProperty) && + domainObjectProperty.of.extends === + DomainObjectVariant.DOMAIN_ENTITY // if its a DIRECT_BY_DECLARATION reference, then replace the name; // todo: upgrade to selecting the full ref-by-unique instead and leverage that to stop renaming adhoc to _uuid + ? snakeCase(domainObjectProperty.name) + .replace(/_refs$/, '_uuids') + .replace(/_ref$/, '_uuid') + : snakeCase(domainObjectProperty.name) + }`, ) .filter(isPresent) .join(',\n ')} diff --git a/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.test.ts b/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.test.ts index 8366982..e74b7a0 100644 --- a/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.test.ts +++ b/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.test.ts @@ -281,4 +281,60 @@ describe('defineDaoUtilCastMethodCodeForDomainObject', () => { it.todo( 'should look correct when the same dobj is referenced more than once in a dobj', ); + it('should look correct for a domain entity which directly declares a reference to another', () => { + // define what we're testing on + const domainObject = new DomainObjectMetadata({ + name: 'CarriageCargo', + extends: DomainObjectVariant.DOMAIN_ENTITY, + properties: { + id: { + name: 'id', + type: DomainObjectPropertyType.NUMBER, + required: false, + }, + uuid: { + name: 'uuid', + type: DomainObjectPropertyType.STRING, + required: false, + }, + carriageRef: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + required: true, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + }, + decorations: { + alias: null, + primary: null, + unique: ['carriageRef'], + updatable: [], + }, + }); + const sqlSchemaRelationship = defineSqlSchemaRelationshipForDomainObject({ + domainObject, + allDomainObjects: [domainObject], + }); + + // run it + const code = defineDaoUtilCastMethodCodeForDomainObject({ + domainObject, + sqlSchemaRelationship, + }); + + // log an example + expect(code).toContain( + "import { CarriageCargo } from '$PATH_TO_DOMAIN_OBJECT'", + ); + expect(code).toContain( + "import { SqlQueryFindCarriageByIdOutput, SqlQueryFindCarriageCargoByIdOutput } from '$PATH_TO_GENERATED_SQL_TYPES';", // todo: should `SqlQueryFindCarriageByIdOutput` really be included? + ); + expect(code).toContain('dbObject: SqlQueryFindCarriageCargoByIdOutput'); + expect(code).toContain('new CarriageCargo({'); + expect(code).toContain('carriageRef: { uuid: dbObject.carriage_uuid }'); + expect(code).toMatchSnapshot(); + }); }); diff --git a/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.ts b/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.ts index 352a259..437a654 100644 --- a/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.ts +++ b/src/logic/define/databaseAccessObjects/defineDaoUtilCastMethodCodeForDomainObject.ts @@ -138,6 +138,31 @@ export const defineDaoUtilCastMethodCodeForDomainObject = ({ }FromDatabaseObject)`; } + // directly declared case + if ( + sqlSchemaProperty.reference.method === + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION + ) { + const nullabilityPrefix = sqlSchemaProperty.isNullable + ? `dbObject.${snakeCase( + domainObjectProperty.name, + )} === null ? null : ` + : ''; + + // solo reference case + if (!sqlSchemaProperty.isArray) + return `${ + domainObjectProperty.name + }: ${nullabilityPrefix}{ uuid: dbObject.${ + snakeCase(domainObjectProperty.name).replace(/_ref$/, '_uuid') // todo: get a ref-by-unique json object back, instead of just the uuid + } }`; + + // array reference case + return `${domainObjectProperty.name}: (dbObject.${ + snakeCase(domainObjectProperty.name).replace(/_refs$/, '_uuids') // todo: get a ref-by-unique json object back, instead of just the uuid + } as string[]).map(uuid => ({ uuid }))`; // as string array since we have an array of uuids - but the type defs generated from sql will complain that it could be string[] or number[] or null (not smart enough to look all the way through fn defs yet) + } + // handle unexpected case (each case should have been handled above) throw new UnexpectedCodePathDetectedError({ reason: diff --git a/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.test.ts b/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.test.ts index 37fb8f3..786b791 100644 --- a/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.test.ts +++ b/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.test.ts @@ -292,6 +292,65 @@ describe('defineQueryFunctionInputExpressionForDomainObjectProperty', () => { 'geocodeId: trainLocatedEvent.geocode === null ? null : trainLocatedEvent.geocode.id ? trainLocatedEvent.geocode.id : (await geocodeDao.upsert({ geocode: trainLocatedEvent.geocode }, context)).id', ); }); + it('should define the input expression correctly for a solo, nullable, DIRECT_BY_DECLARATION reference', () => { + const expression = + defineQueryFunctionInputExpressionForDomainObjectProperty({ + domainObjectName: 'CarriageCargo', + dobjInputVarName: 'carriageCargo', + sqlSchemaProperty: { + name: 'carriage_id', + isArray: false, + isNullable: true, + isUpdatable: false, + isDatabaseGenerated: false, + reference: { + method: SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + }, + domainObjectProperty: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + allSqlSchemaRelationships: [ + new SqlSchemaToDomainObjectRelationship({ + name: { domainObject: 'Carriage', sqlSchema: 'carriage' }, + properties: [ + { + domainObject: { + name: 'uuid', + type: DomainObjectPropertyType.STRING, + }, + sqlSchema: { + name: 'uuid', + isArray: false, + isNullable: false, + isUpdatable: false, + isDatabaseGenerated: true, + reference: null, + }, + }, + // todo: do we need other properties here? + ], + decorations: { + alias: { domainObject: null }, + unique: { sqlSchema: null, domainObject: null }, + }, + }), + ], + context: GetTypescriptCodeForPropertyContext.FOR_UPSERT_QUERY, + }); + expect(expression).toEqual( + `carriageUuid: carriageCargo.carriageRef === null ? null : isPrimaryKeyRef({ of: Carriage })(carriageCargo.carriageRef) ? carriageCargo.carriageRef.uuid : (await carriageDao.findByRef({ ref: carriageCargo.carriageRef }, context).expect('isPresent')).uuid`, + ); + }); it('should define the input expression correctly for an array of IMPLICIT_BY_UUID references', () => { const expression = defineQueryFunctionInputExpressionForDomainObjectProperty({ diff --git a/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.ts b/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.ts index 4614704..dd8d82c 100644 --- a/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.ts +++ b/src/logic/define/databaseAccessObjects/defineQueryFunctionInputExpressionForDomainObjectProperty.ts @@ -69,7 +69,7 @@ export const defineQueryFunctionInputExpressionForDomainObjectProperty = ({ return `${domainObjectProperty.name}`; } - // for direct references, we need to map to the "id" + // for direct nested references, we need to map to the "id" if ( sqlSchemaProperty.reference.method === SqlSchemaReferenceMethod.DIRECT_BY_NESTING @@ -122,6 +122,42 @@ export const defineQueryFunctionInputExpressionForDomainObjectProperty = ({ } } + // for direct declaration references, we need to map to the "uuid" + /** + * invoiceUuid: isPrimaryKeyRef({ of: ProviderAdsInvoice })(item.invoiceRef) + ? item.invoiceRef.uuid + : ( + await daoProviderAdsInvoice + .findByRef({ ref: item.invoiceRef }, context) + .expect('isPresent') + ).uuid, + */ + if ( + sqlSchemaProperty.reference.method === + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION + ) { + // handle solo reference + if (!sqlSchemaProperty.isArray) { + const domainObjectPropertyVariableName = + context === GetTypescriptCodeForPropertyContext.FOR_UPSERT_QUERY + ? `${dobjInputVarName}.${domainObjectProperty.name}` + : domainObjectProperty.name; + + const nullabilityPrefix = sqlSchemaProperty.isNullable + ? `${domainObjectPropertyVariableName} === null ? null : ` + : ''; + + return `${camelCase( + domainObjectProperty.name.replace(/Ref$/, 'Uuid'), + )}: ${nullabilityPrefix}isPrimaryKeyRef({ of: ${referencedDomainObjectName} })(${domainObjectPropertyVariableName}) ? ${domainObjectPropertyVariableName}.uuid : (await ${castDomainObjectNameToDaoName( + referencedDomainObjectName, + )}.findByRef({ ref: ${domainObjectPropertyVariableName} }, context).expect('isPresent')).uuid`; + } + + // handle array of references + if (sqlSchemaProperty.isArray) throw new Error('todo'); // todo: ran out of time allocated for this upgrade; lets fix when we have the usecase + } + // fail fast if we reach here, not expected throw new UnexpectedCodePathDetectedError({ reason: diff --git a/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.test.ts b/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.test.ts index 299092a..976f3c9 100644 --- a/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.test.ts +++ b/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.test.ts @@ -66,6 +66,45 @@ describe('defineQueryInputExpressionForSqlSchemaProperty', () => { '(SELECT id FROM train_engineer WHERE train_engineer.uuid = :leadEngineerUuid)', ); }); + it('should define the input expression correctly for a solo DIRECT_BY_DECLARATION reference', () => { + const expression = defineQueryInputExpressionForSqlSchemaProperty({ + sqlSchemaName: 'carriage_cargo', + sqlSchemaProperty: { + name: 'carriage_id', + isArray: false, + isNullable: false, + isUpdatable: false, + isDatabaseGenerated: false, + reference: { + method: SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + }, + domainObjectProperty: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + }, + allSqlSchemaRelationships: [ + new SqlSchemaToDomainObjectRelationship({ + name: { domainObject: 'Carriage', sqlSchema: 'carriage' }, + properties: [], + decorations: { + alias: { domainObject: null }, + unique: { + sqlSchema: null, + domainObject: null, + }, + }, + }), + ], // not needed for this one + }); + expect(expression).toEqual( + '(SELECT id FROM carriage WHERE carriage.uuid = :carriageUuid)', + ); + }); it('should define the input expression correctly for a solo DIRECT_BY_NESTING reference', () => { const expression = defineQueryInputExpressionForSqlSchemaProperty({ sqlSchemaName: 'train_located_event', diff --git a/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.ts b/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.ts index 4c51543..3fe3312 100644 --- a/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.ts +++ b/src/logic/define/databaseAccessObjects/defineQueryInputExpressionForSqlSchemaProperty.ts @@ -50,6 +50,17 @@ export const defineQueryInputExpressionForSqlSchemaProperty = ({ ) return `(SELECT id FROM ${referencedSqlSchemaName} WHERE ${referencedSqlSchemaName}.uuid = :${domainObjectProperty.name})`; + // if its a solo direct declaration reference, then lookup the id by uuid as the input expression + if ( + sqlSchemaProperty.reference.method === + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION + ) + // todo: prefer refByUnique instead + return `(SELECT id FROM ${referencedSqlSchemaName} WHERE ${referencedSqlSchemaName}.uuid = :${domainObjectProperty.name.replace( + /Ref$/, + 'Uuid', + )})`; + // if its a nested reference, then it will have the id on it already if ( sqlSchemaProperty.reference.method === @@ -60,7 +71,7 @@ export const defineQueryInputExpressionForSqlSchemaProperty = ({ // handle array of references if (sqlSchemaProperty.isArray) { - // if its a solo implicit uuid reference, then lookup the ids by uuids as the input expression + // if its an implicit uuid reference or direct declaration reference, then lookup the ids by uuids as the input expression if ( sqlSchemaProperty.reference.method === SqlSchemaReferenceMethod.IMPLICIT_BY_UUID @@ -74,8 +85,26 @@ export const defineQueryInputExpressionForSqlSchemaProperty = ({ ON ${referencedSqlSchemaName}.uuid = ${referencedSqlSchemaName}_ref.uuid ) `.trim(); + // if its an implicit uuid reference or direct declaration reference, then lookup the ids by uuids as the input expression + if ( + sqlSchemaProperty.reference.method === + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION + ) + // todo: prefer refByUnique instead + return ` + ( + SELECT COALESCE(array_agg(${referencedSqlSchemaName}.id ORDER BY ${referencedSqlSchemaName}_ref.array_order_index), array[]::bigint[]) AS array_agg + FROM ${referencedSqlSchemaName} + JOIN unnest(:${domainObjectProperty.name.replace( + /Refs$/, + 'Uuids', + )}::uuid[]) WITH ORDINALITY + AS ${referencedSqlSchemaName}_ref (uuid, array_order_index) + ON ${referencedSqlSchemaName}.uuid = ${referencedSqlSchemaName}_ref.uuid + ) + `.trim(); - // if its a nested reference, then it will be an array of ids arlready + // if its a nested reference, then it will be an array of ids already if ( sqlSchemaProperty.reference.method === SqlSchemaReferenceMethod.DIRECT_BY_NESTING diff --git a/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.test.ts b/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.test.ts index 8c2b9de..cfd65a3 100644 --- a/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.test.ts +++ b/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.test.ts @@ -889,4 +889,51 @@ describe('defineQuerySelectExpressionForSqlSchemaProperty', () => { expect(expression).toMatchSnapshot(); }); }); + + describe('reference: direct_by_declaration', () => { + it('should define the select expression correctly for a solo DIRECT_BY_DECLARATION reference', () => { + const expression = defineQuerySelectExpressionForSqlSchemaProperty({ + sqlSchemaName: 'carriage_cargo', + sqlSchemaProperty: { + name: 'carriage_id', + isArray: false, + isNullable: false, + isUpdatable: false, + isDatabaseGenerated: false, + reference: { + method: SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + }, + domainObjectProperty: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + }, + allSqlSchemaRelationships: [ + new SqlSchemaToDomainObjectRelationship({ + name: { domainObject: 'Carriage', sqlSchema: 'carriage' }, + properties: [], + decorations: { + alias: { domainObject: null }, + unique: { + sqlSchema: null, + domainObject: null, + }, + }, + }), + ], // not needed for this one + }); + console.log(expression); + expect(expression).toContain('SELECT carriage.uuid'); // should select the uuid + expect(expression).toContain('FROM carriage'); // from the right table + expect(expression).toContain( + 'WHERE carriage.id = carriage_cargo.carriage_id', + ); // filtered on the right id + expect(expression).toContain('AS carriage_uuid'); // with the correct output name + expect(expression).toMatchSnapshot(); + }); + }); }); diff --git a/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.ts b/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.ts index 855d2e1..b52f68d 100644 --- a/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.ts +++ b/src/logic/define/databaseAccessObjects/defineQuerySelectExpressionForSqlSchemaProperty.ts @@ -58,8 +58,8 @@ export const defineQuerySelectExpressionForSqlSchemaProperty = ({ referencedSqlSchemaRelationship.name.sqlSchema; const referencedSqlSchemaHasCurrentView = referencedSqlSchemaRelationship.properties.some( - ({ sqlSchema: sqlSchemaProperty }) => - sqlSchemaProperty.isUpdatable || sqlSchemaProperty.isArray, + ({ sqlSchema: thisSqlSchemaProperty }) => + thisSqlSchemaProperty.isUpdatable || thisSqlSchemaProperty.isArray, ); const fromSqlSchemaExpression = referencedSqlSchemaHasCurrentView ? `view_${referencedSqlSchemaName}_current AS ${referencedSqlSchemaName}` // todo: make the `current` view take the default namespace, so that we dont have to do this check anymore; e.g., the static table should always have `_static` suffix and the view should always be the interface, for backwards compat and evolvability @@ -93,6 +93,38 @@ export const defineQuerySelectExpressionForSqlSchemaProperty = ({ `.trim(); } + // directly declared reference + if ( + sqlSchemaProperty.reference.method === + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION + ) { + // solo case; + if (!sqlSchemaProperty.isArray) + return ` + ( + SELECT ${referencedSqlSchemaName}.uuid + FROM ${fromSqlSchemaExpression} WHERE ${referencedSqlSchemaName}.id = ${sqlSchemaName}.${ + sqlSchemaProperty.name + } + )${ + selectExpressionAlias.replace(/_ref$/, '_uuid') // todo: return a json object of the ref, preferably in ref-by-unique shape + } + `.trim(); + + // array case + return ` + ( + SELECT COALESCE(array_agg(${referencedSqlSchemaName}.uuid ORDER BY ${referencedSqlSchemaName}_ref.array_order_index), array[]::uuid[]) AS array_agg + FROM ${fromSqlSchemaExpression} + JOIN unnest(${sqlSchemaName}.${sqlSchemaProperty.name}) WITH ORDINALITY + AS ${referencedSqlSchemaName}_ref (id, array_order_index) + ON ${referencedSqlSchemaName}.id = ${referencedSqlSchemaName}_ref.id + )${ + selectExpressionAlias.replace(/_refs$/, '_uuids') // todo: return a json object of the ref, preferably in ref-by-unique shape + } + `.trim(); + } + // directly nested reference case if ( sqlSchemaProperty.reference.method === diff --git a/src/logic/define/sqlSchemaControl/__snapshots__/defineDependentReferenceAvailableProvisionOrder.test.ts.snap b/src/logic/define/sqlSchemaControl/__snapshots__/defineDependentReferenceAvailableProvisionOrder.test.ts.snap index 21211ad..93177c7 100644 --- a/src/logic/define/sqlSchemaControl/__snapshots__/defineDependentReferenceAvailableProvisionOrder.test.ts.snap +++ b/src/logic/define/sqlSchemaControl/__snapshots__/defineDependentReferenceAvailableProvisionOrder.test.ts.snap @@ -5,6 +5,7 @@ exports[`defineDependentReferenceAvailableProvisionOrder should work on the exam "depth": 3, "order": [ "Carriage", + "CarriageCargo", "Certificate", "Geocode", "Locomotive", @@ -23,6 +24,9 @@ exports[`defineDependentReferenceAvailableProvisionOrder should work on the exam "TrainLocatedEvent", ], "Carriage": [], + "CarriageCargo": [ + "Carriage", + ], "Certificate": [], "Geocode": [], "Invoice": [ diff --git a/src/logic/define/sqlSchemaControl/__snapshots__/defineSqlSchemaControlCodeFilesForDomainObjects.integration.test.ts.snap b/src/logic/define/sqlSchemaControl/__snapshots__/defineSqlSchemaControlCodeFilesForDomainObjects.integration.test.ts.snap index b5e2530..0cd8a27 100644 --- a/src/logic/define/sqlSchemaControl/__snapshots__/defineSqlSchemaControlCodeFilesForDomainObjects.integration.test.ts.snap +++ b/src/logic/define/sqlSchemaControl/__snapshots__/defineSqlSchemaControlCodeFilesForDomainObjects.integration.test.ts.snap @@ -10,6 +10,20 @@ GeneratedCodeFile { - type: resource path: ./functions/upsert_carriage.sql +# carriage_cargo +- type: resource + path: ./tables/carriage_cargo.sql +- type: resource + path: ./tables/carriage_cargo_version.sql +- type: resource + path: ./tables/carriage_cargo_cvp.sql +- type: resource + path: ./views/view_carriage_cargo_current.sql +- type: resource + path: ./views/view_carriage_cargo_hydrated.sql +- type: resource + path: ./functions/upsert_carriage_cargo.sql + # certificate - type: resource path: ./tables/certificate.sql diff --git a/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeFilesForDomainObjects.integration.test.ts.snap b/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeFilesForDomainObjects.integration.test.ts.snap index f9ed0ad..a6bc167 100644 --- a/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeFilesForDomainObjects.integration.test.ts.snap +++ b/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeFilesForDomainObjects.integration.test.ts.snap @@ -5,6 +5,7 @@ exports[`defineSqlSchemaGeneratorCodeFilesForDomainObjects should work on the ex GeneratedCodeFile { "content": "import { asyncTaskPredictStationCongestion } from './asyncTaskPredictStationCongestion'; import { carriage } from './carriage'; +import { carriageCargo } from './carriageCargo'; import { certificate } from './certificate'; import { geocode } from './geocode'; import { invoice } from './invoice'; @@ -22,6 +23,7 @@ import { trainStation } from './trainStation'; export const generateSqlSchemasFor = [ asyncTaskPredictStationCongestion, carriage, + carriageCargo, certificate, trainEngineer, geocode, @@ -72,6 +74,26 @@ export const carriage: Entity = new Entity({ });", "relpath": "carriage.ts", }, + GeneratedCodeFile { + "content": "import { Entity, prop } from 'sql-schema-generator'; + +import { carriage } from './carriage'; + +/** + * sql-schema for the domain entity 'CarriageCargo' + */ +export const carriageCargo: Entity = new Entity({ + name: 'carriage_cargo', + properties: { + itinerary_uuid: prop.UUID(), + carriage_id: prop.REFERENCES(carriage), + slot: prop.NUMERIC(), + cargo_exid: { ...prop.VARCHAR(), updatable: true, nullable: true }, + }, + unique: ['itinerary_uuid', 'carriage_id', 'slot'], +});", + "relpath": "carriageCargo.ts", + }, GeneratedCodeFile { "content": "import { Literal, prop } from 'sql-schema-generator'; @@ -288,6 +310,21 @@ export const invoice: Entity = new Entity({ FROM carriage;", "relpath": "../sql/views/view_carriage_hydrated.sql", }, + GeneratedCodeFile { + "content": "CREATE OR REPLACE VIEW view_carriage_cargo_hydrated AS + SELECT + carriage_cargo.id, + carriage_cargo.uuid, + carriage_cargo.itinerary_uuid, + ( + SELECT carriage.uuid + FROM carriage WHERE carriage.id = carriage_cargo.carriage_id + ) AS carriage_uuid, + carriage_cargo.slot, + carriage_cargo.cargo_exid + FROM view_carriage_cargo_current carriage_cargo;", + "relpath": "../sql/views/view_carriage_cargo_hydrated.sql", + }, GeneratedCodeFile { "content": "CREATE OR REPLACE VIEW view_certificate_hydrated AS SELECT diff --git a/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeForDomainObject.test.ts.snap b/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeForDomainObject.test.ts.snap index d98e505..d4591c4 100644 --- a/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeForDomainObject.test.ts.snap +++ b/src/logic/define/sqlSchemaGenerator/__snapshots__/defineSqlSchemaGeneratorCodeForDomainObject.test.ts.snap @@ -1,5 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`defineSqlSchemaGeneratorCodeForDomainObject domain object references should create a correct looking sql-schema-generator object for a domain-entity which references another domain-entity via Ref 1`] = ` +"import { Entity, prop } from 'sql-schema-generator'; + +import { carriage } from './carriage'; + +/** + * sql-schema for the domain entity 'CarriageCargo' + */ +export const carriageCargo: Entity = new Entity({ + name: 'carriage_cargo', + properties: { + carriage_id: prop.REFERENCES(carriage), + }, + unique: ['carriage_id'], +});" +`; + exports[`defineSqlSchemaGeneratorCodeForDomainObject domain object variants should create a correct looking sql-schema-generator object for a domain-entity 1`] = ` "import { Entity, prop } from 'sql-schema-generator'; diff --git a/src/logic/define/sqlSchemaGenerator/defineSqlSchemaGeneratorCodeForDomainObject.test.ts b/src/logic/define/sqlSchemaGenerator/defineSqlSchemaGeneratorCodeForDomainObject.test.ts index a35af8f..ac7a93d 100644 --- a/src/logic/define/sqlSchemaGenerator/defineSqlSchemaGeneratorCodeForDomainObject.test.ts +++ b/src/logic/define/sqlSchemaGenerator/defineSqlSchemaGeneratorCodeForDomainObject.test.ts @@ -1,3 +1,4 @@ +import { UnexpectedCodePathError } from '@ehmpathy/error-fns'; import { DomainObjectMetadata, DomainObjectPropertyType, @@ -7,6 +8,45 @@ import { import { defineSqlSchemaRelationshipForDomainObject } from '../sqlSchemaRelationship/defineSqlSchemaRelationshipForDomainObject'; import { defineSqlSchemaGeneratorCodeForDomainObject } from './defineSqlSchemaGeneratorCodeForDomainObject'; +const exampleCarriageDomainEntityMetadata = new DomainObjectMetadata({ + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + properties: { + id: { + name: 'id', + type: DomainObjectPropertyType.NUMBER, + required: false, + }, + uuid: { + name: 'uuid', + type: DomainObjectPropertyType.STRING, + required: false, + }, + cin: { + name: 'cin', + type: DomainObjectPropertyType.STRING, + required: true, + }, + carries: { + name: 'carries', + type: DomainObjectPropertyType.ENUM, + of: ['PASSENGER', 'FREIGHT'], + required: true, + }, + capacity: { + name: 'capacity', + type: DomainObjectPropertyType.NUMBER, + nullable: true, + }, + }, + decorations: { + alias: null, + primary: null, + unique: ['cin'], + updatable: [], + }, +}); + describe('defineSqlSchemaGeneratorCodeForDomainObject', () => { describe('imports', () => { it('should not have extra new lines if there are no imports', () => { @@ -213,44 +253,7 @@ describe('defineSqlSchemaGeneratorCodeForDomainObject', () => { it('should create a correct looking sql-schema-generator object for a domain-entity', () => { // define what we're testing on - const domainObject = new DomainObjectMetadata({ - name: 'Carriage', - extends: DomainObjectVariant.DOMAIN_ENTITY, - properties: { - id: { - name: 'id', - type: DomainObjectPropertyType.NUMBER, - required: false, - }, - uuid: { - name: 'uuid', - type: DomainObjectPropertyType.STRING, - required: false, - }, - cin: { - name: 'cin', - type: DomainObjectPropertyType.STRING, - required: true, - }, - carries: { - name: 'carries', - type: DomainObjectPropertyType.ENUM, - of: ['PASSENGER', 'FREIGHT'], - required: true, - }, - capacity: { - name: 'capacity', - type: DomainObjectPropertyType.NUMBER, - nullable: true, - }, - }, - decorations: { - alias: null, - primary: null, - unique: ['cin'], - updatable: [], - }, - }); + const domainObject = exampleCarriageDomainEntityMetadata; const sqlSchemaRelationship = defineSqlSchemaRelationshipForDomainObject({ domainObject, allDomainObjects: [domainObject], @@ -343,4 +346,62 @@ describe('defineSqlSchemaGeneratorCodeForDomainObject', () => { expect(code).toMatchSnapshot(); }); }); + + describe('domain object references', () => { + it('should create a correct looking sql-schema-generator object for a domain-entity which references another domain-entity via Ref', () => { + // define what we're testing on + const domainObject = new DomainObjectMetadata({ + name: 'CarriageCargo', + extends: DomainObjectVariant.DOMAIN_ENTITY, + properties: { + id: { + name: 'id', + type: DomainObjectPropertyType.NUMBER, + required: false, + }, + uuid: { + name: 'uuid', + type: DomainObjectPropertyType.STRING, + required: false, + }, + carriageRef: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + required: true, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + }, + decorations: { + alias: null, + primary: null, + unique: ['carriageRef'], + updatable: [], + }, + }); + const sqlSchemaRelationship = defineSqlSchemaRelationshipForDomainObject({ + domainObject, + allDomainObjects: [domainObject], + // allDomainObjects: [domainObject, exampleCarriageDomainEntityMetadata], + }); + + // run it + const code = defineSqlSchemaGeneratorCodeForDomainObject({ + domainObject, + sqlSchemaRelationship, + }); + expect(code).toContain( + "import { Entity, prop } from 'sql-schema-generator'", + ); + expect(code).toContain( + 'export const carriageCargo: Entity = new Entity({', + ); + expect(code).toContain("name: 'carriage_cargo'"); + expect(code).toContain('carriage_id: prop.REFERENCES(carriage),'); + expect(code).toContain("unique: ['carriage_id'],"); + expect(code).toMatchSnapshot(); + }); + }); }); diff --git a/src/logic/define/sqlSchemaRelationship/__snapshots__/defineSqlSchemaRelationshipsForDomainObject.integration.test.ts.snap b/src/logic/define/sqlSchemaRelationship/__snapshots__/defineSqlSchemaRelationshipsForDomainObject.integration.test.ts.snap index e85f71c..a307649 100644 --- a/src/logic/define/sqlSchemaRelationship/__snapshots__/defineSqlSchemaRelationshipsForDomainObject.integration.test.ts.snap +++ b/src/logic/define/sqlSchemaRelationship/__snapshots__/defineSqlSchemaRelationshipsForDomainObject.integration.test.ts.snap @@ -285,6 +285,170 @@ exports[`defineSqlSchemaRelationshipsForDomainObjects should work on the example }, ], }, + SqlSchemaToDomainObjectRelationship { + "decorations": { + "alias": { + "domainObject": null, + }, + "unique": { + "domainObject": [ + "itineraryUuid", + "carriageRef", + "slot", + ], + "sqlSchema": [ + "itinerary_uuid", + "carriage_id", + "slot", + ], + }, + }, + "name": { + "domainObject": "CarriageCargo", + "sqlSchema": "carriage_cargo", + }, + "properties": [ + { + "domainObject": DomainObjectPropertyMetadata { + "name": "id", + "nullable": false, + "required": false, + "type": "NUMBER", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": true, + "isNullable": false, + "isUpdatable": false, + "name": "id", + "reference": null, + }, + }, + { + "domainObject": DomainObjectPropertyMetadata { + "name": "uuid", + "nullable": false, + "required": false, + "type": "STRING", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": true, + "isNullable": false, + "isUpdatable": false, + "name": "uuid", + "reference": null, + }, + }, + { + "domainObject": null, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": true, + "isNullable": false, + "isUpdatable": false, + "name": "created_at", + "reference": null, + }, + }, + { + "domainObject": null, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": true, + "isNullable": false, + "isUpdatable": false, + "name": "effective_at", + "reference": null, + }, + }, + { + "domainObject": null, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": true, + "isNullable": false, + "isUpdatable": false, + "name": "updated_at", + "reference": null, + }, + }, + { + "domainObject": DomainObjectPropertyMetadata { + "name": "itineraryUuid", + "nullable": false, + "required": true, + "type": "STRING", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": false, + "isNullable": false, + "isUpdatable": false, + "name": "itinerary_uuid", + "reference": null, + }, + }, + { + "domainObject": DomainObjectPropertyMetadata { + "name": "carriageRef", + "nullable": false, + "of": DomainObjectReferenceMetadata { + "extends": "DomainEntity", + "name": "Carriage", + }, + "required": true, + "type": "REFERENCE", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": false, + "isNullable": false, + "isUpdatable": false, + "name": "carriage_id", + "reference": SqlSchemaReferenceMetadata { + "method": "DIRECT_BY_DECLARATION", + "of": DomainObjectReferenceMetadata { + "extends": "DomainEntity", + "name": "Carriage", + }, + }, + }, + }, + { + "domainObject": DomainObjectPropertyMetadata { + "name": "slot", + "nullable": false, + "required": true, + "type": "NUMBER", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": false, + "isNullable": false, + "isUpdatable": false, + "name": "slot", + "reference": null, + }, + }, + { + "domainObject": DomainObjectPropertyMetadata { + "name": "cargoExid", + "nullable": true, + "required": true, + "type": "STRING", + }, + "sqlSchema": SqlSchemaPropertyMetadata { + "isArray": false, + "isDatabaseGenerated": false, + "isNullable": true, + "isUpdatable": true, + "name": "cargo_exid", + "reference": null, + }, + }, + ], + }, SqlSchemaToDomainObjectRelationship { "decorations": { "alias": { diff --git a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.test.ts b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.test.ts index 72c08a4..d0fb28e 100644 --- a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.test.ts +++ b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.test.ts @@ -3,6 +3,7 @@ import { DomainObjectVariant, } from 'domain-objects-metadata'; +import { SqlSchemaReferenceMethod } from '../../../domain'; import { createExampleDomainObjectMetadata } from '../../__test_assets__/createExampleDomainObject'; import { defineSqlSchemaPropertyForDomainObjectProperty } from './defineSqlSchemaPropertyForDomainObjectProperty'; @@ -53,6 +54,21 @@ describe('defineSqlSchemaPropertyForDomainObjectProperty', () => { }); expect(sqlSchemaProperty.name).toEqual('steam_engine_certificate_id'); }); + it('should add the _id suffix to directly declared reference property names', () => { + const sqlSchemaProperty = defineSqlSchemaPropertyForDomainObjectProperty({ + property: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + domainObject: createExampleDomainObjectMetadata(), + allDomainObjects: [], + }); + expect(sqlSchemaProperty.name).toEqual('carriage_id'); + }); it('should add the _ids suffix and strip plurality to directly nested reference array property names', () => { const sqlSchemaProperty = defineSqlSchemaPropertyForDomainObjectProperty({ property: { @@ -138,6 +154,26 @@ describe('defineSqlSchemaPropertyForDomainObjectProperty', () => { expect(sqlSchemaProperty.reference).toBeDefined(); expect(sqlSchemaProperty.reference!.of.name).toEqual('Certificate'); }); + it('should define the reference correctly when there is a to a DomainEntity via Ref', () => { + const sqlSchemaProperty = defineSqlSchemaPropertyForDomainObjectProperty({ + property: { + name: 'carriageRef', + type: DomainObjectPropertyType.REFERENCE, + of: { + name: 'Carriage', + extends: DomainObjectVariant.DOMAIN_ENTITY, + }, + }, + domainObject: createExampleDomainObjectMetadata(), + allDomainObjects: [], + }); + expect(sqlSchemaProperty.reference).toBeDefined(); + expect(sqlSchemaProperty.name).toEqual('carriage_id'); + expect(sqlSchemaProperty.reference!.of.name).toEqual('Carriage'); + expect(sqlSchemaProperty.reference!.method).toEqual( + SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION, + ); + }); }); describe('modifiers', () => { it('should detect nullable, when nullable', () => { diff --git a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.ts b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.ts index 2fb6392..df164d9 100644 --- a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.ts +++ b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaPropertyForDomainObjectProperty.ts @@ -64,11 +64,15 @@ export const defineSqlSchemaPropertyForDomainObjectProperty = ({ if (isArray) { if (reference.method === SqlSchemaReferenceMethod.DIRECT_BY_NESTING) return `${baseSchemaPropertyName.replace(/s$/, '')}_ids`; // suffix w/ ids, since its an array of fks + if (reference.method === SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION) + return `${baseSchemaPropertyName.replace(/_refs$/, '')}_ids`; // suffix w/ ids, since its an array of fks if (reference.method === SqlSchemaReferenceMethod.IMPLICIT_BY_UUID) return `${baseSchemaPropertyName.replace(/_uuids$/, '')}_ids`; // suffix w/ ids, since its an array of fks } if (reference.method === SqlSchemaReferenceMethod.DIRECT_BY_NESTING) return `${baseSchemaPropertyName}_id`; // suffix w/ id, since its a fk + if (reference.method === SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION) + return `${baseSchemaPropertyName.replace(/_ref$/, '')}_id`; // suffix w/ id, since its a fk if (reference.method === SqlSchemaReferenceMethod.IMPLICIT_BY_UUID) return `${baseSchemaPropertyName.replace(/_uuid$/, '')}_id`; // suffix w/ id, since its a fk } diff --git a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaReferenceForDomainObjectProperty.ts b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaReferenceForDomainObjectProperty.ts index 235f973..06a1f21 100644 --- a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaReferenceForDomainObjectProperty.ts +++ b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaReferenceForDomainObjectProperty.ts @@ -1,5 +1,4 @@ // tslint:disable: max-classes-per-file -import { isPropertyNameAReferenceIntuitively } from 'domain-objects'; import { DomainObjectMetadata, DomainObjectPropertyMetadata, @@ -70,9 +69,9 @@ export class DirectlyNestedNonDomainObjectReferenceForbiddenError extends Error ` ${referencedDomainObject.extends} found directly nested inside of another domain object. '${domainObject.name}.${property.name}' reference '${referencedDomainObject.name}' -This is not allowed, as this is bad practice when persisting domain-objects due to maintainability problems with this pattern in backend code. See the readme for more details. +This is not allowed, as this is bad practice when persisting domain-objects due to maintainability problems with this pattern. See the readme for more details. -Instead, reference the entity by 'uuid' in the backend. For example: +Instead, reference the entity by 'ref' and ensure that the name ends with the 'Ref' suffix. For example: \`\`\`ts -- say you're referencing this domain-entity @@ -86,12 +85,12 @@ interface Profile { -- do this interface Profile { - userUuid: string; -- suggested: reference it by uuid + userRef: Ref; -- suggested: reference it by ref ... } \`\`\` -Note: the generated sql-schema will be the same as if it was a nested reference, but the domain-object referencing by uuid will make your backend code easier to maintain. +Note: the generated sql-schema will be the same as if it was a nested reference, but the domain-object referencing by ref will make your code easier to maintain. `.trim(), ); } @@ -107,11 +106,13 @@ export const defineSqlSchemaReferenceForDomainObjectProperty = ({ allDomainObjects: DomainObjectMetadata[]; }): SqlSchemaReferenceMetadata | null => { // determine what kind of reference it can be - const isDirectNestedReferenceCandidate = - isDomainObjectReferenceProperty(property); - const isDirectNestedReferenceArrayCandidate = + const isDirectReferenceCandidate = isDomainObjectReferenceProperty(property); + const isDirectReferenceArrayCandidate = isDomainObjectArrayProperty(property) && isDomainObjectReferenceProperty(property.of); + const isDirectDeclarationReferenceCandidate = + property.type === DomainObjectPropertyType.REFERENCE && + new RegExp(/Ref$/).test(property.name); const isImplicitUuidReferenceCandidate = property.type === DomainObjectPropertyType.STRING && new RegExp(/Uuid/).test(property.name); @@ -121,15 +122,12 @@ export const defineSqlSchemaReferenceForDomainObjectProperty = ({ new RegExp(/Uuids/).test(property.name); // handle direct nested references - if ( - isDirectNestedReferenceCandidate || - isDirectNestedReferenceArrayCandidate - ) { + if (isDirectReferenceCandidate || isDirectReferenceArrayCandidate) { // grab the referenced object const referencedDomainObject = (() => { - if (isDirectNestedReferenceCandidate) + if (isDirectReferenceCandidate) return property.of as DomainObjectReferenceMetadata; - if (isDirectNestedReferenceArrayCandidate) + if (isDirectReferenceArrayCandidate) return (property.of as DomainObjectPropertyMetadata) .of as DomainObjectReferenceMetadata; throw new UnexpectedCodePathDetectedError({ @@ -162,17 +160,22 @@ export const defineSqlSchemaReferenceForDomainObjectProperty = ({ }, }); - // check that the domain object referenced by direct nesting is not a domain entity or a domain event - if (referencedDomainObject.extends !== DomainObjectVariant.DOMAIN_LITERAL) + // if its a domain literal, then we allow a directly nested reference + if (referencedDomainObject.extends === DomainObjectVariant.DOMAIN_LITERAL) + return new SqlSchemaReferenceMetadata({ + method: SqlSchemaReferenceMethod.DIRECT_BY_NESTING, + of: new DomainObjectReferenceMetadata(referencedDomainObject), + }); + + // otherwise, it must be a directly referenced reference and the name should represent that too // todo: trace that the dobj was defined with `Ref<>` explicitly; today, the metadata calls both `: Dobj` and `: Ref` as a "REFERENCE" + if (!isDirectDeclarationReferenceCandidate) throw new DirectlyNestedNonDomainObjectReferenceForbiddenError({ domainObject, property, referencedDomainObject, }); - - // if the above passes, then we're good to move forward return new SqlSchemaReferenceMetadata({ - method: SqlSchemaReferenceMethod.DIRECT_BY_NESTING, + method: SqlSchemaReferenceMethod.DIRECT_BY_DECLARATION, of: new DomainObjectReferenceMetadata(referencedDomainObject), }); } @@ -191,7 +194,8 @@ export const defineSqlSchemaReferenceForDomainObjectProperty = ({ allDomainObjectNames: allDomainObjects.map(({ name }) => name), }); const foundReferencedDomainObjectMetadata = allDomainObjects.find( - (domainObject) => domainObject.name === foundReferencedDomainObjectName, + (thisDomainObject) => + thisDomainObject.name === foundReferencedDomainObjectName, ); if (!foundReferencedDomainObjectMetadata) return null; if ( diff --git a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaRelationshipForDomainObject.ts b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaRelationshipForDomainObject.ts index fa4ea07..5df866a 100644 --- a/src/logic/define/sqlSchemaRelationship/defineSqlSchemaRelationshipForDomainObject.ts +++ b/src/logic/define/sqlSchemaRelationship/defineSqlSchemaRelationshipForDomainObject.ts @@ -19,7 +19,7 @@ export const defineSqlSchemaRelationshipForDomainObject = ({ }: { domainObject: DomainObjectMetadata; allDomainObjects: DomainObjectMetadata[]; -}) => { +}): SqlSchemaToDomainObjectRelationship => { // figure out some relevant info const sqlSchemaName = snakeCase(domainObject.name); // names are in snake case