From 9f9419c2b60f16ca5a902f9c74e6b6a904002161 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sun, 20 Oct 2024 23:12:07 -0300 Subject: [PATCH] [fabric/domain] Simplify model definition --- .../fabric/domain/models/aggregate-model.ts | 37 ---------- .../domain/models/custom-model-fields.ts | 3 - packages/fabric/domain/models/entity-model.ts | 14 ---- .../models/fields/reference-field.test.ts | 4 +- packages/fabric/domain/models/index.ts | 2 - packages/fabric/domain/models/model.test.ts | 5 +- packages/fabric/domain/models/model.ts | 74 +++++++++++++++---- .../fabric/domain/projections/projection.ts | 3 +- .../sqlite-store/sqlite/filter-to-sql.test.ts | 4 +- .../sqlite-store/sqlite/filter-to-sql.ts | 4 +- .../sqlite-store/sqlite/model-to-sql.test.ts | 4 +- .../sqlite-store/state/state-store.test.ts | 50 +------------ 12 files changed, 74 insertions(+), 130 deletions(-) delete mode 100644 packages/fabric/domain/models/aggregate-model.ts delete mode 100644 packages/fabric/domain/models/custom-model-fields.ts delete mode 100644 packages/fabric/domain/models/entity-model.ts diff --git a/packages/fabric/domain/models/aggregate-model.ts b/packages/fabric/domain/models/aggregate-model.ts deleted file mode 100644 index 6eda9b7..0000000 --- a/packages/fabric/domain/models/aggregate-model.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Keyof } from "../../core/index.ts"; -import type { CustomModelFields } from "./custom-model-fields.ts"; -import { DefaultEntityFields, type EntityModel } from "./entity-model.ts"; -import { Field } from "./fields/index.ts"; - -export const DefaultAggregateFields = { - ...DefaultEntityFields, - streamId: Field.uuid({ isIndexed: true }), - streamVersion: Field.integer({ - isUnsigned: true, - hasArbitraryPrecision: true, - }), - deletedAt: Field.timestamp({ isOptional: true }), -}; - -export interface AggregateModel< - TName extends string = string, - TFields extends CustomModelFields = CustomModelFields, -> extends EntityModel { - fields: typeof DefaultAggregateFields & TFields; -} - -export function defineAggregateModel< - TName extends string, - TFields extends CustomModelFields, ->(name: TName, fields: TFields): AggregateModel { - return { - name, - fields: { ...DefaultAggregateFields, ...fields }, - } as const; -} - -export type ModelAddressableFields = { - [K in Keyof]: TModel["fields"][K] extends { isUnique: true } - ? K - : never; -}[Keyof]; diff --git a/packages/fabric/domain/models/custom-model-fields.ts b/packages/fabric/domain/models/custom-model-fields.ts deleted file mode 100644 index 09c2000..0000000 --- a/packages/fabric/domain/models/custom-model-fields.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { FieldDefinition } from "./fields/index.ts"; - -export type CustomModelFields = Record; diff --git a/packages/fabric/domain/models/entity-model.ts b/packages/fabric/domain/models/entity-model.ts deleted file mode 100644 index 7d9f870..0000000 --- a/packages/fabric/domain/models/entity-model.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CustomModelFields } from "./custom-model-fields.ts"; -import { Field } from "./fields/index.ts"; -import type { Model } from "./model.ts"; - -export const DefaultEntityFields = { - id: Field.uuid({ isPrimaryKey: true }), -}; - -export interface EntityModel< - TName extends string = string, - TFields extends CustomModelFields = CustomModelFields, -> extends Model { - fields: typeof DefaultEntityFields & TFields; -} diff --git a/packages/fabric/domain/models/fields/reference-field.test.ts b/packages/fabric/domain/models/fields/reference-field.test.ts index c6dea22..4685553 100644 --- a/packages/fabric/domain/models/fields/reference-field.test.ts +++ b/packages/fabric/domain/models/fields/reference-field.test.ts @@ -1,6 +1,6 @@ import { isError } from "@fabric/core"; import { describe, expect, test } from "@fabric/testing"; -import { defineAggregateModel } from "../aggregate-model.ts"; +import { Model } from "../model.ts"; import { Field } from "./index.ts"; import { InvalidReferenceFieldError, @@ -9,7 +9,7 @@ import { describe("Validate Reference Field", () => { const schema = { - User: defineAggregateModel("User", { + User: Model.aggregateFrom("User", { name: Field.string(), password: Field.string(), otherUnique: Field.integer({ isUnique: true }), diff --git a/packages/fabric/domain/models/index.ts b/packages/fabric/domain/models/index.ts index 350d673..012833c 100644 --- a/packages/fabric/domain/models/index.ts +++ b/packages/fabric/domain/models/index.ts @@ -1,5 +1,3 @@ -export * from "./aggregate-model.ts"; -export * from "./custom-model-fields.ts"; export * from "./fields/index.ts"; export * from "./model-schema.ts"; export * from "./model.ts"; diff --git a/packages/fabric/domain/models/model.test.ts b/packages/fabric/domain/models/model.test.ts index 77d4bc5..b548872 100644 --- a/packages/fabric/domain/models/model.test.ts +++ b/packages/fabric/domain/models/model.test.ts @@ -1,13 +1,12 @@ import type { UUID } from "@fabric/core"; import { describe, expectTypeOf, test } from "@fabric/testing"; import type { PosixDate } from "../../core/index.ts"; -import { defineAggregateModel } from "./aggregate-model.ts"; import { Field } from "./fields/index.ts"; -import type { ModelToType } from "./model.ts"; +import { Model, type ModelToType } from "./model.ts"; describe("CreateModel", () => { test("should create a model and it's interface type", () => { - const User = defineAggregateModel("User", { + const User = Model.aggregateFrom("User", { name: Field.string(), password: Field.string(), phone: Field.string({ isOptional: true }), diff --git a/packages/fabric/domain/models/model.ts b/packages/fabric/domain/models/model.ts index 2f9705d..a9899eb 100644 --- a/packages/fabric/domain/models/model.ts +++ b/packages/fabric/domain/models/model.ts @@ -1,29 +1,73 @@ import type { Keyof } from "@fabric/core"; -import type { CustomModelFields } from "./custom-model-fields.ts"; import type { FieldToType } from "./fields/field-to-type.ts"; +import { Field, type FieldDefinition } from "./fields/index.ts"; -export interface Model< +/** + * A model is a schema definition for some type of structured data. + */ +export class Model< TName extends string = string, - TFields extends CustomModelFields = CustomModelFields, + TFields extends ModelFields = ModelFields, > { - name: TName; - fields: TFields; + static from( + name: TName, + fields: TFields, + ) { + return new Model(name, fields); + } + + static aggregateFrom( + name: TName, + fields: TFields, + ): Model { + return new Model(name, { ...fields, ...DefaultAggregateFields }); + } + + static entityFrom( + name: TName, + fields: TFields, + ): Model { + return new Model(name, { ...fields, ...DefaultEntityFields }); + } + private constructor(readonly name: TName, readonly fields: TFields) {} } -export function defineModel< - TName extends string, - TFields extends CustomModelFields, ->(name: TName, fields: TFields): Model { - return { - name, - fields, - } as const; -} +export type EntityModel = Model< + string, + typeof DefaultEntityFields & ModelFields +>; + +export type AggregateModel = Model< + string, + typeof DefaultAggregateFields & ModelFields +>; export type ModelToType = { [K in Keyof]: FieldToType; }; -export type ModelFieldNames = Keyof< +export type ModelFieldNames = Keyof< TModel["fields"] >; + +export type ModelAddressableFields = { + [K in Keyof]: TModel["fields"][K] extends { isUnique: true } + ? K + : never; +}[Keyof]; + +type ModelFields = Record; + +const DefaultEntityFields = { + id: Field.uuid({ isPrimaryKey: true }), +} as const; + +const DefaultAggregateFields = { + ...DefaultEntityFields, + streamId: Field.uuid({ isIndexed: true }), + streamVersion: Field.integer({ + isUnsigned: true, + hasArbitraryPrecision: true, + }), + deletedAt: Field.timestamp({ isOptional: true }), +} as const; diff --git a/packages/fabric/domain/projections/projection.ts b/packages/fabric/domain/projections/projection.ts index 3f8bff4..37335c1 100644 --- a/packages/fabric/domain/projections/projection.ts +++ b/packages/fabric/domain/projections/projection.ts @@ -1,8 +1,7 @@ import type { VariantTag } from "@fabric/core"; import type { Event } from "../events/event.ts"; import type { StoredEvent } from "../events/stored-event.ts"; -import type { AggregateModel } from "../models/aggregate-model.ts"; -import type { ModelToType } from "../models/model.ts"; +import type { AggregateModel, ModelToType } from "../models/model.ts"; export interface Projection< TModel extends AggregateModel, diff --git a/packages/fabric/sqlite-store/sqlite/filter-to-sql.test.ts b/packages/fabric/sqlite-store/sqlite/filter-to-sql.test.ts index e74c899..92a665c 100644 --- a/packages/fabric/sqlite-store/sqlite/filter-to-sql.test.ts +++ b/packages/fabric/sqlite-store/sqlite/filter-to-sql.test.ts @@ -1,5 +1,4 @@ import { - defineModel, Field, isGreaterOrEqualTo, isGreaterThan, @@ -8,12 +7,13 @@ import { isLessThan, isLike, isNotEqualTo, + Model, } from "@fabric/domain"; import { describe, expect, test } from "@fabric/testing"; import { filterToParams, filterToSQL } from "./filter-to-sql.ts"; describe("SQL where clause from filter options", () => { - const col = defineModel("users", { + const col = Model.from("users", { name: Field.string(), age: Field.integer(), status: Field.string(), diff --git a/packages/fabric/sqlite-store/sqlite/filter-to-sql.ts b/packages/fabric/sqlite-store/sqlite/filter-to-sql.ts index e3c291f..578db11 100644 --- a/packages/fabric/sqlite-store/sqlite/filter-to-sql.ts +++ b/packages/fabric/sqlite-store/sqlite/filter-to-sql.ts @@ -10,7 +10,7 @@ import { MultiFilterOption, SingleFilterOption, } from "@fabric/domain"; -import { keyToParam } from "./record-utils.ts"; +import { keyToParamKey } from "./record-utils.ts"; import { fieldValueToSQL } from "./value-to-sql.ts"; export function filterToSQL(filterOptions?: FilterOptions) { @@ -57,7 +57,7 @@ function getWhereFromSingleOption( const WHERE_KEY_PREFIX = "where_"; function getWhereParamKey(key: string, opts: { postfix?: string } = {}) { - return keyToParam(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`); + return keyToParamKey(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`); } function getWhereFromKeyValue( diff --git a/packages/fabric/sqlite-store/sqlite/model-to-sql.test.ts b/packages/fabric/sqlite-store/sqlite/model-to-sql.test.ts index ca89898..a26853c 100644 --- a/packages/fabric/sqlite-store/sqlite/model-to-sql.test.ts +++ b/packages/fabric/sqlite-store/sqlite/model-to-sql.test.ts @@ -1,9 +1,9 @@ -import { defineModel, Field } from "@fabric/domain"; +import { Field, Model } from "@fabric/domain"; import { describe, expect, test } from "@fabric/testing"; import { modelToSql } from "./model-to-sql.ts"; describe("ModelToSQL", () => { - const model = defineModel("something", { + const model = Model.from("something", { id: Field.uuid({ isPrimaryKey: true }), name: Field.string(), age: Field.integer(), diff --git a/packages/fabric/sqlite-store/state/state-store.test.ts b/packages/fabric/sqlite-store/state/state-store.test.ts index e207e8d..b969d69 100644 --- a/packages/fabric/sqlite-store/state/state-store.test.ts +++ b/packages/fabric/sqlite-store/state/state-store.test.ts @@ -1,5 +1,5 @@ -import { PosixDate, Run, UUID } from "@fabric/core"; -import { defineAggregateModel, Field, isLike } from "@fabric/domain"; +import { Run, UUID } from "@fabric/core"; +import { Field, isLike, Model } from "@fabric/domain"; import { UUIDGeneratorMock } from "@fabric/domain/mocks"; import { afterEach, @@ -13,11 +13,11 @@ import { SQLiteStateStore } from "./state-store.ts"; describe("State Store", () => { const models = [ - defineAggregateModel("demo", { + Model.entityFrom("demo", { value: Field.float(), owner: Field.reference({ targetModel: "users" }), }), - defineAggregateModel("users", { + Model.entityFrom("users", { name: Field.string(), }), ]; @@ -39,9 +39,6 @@ describe("State Store", () => { await store.insertInto("users", { id: newUUID, name: "test", - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); }); @@ -51,9 +48,6 @@ describe("State Store", () => { await store.insertInto("users", { name: "test", id: newUUID, - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); const result = await store.from("users").select().unwrapOrThrow(); @@ -61,20 +55,14 @@ describe("State Store", () => { expectTypeOf(result).toEqualTypeOf< { id: UUID; - streamId: UUID; - streamVersion: bigint; name: string; - deletedAt: PosixDate | null; }[] >(); expect(result).toEqual([ { id: newUUID, - streamId: newUUID, - streamVersion: 1n, name: "test", - deletedAt: null, }, ]); }); @@ -87,25 +75,16 @@ describe("State Store", () => { store.insertInto("users", { name: "test", id: newUUID, - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }), () => store.insertInto("users", { name: "anotherName", id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - deletedAt: null, }), () => store.insertInto("users", { name: "anotherName2", id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - deletedAt: null, }), ); @@ -119,20 +98,14 @@ describe("State Store", () => { expectTypeOf(result).toEqualTypeOf< { id: UUID; - streamId: UUID; - streamVersion: bigint; name: string; - deletedAt: PosixDate | null; }[] >(); expect(result).toEqual([ { id: newUUID, - streamId: newUUID, - streamVersion: 1n, name: "test", - deletedAt: null, }, ]); }); @@ -143,9 +116,6 @@ describe("State Store", () => { await store.insertInto("users", { name: "test", id: newUUID, - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); await store.update("users", newUUID, { @@ -157,10 +127,7 @@ describe("State Store", () => { expect(result).toEqual({ id: newUUID, - streamId: newUUID, - streamVersion: 1n, name: "updated", - deletedAt: null, }); }); @@ -170,9 +137,6 @@ describe("State Store", () => { await store.insertInto("users", { name: "test", id: newUUID, - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); await store.delete("users", newUUID).orThrow(); @@ -192,18 +156,12 @@ describe("State Store", () => { await store.insertInto("users", { id: ownerUUID, name: "test", - streamId: ownerUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); await store.insertInto("demo", { id: newUUID, value: 1.0, owner: ownerUUID, - streamId: newUUID, - streamVersion: 1n, - deletedAt: null, }).orThrow(); }); });