From 029bf431dd4c6692eede6fd66bf55e0f3483756b Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 21 Sep 2024 02:27:08 -0300 Subject: [PATCH] fabric-core: Improve model definitions --- .../core/src/domain/models/create-model.ts | 32 ----- .../field-to-type.spec.ts} | 0 .../src/domain/models/fields/field-to-type.ts | 18 ++- .../core/src/domain/models/fields/index.ts | 10 +- .../core/src/domain/models/fields/integer.ts | 20 ++++ .../models/fields/reference-field.spec.ts | 113 ++++++++++++++++++ .../domain/models/fields/reference-field.ts | 68 +++++++++++ .../src/domain/models/fields/uuid-field.ts | 12 +- .../fabric/core/src/domain/models/index.ts | 6 +- .../core/src/domain/models/model-schema.ts | 3 + .../core/src/domain/models/model-to-type.ts | 6 - .../core/src/domain/models/model.spec.ts | 25 ++++ .../fabric/core/src/domain/models/model.ts | 23 ++++ .../core/src/domain/models/relations/index.ts | 5 - .../domain/models/relations/one-to-many.ts | 34 ------ .../src/domain/models/relations/one-to-one.ts | 33 ----- .../core/src/domain/models/types/index.ts | 2 + .../domain/models/types/model-field-names.ts | 3 + .../src/domain/models/types/model-to-type.ts | 6 + .../core/src/storage/query/query-builder.ts | 14 +-- .../fabric/core/src/storage/state-store.ts | 18 +-- .../fabric/core/src/storage/storage-driver.ts | 4 +- 22 files changed, 311 insertions(+), 144 deletions(-) delete mode 100644 packages/fabric/core/src/domain/models/create-model.ts rename packages/fabric/core/src/domain/models/{relations/many-to-many.ts => fields/field-to-type.spec.ts} (100%) create mode 100644 packages/fabric/core/src/domain/models/fields/integer.ts create mode 100644 packages/fabric/core/src/domain/models/fields/reference-field.spec.ts create mode 100644 packages/fabric/core/src/domain/models/fields/reference-field.ts create mode 100644 packages/fabric/core/src/domain/models/model-schema.ts delete mode 100644 packages/fabric/core/src/domain/models/model-to-type.ts create mode 100644 packages/fabric/core/src/domain/models/model.spec.ts create mode 100644 packages/fabric/core/src/domain/models/model.ts delete mode 100644 packages/fabric/core/src/domain/models/relations/index.ts delete mode 100644 packages/fabric/core/src/domain/models/relations/one-to-many.ts delete mode 100644 packages/fabric/core/src/domain/models/relations/one-to-one.ts create mode 100644 packages/fabric/core/src/domain/models/types/index.ts create mode 100644 packages/fabric/core/src/domain/models/types/model-field-names.ts create mode 100644 packages/fabric/core/src/domain/models/types/model-to-type.ts diff --git a/packages/fabric/core/src/domain/models/create-model.ts b/packages/fabric/core/src/domain/models/create-model.ts deleted file mode 100644 index 1b22e5d..0000000 --- a/packages/fabric/core/src/domain/models/create-model.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FieldDefinition } from "./fields/index.js"; - -export interface ModelDefinition< - TName extends string = string, - TFields extends Record = Record< - string, - FieldDefinition - >, -> { - name: TName; - fields: TFields; -} - -export type ModelName< - TModel extends ModelDefinition>, -> = TModel["name"]; - -export type ModelFromName< - TModels extends ModelDefinition>, - TName extends ModelName, -> = Extract; - -export type ModelFieldNames< - TModel extends ModelDefinition>, -> = keyof TModel["fields"]; - -export function createModel< - TName extends string, - TFields extends Record, ->(opts: ModelDefinition): ModelDefinition { - return opts; -} diff --git a/packages/fabric/core/src/domain/models/relations/many-to-many.ts b/packages/fabric/core/src/domain/models/fields/field-to-type.spec.ts similarity index 100% rename from packages/fabric/core/src/domain/models/relations/many-to-many.ts rename to packages/fabric/core/src/domain/models/fields/field-to-type.spec.ts diff --git a/packages/fabric/core/src/domain/models/fields/field-to-type.ts b/packages/fabric/core/src/domain/models/fields/field-to-type.ts index 0a7eed5..bbe974e 100644 --- a/packages/fabric/core/src/domain/models/fields/field-to-type.ts +++ b/packages/fabric/core/src/domain/models/fields/field-to-type.ts @@ -1,9 +1,21 @@ import { UUID } from "../../types/uuid.js"; +import { IntegerField } from "./integer.js"; import { StringField } from "./string-field.js"; import { UUIDField } from "./uuid-field.js"; +/** + * Converts a field definition to its corresponding TypeScript type. + */ export type FieldToType = TField extends StringField - ? string + ? ToOptional : TField extends UUIDField - ? UUID - : never; + ? ToOptional + : TField extends IntegerField + ? TField["hasArbitraryPrecision"] extends true + ? ToOptional + : ToOptional + : never; + +type ToOptional = TField extends { isOptional: true } + ? TType | null + : TType; diff --git a/packages/fabric/core/src/domain/models/fields/index.ts b/packages/fabric/core/src/domain/models/fields/index.ts index a1833fd..acdfdc2 100644 --- a/packages/fabric/core/src/domain/models/fields/index.ts +++ b/packages/fabric/core/src/domain/models/fields/index.ts @@ -1,10 +1,18 @@ +import { createIntegerField, IntegerField } from "./integer.js"; +import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createStringField, StringField } from "./string-field.js"; import { createUUIDField, UUIDField } from "./uuid-field.js"; export * from "./base-field.js"; -export type FieldDefinition = StringField | UUIDField; +export type FieldDefinition = + | StringField + | UUIDField + | IntegerField + | ReferenceField; export namespace Field { export const string = createStringField; export const uuid = createUUIDField; + export const integer = createIntegerField; + export const reference = createReferenceField; } diff --git a/packages/fabric/core/src/domain/models/fields/integer.ts b/packages/fabric/core/src/domain/models/fields/integer.ts new file mode 100644 index 0000000..abfef7a --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/integer.ts @@ -0,0 +1,20 @@ +import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { BaseField } from "./base-field.js"; + +export interface IntegerFieldOptions extends BaseField { + isUnsigned?: boolean; + hasArbitraryPrecision?: boolean; +} + +export interface IntegerField + extends TaggedVariant<"IntegerField">, + IntegerFieldOptions {} + +export function createIntegerField( + opts: T = {} as T, +): IntegerField & T { + return { + [VariantTag]: "IntegerField", + ...opts, + } as const; +} diff --git a/packages/fabric/core/src/domain/models/fields/reference-field.spec.ts b/packages/fabric/core/src/domain/models/fields/reference-field.spec.ts new file mode 100644 index 0000000..28e37b7 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/reference-field.spec.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { isError } from "../../../error/is-error.js"; +import { defineModel } from "../model.js"; +import { Field } from "./index.js"; +import { + InvalidReferenceField, + validateReferenceField, +} from "./reference-field.js"; + +describe("Validate Reference Field", () => { + const schema = { + user: defineModel({ + name: Field.string(), + password: Field.string(), + phone: Field.string({ isOptional: true }), + otherUnique: Field.integer({ isUnique: true }), + otherNotUnique: Field.uuid(), + }), + }; + + it("should return an error when the target model is not in the schema", () => { + const result = validateReferenceField( + schema, + "post", + "authorId", + Field.reference({ + model: "foo", + }), + ); + + if (!isError(result)) { + throw "Expected an error"; + } + + expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result.toString()).toBe( + "InvalidReferenceField: post.authorId. The target model 'foo' is not in the schema.", + ); + }); + + it("should not return an error if the target model is in the schema", () => { + const result = validateReferenceField( + schema, + "post", + "authorId", + Field.reference({ + model: "user", + }), + ); + + if (isError(result)) { + throw result.toString(); + } + }); + + it("should return an error if the target key is not in the target model", () => { + const result = validateReferenceField( + schema, + "post", + "authorId", + Field.reference({ + model: "user", + targetKey: "foo", + }), + ); + + if (!isError(result)) { + throw "Expected an error"; + } + + expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result.toString()).toBe( + "InvalidReferenceField: post.authorId. The target key 'foo' is not in the target model 'user'.", + ); + }); + + it("should not return an error if the target key is in the target model", () => { + const result = validateReferenceField( + schema, + "post", + "authorId", + Field.reference({ + model: "user", + targetKey: "otherUnique", + }), + ); + + if (isError(result)) { + throw result.toString(); + } + }); + + it("should return error if the target key is not unique", () => { + const result = validateReferenceField( + schema, + "post", + "authorId", + Field.reference({ + model: "user", + targetKey: "otherNotUnique", + }), + ); + + if (!isError(result)) { + throw "Expected an error"; + } + + expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result.toString()).toBe( + "InvalidReferenceField: post.authorId. The target key 'user'.'otherNotUnique' is not unique.", + ); + }); +}); diff --git a/packages/fabric/core/src/domain/models/fields/reference-field.ts b/packages/fabric/core/src/domain/models/fields/reference-field.ts new file mode 100644 index 0000000..dd93f52 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/reference-field.ts @@ -0,0 +1,68 @@ +import { TaggedError } from "../../../error/tagged-error.js"; +import { Result } from "../../../result/result.js"; +import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { ModelSchema } from "../model-schema.js"; +import { BaseField } from "./base-field.js"; + +export interface ReferenceFieldOptions extends BaseField { + model: string; + targetKey?: string; +} + +export interface ReferenceField + extends TaggedVariant<"ReferenceField">, + ReferenceFieldOptions {} + +export function createReferenceField( + opts: T = {} as T, +): ReferenceField & T { + return { + [VariantTag]: "ReferenceField", + ...opts, + } as const; +} + +export function validateReferenceField( + schema: ModelSchema, + modelName: string, + fieldName: string, + field: ReferenceField, +): Result { + if (!schema[field.model]) { + return new InvalidReferenceField( + modelName, + fieldName, + `The target model '${field.model}' is not in the schema.`, + ); + } + + if (field.targetKey && !schema[field.model][field.targetKey]) { + return new InvalidReferenceField( + modelName, + fieldName, + `The target key '${field.targetKey}' is not in the target model '${field.model}'.`, + ); + } + + if (field.targetKey && !schema[field.model][field.targetKey].isUnique) { + return new InvalidReferenceField( + modelName, + fieldName, + `The target key '${field.model}'.'${field.targetKey}' is not unique.`, + ); + } +} + +export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> { + constructor( + readonly modelName: string, + readonly fieldName: string, + readonly reason: string, + ) { + super("InvalidReferenceField"); + } + + toString() { + return `InvalidReferenceField: ${this.modelName}.${this.fieldName}. ${this.reason}`; + } +} diff --git a/packages/fabric/core/src/domain/models/fields/uuid-field.ts b/packages/fabric/core/src/domain/models/fields/uuid-field.ts index 2a7a75c..7eb717b 100644 --- a/packages/fabric/core/src/domain/models/fields/uuid-field.ts +++ b/packages/fabric/core/src/domain/models/fields/uuid-field.ts @@ -1,15 +1,19 @@ import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; import { BaseField } from "./base-field.js"; -export interface UUIDOptions extends BaseField { +export interface UUIDFieldOptions extends BaseField { isPrimaryKey?: boolean; } -export interface UUIDField extends TaggedVariant<"UUIDField">, UUIDOptions {} +export interface UUIDField + extends TaggedVariant<"UUIDField">, + UUIDFieldOptions {} -export function createUUIDField(opts: UUIDOptions): UUIDField { +export function createUUIDField( + opts: T = {} as T, +): UUIDField & T { return { [VariantTag]: "UUIDField", ...opts, - }; + } as const; } diff --git a/packages/fabric/core/src/domain/models/index.ts b/packages/fabric/core/src/domain/models/index.ts index d7b9d1f..d91f9bc 100644 --- a/packages/fabric/core/src/domain/models/index.ts +++ b/packages/fabric/core/src/domain/models/index.ts @@ -1,4 +1,4 @@ -export * from "./create-model.js"; export * from "./fields/index.js"; -export * from "./model-to-type.js"; -export * from "./relations/index.js"; +export * from "./model-schema.js"; +export * from "./model.js"; +export * from "./types/index.js"; diff --git a/packages/fabric/core/src/domain/models/model-schema.ts b/packages/fabric/core/src/domain/models/model-schema.ts new file mode 100644 index 0000000..260cc66 --- /dev/null +++ b/packages/fabric/core/src/domain/models/model-schema.ts @@ -0,0 +1,3 @@ +import { Model } from "./model.js"; + +export type ModelSchema = Record; diff --git a/packages/fabric/core/src/domain/models/model-to-type.ts b/packages/fabric/core/src/domain/models/model-to-type.ts deleted file mode 100644 index 9a61e72..0000000 --- a/packages/fabric/core/src/domain/models/model-to-type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ModelDefinition } from "./create-model.js"; -import { FieldToType } from "./fields/field-to-type.js"; - -export type ModelToType = { - [K in keyof TModel["fields"]]: FieldToType; -}; diff --git a/packages/fabric/core/src/domain/models/model.spec.ts b/packages/fabric/core/src/domain/models/model.spec.ts new file mode 100644 index 0000000..77d5add --- /dev/null +++ b/packages/fabric/core/src/domain/models/model.spec.ts @@ -0,0 +1,25 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { UUID } from "../types/uuid.js"; +import { Field } from "./fields/index.js"; +import { defineModel } from "./model.js"; +import { ModelToType } from "./types/model-to-type.js"; + +describe("CreateModel", () => { + it("should create a model and it's interface type", () => { + const User = defineModel({ + name: Field.string(), + password: Field.string(), + phone: Field.string({ isOptional: true }), + }); + type User = ModelToType; + + expectTypeOf().toEqualTypeOf<{ + id: UUID; + streamId: UUID; + streamVersion: bigint; + name: string; + password: string; + phone: string | null; + }>(); + }); +}); diff --git a/packages/fabric/core/src/domain/models/model.ts b/packages/fabric/core/src/domain/models/model.ts new file mode 100644 index 0000000..38f31af --- /dev/null +++ b/packages/fabric/core/src/domain/models/model.ts @@ -0,0 +1,23 @@ +import { Field, FieldDefinition } from "./fields/index.js"; + +export type CustomModelFields = Record; + +export const DefaultModelFields = { + id: Field.uuid({ isPrimaryKey: true }), + streamId: Field.uuid({ isIndexed: true }), + streamVersion: Field.integer({ + isUnsigned: true, + hasArbitraryPrecision: true, + }), +}; +export type Model = + typeof DefaultModelFields & TFields; + +export function defineModel( + fields: TFields, +): Model { + return { + ...fields, + ...DefaultModelFields, + } as const; +} diff --git a/packages/fabric/core/src/domain/models/relations/index.ts b/packages/fabric/core/src/domain/models/relations/index.ts deleted file mode 100644 index 96aa0a5..0000000 --- a/packages/fabric/core/src/domain/models/relations/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { OneToOneRelation } from "./one-to-one.js"; - -export type RelationDefinition = OneToOneRelation; - -export namespace Relations {} diff --git a/packages/fabric/core/src/domain/models/relations/one-to-many.ts b/packages/fabric/core/src/domain/models/relations/one-to-many.ts deleted file mode 100644 index 2d72989..0000000 --- a/packages/fabric/core/src/domain/models/relations/one-to-many.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; - -export interface OneToManyRelationOptions< - TOwner extends string, - TTarget extends string, -> { - /** - * The owner of the relation. In this case is the "one" side of the relation. - */ - owner: TOwner; - - /** - * The target of the relation. In this case is the "many" side of the relation. - */ - target: TTarget; -} - -export interface OneToManyRelation< - TOwner extends string, - TTarget extends string, -> extends TaggedVariant<"ONE_TO_MANY_RELATION">, - OneToManyRelationOptions {} - -export function createOneToManyRelation< - TOwner extends string, - TTarget extends string, ->( - opts: OneToManyRelationOptions, -): OneToManyRelation { - return { - [VariantTag]: "ONE_TO_MANY_RELATION", - ...opts, - }; -} diff --git a/packages/fabric/core/src/domain/models/relations/one-to-one.ts b/packages/fabric/core/src/domain/models/relations/one-to-one.ts deleted file mode 100644 index 15f003d..0000000 --- a/packages/fabric/core/src/domain/models/relations/one-to-one.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; - -export interface OneToOneRelationOptions< - TOwner extends string, - TTarget extends string, -> { - /** - * The owner of the relation. - */ - owner: TOwner; - - /** - * The target of the relation - * - */ - target: TTarget; -} - -export interface OneToOneRelation - extends TaggedVariant<"ONE_TO_ONE_RELATION">, - OneToOneRelationOptions {} - -export function createOneToOneRelation< - TOwner extends string, - TTarget extends string, ->( - opts: OneToOneRelationOptions, -): OneToOneRelation { - return { - [VariantTag]: "ONE_TO_ONE_RELATION", - ...opts, - }; -} diff --git a/packages/fabric/core/src/domain/models/types/index.ts b/packages/fabric/core/src/domain/models/types/index.ts new file mode 100644 index 0000000..5d07363 --- /dev/null +++ b/packages/fabric/core/src/domain/models/types/index.ts @@ -0,0 +1,2 @@ +export * from "./model-field-names.js"; +export * from "./model-to-type.js"; diff --git a/packages/fabric/core/src/domain/models/types/model-field-names.ts b/packages/fabric/core/src/domain/models/types/model-field-names.ts new file mode 100644 index 0000000..c9d1b9c --- /dev/null +++ b/packages/fabric/core/src/domain/models/types/model-field-names.ts @@ -0,0 +1,3 @@ +import { CustomModelFields } from "../model.js"; + +export type ModelFieldNames = keyof TModel; diff --git a/packages/fabric/core/src/domain/models/types/model-to-type.ts b/packages/fabric/core/src/domain/models/types/model-to-type.ts new file mode 100644 index 0000000..5f633cf --- /dev/null +++ b/packages/fabric/core/src/domain/models/types/model-to-type.ts @@ -0,0 +1,6 @@ +import { FieldToType } from "../fields/field-to-type.js"; +import { Model } from "../model.js"; + +export type ModelToType = { + [K in keyof TModel]: FieldToType; +}; diff --git a/packages/fabric/core/src/storage/query/query-builder.ts b/packages/fabric/core/src/storage/query/query-builder.ts index 2232682..bedd4c6 100644 --- a/packages/fabric/core/src/storage/query/query-builder.ts +++ b/packages/fabric/core/src/storage/query/query-builder.ts @@ -1,9 +1,5 @@ -import { - ModelDefinition, - ModelFromName, - ModelName, -} from "../../domain/models/create-model.js"; -import { ModelToType } from "../../domain/models/model-to-type.js"; +import { ModelSchema } from "../../domain/index.js"; +import { ModelToType } from "../../domain/models/types/model-to-type.js"; import { AsyncResult } from "../../result/async-result.js"; import { Keyof } from "../../types/index.js"; import { StoreQueryError } from "../errors/query-error.js"; @@ -19,9 +15,9 @@ import { } from "./query.js"; export class QueryBuilder< - TModels extends ModelDefinition, - TEntityName extends ModelName, - T = ModelToType>, + TModels extends ModelSchema, + TEntityName extends Keyof, + T = ModelToType, > implements StoreQuery { constructor( diff --git a/packages/fabric/core/src/storage/state-store.ts b/packages/fabric/core/src/storage/state-store.ts index 9e472f4..fdf8d2e 100644 --- a/packages/fabric/core/src/storage/state-store.ts +++ b/packages/fabric/core/src/storage/state-store.ts @@ -1,16 +1,10 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { - ModelDefinition, - ModelFromName, - ModelName, -} from "../domain/models/create-model.js"; -import { ModelToType } from "../domain/models/model-to-type.js"; +import { ModelSchema } from "../domain/index.js"; +import { ModelToType } from "../domain/models/types/model-to-type.js"; +import { Keyof } from "../types/keyof.js"; import { StoreQuery } from "./query/query.js"; -export interface StateStore< - TModels extends ModelDefinition>, -> { - from>( +export interface StateStore { + from>( entityName: TEntityName, - ): StoreQuery>>; + ): StoreQuery>; } diff --git a/packages/fabric/core/src/storage/storage-driver.ts b/packages/fabric/core/src/storage/storage-driver.ts index 8055183..f80309a 100644 --- a/packages/fabric/core/src/storage/storage-driver.ts +++ b/packages/fabric/core/src/storage/storage-driver.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { ModelDefinition } from "../domain/models/create-model.js"; +import { ModelSchema } from "../domain/index.js"; import { UnexpectedError } from "../error/unexpected-error.js"; import { AsyncResult } from "../result/async-result.js"; import { CircularDependencyError } from "./errors/circular-dependency-error.js"; @@ -30,7 +30,7 @@ export interface StorageDriver { * Sincronice the store with the schema. */ sync( - schema: ModelDefinition[], + schema: ModelSchema, ): AsyncResult; /**