From 527fa87f4febca466d1971f05b7f36ff730a5a81 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 21 Sep 2024 02:20:32 -0300 Subject: [PATCH 01/37] store-sqlite: export sqlite-driver from the main index file --- packages/fabric/store-sqlite/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fabric/store-sqlite/src/index.ts b/packages/fabric/store-sqlite/src/index.ts index e69de29..af380ab 100644 --- a/packages/fabric/store-sqlite/src/index.ts +++ b/packages/fabric/store-sqlite/src/index.ts @@ -0,0 +1 @@ +export * from "./sqlite-driver.js"; From 029bf431dd4c6692eede6fd66bf55e0f3483756b Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 21 Sep 2024 02:27:08 -0300 Subject: [PATCH 02/37] 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; /** From 59810a2118333cc2f0893ab37bb5d0138a3ca7fe Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 21 Sep 2024 21:10:57 -0300 Subject: [PATCH 03/37] fabric-core: Improve some types names --- .../fabric/core/src/domain/entity/files/image-file.ts | 4 ++-- packages/fabric/core/src/domain/entity/files/index.ts | 2 +- .../entity/files/{domain-file.ts => stored-file.ts} | 2 +- packages/fabric/core/src/domain/events/event.ts | 10 ++++++++++ .../core/src/domain/use-case/use-case-definition.ts | 5 ----- .../src/files/{uploaded-file.ts => in-memory-file.ts} | 0 packages/fabric/core/src/files/index.ts | 4 ++-- .../{is-uploaded-file.ts => is-in-memory-file.ts} | 2 +- packages/fabric/core/src/files/media-file.ts | 4 ++-- packages/fabric/core/src/result/result.ts | 8 +++++--- 10 files changed, 24 insertions(+), 17 deletions(-) rename packages/fabric/core/src/domain/entity/files/{domain-file.ts => stored-file.ts} (75%) rename packages/fabric/core/src/files/{uploaded-file.ts => in-memory-file.ts} (100%) rename packages/fabric/core/src/files/{is-uploaded-file.ts => is-in-memory-file.ts} (92%) diff --git a/packages/fabric/core/src/domain/entity/files/image-file.ts b/packages/fabric/core/src/domain/entity/files/image-file.ts index e92aaba..66a17c8 100644 --- a/packages/fabric/core/src/domain/entity/files/image-file.ts +++ b/packages/fabric/core/src/domain/entity/files/image-file.ts @@ -1,9 +1,9 @@ import { ImageMimeType } from "../../../files/mime-type.js"; -import { DomainFile } from "./domain-file.js"; +import { StoredFile } from "./stored-file.js"; /** * Represents an image file. */ -export interface ImageFile extends DomainFile { +export interface ImageFile extends StoredFile { mimeType: ImageMimeType; } diff --git a/packages/fabric/core/src/domain/entity/files/index.ts b/packages/fabric/core/src/domain/entity/files/index.ts index dad6bbf..ebe3117 100644 --- a/packages/fabric/core/src/domain/entity/files/index.ts +++ b/packages/fabric/core/src/domain/entity/files/index.ts @@ -1,2 +1,2 @@ -export * from "./domain-file.js"; export * from "./image-file.js"; +export * from "./stored-file.js"; diff --git a/packages/fabric/core/src/domain/entity/files/domain-file.ts b/packages/fabric/core/src/domain/entity/files/stored-file.ts similarity index 75% rename from packages/fabric/core/src/domain/entity/files/domain-file.ts rename to packages/fabric/core/src/domain/entity/files/stored-file.ts index 74a36ca..4ed5ff1 100644 --- a/packages/fabric/core/src/domain/entity/files/domain-file.ts +++ b/packages/fabric/core/src/domain/entity/files/stored-file.ts @@ -4,6 +4,6 @@ import { Entity } from "../entity.js"; /** * Represents a file as managed by the domain. */ -export interface DomainFile extends BaseFile, Entity { +export interface StoredFile extends BaseFile, Entity { url: string; } diff --git a/packages/fabric/core/src/domain/events/event.ts b/packages/fabric/core/src/domain/events/event.ts index 05b78da..22d0ac5 100644 --- a/packages/fabric/core/src/domain/events/event.ts +++ b/packages/fabric/core/src/domain/events/event.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { PosixDate } from "../../time/posix-date.js"; import { TaggedVariant } from "../../variant/variant.js"; import { UUID } from "../types/uuid.js"; @@ -10,3 +11,12 @@ export interface Event streamId: UUID; payload: TPayload; } + +/** + * A stored event is an inmutable event, already stored, with it's version in the stream and timestamp. + */ +export interface StoredEvent + extends Readonly> { + readonly version: number; + readonly timestamp: PosixDate; +} diff --git a/packages/fabric/core/src/domain/use-case/use-case-definition.ts b/packages/fabric/core/src/domain/use-case/use-case-definition.ts index c16f76e..85c35dd 100644 --- a/packages/fabric/core/src/domain/use-case/use-case-definition.ts +++ b/packages/fabric/core/src/domain/use-case/use-case-definition.ts @@ -25,11 +25,6 @@ interface BasicUseCaseDefinition< */ isAuthRequired: boolean; - /** - * The required permissions to execute the use case. - **/ - requiredPermissions?: string[]; - /** * The use case function. */ diff --git a/packages/fabric/core/src/files/uploaded-file.ts b/packages/fabric/core/src/files/in-memory-file.ts similarity index 100% rename from packages/fabric/core/src/files/uploaded-file.ts rename to packages/fabric/core/src/files/in-memory-file.ts diff --git a/packages/fabric/core/src/files/index.ts b/packages/fabric/core/src/files/index.ts index ed4ec55..3468cb7 100644 --- a/packages/fabric/core/src/files/index.ts +++ b/packages/fabric/core/src/files/index.ts @@ -1,8 +1,8 @@ export * from "./base-file.js"; export * from "./bytes.js"; +export * from "./in-memory-file.js"; export * from "./invalid-file-type-error.js"; +export * from "./is-in-memory-file.js"; export * from "./is-mime-type.js"; -export * from "./is-uploaded-file.js"; export * from "./media-file.js"; export * from "./mime-type.js"; -export * from "./uploaded-file.js"; diff --git a/packages/fabric/core/src/files/is-uploaded-file.ts b/packages/fabric/core/src/files/is-in-memory-file.ts similarity index 92% rename from packages/fabric/core/src/files/is-uploaded-file.ts rename to packages/fabric/core/src/files/is-in-memory-file.ts index 18556df..3adfc31 100644 --- a/packages/fabric/core/src/files/is-uploaded-file.ts +++ b/packages/fabric/core/src/files/is-in-memory-file.ts @@ -1,6 +1,6 @@ import validator from "validator"; import { isRecord } from "../record/is-record.js"; -import { InMemoryFile } from "./uploaded-file.js"; +import { InMemoryFile } from "./in-memory-file.js"; const { isBase64, isMimeType } = validator; diff --git a/packages/fabric/core/src/files/media-file.ts b/packages/fabric/core/src/files/media-file.ts index edf0b82..5c86a4e 100644 --- a/packages/fabric/core/src/files/media-file.ts +++ b/packages/fabric/core/src/files/media-file.ts @@ -1,8 +1,8 @@ -import { DomainFile } from "../domain/entity/files/domain-file.js"; +import { StoredFile } from "../domain/entity/files/stored-file.js"; /** * Represents a media file, either an image, a video or an audio file. */ -export interface MediaFile extends DomainFile { +export interface MediaFile extends StoredFile { mimeType: `image/${string}` | `video/${string}` | `audio/${string}`; } diff --git a/packages/fabric/core/src/result/result.ts b/packages/fabric/core/src/result/result.ts index 48ee32f..a7c8d7f 100644 --- a/packages/fabric/core/src/result/result.ts +++ b/packages/fabric/core/src/result/result.ts @@ -1,9 +1,11 @@ import { TaggedError } from "../error/tagged-error.js"; +import { UnexpectedError } from "../error/unexpected-error.js"; /** * Un Result representa el resultado de una operación * que puede ser un valor de tipo `TValue` o un error `TError`. */ -export type Result> = - | TValue - | TError; +export type Result< + TValue, + TError extends TaggedError = UnexpectedError, +> = TValue | TError; From 80c34e46497b465b07ee1645d052d967e9ea6f8f Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Wed, 25 Sep 2024 08:20:40 -0300 Subject: [PATCH 04/37] @fabric/domain: extract domain utils from fabric/core and create a new package --- packages/fabric/core/README.md | 2 +- packages/fabric/core/package.json | 2 +- .../core/src/domain/entity/files/index.ts | 2 - .../fabric/core/src/domain/entity/index.ts | 1 - packages/fabric/core/src/domain/index.ts | 5 --- packages/fabric/core/src/index.ts | 2 - .../fabric/core/src/storage/state-store.ts | 10 ----- packages/fabric/core/src/types/enum.ts | 2 +- packages/fabric/domain/README.md | 1 + packages/fabric/domain/package.json | 22 +++++++++++ .../src}/errors/circular-dependency-error.ts | 2 +- .../storage => domain/src}/errors/index.ts | 0 .../src}/errors/query-error.ts | 3 +- .../src/domain => domain/src}/events/event.ts | 8 ++-- packages/fabric/domain/src/events/index.ts | 1 + .../{core => domain}/src/files/base-file.ts | 0 .../{core => domain}/src/files/bytes.ts | 0 .../entity => domain/src}/files/image-file.ts | 2 +- .../src/files/in-memory-file.ts | 2 +- .../{core => domain}/src/files/index.ts | 2 + .../src/files/invalid-file-type-error.ts | 2 +- .../src/files/is-in-memory-file.ts | 2 +- .../src/files/is-mime-type.spec.ts | 0 .../src/files/is-mime-type.ts | 0 .../{core => domain}/src/files/media-file.ts | 2 +- .../{core => domain}/src/files/mime-type.ts | 0 .../src}/files/stored-file.ts | 4 +- packages/fabric/domain/src/index.ts | 9 +++++ .../src}/models/fields/base-field.ts | 0 .../src}/models/fields/field-to-type.spec.ts | 0 .../src}/models/fields/field-to-type.ts | 0 .../src}/models/fields/index.ts | 0 .../src}/models/fields/integer.ts | 2 +- .../models/fields/reference-field.spec.ts | 2 +- .../src}/models/fields/reference-field.ts | 4 +- .../src}/models/fields/string-field.ts | 2 +- .../src}/models/fields/uuid-field.ts | 2 +- .../src/domain => domain/src}/models/index.ts | 0 .../src}/models/model-schema.ts | 0 .../src}/models/model.spec.ts | 0 .../src/domain => domain/src}/models/model.ts | 0 .../src}/models/types/index.ts | 0 .../src}/models/types/model-field-names.ts | 0 .../src}/models/types/model-to-type.ts | 0 .../domain/src/query/aggregate-options.ts | 24 ++++++++++++ .../src}/query/filter-options.ts | 0 .../src/storage => domain/src}/query/index.ts | 0 .../src}/query/order-by-options.ts | 0 .../src}/query/query-builder.ts | 13 ++++--- .../src/storage => domain/src}/query/query.ts | 6 ++- .../domain => domain/src}/security/index.ts | 0 .../domain => domain/src}/security/policy.ts | 0 .../src/storage/event-store.ts | 15 ++----- .../{core => domain}/src/storage/index.ts | 2 - .../fabric/domain/src/storage/state-store.ts | 18 +++++++++ .../src/storage/storage-driver.ts | 11 +++--- .../domain => domain/src}/types/base-64.ts | 0 .../src/domain => domain/src}/types/email.ts | 0 .../entity => domain/src/types}/entity.ts | 2 +- .../src/domain => domain/src}/types/index.ts | 2 + .../src/domain => domain/src}/types/semver.ts | 0 .../src/domain => domain/src}/types/uuid.ts | 0 .../domain => domain/src}/use-case/index.ts | 0 .../src}/use-case/use-case-definition.ts | 2 +- .../src}/use-case/use-case.ts | 3 +- packages/fabric/domain/src/utils/index.ts | 1 + .../src}/utils/sort-by-dependencies.spec.ts | 0 .../src}/utils/sort-by-dependencies.ts | 2 +- packages/fabric/domain/tsconfig.build.json | 15 +++++++ packages/fabric/domain/tsconfig.json | 4 ++ packages/fabric/domain/vitest.config.ts | 10 +++++ packages/fabric/store-sqlite/package.json | 3 +- .../fabric/store-sqlite/src/model-to-sql.ts | 16 ++++---- .../store-sqlite/src/sqlite-driver.spec.ts | 2 +- .../fabric/store-sqlite/src/sqlite-driver.ts | 10 ++--- packages/templates/domain/package.json | 3 +- .../domain/src/security/permission.ts | 4 +- .../templates/domain/src/security/policy.ts | 2 +- .../templates/domain/src/security/users.ts | 4 +- packages/templates/domain/src/use-cases.ts | 2 +- packages/templates/lib/package.json | 2 +- yarn.lock | 39 ++++++++++++------- 82 files changed, 205 insertions(+), 112 deletions(-) delete mode 100644 packages/fabric/core/src/domain/entity/files/index.ts delete mode 100644 packages/fabric/core/src/domain/entity/index.ts delete mode 100644 packages/fabric/core/src/domain/index.ts delete mode 100644 packages/fabric/core/src/storage/state-store.ts create mode 100644 packages/fabric/domain/README.md create mode 100644 packages/fabric/domain/package.json rename packages/fabric/{core/src/storage => domain/src}/errors/circular-dependency-error.ts (80%) rename packages/fabric/{core/src/storage => domain/src}/errors/index.ts (100%) rename packages/fabric/{core/src/storage => domain/src}/errors/query-error.ts (80%) rename packages/fabric/{core/src/domain => domain/src}/events/event.ts (67%) create mode 100644 packages/fabric/domain/src/events/index.ts rename packages/fabric/{core => domain}/src/files/base-file.ts (100%) rename packages/fabric/{core => domain}/src/files/bytes.ts (100%) rename packages/fabric/{core/src/domain/entity => domain/src}/files/image-file.ts (72%) rename packages/fabric/{core => domain}/src/files/in-memory-file.ts (74%) rename packages/fabric/{core => domain}/src/files/index.ts (80%) rename packages/fabric/{core => domain}/src/files/invalid-file-type-error.ts (71%) rename packages/fabric/{core => domain}/src/files/is-in-memory-file.ts (93%) rename packages/fabric/{core => domain}/src/files/is-mime-type.spec.ts (100%) rename packages/fabric/{core => domain}/src/files/is-mime-type.ts (100%) rename packages/fabric/{core => domain}/src/files/media-file.ts (74%) rename packages/fabric/{core => domain}/src/files/mime-type.ts (100%) rename packages/fabric/{core/src/domain/entity => domain/src}/files/stored-file.ts (57%) create mode 100644 packages/fabric/domain/src/index.ts rename packages/fabric/{core/src/domain => domain/src}/models/fields/base-field.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/field-to-type.spec.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/field-to-type.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/index.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/integer.ts (85%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/reference-field.spec.ts (98%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/reference-field.ts (89%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/string-field.ts (85%) rename packages/fabric/{core/src/domain => domain/src}/models/fields/uuid-field.ts (84%) rename packages/fabric/{core/src/domain => domain/src}/models/index.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/model-schema.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/model.spec.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/model.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/types/index.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/types/model-field-names.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/models/types/model-to-type.ts (100%) create mode 100644 packages/fabric/domain/src/query/aggregate-options.ts rename packages/fabric/{core/src/storage => domain/src}/query/filter-options.ts (100%) rename packages/fabric/{core/src/storage => domain/src}/query/index.ts (100%) rename packages/fabric/{core/src/storage => domain/src}/query/order-by-options.ts (100%) rename packages/fabric/{core/src/storage => domain/src}/query/query-builder.ts (77%) rename packages/fabric/{core/src/storage => domain/src}/query/query.ts (91%) rename packages/fabric/{core/src/domain => domain/src}/security/index.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/security/policy.ts (100%) rename packages/fabric/{core => domain}/src/storage/event-store.ts (66%) rename packages/fabric/{core => domain}/src/storage/index.ts (60%) create mode 100644 packages/fabric/domain/src/storage/state-store.ts rename packages/fabric/{core => domain}/src/storage/storage-driver.ts (76%) rename packages/fabric/{core/src/domain => domain/src}/types/base-64.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/types/email.ts (100%) rename packages/fabric/{core/src/domain/entity => domain/src/types}/entity.ts (83%) rename packages/fabric/{core/src/domain => domain/src}/types/index.ts (58%) rename packages/fabric/{core/src/domain => domain/src}/types/semver.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/types/uuid.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/use-case/index.ts (100%) rename packages/fabric/{core/src/domain => domain/src}/use-case/use-case-definition.ts (91%) rename packages/fabric/{core/src/domain => domain/src}/use-case/use-case.ts (75%) create mode 100644 packages/fabric/domain/src/utils/index.ts rename packages/fabric/{core/src/storage => domain/src}/utils/sort-by-dependencies.spec.ts (100%) rename packages/fabric/{core/src/storage => domain/src}/utils/sort-by-dependencies.ts (95%) create mode 100644 packages/fabric/domain/tsconfig.build.json create mode 100644 packages/fabric/domain/tsconfig.json create mode 100644 packages/fabric/domain/vitest.config.ts diff --git a/packages/fabric/core/README.md b/packages/fabric/core/README.md index 9bd5c19..fb16799 100644 --- a/packages/fabric/core/README.md +++ b/packages/fabric/core/README.md @@ -1 +1 @@ -# @ulthar/fabric-core +# @fabric/core diff --git a/packages/fabric/core/package.json b/packages/fabric/core/package.json index 0263d15..fd94849 100644 --- a/packages/fabric/core/package.json +++ b/packages/fabric/core/package.json @@ -1,5 +1,5 @@ { - "name": "@ulthar/fabric-core", + "name": "@fabric/core", "type": "module", "module": "dist/index.js", "main": "dist/index.js", diff --git a/packages/fabric/core/src/domain/entity/files/index.ts b/packages/fabric/core/src/domain/entity/files/index.ts deleted file mode 100644 index ebe3117..0000000 --- a/packages/fabric/core/src/domain/entity/files/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./image-file.js"; -export * from "./stored-file.js"; diff --git a/packages/fabric/core/src/domain/entity/index.ts b/packages/fabric/core/src/domain/entity/index.ts deleted file mode 100644 index c42a26d..0000000 --- a/packages/fabric/core/src/domain/entity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./entity.js"; diff --git a/packages/fabric/core/src/domain/index.ts b/packages/fabric/core/src/domain/index.ts deleted file mode 100644 index f227b7a..0000000 --- a/packages/fabric/core/src/domain/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./entity/index.js"; -export * from "./models/index.js"; -export * from "./security/index.js"; -export * from "./types/index.js"; -export * from "./use-case/index.js"; diff --git a/packages/fabric/core/src/index.ts b/packages/fabric/core/src/index.ts index ef61d0a..dbc3c9e 100644 --- a/packages/fabric/core/src/index.ts +++ b/packages/fabric/core/src/index.ts @@ -1,9 +1,7 @@ export * from "./array/index.js"; -export * from "./domain/index.js"; export * from "./error/index.js"; export * from "./record/index.js"; export * from "./result/index.js"; -export * from "./storage/index.js"; export * from "./time/index.js"; export * from "./types/index.js"; export * from "./variant/index.js"; diff --git a/packages/fabric/core/src/storage/state-store.ts b/packages/fabric/core/src/storage/state-store.ts deleted file mode 100644 index fdf8d2e..0000000 --- a/packages/fabric/core/src/storage/state-store.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 { - from>( - entityName: TEntityName, - ): StoreQuery>; -} diff --git a/packages/fabric/core/src/types/enum.ts b/packages/fabric/core/src/types/enum.ts index 0237c2e..fe3a94c 100644 --- a/packages/fabric/core/src/types/enum.ts +++ b/packages/fabric/core/src/types/enum.ts @@ -1 +1 @@ -export type EnumToValues> = T[keyof T]; +export type EnumToType> = T[keyof T]; diff --git a/packages/fabric/domain/README.md b/packages/fabric/domain/README.md new file mode 100644 index 0000000..e339422 --- /dev/null +++ b/packages/fabric/domain/README.md @@ -0,0 +1 @@ +# model diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json new file mode 100644 index 0000000..913751b --- /dev/null +++ b/packages/fabric/domain/package.json @@ -0,0 +1,22 @@ +{ + "name": "@fabric/domain", + "type": "module", + "module": "dist/index.js", + "main": "dist/index.js", + "files": [ + "dist" + ], + "private": true, + "packageManager": "yarn@4.1.1", + "devDependencies": { + "typescript": "^5.6.2", + "vitest": "^2.1.1" + }, + "dependencies": { + "@fabric/core": "workspace:^" + }, + "scripts": { + "test": "vitest", + "build": "tsc -p tsconfig.build.json" + } +} diff --git a/packages/fabric/core/src/storage/errors/circular-dependency-error.ts b/packages/fabric/domain/src/errors/circular-dependency-error.ts similarity index 80% rename from packages/fabric/core/src/storage/errors/circular-dependency-error.ts rename to packages/fabric/domain/src/errors/circular-dependency-error.ts index 0896c8f..954c8f2 100644 --- a/packages/fabric/core/src/storage/errors/circular-dependency-error.ts +++ b/packages/fabric/domain/src/errors/circular-dependency-error.ts @@ -1,4 +1,4 @@ -import { TaggedError } from "../../error/tagged-error.js"; +import { TaggedError } from "@fabric/core"; export class CircularDependencyError extends TaggedError<"CircularDependencyError"> { context: { key: string; dep: string }; diff --git a/packages/fabric/core/src/storage/errors/index.ts b/packages/fabric/domain/src/errors/index.ts similarity index 100% rename from packages/fabric/core/src/storage/errors/index.ts rename to packages/fabric/domain/src/errors/index.ts diff --git a/packages/fabric/core/src/storage/errors/query-error.ts b/packages/fabric/domain/src/errors/query-error.ts similarity index 80% rename from packages/fabric/core/src/storage/errors/query-error.ts rename to packages/fabric/domain/src/errors/query-error.ts index 9c3dc83..fca7f63 100644 --- a/packages/fabric/core/src/storage/errors/query-error.ts +++ b/packages/fabric/domain/src/errors/query-error.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { TaggedError } from "../../error/tagged-error.js"; + +import { TaggedError } from "@fabric/core"; export class StoreQueryError extends TaggedError<"StoreQueryError"> { constructor( diff --git a/packages/fabric/core/src/domain/events/event.ts b/packages/fabric/domain/src/events/event.ts similarity index 67% rename from packages/fabric/core/src/domain/events/event.ts rename to packages/fabric/domain/src/events/event.ts index 22d0ac5..be843d1 100644 --- a/packages/fabric/core/src/domain/events/event.ts +++ b/packages/fabric/domain/src/events/event.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PosixDate } from "../../time/posix-date.js"; -import { TaggedVariant } from "../../variant/variant.js"; +import { PosixDate, TaggedVariant } from "@fabric/core"; import { UUID } from "../types/uuid.js"; /** @@ -15,8 +14,7 @@ export interface Event /** * A stored event is an inmutable event, already stored, with it's version in the stream and timestamp. */ -export interface StoredEvent - extends Readonly> { +export type StoredEvent = Readonly & { readonly version: number; readonly timestamp: PosixDate; -} +}; diff --git a/packages/fabric/domain/src/events/index.ts b/packages/fabric/domain/src/events/index.ts new file mode 100644 index 0000000..8aad408 --- /dev/null +++ b/packages/fabric/domain/src/events/index.ts @@ -0,0 +1 @@ +export * from "./event.js"; diff --git a/packages/fabric/core/src/files/base-file.ts b/packages/fabric/domain/src/files/base-file.ts similarity index 100% rename from packages/fabric/core/src/files/base-file.ts rename to packages/fabric/domain/src/files/base-file.ts diff --git a/packages/fabric/core/src/files/bytes.ts b/packages/fabric/domain/src/files/bytes.ts similarity index 100% rename from packages/fabric/core/src/files/bytes.ts rename to packages/fabric/domain/src/files/bytes.ts diff --git a/packages/fabric/core/src/domain/entity/files/image-file.ts b/packages/fabric/domain/src/files/image-file.ts similarity index 72% rename from packages/fabric/core/src/domain/entity/files/image-file.ts rename to packages/fabric/domain/src/files/image-file.ts index 66a17c8..76d772c 100644 --- a/packages/fabric/core/src/domain/entity/files/image-file.ts +++ b/packages/fabric/domain/src/files/image-file.ts @@ -1,4 +1,4 @@ -import { ImageMimeType } from "../../../files/mime-type.js"; +import { ImageMimeType } from "./mime-type.js"; import { StoredFile } from "./stored-file.js"; /** diff --git a/packages/fabric/core/src/files/in-memory-file.ts b/packages/fabric/domain/src/files/in-memory-file.ts similarity index 74% rename from packages/fabric/core/src/files/in-memory-file.ts rename to packages/fabric/domain/src/files/in-memory-file.ts index 8c25367..4ec31ed 100644 --- a/packages/fabric/core/src/files/in-memory-file.ts +++ b/packages/fabric/domain/src/files/in-memory-file.ts @@ -1,4 +1,4 @@ -import { Base64String } from "../domain/types/base-64.js"; +import { Base64String } from "../types/base-64.js"; import { BaseFile } from "./base-file.js"; /** diff --git a/packages/fabric/core/src/files/index.ts b/packages/fabric/domain/src/files/index.ts similarity index 80% rename from packages/fabric/core/src/files/index.ts rename to packages/fabric/domain/src/files/index.ts index 3468cb7..381360a 100644 --- a/packages/fabric/core/src/files/index.ts +++ b/packages/fabric/domain/src/files/index.ts @@ -1,8 +1,10 @@ export * from "./base-file.js"; export * from "./bytes.js"; +export * from "./image-file.js"; export * from "./in-memory-file.js"; export * from "./invalid-file-type-error.js"; export * from "./is-in-memory-file.js"; export * from "./is-mime-type.js"; export * from "./media-file.js"; export * from "./mime-type.js"; +export * from "./stored-file.js"; diff --git a/packages/fabric/core/src/files/invalid-file-type-error.ts b/packages/fabric/domain/src/files/invalid-file-type-error.ts similarity index 71% rename from packages/fabric/core/src/files/invalid-file-type-error.ts rename to packages/fabric/domain/src/files/invalid-file-type-error.ts index 5bc5f11..66e63bb 100644 --- a/packages/fabric/core/src/files/invalid-file-type-error.ts +++ b/packages/fabric/domain/src/files/invalid-file-type-error.ts @@ -1,4 +1,4 @@ -import { TaggedError } from "../error/tagged-error.js"; +import { TaggedError } from "@fabric/core"; export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> { constructor() { diff --git a/packages/fabric/core/src/files/is-in-memory-file.ts b/packages/fabric/domain/src/files/is-in-memory-file.ts similarity index 93% rename from packages/fabric/core/src/files/is-in-memory-file.ts rename to packages/fabric/domain/src/files/is-in-memory-file.ts index 3adfc31..c3a07f0 100644 --- a/packages/fabric/core/src/files/is-in-memory-file.ts +++ b/packages/fabric/domain/src/files/is-in-memory-file.ts @@ -1,5 +1,5 @@ +import { isRecord } from "@fabric/core"; import validator from "validator"; -import { isRecord } from "../record/is-record.js"; import { InMemoryFile } from "./in-memory-file.js"; const { isBase64, isMimeType } = validator; diff --git a/packages/fabric/core/src/files/is-mime-type.spec.ts b/packages/fabric/domain/src/files/is-mime-type.spec.ts similarity index 100% rename from packages/fabric/core/src/files/is-mime-type.spec.ts rename to packages/fabric/domain/src/files/is-mime-type.spec.ts diff --git a/packages/fabric/core/src/files/is-mime-type.ts b/packages/fabric/domain/src/files/is-mime-type.ts similarity index 100% rename from packages/fabric/core/src/files/is-mime-type.ts rename to packages/fabric/domain/src/files/is-mime-type.ts diff --git a/packages/fabric/core/src/files/media-file.ts b/packages/fabric/domain/src/files/media-file.ts similarity index 74% rename from packages/fabric/core/src/files/media-file.ts rename to packages/fabric/domain/src/files/media-file.ts index 5c86a4e..9f1e4ce 100644 --- a/packages/fabric/core/src/files/media-file.ts +++ b/packages/fabric/domain/src/files/media-file.ts @@ -1,4 +1,4 @@ -import { StoredFile } from "../domain/entity/files/stored-file.js"; +import { StoredFile } from "./stored-file.js"; /** * Represents a media file, either an image, a video or an audio file. diff --git a/packages/fabric/core/src/files/mime-type.ts b/packages/fabric/domain/src/files/mime-type.ts similarity index 100% rename from packages/fabric/core/src/files/mime-type.ts rename to packages/fabric/domain/src/files/mime-type.ts diff --git a/packages/fabric/core/src/domain/entity/files/stored-file.ts b/packages/fabric/domain/src/files/stored-file.ts similarity index 57% rename from packages/fabric/core/src/domain/entity/files/stored-file.ts rename to packages/fabric/domain/src/files/stored-file.ts index 4ed5ff1..dbc71da 100644 --- a/packages/fabric/core/src/domain/entity/files/stored-file.ts +++ b/packages/fabric/domain/src/files/stored-file.ts @@ -1,5 +1,5 @@ -import { BaseFile } from "../../../files/base-file.js"; -import { Entity } from "../entity.js"; +import { Entity } from "../types/entity.js"; +import { BaseFile } from "./base-file.js"; /** * Represents a file as managed by the domain. diff --git a/packages/fabric/domain/src/index.ts b/packages/fabric/domain/src/index.ts new file mode 100644 index 0000000..8d7b7da --- /dev/null +++ b/packages/fabric/domain/src/index.ts @@ -0,0 +1,9 @@ +export * from "./errors/index.js"; +export * from "./events/index.js"; +export * from "./files/index.js"; +export * from "./models/index.js"; +export * from "./query/index.js"; +export * from "./security/index.js"; +export * from "./storage/index.js"; +export * from "./types/index.js"; +export * from "./use-case/index.js"; diff --git a/packages/fabric/core/src/domain/models/fields/base-field.ts b/packages/fabric/domain/src/models/fields/base-field.ts similarity index 100% rename from packages/fabric/core/src/domain/models/fields/base-field.ts rename to packages/fabric/domain/src/models/fields/base-field.ts diff --git a/packages/fabric/core/src/domain/models/fields/field-to-type.spec.ts b/packages/fabric/domain/src/models/fields/field-to-type.spec.ts similarity index 100% rename from packages/fabric/core/src/domain/models/fields/field-to-type.spec.ts rename to packages/fabric/domain/src/models/fields/field-to-type.spec.ts diff --git a/packages/fabric/core/src/domain/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts similarity index 100% rename from packages/fabric/core/src/domain/models/fields/field-to-type.ts rename to packages/fabric/domain/src/models/fields/field-to-type.ts diff --git a/packages/fabric/core/src/domain/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts similarity index 100% rename from packages/fabric/core/src/domain/models/fields/index.ts rename to packages/fabric/domain/src/models/fields/index.ts diff --git a/packages/fabric/core/src/domain/models/fields/integer.ts b/packages/fabric/domain/src/models/fields/integer.ts similarity index 85% rename from packages/fabric/core/src/domain/models/fields/integer.ts rename to packages/fabric/domain/src/models/fields/integer.ts index abfef7a..48b9a69 100644 --- a/packages/fabric/core/src/domain/models/fields/integer.ts +++ b/packages/fabric/domain/src/models/fields/integer.ts @@ -1,4 +1,4 @@ -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { TaggedVariant, VariantTag } from "@fabric/core"; import { BaseField } from "./base-field.js"; export interface IntegerFieldOptions extends BaseField { diff --git a/packages/fabric/core/src/domain/models/fields/reference-field.spec.ts b/packages/fabric/domain/src/models/fields/reference-field.spec.ts similarity index 98% rename from packages/fabric/core/src/domain/models/fields/reference-field.spec.ts rename to packages/fabric/domain/src/models/fields/reference-field.spec.ts index 28e37b7..cbd58f7 100644 --- a/packages/fabric/core/src/domain/models/fields/reference-field.spec.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.spec.ts @@ -1,5 +1,5 @@ +import { isError } from "@fabric/core"; import { describe, expect, it } from "vitest"; -import { isError } from "../../../error/is-error.js"; import { defineModel } from "../model.js"; import { Field } from "./index.js"; import { diff --git a/packages/fabric/core/src/domain/models/fields/reference-field.ts b/packages/fabric/domain/src/models/fields/reference-field.ts similarity index 89% rename from packages/fabric/core/src/domain/models/fields/reference-field.ts rename to packages/fabric/domain/src/models/fields/reference-field.ts index dd93f52..3d07b90 100644 --- a/packages/fabric/core/src/domain/models/fields/reference-field.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.ts @@ -1,6 +1,4 @@ -import { TaggedError } from "../../../error/tagged-error.js"; -import { Result } from "../../../result/result.js"; -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { Result, TaggedError, TaggedVariant, VariantTag } from "@fabric/core"; import { ModelSchema } from "../model-schema.js"; import { BaseField } from "./base-field.js"; diff --git a/packages/fabric/core/src/domain/models/fields/string-field.ts b/packages/fabric/domain/src/models/fields/string-field.ts similarity index 85% rename from packages/fabric/core/src/domain/models/fields/string-field.ts rename to packages/fabric/domain/src/models/fields/string-field.ts index b15f2e5..2df7b35 100644 --- a/packages/fabric/core/src/domain/models/fields/string-field.ts +++ b/packages/fabric/domain/src/models/fields/string-field.ts @@ -1,4 +1,4 @@ -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { TaggedVariant, VariantTag } from "@fabric/core"; import { BaseField } from "./base-field.js"; export interface StringFieldOptions extends BaseField { diff --git a/packages/fabric/core/src/domain/models/fields/uuid-field.ts b/packages/fabric/domain/src/models/fields/uuid-field.ts similarity index 84% rename from packages/fabric/core/src/domain/models/fields/uuid-field.ts rename to packages/fabric/domain/src/models/fields/uuid-field.ts index 7eb717b..f4bcafc 100644 --- a/packages/fabric/core/src/domain/models/fields/uuid-field.ts +++ b/packages/fabric/domain/src/models/fields/uuid-field.ts @@ -1,4 +1,4 @@ -import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { TaggedVariant, VariantTag } from "@fabric/core"; import { BaseField } from "./base-field.js"; export interface UUIDFieldOptions extends BaseField { diff --git a/packages/fabric/core/src/domain/models/index.ts b/packages/fabric/domain/src/models/index.ts similarity index 100% rename from packages/fabric/core/src/domain/models/index.ts rename to packages/fabric/domain/src/models/index.ts diff --git a/packages/fabric/core/src/domain/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts similarity index 100% rename from packages/fabric/core/src/domain/models/model-schema.ts rename to packages/fabric/domain/src/models/model-schema.ts diff --git a/packages/fabric/core/src/domain/models/model.spec.ts b/packages/fabric/domain/src/models/model.spec.ts similarity index 100% rename from packages/fabric/core/src/domain/models/model.spec.ts rename to packages/fabric/domain/src/models/model.spec.ts diff --git a/packages/fabric/core/src/domain/models/model.ts b/packages/fabric/domain/src/models/model.ts similarity index 100% rename from packages/fabric/core/src/domain/models/model.ts rename to packages/fabric/domain/src/models/model.ts diff --git a/packages/fabric/core/src/domain/models/types/index.ts b/packages/fabric/domain/src/models/types/index.ts similarity index 100% rename from packages/fabric/core/src/domain/models/types/index.ts rename to packages/fabric/domain/src/models/types/index.ts diff --git a/packages/fabric/core/src/domain/models/types/model-field-names.ts b/packages/fabric/domain/src/models/types/model-field-names.ts similarity index 100% rename from packages/fabric/core/src/domain/models/types/model-field-names.ts rename to packages/fabric/domain/src/models/types/model-field-names.ts diff --git a/packages/fabric/core/src/domain/models/types/model-to-type.ts b/packages/fabric/domain/src/models/types/model-to-type.ts similarity index 100% rename from packages/fabric/core/src/domain/models/types/model-to-type.ts rename to packages/fabric/domain/src/models/types/model-to-type.ts diff --git a/packages/fabric/domain/src/query/aggregate-options.ts b/packages/fabric/domain/src/query/aggregate-options.ts new file mode 100644 index 0000000..361d650 --- /dev/null +++ b/packages/fabric/domain/src/query/aggregate-options.ts @@ -0,0 +1,24 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Keyof, TaggedVariant } from "@fabric/core"; + +export type AggregateOptions = Record>; + +export type AggregateFn = CountAggregate; + +export interface CountAggregate extends TaggedVariant<"AggregateCount"> { + field: Keyof; +} +export interface SumAggregate extends TaggedVariant<"AggregateSum"> { + field: Keyof; +} +export interface AvgAggregate extends TaggedVariant<"AggregateAvg"> { + field: Keyof; +} + +export interface MinAggregate extends TaggedVariant<"AggregateMin"> { + field: Keyof; +} +export interface MaxAggregate extends TaggedVariant<"AggregateMax"> { + field: Keyof; +} diff --git a/packages/fabric/core/src/storage/query/filter-options.ts b/packages/fabric/domain/src/query/filter-options.ts similarity index 100% rename from packages/fabric/core/src/storage/query/filter-options.ts rename to packages/fabric/domain/src/query/filter-options.ts diff --git a/packages/fabric/core/src/storage/query/index.ts b/packages/fabric/domain/src/query/index.ts similarity index 100% rename from packages/fabric/core/src/storage/query/index.ts rename to packages/fabric/domain/src/query/index.ts diff --git a/packages/fabric/core/src/storage/query/order-by-options.ts b/packages/fabric/domain/src/query/order-by-options.ts similarity index 100% rename from packages/fabric/core/src/storage/query/order-by-options.ts rename to packages/fabric/domain/src/query/order-by-options.ts diff --git a/packages/fabric/core/src/storage/query/query-builder.ts b/packages/fabric/domain/src/query/query-builder.ts similarity index 77% rename from packages/fabric/core/src/storage/query/query-builder.ts rename to packages/fabric/domain/src/query/query-builder.ts index bedd4c6..94df580 100644 --- a/packages/fabric/core/src/storage/query/query-builder.ts +++ b/packages/fabric/domain/src/query/query-builder.ts @@ -1,9 +1,9 @@ -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 { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; -import { StorageDriver } from "../storage-driver.js"; +import { ModelToType } from "../models/index.js"; +import { ModelSchema } from "../models/model-schema.js"; +import { StorageDriver } from "../storage/storage-driver.js"; +import { AggregateOptions } from "./aggregate-options.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; import { @@ -24,6 +24,9 @@ export class QueryBuilder< private driver: StorageDriver, private query: QueryDefinition, ) {} + aggregate>(): SelectableQuery { + throw new Error("Method not implemented."); + } where(where: FilterOptions): StoreSortableQuery { this.query = { diff --git a/packages/fabric/core/src/storage/query/query.ts b/packages/fabric/domain/src/query/query.ts similarity index 91% rename from packages/fabric/core/src/storage/query/query.ts rename to packages/fabric/domain/src/query/query.ts index 608b7af..e55b0dd 100644 --- a/packages/fabric/core/src/storage/query/query.ts +++ b/packages/fabric/domain/src/query/query.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AsyncResult } from "../../result/async-result.js"; -import { Keyof } from "../../types/keyof.js"; +import { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; +import { AggregateOptions } from "./aggregate-options.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; @@ -10,6 +10,8 @@ export interface StoreQuery { orderBy(opts: OrderByOptions): StoreLimitableQuery; limit(limit: number, offset?: number): SelectableQuery; + aggregate>(opts: K): SelectableQuery; + select(): AsyncResult; select>( keys: K[], diff --git a/packages/fabric/core/src/domain/security/index.ts b/packages/fabric/domain/src/security/index.ts similarity index 100% rename from packages/fabric/core/src/domain/security/index.ts rename to packages/fabric/domain/src/security/index.ts diff --git a/packages/fabric/core/src/domain/security/policy.ts b/packages/fabric/domain/src/security/policy.ts similarity index 100% rename from packages/fabric/core/src/domain/security/policy.ts rename to packages/fabric/domain/src/security/policy.ts diff --git a/packages/fabric/core/src/storage/event-store.ts b/packages/fabric/domain/src/storage/event-store.ts similarity index 66% rename from packages/fabric/core/src/storage/event-store.ts rename to packages/fabric/domain/src/storage/event-store.ts index c0fe1d3..6918397 100644 --- a/packages/fabric/core/src/storage/event-store.ts +++ b/packages/fabric/domain/src/storage/event-store.ts @@ -1,9 +1,7 @@ -import { Event } from "../domain/events/event.js"; -import { UUID } from "../domain/index.js"; -import { AsyncResult } from "../result/async-result.js"; -import { PosixDate } from "../time/posix-date.js"; -import { MaybePromise } from "../types/maybe-promise.js"; -import { StoreQueryError } from "./errors/query-error.js"; +import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; +import { StoreQueryError } from "../errors/query-error.js"; +import { Event, StoredEvent } from "../events/event.js"; +import { UUID } from "../types/uuid.js"; export interface EventStore { getStream( @@ -36,8 +34,3 @@ export interface EventFilterOptions { limit?: number; offset?: number; } - -export type StoredEvent = TEvent & { - version: bigint; - timestamp: number; -}; diff --git a/packages/fabric/core/src/storage/index.ts b/packages/fabric/domain/src/storage/index.ts similarity index 60% rename from packages/fabric/core/src/storage/index.ts rename to packages/fabric/domain/src/storage/index.ts index a22b9df..cab4965 100644 --- a/packages/fabric/core/src/storage/index.ts +++ b/packages/fabric/domain/src/storage/index.ts @@ -1,5 +1,3 @@ -export * from "./errors/index.js"; export * from "./event-store.js"; -export * from "./query/index.js"; export * from "./state-store.js"; export * from "./storage-driver.js"; diff --git a/packages/fabric/domain/src/storage/state-store.ts b/packages/fabric/domain/src/storage/state-store.ts new file mode 100644 index 0000000..9df3acc --- /dev/null +++ b/packages/fabric/domain/src/storage/state-store.ts @@ -0,0 +1,18 @@ +import { Keyof } from "@fabric/core"; +import { ModelToType } from "../models/index.js"; +import { ModelSchema } from "../models/model-schema.js"; +import { QueryBuilder } from "../query/query-builder.js"; +import { StoreQuery } from "../query/query.js"; +import { StorageDriver } from "./storage-driver.js"; + +export class StateStore { + constructor(private driver: StorageDriver) {} + + from>( + entityName: TEntityName, + ): StoreQuery> { + return new QueryBuilder(this.driver, { + from: entityName, + }) as StoreQuery>; + } +} diff --git a/packages/fabric/core/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts similarity index 76% rename from packages/fabric/core/src/storage/storage-driver.ts rename to packages/fabric/domain/src/storage/storage-driver.ts index f80309a..02269da 100644 --- a/packages/fabric/core/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -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"; -import { StoreQueryError } from "./errors/query-error.js"; -import { QueryDefinition } from "./query/query.js"; +import { AsyncResult, UnexpectedError } from "@fabric/core"; +import { CircularDependencyError } from "../errors/circular-dependency-error.js"; +import { StoreQueryError } from "../errors/query-error.js"; +import { ModelSchema } from "../models/model-schema.js"; +import { QueryDefinition } from "../query/query.js"; export interface StorageDriver { /** diff --git a/packages/fabric/core/src/domain/types/base-64.ts b/packages/fabric/domain/src/types/base-64.ts similarity index 100% rename from packages/fabric/core/src/domain/types/base-64.ts rename to packages/fabric/domain/src/types/base-64.ts diff --git a/packages/fabric/core/src/domain/types/email.ts b/packages/fabric/domain/src/types/email.ts similarity index 100% rename from packages/fabric/core/src/domain/types/email.ts rename to packages/fabric/domain/src/types/email.ts diff --git a/packages/fabric/core/src/domain/entity/entity.ts b/packages/fabric/domain/src/types/entity.ts similarity index 83% rename from packages/fabric/core/src/domain/entity/entity.ts rename to packages/fabric/domain/src/types/entity.ts index 17d240e..7b2ffab 100644 --- a/packages/fabric/core/src/domain/entity/entity.ts +++ b/packages/fabric/domain/src/types/entity.ts @@ -1,4 +1,4 @@ -import { UUID } from "../types/uuid.js"; +import { UUID } from "./uuid.js"; /** * An entity is a domain object that is defined by its identity. diff --git a/packages/fabric/core/src/domain/types/index.ts b/packages/fabric/domain/src/types/index.ts similarity index 58% rename from packages/fabric/core/src/domain/types/index.ts rename to packages/fabric/domain/src/types/index.ts index ea45bfc..380f939 100644 --- a/packages/fabric/core/src/domain/types/index.ts +++ b/packages/fabric/domain/src/types/index.ts @@ -1,3 +1,5 @@ +export * from "./base-64.js"; export * from "./email.js"; +export * from "./entity.js"; export * from "./semver.js"; export * from "./uuid.js"; diff --git a/packages/fabric/core/src/domain/types/semver.ts b/packages/fabric/domain/src/types/semver.ts similarity index 100% rename from packages/fabric/core/src/domain/types/semver.ts rename to packages/fabric/domain/src/types/semver.ts diff --git a/packages/fabric/core/src/domain/types/uuid.ts b/packages/fabric/domain/src/types/uuid.ts similarity index 100% rename from packages/fabric/core/src/domain/types/uuid.ts rename to packages/fabric/domain/src/types/uuid.ts diff --git a/packages/fabric/core/src/domain/use-case/index.ts b/packages/fabric/domain/src/use-case/index.ts similarity index 100% rename from packages/fabric/core/src/domain/use-case/index.ts rename to packages/fabric/domain/src/use-case/index.ts diff --git a/packages/fabric/core/src/domain/use-case/use-case-definition.ts b/packages/fabric/domain/src/use-case/use-case-definition.ts similarity index 91% rename from packages/fabric/core/src/domain/use-case/use-case-definition.ts rename to packages/fabric/domain/src/use-case/use-case-definition.ts index 85c35dd..b7e9768 100644 --- a/packages/fabric/core/src/domain/use-case/use-case-definition.ts +++ b/packages/fabric/domain/src/use-case/use-case-definition.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { TaggedError } from "../../error/tagged-error.js"; +import { TaggedError } from "@fabric/core"; import { UseCase } from "./use-case.js"; export type UseCaseDefinition< diff --git a/packages/fabric/core/src/domain/use-case/use-case.ts b/packages/fabric/domain/src/use-case/use-case.ts similarity index 75% rename from packages/fabric/core/src/domain/use-case/use-case.ts rename to packages/fabric/domain/src/use-case/use-case.ts index 54610a5..de67136 100644 --- a/packages/fabric/core/src/domain/use-case/use-case.ts +++ b/packages/fabric/domain/src/use-case/use-case.ts @@ -1,5 +1,4 @@ -import { TaggedError } from "../../error/tagged-error.js"; -import { AsyncResult } from "../../result/async-result.js"; +import { AsyncResult, TaggedError } from "@fabric/core"; /** * A use case is a piece of domain logic that can be executed. diff --git a/packages/fabric/domain/src/utils/index.ts b/packages/fabric/domain/src/utils/index.ts new file mode 100644 index 0000000..245b7df --- /dev/null +++ b/packages/fabric/domain/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./sort-by-dependencies.js"; diff --git a/packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts similarity index 100% rename from packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts rename to packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts diff --git a/packages/fabric/core/src/storage/utils/sort-by-dependencies.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.ts similarity index 95% rename from packages/fabric/core/src/storage/utils/sort-by-dependencies.ts rename to packages/fabric/domain/src/utils/sort-by-dependencies.ts index 17bf35e..8a7d2c9 100644 --- a/packages/fabric/core/src/storage/utils/sort-by-dependencies.ts +++ b/packages/fabric/domain/src/utils/sort-by-dependencies.ts @@ -1,4 +1,4 @@ -import { Result } from "../../result/result.js"; +import { Result } from "@fabric/core"; import { CircularDependencyError } from "../errors/circular-dependency-error.js"; export function sortByDependencies( diff --git a/packages/fabric/domain/tsconfig.build.json b/packages/fabric/domain/tsconfig.build.json new file mode 100644 index 0000000..7706c0e --- /dev/null +++ b/packages/fabric/domain/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "allowImportingTsExtensions": false, + "outDir": "dist" + }, + "exclude": [ + "src/**/*.spec.ts", + "dist", + "node_modules", + "coverage", + "vitest.config.ts" + ] +} diff --git a/packages/fabric/domain/tsconfig.json b/packages/fabric/domain/tsconfig.json new file mode 100644 index 0000000..7a7fde8 --- /dev/null +++ b/packages/fabric/domain/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "exclude": ["dist", "node_modules"] +} diff --git a/packages/fabric/domain/vitest.config.ts b/packages/fabric/domain/vitest.config.ts new file mode 100644 index 0000000..f1362e1 --- /dev/null +++ b/packages/fabric/domain/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + coverage: { + exclude: ["**/index.ts"], + }, + passWithNoTests: true, + }, +}); diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/store-sqlite/package.json index 0e25aba..b89b03f 100644 --- a/packages/fabric/store-sqlite/package.json +++ b/packages/fabric/store-sqlite/package.json @@ -13,7 +13,8 @@ "vitest": "^2.1.1" }, "dependencies": { - "@ulthar/fabric-core": "workspace:^", + "@fabric/core": "workspace:^", + "@fabric/domain": "workspace:^", "sqlite3": "^5.1.7" }, "scripts": { diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index e596463..2859b6c 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -1,10 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { - BaseField, - FieldDefinition, - ModelDefinition, - VariantTag, -} from "@ulthar/fabric-core"; +import { VariantTag } from "@fabric/core"; +import { BaseField, FieldDefinition, Model } from "@fabric/domain"; type FieldMap = { [K in FieldDefinition[VariantTag]]: ( @@ -23,6 +19,12 @@ const FieldMap: FieldMap = { modifiersFromOpts(f), ].join(" "); }, + IntegerField: function (): string { + throw new Error("Function not implemented."); + }, + ReferenceField: function (): string { + throw new Error("Function not implemented."); + }, }; function modifiersFromOpts(options: BaseField) { @@ -37,7 +39,7 @@ function fieldDefinitionToSQL(field: FieldDefinition) { } export function modelToSql( - model: ModelDefinition>, + model: Model>, ) { return Object.entries(model.fields) .map(([name, type]) => `${name} ${fieldDefinitionToSQL(type)}`) diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 4331d9f..cd71e6c 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -1,4 +1,4 @@ -import { createModel, Field, isError } from "@ulthar/fabric-core"; +import { createModel, Field, isError } from "@fabric/core"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { SQLiteStorageDriver } from "./sqlite-driver.js"; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 25e8b26..d900b00 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -1,15 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { AsyncResult, UnexpectedError } from "@fabric/core"; +import { unlink } from "fs/promises"; + import { - AsyncResult, CircularDependencyError, - ModelDefinition, QueryDefinition, StorageDriver, StoreQueryError, - UnexpectedError, -} from "@ulthar/fabric-core"; -import { unlink } from "fs/promises"; - +} from "@fabric/domain"; import { Database, Statement } from "sqlite3"; import { modelToSql } from "./model-to-sql.js"; import { diff --git a/packages/templates/domain/package.json b/packages/templates/domain/package.json index b55616f..191826f 100644 --- a/packages/templates/domain/package.json +++ b/packages/templates/domain/package.json @@ -13,7 +13,8 @@ "vitest": "^2.1.1" }, "dependencies": { - "@ulthar/fabric-core": "workspace:^" + "@fabric/core": "workspace:^", + "@fabric/domain": "workspace:^" }, "scripts": { "test": "vitest", diff --git a/packages/templates/domain/src/security/permission.ts b/packages/templates/domain/src/security/permission.ts index a40047a..0f5c88a 100644 --- a/packages/templates/domain/src/security/permission.ts +++ b/packages/templates/domain/src/security/permission.ts @@ -1,4 +1,4 @@ -import { EnumToValues } from "@ulthar/fabric-core"; +import { EnumToType } from "@fabric/core"; /** * A permission is a string that represents a something that a user is allowed to do in the system. It should be in the form of: `ACTION_ENTITY`. @@ -7,4 +7,4 @@ import { EnumToValues } from "@ulthar/fabric-core"; */ export const Permission = {} as const; -export type Permission = EnumToValues; +export type Permission = EnumToType; diff --git a/packages/templates/domain/src/security/policy.ts b/packages/templates/domain/src/security/policy.ts index c6310f0..b70b6dd 100644 --- a/packages/templates/domain/src/security/policy.ts +++ b/packages/templates/domain/src/security/policy.ts @@ -1,4 +1,4 @@ -import { Policy } from "@ulthar/fabric-core"; +import { Policy } from "@fabric/domain"; import { Permission } from "./permission.js"; import { UserType } from "./users.js"; diff --git a/packages/templates/domain/src/security/users.ts b/packages/templates/domain/src/security/users.ts index 35ebcdf..301665a 100644 --- a/packages/templates/domain/src/security/users.ts +++ b/packages/templates/domain/src/security/users.ts @@ -1,4 +1,4 @@ -import { EnumToValues } from "@ulthar/fabric-core"; +import { EnumToType } from "@fabric/core"; /** * A User Type is a string that represents a user type. @@ -8,4 +8,4 @@ export const UserType = { // ADMIN: "ADMIN", // SPECIAL_USER: "SPECIAL_USER", }; -export type UserType = EnumToValues; +export type UserType = EnumToType; diff --git a/packages/templates/domain/src/use-cases.ts b/packages/templates/domain/src/use-cases.ts index 5c861f2..5cebd66 100644 --- a/packages/templates/domain/src/use-cases.ts +++ b/packages/templates/domain/src/use-cases.ts @@ -1,4 +1,4 @@ -import { UseCaseDefinition } from "@ulthar/fabric-core"; +import { UseCaseDefinition } from "@fabric/domain"; export const UseCases = [] as const satisfies UseCaseDefinition[]; diff --git a/packages/templates/lib/package.json b/packages/templates/lib/package.json index efe2e53..8338fb0 100644 --- a/packages/templates/lib/package.json +++ b/packages/templates/lib/package.json @@ -13,7 +13,7 @@ "vitest": "^2.1.1" }, "dependencies": { - "@ulthar/fabric-core": "workspace:^" + "@fabric/core": "workspace:^" }, "scripts": { "test": "vitest", diff --git a/yarn.lock b/yarn.lock index abc51d4..b4d4b7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -403,6 +403,27 @@ __metadata: languageName: node linkType: hard +"@fabric/core@workspace:^, @fabric/core@workspace:packages/fabric/core": + version: 0.0.0-use.local + resolution: "@fabric/core@workspace:packages/fabric/core" + dependencies: + "@types/validator": "npm:^13.12.2" + typescript: "npm:^5.6.2" + validator: "npm:^13.12.0" + vitest: "npm:^2.1.1" + languageName: unknown + linkType: soft + +"@fabric/domain@workspace:^, @fabric/domain@workspace:packages/fabric/domain": + version: 0.0.0-use.local + resolution: "@fabric/domain@workspace:packages/fabric/domain" + dependencies: + "@fabric/core": "workspace:^" + typescript: "npm:^5.6.2" + vitest: "npm:^2.1.1" + languageName: unknown + linkType: soft + "@gar/promisify@npm:^1.0.1": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -824,22 +845,11 @@ __metadata: languageName: node linkType: hard -"@ulthar/fabric-core@workspace:^, @ulthar/fabric-core@workspace:packages/fabric/core": - version: 0.0.0-use.local - resolution: "@ulthar/fabric-core@workspace:packages/fabric/core" - dependencies: - "@types/validator": "npm:^13.12.2" - typescript: "npm:^5.6.2" - validator: "npm:^13.12.0" - vitest: "npm:^2.1.1" - languageName: unknown - linkType: soft - "@ulthar/lib-template@workspace:packages/templates/lib": version: 0.0.0-use.local resolution: "@ulthar/lib-template@workspace:packages/templates/lib" dependencies: - "@ulthar/fabric-core": "workspace:^" + "@fabric/core": "workspace:^" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown @@ -849,7 +859,8 @@ __metadata: version: 0.0.0-use.local resolution: "@ulthar/store-sqlite@workspace:packages/fabric/store-sqlite" dependencies: - "@ulthar/fabric-core": "workspace:^" + "@fabric/core": "workspace:^" + "@fabric/domain": "workspace:^" sqlite3: "npm:^5.1.7" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" @@ -860,7 +871,7 @@ __metadata: version: 0.0.0-use.local resolution: "@ulthar/template-domain@workspace:packages/templates/domain" dependencies: - "@ulthar/fabric-core": "workspace:^" + "@fabric/core": "workspace:^" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown From 3f91e35790be6e3d7111295121441e492e03a263 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Fri, 4 Oct 2024 11:07:57 -0300 Subject: [PATCH 05/37] [fabric/core] Add isVariant utility function --- .../core/src/variant/is-variant.spec.ts | 40 +++++++++++++++++++ .../fabric/core/src/variant/is-variant.ts | 11 +++++ 2 files changed, 51 insertions(+) create mode 100644 packages/fabric/core/src/variant/is-variant.spec.ts create mode 100644 packages/fabric/core/src/variant/is-variant.ts diff --git a/packages/fabric/core/src/variant/is-variant.spec.ts b/packages/fabric/core/src/variant/is-variant.spec.ts new file mode 100644 index 0000000..7632352 --- /dev/null +++ b/packages/fabric/core/src/variant/is-variant.spec.ts @@ -0,0 +1,40 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import { isVariant } from "./is-variant.js"; +import { TaggedVariant, VariantTag } from "./variant.js"; + +interface SuccessVariant extends TaggedVariant<"success"> { + [VariantTag]: "success"; + data: string; +} + +interface ErrorVariant extends TaggedVariant<"error"> { + [VariantTag]: "error"; + message: string; +} + +describe("isVariant", () => { + const successVariant = { + [VariantTag]: "success", + data: "Operation successful", + } as SuccessVariant | ErrorVariant; + + const errorVariant = { + [VariantTag]: "error", + message: "Operation failed", + } as SuccessVariant | ErrorVariant; + + it("should return true for a matching tag and correctly infer it", () => { + if (isVariant(successVariant, "success")) { + expectTypeOf(successVariant).toEqualTypeOf(); + } + + if (isVariant(errorVariant, "error")) { + expectTypeOf(errorVariant).toEqualTypeOf(); + } + }); + + it("should return false for a non-matching tag", () => { + expect(isVariant(successVariant, "error")).toBe(false); + expect(isVariant(errorVariant, "success")).toBe(false); + }); +}); diff --git a/packages/fabric/core/src/variant/is-variant.ts b/packages/fabric/core/src/variant/is-variant.ts new file mode 100644 index 0000000..c4f86e1 --- /dev/null +++ b/packages/fabric/core/src/variant/is-variant.ts @@ -0,0 +1,11 @@ +import { TaggedVariant, VariantTag } from "./variant.js"; + +export function isVariant< + TVariant extends TaggedVariant, + TTag extends TVariant[VariantTag], +>( + variant: TVariant, + tag: TTag, +): variant is Extract { + return variant[VariantTag] === tag; +} From 9092b032b350dac91d3f1dc5c851f0b948ce8750 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Fri, 4 Oct 2024 11:10:29 -0300 Subject: [PATCH 06/37] [fabric/domain] improve structure --- .../src/{storage => events}/event-store.ts | 2 +- packages/fabric/domain/src/events/index.ts | 1 + packages/fabric/domain/src/index.ts | 1 - .../src/models/fields/reference-field.spec.ts | 61 +++++++------------ .../src/models/fields/reference-field.ts | 35 ++++------- packages/fabric/domain/src/models/index.ts | 3 +- .../fabric/domain/src/models/model.spec.ts | 5 +- packages/fabric/domain/src/models/model.ts | 36 ++++++++--- .../{ => models}/query/aggregate-options.ts | 0 .../src/{ => models}/query/filter-options.ts | 6 +- .../domain/src/{ => models}/query/index.ts | 0 .../{ => models}/query/order-by-options.ts | 0 .../src/{ => models}/query/query-builder.ts | 35 +++-------- .../domain/src/{ => models}/query/query.ts | 5 +- .../fabric/domain/src/models/state-store.ts | 5 ++ .../fabric/domain/src/models/types/index.ts | 2 - .../src/models/types/model-field-names.ts | 3 - .../domain/src/models/types/model-to-type.ts | 6 -- packages/fabric/domain/src/storage/index.ts | 2 - .../fabric/domain/src/storage/state-store.ts | 18 ------ .../domain/src/storage/storage-driver.ts | 2 +- 21 files changed, 88 insertions(+), 140 deletions(-) rename packages/fabric/domain/src/{storage => events}/event-store.ts (94%) rename packages/fabric/domain/src/{ => models}/query/aggregate-options.ts (100%) rename packages/fabric/domain/src/{ => models}/query/filter-options.ts (92%) rename packages/fabric/domain/src/{ => models}/query/index.ts (100%) rename packages/fabric/domain/src/{ => models}/query/order-by-options.ts (100%) rename packages/fabric/domain/src/{ => models}/query/query-builder.ts (57%) rename packages/fabric/domain/src/{ => models}/query/query.ts (91%) create mode 100644 packages/fabric/domain/src/models/state-store.ts delete mode 100644 packages/fabric/domain/src/models/types/index.ts delete mode 100644 packages/fabric/domain/src/models/types/model-field-names.ts delete mode 100644 packages/fabric/domain/src/models/types/model-to-type.ts delete mode 100644 packages/fabric/domain/src/storage/state-store.ts diff --git a/packages/fabric/domain/src/storage/event-store.ts b/packages/fabric/domain/src/events/event-store.ts similarity index 94% rename from packages/fabric/domain/src/storage/event-store.ts rename to packages/fabric/domain/src/events/event-store.ts index 6918397..c58102b 100644 --- a/packages/fabric/domain/src/storage/event-store.ts +++ b/packages/fabric/domain/src/events/event-store.ts @@ -1,7 +1,7 @@ import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; -import { Event, StoredEvent } from "../events/event.js"; import { UUID } from "../types/uuid.js"; +import { Event, StoredEvent } from "./event.js"; export interface EventStore { getStream( diff --git a/packages/fabric/domain/src/events/index.ts b/packages/fabric/domain/src/events/index.ts index 8aad408..a28ecf2 100644 --- a/packages/fabric/domain/src/events/index.ts +++ b/packages/fabric/domain/src/events/index.ts @@ -1 +1,2 @@ +export * from "./event-store.js"; export * from "./event.js"; diff --git a/packages/fabric/domain/src/index.ts b/packages/fabric/domain/src/index.ts index 8d7b7da..552b447 100644 --- a/packages/fabric/domain/src/index.ts +++ b/packages/fabric/domain/src/index.ts @@ -2,7 +2,6 @@ export * from "./errors/index.js"; export * from "./events/index.js"; export * from "./files/index.js"; export * from "./models/index.js"; -export * from "./query/index.js"; export * from "./security/index.js"; export * from "./storage/index.js"; export * from "./types/index.js"; diff --git a/packages/fabric/domain/src/models/fields/reference-field.spec.ts b/packages/fabric/domain/src/models/fields/reference-field.spec.ts index cbd58f7..d5e75b4 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.spec.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.spec.ts @@ -9,22 +9,22 @@ import { describe("Validate Reference Field", () => { const schema = { - user: defineModel({ + User: defineModel("User", { name: Field.string(), password: Field.string(), - phone: Field.string({ isOptional: true }), otherUnique: Field.integer({ isUnique: true }), otherNotUnique: Field.uuid(), + otherUser: Field.reference({ + targetModel: "User", + }), }), }; it("should return an error when the target model is not in the schema", () => { const result = validateReferenceField( schema, - "post", - "authorId", Field.reference({ - model: "foo", + targetModel: "foo", }), ); @@ -33,33 +33,26 @@ describe("Validate Reference Field", () => { } 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", + targetModel: "User", }), ); if (isError(result)) { - throw result.toString(); + throw result.reason; } }); 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", + targetModel: "User", targetKey: "foo", }), ); @@ -69,34 +62,13 @@ describe("Validate Reference Field", () => { } 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", + targetModel: "User", targetKey: "otherNotUnique", }), ); @@ -106,8 +78,19 @@ describe("Validate Reference Field", () => { } expect(result).toBeInstanceOf(InvalidReferenceField); - expect(result.toString()).toBe( - "InvalidReferenceField: post.authorId. The target key 'user'.'otherNotUnique' is not unique.", + }); + + it("should not return an error if the target key is in the target model and is unique", () => { + const result = validateReferenceField( + schema, + Field.reference({ + targetModel: "User", + targetKey: "otherUnique", + }), ); + + if (isError(result)) { + throw result.toString(); + } }); }); diff --git a/packages/fabric/domain/src/models/fields/reference-field.ts b/packages/fabric/domain/src/models/fields/reference-field.ts index 3d07b90..9dbcfc9 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.ts @@ -3,7 +3,7 @@ import { ModelSchema } from "../model-schema.js"; import { BaseField } from "./base-field.js"; export interface ReferenceFieldOptions extends BaseField { - model: string; + targetModel: string; targetKey?: string; } @@ -22,45 +22,32 @@ export function createReferenceField( export function validateReferenceField( schema: ModelSchema, - modelName: string, - fieldName: string, field: ReferenceField, ): Result { - if (!schema[field.model]) { + if (!schema[field.targetModel]) { return new InvalidReferenceField( - modelName, - fieldName, - `The target model '${field.model}' is not in the schema.`, + `The target model '${field.targetModel}' is not in the schema.`, ); } - if (field.targetKey && !schema[field.model][field.targetKey]) { + if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) { return new InvalidReferenceField( - modelName, - fieldName, - `The target key '${field.targetKey}' is not in the target model '${field.model}'.`, + `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, ); } - if (field.targetKey && !schema[field.model][field.targetKey].isUnique) { + if ( + field.targetKey && + !schema[field.targetModel].fields[field.targetKey].isUnique + ) { return new InvalidReferenceField( - modelName, - fieldName, - `The target key '${field.model}'.'${field.targetKey}' is not unique.`, + `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, ); } } export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> { - constructor( - readonly modelName: string, - readonly fieldName: string, - readonly reason: string, - ) { + constructor(readonly reason: string) { super("InvalidReferenceField"); } - - toString() { - return `InvalidReferenceField: ${this.modelName}.${this.fieldName}. ${this.reason}`; - } } diff --git a/packages/fabric/domain/src/models/index.ts b/packages/fabric/domain/src/models/index.ts index d91f9bc..bb4c9d7 100644 --- a/packages/fabric/domain/src/models/index.ts +++ b/packages/fabric/domain/src/models/index.ts @@ -1,4 +1,5 @@ export * from "./fields/index.js"; export * from "./model-schema.js"; export * from "./model.js"; -export * from "./types/index.js"; +export * from "./query/index.js"; +export * from "./state-store.js"; diff --git a/packages/fabric/domain/src/models/model.spec.ts b/packages/fabric/domain/src/models/model.spec.ts index 77d5add..f3b2dd2 100644 --- a/packages/fabric/domain/src/models/model.spec.ts +++ b/packages/fabric/domain/src/models/model.spec.ts @@ -1,12 +1,11 @@ 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"; +import { defineModel, ModelToType } from "./model.js"; describe("CreateModel", () => { it("should create a model and it's interface type", () => { - const User = defineModel({ + const User = defineModel("User", { name: Field.string(), password: Field.string(), phone: Field.string({ isOptional: true }), diff --git a/packages/fabric/domain/src/models/model.ts b/packages/fabric/domain/src/models/model.ts index 38f31af..34520bb 100644 --- a/packages/fabric/domain/src/models/model.ts +++ b/packages/fabric/domain/src/models/model.ts @@ -1,3 +1,5 @@ +import { Keyof } from "@fabric/core"; +import { FieldToType } from "./fields/field-to-type.js"; import { Field, FieldDefinition } from "./fields/index.js"; export type CustomModelFields = Record; @@ -10,14 +12,34 @@ export const DefaultModelFields = { hasArbitraryPrecision: true, }), }; -export type Model = - typeof DefaultModelFields & TFields; +export interface Model< + TName extends string = string, + TFields extends CustomModelFields = CustomModelFields, +> { + name: TName; + fields: typeof DefaultModelFields & TFields; +} -export function defineModel( - fields: TFields, -): Model { +export function defineModel< + TName extends string, + TFields extends CustomModelFields, +>(name: TName, fields: TFields): Model { return { - ...fields, - ...DefaultModelFields, + name, + fields: { ...DefaultModelFields, ...fields }, } as const; } + +export type ModelToType = { + [K in Keyof]: FieldToType; +}; + +export type ModelFieldNames = Keyof< + TModel["fields"] +>; + +export type ModelAddressableFields = { + [K in Keyof]: TModel["fields"][K] extends { isUnique: true } + ? K + : never; +}[Keyof]; diff --git a/packages/fabric/domain/src/query/aggregate-options.ts b/packages/fabric/domain/src/models/query/aggregate-options.ts similarity index 100% rename from packages/fabric/domain/src/query/aggregate-options.ts rename to packages/fabric/domain/src/models/query/aggregate-options.ts diff --git a/packages/fabric/domain/src/query/filter-options.ts b/packages/fabric/domain/src/models/query/filter-options.ts similarity index 92% rename from packages/fabric/domain/src/query/filter-options.ts rename to packages/fabric/domain/src/models/query/filter-options.ts index 7a6630e..4a18f7c 100644 --- a/packages/fabric/domain/src/query/filter-options.ts +++ b/packages/fabric/domain/src/models/query/filter-options.ts @@ -14,9 +14,9 @@ export type SingleFilterOption = { export type MultiFilterOption = SingleFilterOption[]; -export const FILTER_OPTION_TYPE_SYMBOL = Symbol("$type"); -export const FILTER_OPTION_VALUE_SYMBOL = Symbol("$value"); -export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("$operator"); +export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type"); +export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value"); +export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator"); export type LikeFilterOption = T extends string ? { diff --git a/packages/fabric/domain/src/query/index.ts b/packages/fabric/domain/src/models/query/index.ts similarity index 100% rename from packages/fabric/domain/src/query/index.ts rename to packages/fabric/domain/src/models/query/index.ts diff --git a/packages/fabric/domain/src/query/order-by-options.ts b/packages/fabric/domain/src/models/query/order-by-options.ts similarity index 100% rename from packages/fabric/domain/src/query/order-by-options.ts rename to packages/fabric/domain/src/models/query/order-by-options.ts diff --git a/packages/fabric/domain/src/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts similarity index 57% rename from packages/fabric/domain/src/query/query-builder.ts rename to packages/fabric/domain/src/models/query/query-builder.ts index 94df580..d3f5a95 100644 --- a/packages/fabric/domain/src/query/query-builder.ts +++ b/packages/fabric/domain/src/models/query/query-builder.ts @@ -1,9 +1,6 @@ import { AsyncResult, Keyof } from "@fabric/core"; -import { StoreQueryError } from "../errors/query-error.js"; -import { ModelToType } from "../models/index.js"; -import { ModelSchema } from "../models/model-schema.js"; -import { StorageDriver } from "../storage/storage-driver.js"; -import { AggregateOptions } from "./aggregate-options.js"; +import { StoreQueryError } from "../../errors/query-error.js"; +import { StorageDriver } from "../../storage/storage-driver.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; import { @@ -14,44 +11,32 @@ import { StoreSortableQuery, } from "./query.js"; -export class QueryBuilder< - TModels extends ModelSchema, - TEntityName extends Keyof, - T = ModelToType, -> implements StoreQuery -{ +export class QueryBuilder implements StoreQuery { constructor( private driver: StorageDriver, - private query: QueryDefinition, + private query: QueryDefinition, ) {} - aggregate>(): SelectableQuery { - throw new Error("Method not implemented."); - } where(where: FilterOptions): StoreSortableQuery { - this.query = { + return new QueryBuilder(this.driver, { ...this.query, where, - }; - return this; + }); } orderBy(opts: OrderByOptions): StoreLimitableQuery { - this.query = { + return new QueryBuilder(this.driver, { ...this.query, orderBy: opts, - }; - return this; + }); } limit(limit: number, offset?: number | undefined): SelectableQuery { - this.query = { + return new QueryBuilder(this.driver, { ...this.query, limit, offset, - }; - - return this; + }); } select>( diff --git a/packages/fabric/domain/src/query/query.ts b/packages/fabric/domain/src/models/query/query.ts similarity index 91% rename from packages/fabric/domain/src/query/query.ts rename to packages/fabric/domain/src/models/query/query.ts index e55b0dd..febc5d8 100644 --- a/packages/fabric/domain/src/query/query.ts +++ b/packages/fabric/domain/src/models/query/query.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { AsyncResult, Keyof } from "@fabric/core"; -import { StoreQueryError } from "../errors/query-error.js"; -import { AggregateOptions } from "./aggregate-options.js"; +import { StoreQueryError } from "../../errors/query-error.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; @@ -10,8 +9,6 @@ export interface StoreQuery { orderBy(opts: OrderByOptions): StoreLimitableQuery; limit(limit: number, offset?: number): SelectableQuery; - aggregate>(opts: K): SelectableQuery; - select(): AsyncResult; select>( keys: K[], diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts new file mode 100644 index 0000000..d72f93b --- /dev/null +++ b/packages/fabric/domain/src/models/state-store.ts @@ -0,0 +1,5 @@ +import { StorageDriver } from "../storage/storage-driver.js"; + +export class StateStore { + constructor(private driver: StorageDriver) {} +} diff --git a/packages/fabric/domain/src/models/types/index.ts b/packages/fabric/domain/src/models/types/index.ts deleted file mode 100644 index 5d07363..0000000 --- a/packages/fabric/domain/src/models/types/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./model-field-names.js"; -export * from "./model-to-type.js"; diff --git a/packages/fabric/domain/src/models/types/model-field-names.ts b/packages/fabric/domain/src/models/types/model-field-names.ts deleted file mode 100644 index c9d1b9c..0000000 --- a/packages/fabric/domain/src/models/types/model-field-names.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { CustomModelFields } from "../model.js"; - -export type ModelFieldNames = keyof TModel; diff --git a/packages/fabric/domain/src/models/types/model-to-type.ts b/packages/fabric/domain/src/models/types/model-to-type.ts deleted file mode 100644 index 5f633cf..0000000 --- a/packages/fabric/domain/src/models/types/model-to-type.ts +++ /dev/null @@ -1,6 +0,0 @@ -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/domain/src/storage/index.ts b/packages/fabric/domain/src/storage/index.ts index cab4965..1d74826 100644 --- a/packages/fabric/domain/src/storage/index.ts +++ b/packages/fabric/domain/src/storage/index.ts @@ -1,3 +1 @@ -export * from "./event-store.js"; -export * from "./state-store.js"; export * from "./storage-driver.js"; diff --git a/packages/fabric/domain/src/storage/state-store.ts b/packages/fabric/domain/src/storage/state-store.ts deleted file mode 100644 index 9df3acc..0000000 --- a/packages/fabric/domain/src/storage/state-store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Keyof } from "@fabric/core"; -import { ModelToType } from "../models/index.js"; -import { ModelSchema } from "../models/model-schema.js"; -import { QueryBuilder } from "../query/query-builder.js"; -import { StoreQuery } from "../query/query.js"; -import { StorageDriver } from "./storage-driver.js"; - -export class StateStore { - constructor(private driver: StorageDriver) {} - - from>( - entityName: TEntityName, - ): StoreQuery> { - return new QueryBuilder(this.driver, { - from: entityName, - }) as StoreQuery>; - } -} diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 02269da..0ba690b 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -4,7 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core"; import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { ModelSchema } from "../models/model-schema.js"; -import { QueryDefinition } from "../query/query.js"; +import { QueryDefinition } from "../models/query/query.js"; export interface StorageDriver { /** From 290544dc9a99f2e6eef90af23aab0fc07a38e270 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Fri, 4 Oct 2024 11:54:35 -0300 Subject: [PATCH 07/37] [fabric/core] rename isVariant to Variant.is --- .../core/src/variant/is-variant.spec.ts | 40 ------------------ .../fabric/core/src/variant/is-variant.ts | 11 ----- .../fabric/core/src/variant/variant.spec.ts | 41 +++++++++++++++++++ packages/fabric/core/src/variant/variant.ts | 12 ++++++ 4 files changed, 53 insertions(+), 51 deletions(-) delete mode 100644 packages/fabric/core/src/variant/is-variant.spec.ts delete mode 100644 packages/fabric/core/src/variant/is-variant.ts create mode 100644 packages/fabric/core/src/variant/variant.spec.ts diff --git a/packages/fabric/core/src/variant/is-variant.spec.ts b/packages/fabric/core/src/variant/is-variant.spec.ts deleted file mode 100644 index 7632352..0000000 --- a/packages/fabric/core/src/variant/is-variant.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, expectTypeOf, it } from "vitest"; -import { isVariant } from "./is-variant.js"; -import { TaggedVariant, VariantTag } from "./variant.js"; - -interface SuccessVariant extends TaggedVariant<"success"> { - [VariantTag]: "success"; - data: string; -} - -interface ErrorVariant extends TaggedVariant<"error"> { - [VariantTag]: "error"; - message: string; -} - -describe("isVariant", () => { - const successVariant = { - [VariantTag]: "success", - data: "Operation successful", - } as SuccessVariant | ErrorVariant; - - const errorVariant = { - [VariantTag]: "error", - message: "Operation failed", - } as SuccessVariant | ErrorVariant; - - it("should return true for a matching tag and correctly infer it", () => { - if (isVariant(successVariant, "success")) { - expectTypeOf(successVariant).toEqualTypeOf(); - } - - if (isVariant(errorVariant, "error")) { - expectTypeOf(errorVariant).toEqualTypeOf(); - } - }); - - it("should return false for a non-matching tag", () => { - expect(isVariant(successVariant, "error")).toBe(false); - expect(isVariant(errorVariant, "success")).toBe(false); - }); -}); diff --git a/packages/fabric/core/src/variant/is-variant.ts b/packages/fabric/core/src/variant/is-variant.ts deleted file mode 100644 index c4f86e1..0000000 --- a/packages/fabric/core/src/variant/is-variant.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { TaggedVariant, VariantTag } from "./variant.js"; - -export function isVariant< - TVariant extends TaggedVariant, - TTag extends TVariant[VariantTag], ->( - variant: TVariant, - tag: TTag, -): variant is Extract { - return variant[VariantTag] === tag; -} diff --git a/packages/fabric/core/src/variant/variant.spec.ts b/packages/fabric/core/src/variant/variant.spec.ts new file mode 100644 index 0000000..f976a0a --- /dev/null +++ b/packages/fabric/core/src/variant/variant.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import { TaggedVariant, Variant, VariantTag } from "./variant.js"; + +interface SuccessVariant extends TaggedVariant<"success"> { + [VariantTag]: "success"; + data: string; +} + +interface ErrorVariant extends TaggedVariant<"error"> { + [VariantTag]: "error"; + message: string; +} + +describe("Variant", () => { + describe("isVariant", () => { + const successVariant = { + [VariantTag]: "success", + data: "Operation successful", + } as SuccessVariant | ErrorVariant; + + const errorVariant = { + [VariantTag]: "error", + message: "Operation failed", + } as SuccessVariant | ErrorVariant; + + it("should return true for a matching tag and correctly infer it", () => { + if (Variant.is(successVariant, "success")) { + expectTypeOf(successVariant).toEqualTypeOf(); + } + + if (Variant.is(errorVariant, "error")) { + expectTypeOf(errorVariant).toEqualTypeOf(); + } + }); + + it("should return false for a non-matching tag", () => { + expect(Variant.is(successVariant, "error")).toBe(false); + expect(Variant.is(errorVariant, "success")).toBe(false); + }); + }); +}); diff --git a/packages/fabric/core/src/variant/variant.ts b/packages/fabric/core/src/variant/variant.ts index dc1b464..12d5b47 100644 --- a/packages/fabric/core/src/variant/variant.ts +++ b/packages/fabric/core/src/variant/variant.ts @@ -9,3 +9,15 @@ export type VariantFromTag< TVariant extends TaggedVariant, TTag extends TVariant[typeof VariantTag], > = Extract; + +export namespace Variant { + export function is< + TVariant extends TaggedVariant, + TTag extends TVariant[VariantTag], + >( + variant: TVariant, + tag: TTag, + ): variant is Extract { + return variant[VariantTag] === tag; + } +} From 09f045daf68a4f96c4b9f769e5318542069229a6 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Fri, 4 Oct 2024 11:55:44 -0300 Subject: [PATCH 08/37] [fabric/domain] Add reference-field utility to get target key --- packages/fabric/domain/src/models/fields/index.ts | 1 + packages/fabric/domain/src/models/fields/reference-field.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index acdfdc2..f70e4e4 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -3,6 +3,7 @@ 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 * from "./reference-field.js"; export type FieldDefinition = | StringField diff --git a/packages/fabric/domain/src/models/fields/reference-field.ts b/packages/fabric/domain/src/models/fields/reference-field.ts index 9dbcfc9..b8272ec 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.ts @@ -20,6 +20,10 @@ export function createReferenceField( } as const; } +export function getTargetKey(field: ReferenceField): string { + return field.targetKey || "id"; +} + export function validateReferenceField( schema: ModelSchema, field: ReferenceField, From 27dbd447411f95de2bf2be43e1e8f926ecbc48f5 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Fri, 4 Oct 2024 11:56:35 -0300 Subject: [PATCH 09/37] [fabric/store-sqlite] Pass all tests for base sqlite-driver implementation --- .../fabric/store-sqlite/src/model-to-sql.ts | 45 +++++--- .../store-sqlite/src/sqlite-driver.spec.ts | 105 ++++++++++++------ .../fabric/store-sqlite/src/sqlite-driver.ts | 10 +- 3 files changed, 109 insertions(+), 51 deletions(-) diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index 2859b6c..9032b8d 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -1,47 +1,60 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { VariantTag } from "@fabric/core"; -import { BaseField, FieldDefinition, Model } from "@fabric/domain"; +import { Variant, VariantTag } from "@fabric/core"; +import { FieldDefinition, getTargetKey, Model } from "@fabric/domain"; type FieldMap = { [K in FieldDefinition[VariantTag]]: ( + name: string, field: Extract, ) => string; }; const FieldMap: FieldMap = { - StringField: (f) => { - return "TEXT" + modifiersFromOpts(f); + StringField: (n, f) => { + return [n, "TEXT", modifiersFromOpts(f)].join(" "); }, - UUIDField: (f) => { + UUIDField: (n, f) => { return [ + n, "TEXT", f.isPrimaryKey ? "PRIMARY KEY" : "", modifiersFromOpts(f), ].join(" "); }, - IntegerField: function (): string { - throw new Error("Function not implemented."); + IntegerField: function (n, f): string { + return [n, "INTEGER", modifiersFromOpts(f)].join(" "); }, - ReferenceField: function (): string { - throw new Error("Function not implemented."); + ReferenceField: function (n, f): string { + return [ + n, + "TEXT", + modifiersFromOpts(f), + ",", + `FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`, + ].join(" "); }, }; -function modifiersFromOpts(options: BaseField) { +function modifiersFromOpts(field: FieldDefinition) { + if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { + return; + } return [ - !options.isOptional ? "NOT NULL" : "", - options.isUnique ? "UNIQUE" : "", + !field.isOptional ? "NOT NULL" : "", + field.isUnique ? "UNIQUE" : "", ].join(" "); } -function fieldDefinitionToSQL(field: FieldDefinition) { - return FieldMap[field[VariantTag]](field as any); +function fieldDefinitionToSQL(name: string, field: FieldDefinition) { + return FieldMap[field[VariantTag]](name, field as any); } export function modelToSql( model: Model>, ) { - return Object.entries(model.fields) - .map(([name, type]) => `${name} ${fieldDefinitionToSQL(type)}`) + const fields = Object.entries(model.fields) + .map(([name, type]) => fieldDefinitionToSQL(name, type)) .join(", "); + + return `CREATE TABLE ${model.name} (${fields})`; } diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index cd71e6c..8dc3f8c 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -1,15 +1,14 @@ -import { createModel, Field, isError } from "@fabric/core"; +import { isError } from "@fabric/core"; +import { defineModel, Field } from "@fabric/domain"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { SQLiteStorageDriver } from "./sqlite-driver.js"; describe("SQLite Store Driver", () => { - const model = createModel({ - name: "test", - fields: { - id: Field.uuid({}), + const schema = { + users: defineModel("users", { name: Field.string(), - }, - }); + }), + }; let store: SQLiteStorageDriver; @@ -23,71 +22,115 @@ describe("SQLite Store Driver", () => { }); test("should be able to synchronize the store and insert a record", async () => { - const result = await store.sync([model]); + const result = await store.sync(schema); if (isError(result)) throw result; - await store.insert("test", { id: "1", name: "test" }); + await store.insert("users", { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); - const records = await store.select({ from: "test" }); + const records = await store.select({ from: "users" }); - expect(records).toEqual([{ id: "1", name: "test" }]); + expect(records).toEqual([ + { id: "1", name: "test", streamId: "1", streamVersion: 1 }, + ]); }); test("should be able to update a record", async () => { - const result = await store.sync([model]); + const result = await store.sync(schema); if (isError(result)) throw result; - await store.insert("test", { id: "1", name: "test" }); + await store.insert("users", { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); - await store.update("test", "1", { name: "updated" }); + await store.update("users", "1", { name: "updated" }); - const records = await store.select({ from: "test" }); + const records = await store.select({ from: "users" }); - expect(records).toEqual([{ id: "1", name: "updated" }]); + expect(records).toEqual([ + { id: "1", name: "updated", streamId: "1", streamVersion: 1 }, + ]); }); test("should be able to delete a record", async () => { - const result = await store.sync([model]); + const result = await store.sync(schema); if (isError(result)) throw result; - await store.insert("test", { id: "1", name: "test" }); + await store.insert("users", { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); - await store.delete("test", "1"); + await store.delete("users", "1"); - const records = await store.select({ from: "test" }); + const records = await store.select({ from: "users" }); expect(records).toEqual([]); }); test("should be able to select records", async () => { - const result = await store.sync([model]); + const result = await store.sync(schema); if (isError(result)) throw result; - await store.insert("test", { id: "1", name: "test" }); - await store.insert("test", { id: "2", name: "test" }); + await store.insert("users", { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); + await store.insert("users", { + id: "2", + name: "test", + streamId: "2", + streamVersion: 1, + }); - const records = await store.select({ from: "test" }); + const records = await store.select({ from: "users" }); expect(records).toEqual([ - { id: "1", name: "test" }, - { id: "2", name: "test" }, + { id: "1", name: "test", streamId: "1", streamVersion: 1 }, + { id: "2", name: "test", streamId: "2", streamVersion: 1 }, ]); }); test("should be able to select one record", async () => { - const result = await store.sync([model]); + const result = await store.sync(schema); if (isError(result)) throw result; - await store.insert("test", { id: "1", name: "test" }); - await store.insert("test", { id: "2", name: "test" }); + await store.insert("users", { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); + await store.insert("users", { + id: "2", + name: "test", + streamId: "2", + streamVersion: 1, + }); - const record = await store.selectOne({ from: "test" }); + const record = await store.selectOne({ from: "users" }); - expect(record).toEqual({ id: "1", name: "test" }); + expect(record).toEqual({ + id: "1", + name: "test", + streamId: "1", + streamVersion: 1, + }); }); }); diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index d900b00..24fa8a0 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -4,6 +4,7 @@ import { unlink } from "fs/promises"; import { CircularDependencyError, + ModelSchema, QueryDefinition, StorageDriver, StoreQueryError, @@ -35,6 +36,7 @@ export class SQLiteStorageDriver implements StorageDriver { // Enable Write-Ahead Logging, which is faster and more reliable. this.db.run("PRAGMA journal_mode= WAL;"); + this.db.run("PRAGMA foreign_keys = ON;"); } /** @@ -109,13 +111,13 @@ export class SQLiteStorageDriver implements StorageDriver { * Sincronice the store with the schema. */ async sync( - schema: ModelDefinition[], + schema: ModelSchema, ): AsyncResult { try { await dbRun(this.db, "BEGIN TRANSACTION;"); - for (const model of schema) { - const query = `CREATE TABLE ${model.name} (${modelToSql(model)});`; - await dbRun(this.db, query); + for (const modelKey in schema) { + const model = schema[modelKey]; + await dbRun(this.db, modelToSql(model)); } await dbRun(this.db, "COMMIT;"); } catch (error: any) { From f0c77398e61949d8174a379da8b717dd36169be1 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 5 Oct 2024 11:10:04 -0300 Subject: [PATCH 10/37] [fabric/domain] Add typed insertion to state-store --- packages/fabric/domain/package.json | 1 + .../domain/src/models/fields/field-to-type.ts | 9 +++- .../fabric/domain/src/models/fields/index.ts | 1 + .../fabric/domain/src/models/model-schema.ts | 4 ++ .../domain/src/models/query/query-builder.ts | 12 ++--- .../domain/src/models/state-store.spec.ts | 44 +++++++++++++++++++ .../fabric/domain/src/models/state-store.ts | 30 ++++++++++++- .../domain/src/storage/storage-driver.ts | 20 +++++---- packages/fabric/domain/src/types/uuid.ts | 4 ++ packages/fabric/store-sqlite/package.json | 2 +- .../fabric/store-sqlite/src/model-to-sql.ts | 11 +++-- .../fabric/store-sqlite/src/record-utils.ts | 10 ++++- .../fabric/store-sqlite/src/sql-to-value.ts | 37 ++++++++++++++++ .../store-sqlite/src/sqlite-driver.spec.ts | 42 +++++++++--------- .../fabric/store-sqlite/src/sqlite-driver.ts | 43 ++++++++++-------- .../fabric/store-sqlite/src/sqlite-wrapper.ts | 14 ++++-- .../fabric/store-sqlite/src/value-to-sql.ts | 26 +++++++++++ yarn.lock | 26 ++++++----- 18 files changed, 255 insertions(+), 81 deletions(-) create mode 100644 packages/fabric/domain/src/models/state-store.spec.ts create mode 100644 packages/fabric/store-sqlite/src/sql-to-value.ts create mode 100644 packages/fabric/store-sqlite/src/value-to-sql.ts diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index 913751b..a9cd059 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -9,6 +9,7 @@ "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { + "@fabric/store-sqlite": "workspace:^", "typescript": "^5.6.2", "vitest": "^2.1.1" }, diff --git a/packages/fabric/domain/src/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts index bbe974e..16d1bff 100644 --- a/packages/fabric/domain/src/models/fields/field-to-type.ts +++ b/packages/fabric/domain/src/models/fields/field-to-type.ts @@ -1,5 +1,6 @@ import { UUID } from "../../types/uuid.js"; import { IntegerField } from "./integer.js"; +import { ReferenceField } from "./reference-field.js"; import { StringField } from "./string-field.js"; import { UUIDField } from "./uuid-field.js"; @@ -13,8 +14,12 @@ export type FieldToType = TField extends StringField : TField extends IntegerField ? TField["hasArbitraryPrecision"] extends true ? ToOptional - : ToOptional - : never; + : TField["hasArbitraryPrecision"] extends false + ? ToOptional + : ToOptional + : TField extends ReferenceField + ? ToOptional + : never; type ToOptional = TField extends { isOptional: true } ? TType | null diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index f70e4e4..b29c267 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -3,6 +3,7 @@ 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 * from "./field-to-type.js"; export * from "./reference-field.js"; export type FieldDefinition = diff --git a/packages/fabric/domain/src/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts index 260cc66..8150980 100644 --- a/packages/fabric/domain/src/models/model-schema.ts +++ b/packages/fabric/domain/src/models/model-schema.ts @@ -1,3 +1,7 @@ import { Model } from "./model.js"; export type ModelSchema = Record; + +export type ModelSchemaFromModels = { + [K in TModels["name"]]: Extract; +}; diff --git a/packages/fabric/domain/src/models/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts index d3f5a95..40304e6 100644 --- a/packages/fabric/domain/src/models/query/query-builder.ts +++ b/packages/fabric/domain/src/models/query/query-builder.ts @@ -1,6 +1,7 @@ import { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../../errors/query-error.js"; import { StorageDriver } from "../../storage/storage-driver.js"; +import { ModelSchema } from "../model-schema.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; import { @@ -14,25 +15,26 @@ import { export class QueryBuilder implements StoreQuery { constructor( private driver: StorageDriver, + private schema: ModelSchema, private query: QueryDefinition, ) {} where(where: FilterOptions): StoreSortableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, where, }); } orderBy(opts: OrderByOptions): StoreLimitableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, orderBy: opts, }); } limit(limit: number, offset?: number | undefined): SelectableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, limit, offset, @@ -42,7 +44,7 @@ export class QueryBuilder implements StoreQuery { select>( keys?: K[], ): AsyncResult[], StoreQueryError> { - return this.driver.select({ + return this.driver.select(this.schema[this.query.from], { ...this.query, keys, }); @@ -51,7 +53,7 @@ export class QueryBuilder implements StoreQuery { selectOne>( keys?: K[], ): AsyncResult, StoreQueryError> { - return this.driver.selectOne({ + return this.driver.selectOne(this.schema[this.query.from], { ...this.query, keys, }); diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts new file mode 100644 index 0000000..1f63dd1 --- /dev/null +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -0,0 +1,44 @@ +import { isError } from "@fabric/core"; +import { SQLiteStorageDriver } from "@fabric/store-sqlite"; +import { describe, it } from "vitest"; +import { generateUUID } from "../types/uuid.js"; +import { Field } from "./fields/index.js"; +import { defineModel } from "./model.js"; +import { StateStore } from "./state-store.js"; + +describe("State Store", () => { + const driver = new SQLiteStorageDriver(":memory:"); + + const models = [ + defineModel("users", { + name: Field.string(), + }), + ]; + + it("should be able to create a new state store and migrate", async () => { + const store = new StateStore(driver, models); + + const migrationResult = await store.migrate(); + + if (isError(migrationResult)) throw migrationResult; + }); + + it("should be able to insert a record", async () => { + const store = new StateStore(driver, models); + + const migrationResult = await store.migrate(); + + if (isError(migrationResult)) throw migrationResult; + + const newUUID = generateUUID(); + + const insertResult = await store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }); + + if (isError(insertResult)) throw insertResult; + }); +}); diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index d72f93b..1dcfa25 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -1,5 +1,31 @@ +import { AsyncResult } from "@fabric/core"; +import { StoreQueryError } from "../errors/query-error.js"; import { StorageDriver } from "../storage/storage-driver.js"; +import { ModelSchemaFromModels } from "./model-schema.js"; +import { Model, ModelToType } from "./model.js"; -export class StateStore { - constructor(private driver: StorageDriver) {} +export class StateStore { + private schema: ModelSchemaFromModels; + constructor( + private driver: StorageDriver, + models: TModel[], + ) { + this.schema = models.reduce((acc, model: TModel) => { + return { + ...acc, + [model.name]: model, + }; + }, {} as ModelSchemaFromModels); + } + + async migrate(): AsyncResult { + await this.driver.sync(this.schema); + } + + async insertInto>( + collection: T, + record: ModelToType[T]>, + ): AsyncResult { + return this.driver.insert(this.schema[collection], record); + } } diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 0ba690b..730463d 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -4,6 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core"; import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { ModelSchema } from "../models/model-schema.js"; +import { Model } from "../models/model.js"; import { QueryDefinition } from "../models/query/query.js"; export interface StorageDriver { @@ -11,19 +12,25 @@ export interface StorageDriver { * Insert data into the store */ insert( - collectionName: string, + model: Model, record: Record, ): AsyncResult; /** * Run a select query against the store. */ - select(query: QueryDefinition): AsyncResult; + select( + model: Model, + query: QueryDefinition, + ): AsyncResult; /** * Run a select query against the store. */ - selectOne(query: QueryDefinition): AsyncResult; + selectOne( + model: Model, + query: QueryDefinition, + ): AsyncResult; /** * Sincronice the store with the schema. @@ -46,7 +53,7 @@ export interface StorageDriver { * Update a record in the store. */ update( - collectionName: string, + model: Model, id: string, record: Record, ): AsyncResult; @@ -54,8 +61,5 @@ export interface StorageDriver { /** * Delete a record from the store. */ - delete( - collectionName: string, - id: string, - ): AsyncResult; + delete(model: Model, id: string): AsyncResult; } diff --git a/packages/fabric/domain/src/types/uuid.ts b/packages/fabric/domain/src/types/uuid.ts index 07dd212..7c495b0 100644 --- a/packages/fabric/domain/src/types/uuid.ts +++ b/packages/fabric/domain/src/types/uuid.ts @@ -1 +1,5 @@ export type UUID = `${string}-${string}-${string}-${string}-${string}`; + +export function generateUUID(): UUID { + return crypto.randomUUID() as UUID; +} diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/store-sqlite/package.json index b89b03f..813d1f6 100644 --- a/packages/fabric/store-sqlite/package.json +++ b/packages/fabric/store-sqlite/package.json @@ -1,5 +1,5 @@ { - "name": "@ulthar/store-sqlite", + "name": "@fabric/store-sqlite", "type": "module", "module": "dist/index.js", "main": "dist/index.js", diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index 9032b8d..270ed76 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -2,14 +2,14 @@ import { Variant, VariantTag } from "@fabric/core"; import { FieldDefinition, getTargetKey, Model } from "@fabric/domain"; -type FieldMap = { +type FieldSQLDefinitionMap = { [K in FieldDefinition[VariantTag]]: ( name: string, field: Extract, ) => string; }; -const FieldMap: FieldMap = { +const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { StringField: (n, f) => { return [n, "TEXT", modifiersFromOpts(f)].join(" "); }, @@ -34,6 +34,9 @@ const FieldMap: FieldMap = { ].join(" "); }, }; +function fieldDefinitionToSQL(name: string, field: FieldDefinition) { + return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); +} function modifiersFromOpts(field: FieldDefinition) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { @@ -45,10 +48,6 @@ function modifiersFromOpts(field: FieldDefinition) { ].join(" "); } -function fieldDefinitionToSQL(name: string, field: FieldDefinition) { - return FieldMap[field[VariantTag]](name, field as any); -} - export function modelToSql( model: Model>, ) { diff --git a/packages/fabric/store-sqlite/src/record-utils.ts b/packages/fabric/store-sqlite/src/record-utils.ts index 66bc001..7d26eeb 100644 --- a/packages/fabric/store-sqlite/src/record-utils.ts +++ b/packages/fabric/store-sqlite/src/record-utils.ts @@ -1,5 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Model } from "@fabric/domain"; +import { fieldValueToSQL } from "./value-to-sql.js"; + /** * Unfold a record into a string of it's keys separated by commas. */ @@ -12,9 +15,12 @@ export function recordToKeys(record: Record, prefix = "") { /** * Unfold a record into a string of it's keys separated by commas. */ -export function recordToParams(record: Record) { +export function recordToParams(model: Model, record: Record) { return Object.keys(record).reduce( - (acc, key) => ({ ...acc, [`:${key}`]: record[key] }), + (acc, key) => ({ + ...acc, + [`:${key}`]: fieldValueToSQL(model.fields[key], record[key]), + }), {}, ); } diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sql-to-value.ts new file mode 100644 index 0000000..584999d --- /dev/null +++ b/packages/fabric/store-sqlite/src/sql-to-value.ts @@ -0,0 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { VariantTag } from "@fabric/core"; +import { FieldDefinition, FieldToType, Model } from "@fabric/domain"; + +export function transformRow(model: Model) { + return (row: Record) => { + const result: Record = {}; + for (const key in row) { + const field = model.fields[key]; + result[key] = valueFromSQL(field, row[key]); + } + return result; + }; +} + +function valueFromSQL(field: FieldDefinition, value: any): any { + const r = FieldSQLInsertMap[field[VariantTag]]; + return r(field as any, value); +} + +type FieldSQLInsertMap = { + [K in FieldDefinition[VariantTag]]: ( + field: Extract, + value: any, + ) => FieldToType>; +}; +const FieldSQLInsertMap: FieldSQLInsertMap = { + StringField: (f, v) => v, + UUIDField: (f, v) => v, + IntegerField: (f, v) => { + if (f.hasArbitraryPrecision) { + return BigInt(v); + } + return v; + }, + ReferenceField: (f, v) => v, +}; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 8dc3f8c..36e4ee6 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -26,17 +26,19 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + const insertResult = await store.insert(schema.users, { id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); - const records = await store.select({ from: "users" }); + if (isError(insertResult)) throw insertResult; + + const records = await store.select(schema.users, { from: "users" }); expect(records).toEqual([ - { id: "1", name: "test", streamId: "1", streamVersion: 1 }, + { id: "1", name: "test", streamId: "1", streamVersion: 1n }, ]); }); @@ -45,19 +47,19 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.update("users", "1", { name: "updated" }); + await store.update(schema.users, "1", { name: "updated" }); - const records = await store.select({ from: "users" }); + const records = await store.select(schema.users, { from: "users" }); expect(records).toEqual([ - { id: "1", name: "updated", streamId: "1", streamVersion: 1 }, + { id: "1", name: "updated", streamId: "1", streamVersion: 1n }, ]); }); @@ -66,16 +68,16 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.delete("users", "1"); + await store.delete(schema.users, "1"); - const records = await store.select({ from: "users" }); + const records = await store.select(schema.users, { from: "users" }); expect(records).toEqual([]); }); @@ -85,24 +87,24 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.insert("users", { + await store.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1, }); - const records = await store.select({ from: "users" }); + const records = await store.select(schema.users, { from: "users" }); expect(records).toEqual([ - { id: "1", name: "test", streamId: "1", streamVersion: 1 }, - { id: "2", name: "test", streamId: "2", streamVersion: 1 }, + { id: "1", name: "test", streamId: "1", streamVersion: 1n }, + { id: "2", name: "test", streamId: "2", streamVersion: 1n }, ]); }); @@ -111,26 +113,26 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.insert("users", { + await store.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1, }); - const record = await store.selectOne({ from: "users" }); + const record = await store.selectOne(schema.users, { from: "users" }); expect(record).toEqual({ id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); }); }); diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 24fa8a0..30c95d0 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -4,6 +4,7 @@ import { unlink } from "fs/promises"; import { CircularDependencyError, + Model, ModelSchema, QueryDefinition, StorageDriver, @@ -16,6 +17,7 @@ import { recordToParams, recordToSQLSet, } from "./record-utils.js"; +import { transformRow } from "./sql-to-value.js"; import { dbClose, dbRun, @@ -35,7 +37,7 @@ export class SQLiteStorageDriver implements StorageDriver { this.db = new Database(path); // Enable Write-Ahead Logging, which is faster and more reliable. - this.db.run("PRAGMA journal_mode= WAL;"); + this.db.run("PRAGMA journal_mode = WAL;"); this.db.run("PRAGMA foreign_keys = ON;"); } @@ -58,17 +60,17 @@ export class SQLiteStorageDriver implements StorageDriver { * Insert data into the store */ async insert( - collectionName: string, + model: Model, record: Record, ): AsyncResult { try { - const sql = `INSERT INTO ${collectionName} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`; + const sql = `INSERT INTO ${model.name} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, recordToParams(record)); + return await run(stmt, recordToParams(model, record)); } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, record, }); } @@ -77,11 +79,14 @@ export class SQLiteStorageDriver implements StorageDriver { /** * Run a select query against the store. */ - async select(query: QueryDefinition): AsyncResult { + async select( + model: Model, + query: QueryDefinition, + ): AsyncResult { try { const sql = `SELECT * FROM ${query.from}`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await getAll(stmt); + return await getAll(stmt, transformRow(model)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -93,12 +98,15 @@ export class SQLiteStorageDriver implements StorageDriver { /** * Run a select query against the store. */ - async selectOne(query: QueryDefinition): AsyncResult { + async selectOne( + model: Model, + query: QueryDefinition, + ): AsyncResult { try { const sql = `SELECT * FROM ${query.from}`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await getOne(stmt); + return await getOne(stmt, transformRow(model)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -161,16 +169,16 @@ export class SQLiteStorageDriver implements StorageDriver { * Update a record in the store. */ async update( - collectionName: string, + model: Model, id: string, record: Record, ): AsyncResult { try { - const sql = `UPDATE ${collectionName} SET ${recordToSQLSet(record)} WHERE id = :id`; + const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = :id`; const stmt = await this.getOrCreatePreparedStatement(sql); return await run( stmt, - recordToParams({ + recordToParams(model, { ...record, id, }), @@ -178,7 +186,7 @@ export class SQLiteStorageDriver implements StorageDriver { } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, record, }); } @@ -188,18 +196,15 @@ export class SQLiteStorageDriver implements StorageDriver { * Delete a record from the store. */ - async delete( - collectionName: string, - id: string, - ): AsyncResult { + async delete(model: Model, id: string): AsyncResult { try { - const sql = `DELETE FROM ${collectionName} WHERE id = :id`; + const sql = `DELETE FROM ${model.name} WHERE id = :id`; const stmt = await this.getOrCreatePreparedStatement(sql); return await run(stmt, { ":id": id }); } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, id, }); } diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts index ecc12a8..f98fc83 100644 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts @@ -52,25 +52,31 @@ export function run( }); } -export function getAll(stmt: Statement): Promise[]> { +export function getAll( + stmt: Statement, + transformer: (row: any) => any, +): Promise[]> { return new Promise((resolve, reject) => { stmt.all((err: Error | null, rows: Record[]) => { if (err) { reject(err); } else { - resolve(rows); + resolve(rows.map(transformer)); } }); }); } -export function getOne(stmt: Statement): Promise> { +export function getOne( + stmt: Statement, + transformer: (row: any) => any, +): Promise> { return new Promise((resolve, reject) => { stmt.get((err: Error | null, row: Record) => { if (err) { reject(err); } else { - resolve(row); + resolve(transformer(row)); } }); }); diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/value-to-sql.ts new file mode 100644 index 0000000..d39bb13 --- /dev/null +++ b/packages/fabric/store-sqlite/src/value-to-sql.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { VariantTag } from "@fabric/core"; +import { FieldDefinition, FieldToType } from "@fabric/domain"; + +type FieldSQLInsertMap = { + [K in FieldDefinition[VariantTag]]: ( + field: Extract, + value: FieldToType>, + ) => any; +}; +const FieldSQLInsertMap: FieldSQLInsertMap = { + StringField: (f, v) => v, + UUIDField: (f, v) => v, + IntegerField: (f, v: number | bigint) => { + if (f.hasArbitraryPrecision) { + return String(v); + } + return v as number; + }, + ReferenceField: (f, v) => v, +}; + +export function fieldValueToSQL(field: FieldDefinition, value: any) { + const r = FieldSQLInsertMap[field[VariantTag]] as any; + return r(field as any, value); +} diff --git a/yarn.lock b/yarn.lock index b4d4b7d..dc7d075 100644 --- a/yarn.lock +++ b/yarn.lock @@ -419,6 +419,19 @@ __metadata: resolution: "@fabric/domain@workspace:packages/fabric/domain" dependencies: "@fabric/core": "workspace:^" + "@fabric/store-sqlite": "workspace:^" + typescript: "npm:^5.6.2" + vitest: "npm:^2.1.1" + languageName: unknown + linkType: soft + +"@fabric/store-sqlite@workspace:^, @fabric/store-sqlite@workspace:packages/fabric/store-sqlite": + version: 0.0.0-use.local + resolution: "@fabric/store-sqlite@workspace:packages/fabric/store-sqlite" + dependencies: + "@fabric/core": "workspace:^" + "@fabric/domain": "workspace:^" + sqlite3: "npm:^5.1.7" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown @@ -855,23 +868,12 @@ __metadata: languageName: unknown linkType: soft -"@ulthar/store-sqlite@workspace:packages/fabric/store-sqlite": - version: 0.0.0-use.local - resolution: "@ulthar/store-sqlite@workspace:packages/fabric/store-sqlite" - dependencies: - "@fabric/core": "workspace:^" - "@fabric/domain": "workspace:^" - sqlite3: "npm:^5.1.7" - typescript: "npm:^5.6.2" - vitest: "npm:^2.1.1" - languageName: unknown - linkType: soft - "@ulthar/template-domain@workspace:packages/templates/domain": version: 0.0.0-use.local resolution: "@ulthar/template-domain@workspace:packages/templates/domain" dependencies: "@fabric/core": "workspace:^" + "@fabric/domain": "workspace:^" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown From f07928b89349f80e25adca77d8e5211a2a8e999f Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 8 Oct 2024 15:33:59 -0300 Subject: [PATCH 11/37] [fabric/domain] Add StateStore filtering --- package.json | 2 +- packages/fabric/domain/package.json | 3 +- .../domain/src/models/fields/decimal.ts | 21 +++ .../domain/src/models/fields/field-to-type.ts | 34 ++-- .../fabric/domain/src/models/fields/float.ts | 18 ++ .../fabric/domain/src/models/fields/index.ts | 6 + packages/fabric/domain/src/models/model.ts | 22 ++- .../domain/src/models/query/filter-options.ts | 12 +- .../domain/src/models/state-store.spec.ts | 117 ++++++++++-- .../fabric/domain/src/models/state-store.ts | 10 ++ .../src/services/uuid-generator.mock.ts | 8 + .../domain/src/services/uuid-generator.ts | 5 + .../domain/src/storage/storage-driver.ts | 12 +- packages/fabric/domain/src/types/decimal.ts | 1 + packages/fabric/domain/src/types/uuid.ts | 4 - .../store-sqlite/src/filter-to-sql.spec.ts | 150 ++++++++++++++++ .../fabric/store-sqlite/src/filter-to-sql.ts | 166 ++++++++++++++++++ .../fabric/store-sqlite/src/model-to-sql.ts | 10 +- .../fabric/store-sqlite/src/record-utils.ts | 22 ++- .../fabric/store-sqlite/src/sql-to-value.ts | 6 +- .../store-sqlite/src/sqlite-driver.spec.ts | 145 ++++++++++++--- .../fabric/store-sqlite/src/sqlite-driver.ts | 67 ++++--- .../fabric/store-sqlite/src/sqlite-wrapper.ts | 8 +- .../fabric/store-sqlite/src/value-to-sql.ts | 2 + yarn.lock | 8 + 25 files changed, 753 insertions(+), 106 deletions(-) create mode 100644 packages/fabric/domain/src/models/fields/decimal.ts create mode 100644 packages/fabric/domain/src/models/fields/float.ts create mode 100644 packages/fabric/domain/src/services/uuid-generator.mock.ts create mode 100644 packages/fabric/domain/src/services/uuid-generator.ts create mode 100644 packages/fabric/domain/src/types/decimal.ts create mode 100644 packages/fabric/store-sqlite/src/filter-to-sql.spec.ts create mode 100644 packages/fabric/store-sqlite/src/filter-to-sql.ts diff --git a/package.json b/package.json index 70fa104..bf7f334 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint": "eslint . --fix --report-unused-disable-directives", "format": "prettier --write .", "test": "yarn workspaces foreach -vvpA run test --run --clearScreen false", - "build": "yarn workspaces foreach -vvpA --topological-dev run build", + "build": "yarn workspaces foreach -vvpA --topological run build", "add-package": "tsx ./scripts/add-package.ts", "postinstall": "husky" } diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index a9cd059..c44c9e6 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -14,7 +14,8 @@ "vitest": "^2.1.1" }, "dependencies": { - "@fabric/core": "workspace:^" + "@fabric/core": "workspace:^", + "decimal.js": "^10.4.3" }, "scripts": { "test": "vitest", diff --git a/packages/fabric/domain/src/models/fields/decimal.ts b/packages/fabric/domain/src/models/fields/decimal.ts new file mode 100644 index 0000000..568feff --- /dev/null +++ b/packages/fabric/domain/src/models/fields/decimal.ts @@ -0,0 +1,21 @@ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +export interface DecimalFieldOptions extends BaseField { + isUnsigned?: boolean; + precision?: number; + scale?: number; +} + +export interface DecimalField + extends TaggedVariant<"DecimalField">, + DecimalFieldOptions {} + +export function createDecimalField( + opts: T = {} as T, +): DecimalField & T { + return { + [VariantTag]: "DecimalField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts index 16d1bff..b7f67b9 100644 --- a/packages/fabric/domain/src/models/fields/field-to-type.ts +++ b/packages/fabric/domain/src/models/fields/field-to-type.ts @@ -1,4 +1,7 @@ +import { Decimal } from "decimal.js"; import { UUID } from "../../types/uuid.js"; +import { DecimalField } from "./decimal.js"; +import { FloatField } from "./float.js"; import { IntegerField } from "./integer.js"; import { ReferenceField } from "./reference-field.js"; import { StringField } from "./string-field.js"; @@ -7,20 +10,23 @@ import { UUIDField } from "./uuid-field.js"; /** * Converts a field definition to its corresponding TypeScript type. */ -export type FieldToType = TField extends StringField - ? ToOptional - : TField extends UUIDField - ? ToOptional - : TField extends IntegerField - ? TField["hasArbitraryPrecision"] extends true - ? ToOptional - : TField["hasArbitraryPrecision"] extends false - ? ToOptional - : ToOptional - : TField extends ReferenceField - ? ToOptional - : never; +//prettier-ignore +export type FieldToType = + TField extends StringField ? MaybeOptional + : TField extends UUIDField ? MaybeOptional + : TField extends IntegerField ? IntegerFieldToType + : TField extends ReferenceField ? MaybeOptional + : TField extends DecimalField ? MaybeOptional + : TField extends FloatField ? MaybeOptional + : never; -type ToOptional = TField extends { isOptional: true } +//prettier-ignore +type IntegerFieldToType = TField["hasArbitraryPrecision"] extends true + ? MaybeOptional + : TField["hasArbitraryPrecision"] extends false + ? MaybeOptional + : MaybeOptional; + +type MaybeOptional = TField extends { isOptional: true } ? TType | null : TType; diff --git a/packages/fabric/domain/src/models/fields/float.ts b/packages/fabric/domain/src/models/fields/float.ts new file mode 100644 index 0000000..fd67bcc --- /dev/null +++ b/packages/fabric/domain/src/models/fields/float.ts @@ -0,0 +1,18 @@ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface FloatFieldOptions extends BaseField {} + +export interface FloatField + extends TaggedVariant<"FloatField">, + FloatFieldOptions {} + +export function createFloatField( + opts: T = {} as T, +): FloatField & T { + return { + [VariantTag]: "FloatField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index b29c267..0f1b30a 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -1,3 +1,5 @@ +import { createDecimalField, DecimalField } from "./decimal.js"; +import { createFloatField, FloatField } from "./float.js"; import { createIntegerField, IntegerField } from "./integer.js"; import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createStringField, StringField } from "./string-field.js"; @@ -10,6 +12,8 @@ export type FieldDefinition = | StringField | UUIDField | IntegerField + | FloatField + | DecimalField | ReferenceField; export namespace Field { @@ -17,4 +21,6 @@ export namespace Field { export const uuid = createUUIDField; export const integer = createIntegerField; export const reference = createReferenceField; + export const decimal = createDecimalField; + export const float = createFloatField; } diff --git a/packages/fabric/domain/src/models/model.ts b/packages/fabric/domain/src/models/model.ts index 34520bb..a79321b 100644 --- a/packages/fabric/domain/src/models/model.ts +++ b/packages/fabric/domain/src/models/model.ts @@ -4,6 +4,14 @@ import { Field, FieldDefinition } from "./fields/index.js"; export type CustomModelFields = Record; +export interface Collection< + TName extends string = string, + TFields extends CustomModelFields = CustomModelFields, +> { + name: TName; + fields: TFields; +} + export const DefaultModelFields = { id: Field.uuid({ isPrimaryKey: true }), streamId: Field.uuid({ isIndexed: true }), @@ -12,11 +20,11 @@ export const DefaultModelFields = { hasArbitraryPrecision: true, }), }; + export interface Model< TName extends string = string, TFields extends CustomModelFields = CustomModelFields, -> { - name: TName; +> extends Collection { fields: typeof DefaultModelFields & TFields; } @@ -30,6 +38,16 @@ export function defineModel< } as const; } +export function defineCollection< + TName extends string, + TFields extends CustomModelFields, +>(name: TName, fields: TFields): Collection { + return { + name, + fields, + } as const; +} + export type ModelToType = { [K in Keyof]: FieldToType; }; diff --git a/packages/fabric/domain/src/models/query/filter-options.ts b/packages/fabric/domain/src/models/query/filter-options.ts index 4a18f7c..9a86305 100644 --- a/packages/fabric/domain/src/models/query/filter-options.ts +++ b/packages/fabric/domain/src/models/query/filter-options.ts @@ -4,12 +4,14 @@ export type FilterOptions = | SingleFilterOption | MultiFilterOption; +export type FilterValue = + | T[K] + | LikeFilterOption + | ComparisonFilterOption + | InFilterOption; + export type SingleFilterOption = { - [K in keyof T]?: - | T[K] - | LikeFilterOption - | ComparisonFilterOption - | InFilterOption; + [K in keyof T]?: FilterValue; }; export type MultiFilterOption = SingleFilterOption[]; diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts index 1f63dd1..7e30408 100644 --- a/packages/fabric/domain/src/models/state-store.spec.ts +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -1,37 +1,54 @@ import { isError } from "@fabric/core"; import { SQLiteStorageDriver } from "@fabric/store-sqlite"; -import { describe, it } from "vitest"; -import { generateUUID } from "../types/uuid.js"; +import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, +} from "vitest"; +import { UUIDGeneratorMock } from "../services/uuid-generator.mock.js"; +import { UUID } from "../types/uuid.js"; import { Field } from "./fields/index.js"; import { defineModel } from "./model.js"; +import { isLike } from "./query/filter-options.js"; import { StateStore } from "./state-store.js"; describe("State Store", () => { - const driver = new SQLiteStorageDriver(":memory:"); - const models = [ defineModel("users", { name: Field.string(), }), ]; - it("should be able to create a new state store and migrate", async () => { - const store = new StateStore(driver, models); + let driver: SQLiteStorageDriver; + let store: StateStore<(typeof models)[number]>; + beforeEach(async () => { + driver = new SQLiteStorageDriver(":memory:"); + store = new StateStore(driver, models); const migrationResult = await store.migrate(); - if (isError(migrationResult)) throw migrationResult; }); - it("should be able to insert a record", async () => { - const store = new StateStore(driver, models); + afterEach(async () => { + await driver.close(); + }); - const migrationResult = await store.migrate(); - - if (isError(migrationResult)) throw migrationResult; - - const newUUID = generateUUID(); + it("should insert a record", async () => { + const newUUID = UUIDGeneratorMock.generate(); + const insertResult = await store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }); + if (isError(insertResult)) throw insertResult; + }); + it("should query with a basic select", async () => { + const newUUID = UUIDGeneratorMock.generate(); const insertResult = await store.insertInto("users", { name: "test", id: newUUID, @@ -40,5 +57,77 @@ describe("State Store", () => { }); if (isError(insertResult)) throw insertResult; + + const result = await store.from("users").select(); + + if (isError(result)) throw result; + + expectTypeOf(result).toEqualTypeOf< + { + id: UUID; + streamId: UUID; + streamVersion: bigint; + name: string; + }[] + >(); + + expect(result).toEqual([ + { + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + name: "test", + }, + ]); + }); + + it("should query with a where clause", async () => { + const newUUID = UUIDGeneratorMock.generate(); + await store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }); + + await store.insertInto("users", { + name: "anotherName", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }); + await store.insertInto("users", { + name: "anotherName2", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }); + + const result = await store + .from("users") + .where({ + name: isLike("te*"), + }) + .select(); + + if (isError(result)) throw result; + + expectTypeOf(result).toEqualTypeOf< + { + id: UUID; + streamId: UUID; + streamVersion: bigint; + name: string; + }[] + >(); + + expect(result).toEqual([ + { + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + name: "test", + }, + ]); }); }); diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index 1dcfa25..292bc7e 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -3,6 +3,8 @@ import { StoreQueryError } from "../errors/query-error.js"; import { StorageDriver } from "../storage/storage-driver.js"; import { ModelSchemaFromModels } from "./model-schema.js"; import { Model, ModelToType } from "./model.js"; +import { QueryBuilder } from "./query/query-builder.js"; +import { StoreQuery } from "./query/query.js"; export class StateStore { private schema: ModelSchemaFromModels; @@ -28,4 +30,12 @@ export class StateStore { ): AsyncResult { return this.driver.insert(this.schema[collection], record); } + + from>( + collection: T, + ): StoreQuery[T]>> { + return new QueryBuilder(this.driver, this.schema, { + from: collection, + }) as StoreQuery[T]>>; + } } diff --git a/packages/fabric/domain/src/services/uuid-generator.mock.ts b/packages/fabric/domain/src/services/uuid-generator.mock.ts new file mode 100644 index 0000000..3b9674d --- /dev/null +++ b/packages/fabric/domain/src/services/uuid-generator.mock.ts @@ -0,0 +1,8 @@ +import { UUID } from "../types/uuid.js"; +import { UUIDGenerator } from "./uuid-generator.js"; + +export const UUIDGeneratorMock: UUIDGenerator = { + generate(): UUID { + return crypto.randomUUID() as UUID; + }, +}; diff --git a/packages/fabric/domain/src/services/uuid-generator.ts b/packages/fabric/domain/src/services/uuid-generator.ts new file mode 100644 index 0000000..34c145e --- /dev/null +++ b/packages/fabric/domain/src/services/uuid-generator.ts @@ -0,0 +1,5 @@ +import { UUID } from "../types/uuid.js"; + +export interface UUIDGenerator { + generate(): UUID; +} diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 730463d..6fc6c03 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -4,7 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core"; import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { ModelSchema } from "../models/model-schema.js"; -import { Model } from "../models/model.js"; +import { Collection } from "../models/model.js"; import { QueryDefinition } from "../models/query/query.js"; export interface StorageDriver { @@ -12,7 +12,7 @@ export interface StorageDriver { * Insert data into the store */ insert( - model: Model, + model: Collection, record: Record, ): AsyncResult; @@ -20,7 +20,7 @@ export interface StorageDriver { * Run a select query against the store. */ select( - model: Model, + model: Collection, query: QueryDefinition, ): AsyncResult; @@ -28,7 +28,7 @@ export interface StorageDriver { * Run a select query against the store. */ selectOne( - model: Model, + model: Collection, query: QueryDefinition, ): AsyncResult; @@ -53,7 +53,7 @@ export interface StorageDriver { * Update a record in the store. */ update( - model: Model, + model: Collection, id: string, record: Record, ): AsyncResult; @@ -61,5 +61,5 @@ export interface StorageDriver { /** * Delete a record from the store. */ - delete(model: Model, id: string): AsyncResult; + delete(model: Collection, id: string): AsyncResult; } diff --git a/packages/fabric/domain/src/types/decimal.ts b/packages/fabric/domain/src/types/decimal.ts new file mode 100644 index 0000000..a760328 --- /dev/null +++ b/packages/fabric/domain/src/types/decimal.ts @@ -0,0 +1 @@ +export { Decimal } from "decimal.js"; diff --git a/packages/fabric/domain/src/types/uuid.ts b/packages/fabric/domain/src/types/uuid.ts index 7c495b0..07dd212 100644 --- a/packages/fabric/domain/src/types/uuid.ts +++ b/packages/fabric/domain/src/types/uuid.ts @@ -1,5 +1 @@ export type UUID = `${string}-${string}-${string}-${string}-${string}`; - -export function generateUUID(): UUID { - return crypto.randomUUID() as UUID; -} diff --git a/packages/fabric/store-sqlite/src/filter-to-sql.spec.ts b/packages/fabric/store-sqlite/src/filter-to-sql.spec.ts new file mode 100644 index 0000000..ccef5a3 --- /dev/null +++ b/packages/fabric/store-sqlite/src/filter-to-sql.spec.ts @@ -0,0 +1,150 @@ +import { + defineCollection, + Field, + isGreaterOrEqualTo, + isGreaterThan, + isIn, + isLessOrEqualTo, + isLessThan, + isLike, + isNotEqualTo, +} from "@fabric/domain"; +import { describe, expect, it } from "vitest"; +import { filterToParams, filterToSQL } from "./filter-to-sql.js"; + +describe("SQL where clause from filter options", () => { + const col = defineCollection("users", { + name: Field.string(), + age: Field.integer(), + status: Field.string(), + salary: Field.decimal(), + rating: Field.float(), + quantity: Field.integer({ + isUnsigned: true, + }), + price: Field.decimal(), + }); + + it("should create a where clause from options with IN option", () => { + const opts = { + name: isIn(["John", "Jane"]), + }; + const result = filterToSQL(opts); + + const params = filterToParams(col, opts); + + expect(result).toEqual("WHERE name IN ($where_name_0,$where_name_1)"); + expect(params).toEqual({ $where_name_0: "John", $where_name_1: "Jane" }); + }); + + it("should create a where clause from options with LIKE option", () => { + const opts = { + name: isLike("%John%"), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE name LIKE $where_name"); + expect(params).toEqual({ $where_name: "%John%" }); + }); + + it("should create a where clause from options with EQUALS option", () => { + const opts = { + age: 25, + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE age = $where_age"); + expect(params).toEqual({ $where_age: 25 }); + }); + + it("should create a where clause from options with NOT EQUALS option", () => { + const opts = { + status: isNotEqualTo("inactive"), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE status <> $where_status"); + expect(params).toEqual({ $where_status: "inactive" }); + }); + + it("should create a where clause from options with GREATER THAN option", () => { + const opts = { + salary: isGreaterThan(50000), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE salary > $where_salary"); + expect(params).toEqual({ $where_salary: 50000 }); + }); + + it("should create a where clause from options with LESS THAN option", () => { + const opts = { + rating: isLessThan(4.5), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE rating < $where_rating"); + expect(params).toEqual({ $where_rating: 4.5 }); + }); + + it("should create a where clause from options with GREATER THAN OR EQUALS option", () => { + const opts = { + quantity: isGreaterOrEqualTo(10), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE quantity >= $where_quantity"); + expect(params).toEqual({ $where_quantity: 10 }); + }); + + it("should create a where clause from options with LESS THAN OR EQUALS option", () => { + const opts = { + price: isLessOrEqualTo(100), + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE price <= $where_price"); + expect(params).toEqual({ $where_price: 100 }); + }); + + it("should create a where clause from options with IS NULL option", () => { + const opts = { + price: undefined, + }; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual("WHERE price IS NULL"); + expect(params).toEqual({}); + }); + + it("should create a where clause from options with OR combination", () => { + const opts = [ + { + name: isIn(["John", "Jane"]), + age: isGreaterThan(30), + }, + { + status: isNotEqualTo("inactive"), + salary: isGreaterThan(50000), + }, + { + rating: isLessThan(4.5), + quantity: isGreaterOrEqualTo(10), + }, + ]; + const result = filterToSQL(opts); + const params = filterToParams(col, opts); + expect(result).toEqual( + "WHERE (name IN ($where_name_0_0,$where_name_0_1) AND age > $where_age_0) OR (status <> $where_status_1 AND salary > $where_salary_1) OR (rating < $where_rating_2 AND quantity >= $where_quantity_2)", + ); + expect(params).toEqual({ + $where_name_0_0: "John", + $where_name_0_1: "Jane", + $where_age_0: 30, + $where_status_1: "inactive", + $where_salary_1: 50000, + $where_rating_2: 4.5, + $where_quantity_2: 10, + }); + }); +}); diff --git a/packages/fabric/store-sqlite/src/filter-to-sql.ts b/packages/fabric/store-sqlite/src/filter-to-sql.ts new file mode 100644 index 0000000..da4f3b4 --- /dev/null +++ b/packages/fabric/store-sqlite/src/filter-to-sql.ts @@ -0,0 +1,166 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { + Collection, + FieldDefinition, + FILTER_OPTION_OPERATOR_SYMBOL, + FILTER_OPTION_TYPE_SYMBOL, + FILTER_OPTION_VALUE_SYMBOL, + FilterOptions, + FilterValue, + MultiFilterOption, + SingleFilterOption, +} from "@fabric/domain"; +import { keyToParam } from "./record-utils.js"; +import { fieldValueToSQL } from "./value-to-sql.js"; + +export function filterToSQL(filterOptions?: FilterOptions) { + if (!filterOptions) return ""; + + if (Array.isArray(filterOptions)) + return `WHERE ${getWhereFromMultiOption(filterOptions)}`; + + return `WHERE ${getWhereFromSingleOption(filterOptions)}`; +} + +export function filterToParams( + collection: Collection, + filterOptions?: FilterOptions, +) { + if (!filterOptions) return {}; + + if (Array.isArray(filterOptions)) + return getParamsFromMultiFilterOption(collection, filterOptions); + + return getParamsFromSingleFilterOption(collection, filterOptions); +} + +function getWhereFromMultiOption(filterOptions: MultiFilterOption) { + return filterOptions + .map( + (option, i) => + `(${getWhereFromSingleOption(option, { postfix: `_${i}` })})`, + ) + .join(" OR "); +} + +function getWhereFromSingleOption( + filterOptions: SingleFilterOption, + opts: { postfix?: string } = {}, +) { + return Object.entries(filterOptions) + .map(([key, value]) => getWhereFromKeyValue(key, value, opts)) + .join(" AND "); +} + +const WHERE_KEY_PREFIX = "where_"; + +function getWhereParamKey(key: string, opts: { postfix?: string } = {}) { + return keyToParam(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`); +} + +function getWhereFromKeyValue( + key: string, + value: FilterValue, + opts: { postfix?: string } = {}, +) { + if (value == undefined) { + return `${key} IS NULL`; + } + + if (typeof value === "object") { + if (value[FILTER_OPTION_TYPE_SYMBOL] === "like") { + return `${key} LIKE ${getWhereParamKey(key, opts)}`; + } + + if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") { + return `${key} IN (${value[FILTER_OPTION_VALUE_SYMBOL].map( + (v: any, i: number) => + `${getWhereParamKey(key, { + postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`, + })}`, + ).join(",")})`; + } + + if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") { + return `${key} ${value[FILTER_OPTION_OPERATOR_SYMBOL]} ${getWhereParamKey( + key, + opts, + )}`; + } + } + return `${key} = ${getWhereParamKey(key, opts)}`; +} + +function getParamsFromMultiFilterOption( + collection: Collection, + filterOptions: MultiFilterOption, +) { + return filterOptions.reduce( + (acc, filterOption, i) => ({ + ...acc, + ...getParamsFromSingleFilterOption(collection, filterOption, { + postfix: `_${i}`, + }), + }), + {}, + ); +} + +function getParamsFromSingleFilterOption( + collection: Collection, + filterOptions: SingleFilterOption, + opts: { postfix?: string } = {}, +) { + return Object.entries(filterOptions) + .filter(([, value]) => { + return value !== undefined; + }) + .reduce( + (acc, [key, value]) => ({ + ...acc, + ...getParamsForFilterKeyValue(collection.fields[key], key, value, opts), + }), + {}, + ); +} + +function getParamValueFromOptionValue(field: FieldDefinition, value: any) { + if (typeof value === "object") { + if (value[FILTER_OPTION_TYPE_SYMBOL] === "like") { + return value[FILTER_OPTION_VALUE_SYMBOL]; + } + + if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") { + return fieldValueToSQL(field, value[FILTER_OPTION_VALUE_SYMBOL]); + } + } + + return fieldValueToSQL(field, value); +} + +function getParamsForFilterKeyValue( + field: FieldDefinition, + key: string, + value: FilterValue, + opts: { postfix?: string } = {}, +) { + if (typeof value === "object") { + if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") { + return value[FILTER_OPTION_VALUE_SYMBOL].reduce( + (acc: Record, v: any, i: number) => { + return { + ...acc, + [getWhereParamKey(key, { + postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`, + })]: value[FILTER_OPTION_VALUE_SYMBOL][i], + }; + }, + {}, + ); + } + } + + return { + [getWhereParamKey(key, opts)]: getParamValueFromOptionValue(field, value), + }; +} diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index 270ed76..ede39f4 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -21,10 +21,10 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { modifiersFromOpts(f), ].join(" "); }, - IntegerField: function (n, f): string { + IntegerField: (n, f): string => { return [n, "INTEGER", modifiersFromOpts(f)].join(" "); }, - ReferenceField: function (n, f): string { + ReferenceField: (n, f): string => { return [ n, "TEXT", @@ -33,6 +33,12 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { `FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`, ].join(" "); }, + FloatField: (n, f): string => { + return [n, "REAL", modifiersFromOpts(f)].join(" "); + }, + DecimalField: (n, f): string => { + return [n, "REAL", modifiersFromOpts(f)].join(" "); + }, }; function fieldDefinitionToSQL(name: string, field: FieldDefinition) { return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); diff --git a/packages/fabric/store-sqlite/src/record-utils.ts b/packages/fabric/store-sqlite/src/record-utils.ts index 7d26eeb..50dfd7c 100644 --- a/packages/fabric/store-sqlite/src/record-utils.ts +++ b/packages/fabric/store-sqlite/src/record-utils.ts @@ -6,20 +6,28 @@ import { fieldValueToSQL } from "./value-to-sql.js"; /** * Unfold a record into a string of it's keys separated by commas. */ -export function recordToKeys(record: Record, prefix = "") { +export function recordToSQLKeys(record: Record) { return Object.keys(record) - .map((key) => `${prefix}${key}`) + .map((key) => key) + .join(", "); +} +/** + * Unfold a record into a string of it's keys separated by commas. + */ +export function recordToSQLKeyParams(record: Record) { + return Object.keys(record) + .map((key) => keyToParam(key)) .join(", "); } /** * Unfold a record into a string of it's keys separated by commas. */ -export function recordToParams(model: Model, record: Record) { +export function recordToSQLParams(model: Model, record: Record) { return Object.keys(record).reduce( (acc, key) => ({ ...acc, - [`:${key}`]: fieldValueToSQL(model.fields[key], record[key]), + [keyToParam(key)]: fieldValueToSQL(model.fields[key], record[key]), }), {}, ); @@ -27,6 +35,10 @@ export function recordToParams(model: Model, record: Record) { export function recordToSQLSet(record: Record) { return Object.keys(record) - .map((key) => `${key} = :${key}`) + .map((key) => `${key} = ${keyToParam(key)}`) .join(", "); } + +export function keyToParam(key: string) { + return `$${key}`; +} diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sql-to-value.ts index 584999d..152748c 100644 --- a/packages/fabric/store-sqlite/src/sql-to-value.ts +++ b/packages/fabric/store-sqlite/src/sql-to-value.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { VariantTag } from "@fabric/core"; -import { FieldDefinition, FieldToType, Model } from "@fabric/domain"; +import { Collection, FieldDefinition, FieldToType } from "@fabric/domain"; -export function transformRow(model: Model) { +export function transformRow(model: Collection) { return (row: Record) => { const result: Record = {}; for (const key in row) { @@ -34,4 +34,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { return v; }, ReferenceField: (f, v) => v, + FloatField: (f, v) => v, + DecimalField: (f, v) => v, }; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 36e4ee6..514c9a7 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -1,6 +1,6 @@ import { isError } from "@fabric/core"; -import { defineModel, Field } from "@fabric/domain"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { defineModel, Field, isLike } from "@fabric/domain"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { SQLiteStorageDriver } from "./sqlite-driver.js"; describe("SQLite Store Driver", () => { @@ -21,7 +21,7 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; }); - test("should be able to synchronize the store and insert a record", async () => { + it("should synchronize the store and insert a record", async () => { const result = await store.sync(schema); if (isError(result)) throw result; @@ -42,19 +42,18 @@ describe("SQLite Store Driver", () => { ]); }); - test("should be able to update a record", async () => { - const result = await store.sync(schema); - - if (isError(result)) throw result; + it("should be update a record", async () => { + await store.sync(schema); await store.insert(schema.users, { id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); - await store.update(schema.users, "1", { name: "updated" }); + const err = await store.update(schema.users, "1", { name: "updated" }); + if (isError(err)) throw err; const records = await store.select(schema.users, { from: "users" }); @@ -63,16 +62,14 @@ describe("SQLite Store Driver", () => { ]); }); - test("should be able to delete a record", async () => { - const result = await store.sync(schema); - - if (isError(result)) throw result; + it("should be able to delete a record", async () => { + await store.sync(schema); await store.insert(schema.users, { id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); await store.delete(schema.users, "1"); @@ -82,22 +79,20 @@ describe("SQLite Store Driver", () => { expect(records).toEqual([]); }); - test("should be able to select records", async () => { - const result = await store.sync(schema); - - if (isError(result)) throw result; + it("should be able to select records", async () => { + await store.sync(schema); await store.insert(schema.users, { id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); await store.insert(schema.users, { id: "2", name: "test", streamId: "2", - streamVersion: 1, + streamVersion: 1n, }); const records = await store.select(schema.users, { from: "users" }); @@ -108,22 +103,20 @@ describe("SQLite Store Driver", () => { ]); }); - test("should be able to select one record", async () => { - const result = await store.sync(schema); - - if (isError(result)) throw result; + it("should be able to select one record", async () => { + await store.sync(schema); await store.insert(schema.users, { id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); await store.insert(schema.users, { id: "2", name: "test", streamId: "2", - streamVersion: 1, + streamVersion: 1n, }); const record = await store.selectOne(schema.users, { from: "users" }); @@ -135,4 +128,104 @@ describe("SQLite Store Driver", () => { streamVersion: 1n, }); }); + + it("should select a record with a where clause", async () => { + await store.sync(schema); + + await store.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }); + await store.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }); + + const result = await store.select(schema.users, { + from: "users", + where: { name: isLike("te%") }, + }); + + expect(result).toEqual([ + { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }, + ]); + }); + + it("should select a record with a where clause of a specific type", async () => { + await store.sync(schema); + + await store.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }); + await store.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }); + + const result = await store.select(schema.users, { + from: "users", + where: { streamVersion: 1n }, + }); + + expect(result).toEqual([ + { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }, + { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }, + ]); + }); + + it("should select with a limit and offset", async () => { + await store.sync(schema); + + await store.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }); + await store.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }); + + const result = await store.select(schema.users, { + from: "users", + limit: 1, + offset: 1, + }); + + expect(result).toEqual([ + { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }, + ]); + }); }); diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 30c95d0..2071776 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -4,6 +4,7 @@ import { unlink } from "fs/promises"; import { CircularDependencyError, + Collection, Model, ModelSchema, QueryDefinition, @@ -11,10 +12,13 @@ import { StoreQueryError, } from "@fabric/domain"; import { Database, Statement } from "sqlite3"; +import { filterToParams, filterToSQL } from "./filter-to-sql.js"; import { modelToSql } from "./model-to-sql.js"; import { - recordToKeys, - recordToParams, + keyToParam, + recordToSQLKeyParams, + recordToSQLKeys, + recordToSQLParams, recordToSQLSet, } from "./record-utils.js"; import { transformRow } from "./sql-to-value.js"; @@ -56,6 +60,32 @@ export class SQLiteStorageDriver implements StorageDriver { return stmt; } + private async getSelectStatement( + collection: Collection, + query: QueryDefinition, + ): Promise<[Statement, Record]> { + const selectFields = query.keys ? query.keys.join(", ") : "*"; + + const queryFilter = filterToSQL(query.where); + const limit = query.limit ? `LIMIT ${query.limit}` : ""; + const offset = query.offset ? `OFFSET ${query.offset}` : ""; + + const sql = [ + `SELECT ${selectFields}`, + `FROM ${query.from}`, + queryFilter, + limit, + offset, + ].join(" "); + + return [ + await this.getOrCreatePreparedStatement(sql), + { + ...filterToParams(collection, query.where), + }, + ]; + } + /** * Insert data into the store */ @@ -64,9 +94,9 @@ export class SQLiteStorageDriver implements StorageDriver { record: Record, ): AsyncResult { try { - const sql = `INSERT INTO ${model.name} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`; + const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, recordToParams(model, record)); + return await run(stmt, recordToSQLParams(model, record)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -80,13 +110,12 @@ export class SQLiteStorageDriver implements StorageDriver { * Run a select query against the store. */ async select( - model: Model, + collection: Collection, query: QueryDefinition, ): AsyncResult { try { - const sql = `SELECT * FROM ${query.from}`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await getAll(stmt, transformRow(model)); + const [stmt, params] = await this.getSelectStatement(collection, query); + return await getAll(stmt, params, transformRow(collection)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -99,14 +128,12 @@ export class SQLiteStorageDriver implements StorageDriver { * Run a select query against the store. */ async selectOne( - model: Model, + collection: Collection, query: QueryDefinition, ): AsyncResult { try { - const sql = `SELECT * FROM ${query.from}`; - const stmt = await this.getOrCreatePreparedStatement(sql); - - return await getOne(stmt, transformRow(model)); + const [stmt, params] = await this.getSelectStatement(collection, query); + return await getOne(stmt, params, transformRow(collection)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -174,15 +201,13 @@ export class SQLiteStorageDriver implements StorageDriver { record: Record, ): AsyncResult { try { - const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = :id`; + const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await run( - stmt, - recordToParams(model, { - ...record, - id, - }), - ); + const params = recordToSQLParams(model, { + ...record, + id, + }); + return await run(stmt, params); } catch (error: any) { return new StoreQueryError(error.message, { error, diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts index f98fc83..855c564 100644 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts @@ -54,10 +54,11 @@ export function run( export function getAll( stmt: Statement, + params: Record, transformer: (row: any) => any, ): Promise[]> { return new Promise((resolve, reject) => { - stmt.all((err: Error | null, rows: Record[]) => { + stmt.all(params, (err: Error | null, rows: Record[]) => { if (err) { reject(err); } else { @@ -69,14 +70,15 @@ export function getAll( export function getOne( stmt: Statement, + params: Record, transformer: (row: any) => any, ): Promise> { return new Promise((resolve, reject) => { - stmt.get((err: Error | null, row: Record) => { + stmt.all(params, (err: Error | null, rows: Record[]) => { if (err) { reject(err); } else { - resolve(transformer(row)); + resolve(rows.map(transformer)[0]); } }); }); diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/value-to-sql.ts index d39bb13..d55618a 100644 --- a/packages/fabric/store-sqlite/src/value-to-sql.ts +++ b/packages/fabric/store-sqlite/src/value-to-sql.ts @@ -18,6 +18,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { return v as number; }, ReferenceField: (f, v) => v, + FloatField: (f, v) => v, + DecimalField: (f, v) => v, }; export function fieldValueToSQL(field: FieldDefinition, value: any) { diff --git a/yarn.lock b/yarn.lock index dc7d075..80b024b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -420,6 +420,7 @@ __metadata: dependencies: "@fabric/core": "workspace:^" "@fabric/store-sqlite": "workspace:^" + decimal.js: "npm:^10.4.3" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown @@ -1409,6 +1410,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.4.3": + version: 10.4.3 + resolution: "decimal.js@npm:10.4.3" + checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee + languageName: node + linkType: hard + "decompress-response@npm:^6.0.0": version: 6.0.0 resolution: "decompress-response@npm:6.0.0" From 14ca23ef74d011fc1bfc31f2e637caeeec95eb8d Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 8 Oct 2024 15:40:07 -0300 Subject: [PATCH 12/37] [fabric/domain] Setup service and mock exports --- packages/fabric/domain/src/index.ts | 1 + packages/fabric/domain/src/mocks.ts | 1 + packages/fabric/domain/src/services/index.ts | 1 + packages/fabric/domain/src/services/mocks.ts | 1 + 4 files changed, 4 insertions(+) create mode 100644 packages/fabric/domain/src/mocks.ts create mode 100644 packages/fabric/domain/src/services/index.ts create mode 100644 packages/fabric/domain/src/services/mocks.ts diff --git a/packages/fabric/domain/src/index.ts b/packages/fabric/domain/src/index.ts index 552b447..af3bf8e 100644 --- a/packages/fabric/domain/src/index.ts +++ b/packages/fabric/domain/src/index.ts @@ -3,6 +3,7 @@ export * from "./events/index.js"; export * from "./files/index.js"; export * from "./models/index.js"; export * from "./security/index.js"; +export * from "./services/index.js"; export * from "./storage/index.js"; export * from "./types/index.js"; export * from "./use-case/index.js"; diff --git a/packages/fabric/domain/src/mocks.ts b/packages/fabric/domain/src/mocks.ts new file mode 100644 index 0000000..ff20c22 --- /dev/null +++ b/packages/fabric/domain/src/mocks.ts @@ -0,0 +1 @@ +export * from "./services/mocks.js"; diff --git a/packages/fabric/domain/src/services/index.ts b/packages/fabric/domain/src/services/index.ts new file mode 100644 index 0000000..62534e8 --- /dev/null +++ b/packages/fabric/domain/src/services/index.ts @@ -0,0 +1 @@ +export * from "./uuid-generator.js"; diff --git a/packages/fabric/domain/src/services/mocks.ts b/packages/fabric/domain/src/services/mocks.ts new file mode 100644 index 0000000..478d835 --- /dev/null +++ b/packages/fabric/domain/src/services/mocks.ts @@ -0,0 +1 @@ +export * from "./uuid-generator.mock.js"; From f30d2c47c5f35f08dd5c6f3a105f4a7c9f6c8f40 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Wed, 9 Oct 2024 09:29:52 -0300 Subject: [PATCH 13/37] [fabric/domain] Add decimal type export --- packages/fabric/domain/src/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fabric/domain/src/types/index.ts b/packages/fabric/domain/src/types/index.ts index 380f939..72d810d 100644 --- a/packages/fabric/domain/src/types/index.ts +++ b/packages/fabric/domain/src/types/index.ts @@ -1,4 +1,5 @@ export * from "./base-64.js"; +export * from "./decimal.js"; export * from "./email.js"; export * from "./entity.js"; export * from "./semver.js"; From 4fff9f91f5fcadb935242b051b04394d5e0ca4d8 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Thu, 10 Oct 2024 13:42:30 -0300 Subject: [PATCH 14/37] [fabric/domain] Improve Result & AsyncResult types; Refactor model types; Add timestamp and embedded field support --- .../fabric/core/src/error/tagged-error.ts | 2 +- packages/fabric/core/src/index.ts | 1 + .../fabric/core/src/result/async-result.ts | 29 ++- .../fabric/core/src/result/result.spec.ts | 57 +++++ packages/fabric/core/src/result/result.ts | 154 +++++++++++++- packages/fabric/core/src/run/index.ts | 1 + packages/fabric/core/src/run/run.spec.ts | 28 +++ packages/fabric/core/src/run/run.ts | 75 +++++++ packages/fabric/core/src/time/posix-date.ts | 31 ++- .../domain/src/models/fields/embedded.ts | 20 ++ .../domain/src/models/fields/field-to-type.ts | 5 + .../fabric/domain/src/models/fields/index.ts | 8 +- .../src/models/fields/reference-field.spec.ts | 34 +-- .../src/models/fields/reference-field.ts | 24 ++- .../domain/src/models/fields/timestamp.ts | 18 ++ .../fabric/domain/src/models/model-schema.ts | 6 +- packages/fabric/domain/src/models/model.ts | 2 +- .../domain/src/models/query/filter-options.ts | 6 +- .../domain/src/models/query/query-builder.ts | 17 +- .../domain/src/models/state-store.spec.ts | 63 +++--- .../fabric/domain/src/models/state-store.ts | 5 +- .../domain/src/storage/storage-driver.ts | 4 +- .../fabric/domain/src/utils/json-utils.ts | 14 ++ .../src/utils/sort-by-dependencies.spec.ts | 6 +- .../domain/src/utils/sort-by-dependencies.ts | 19 +- .../store-sqlite/src/model-to-sql.spec.ts | 22 ++ .../fabric/store-sqlite/src/model-to-sql.ts | 27 ++- .../fabric/store-sqlite/src/sql-to-value.ts | 4 +- .../store-sqlite/src/sqlite-driver.spec.ts | 108 ++++++---- .../fabric/store-sqlite/src/sqlite-driver.ts | 200 ++++++++++-------- .../fabric/store-sqlite/src/sqlite-wrapper.ts | 6 +- .../fabric/store-sqlite/src/value-to-sql.ts | 2 + 32 files changed, 745 insertions(+), 253 deletions(-) create mode 100644 packages/fabric/core/src/result/result.spec.ts create mode 100644 packages/fabric/core/src/run/index.ts create mode 100644 packages/fabric/core/src/run/run.spec.ts create mode 100644 packages/fabric/core/src/run/run.ts create mode 100644 packages/fabric/domain/src/models/fields/embedded.ts create mode 100644 packages/fabric/domain/src/models/fields/timestamp.ts create mode 100644 packages/fabric/domain/src/utils/json-utils.ts create mode 100644 packages/fabric/store-sqlite/src/model-to-sql.spec.ts diff --git a/packages/fabric/core/src/error/tagged-error.ts b/packages/fabric/core/src/error/tagged-error.ts index 01175a1..876c87e 100644 --- a/packages/fabric/core/src/error/tagged-error.ts +++ b/packages/fabric/core/src/error/tagged-error.ts @@ -3,7 +3,7 @@ import { TaggedVariant, VariantTag } from "../variant/index.js"; /** * A TaggedError is a tagged variant with an error message. */ -export class TaggedError +export class TaggedError extends Error implements TaggedVariant { diff --git a/packages/fabric/core/src/index.ts b/packages/fabric/core/src/index.ts index dbc3c9e..b17c448 100644 --- a/packages/fabric/core/src/index.ts +++ b/packages/fabric/core/src/index.ts @@ -2,6 +2,7 @@ export * from "./array/index.js"; export * from "./error/index.js"; export * from "./record/index.js"; export * from "./result/index.js"; +export * from "./run/index.js"; export * from "./time/index.js"; export * from "./types/index.js"; export * from "./variant/index.js"; diff --git a/packages/fabric/core/src/result/async-result.ts b/packages/fabric/core/src/result/async-result.ts index be0600e..22bc787 100644 --- a/packages/fabric/core/src/result/async-result.ts +++ b/packages/fabric/core/src/result/async-result.ts @@ -1,11 +1,32 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { TaggedError } from "../error/tagged-error.js"; +import { UnexpectedError } from "../error/unexpected-error.js"; import { Result } from "./result.js"; /** - * Un AsyncResult representa el resultado de una operación asíncrona que puede - * resolver en un valor de tipo `TValue` o en un error de tipo `TError`. + * An AsyncResult represents the result of an asynchronous operation that can + * resolve to a value of type `TValue` or an error of type `TError`. */ export type AsyncResult< - TValue, - TError extends TaggedError = never, + TValue = any, + TError extends TaggedError = never, > = Promise>; + +export namespace AsyncResult { + export async function tryFrom( + fn: () => Promise, + errorMapper: (error: any) => TError, + ): AsyncResult { + try { + return Result.succeedWith(await fn()); + } catch (error) { + return Result.failWith(errorMapper(error)); + } + } + + export async function from( + fn: () => Promise, + ): AsyncResult { + return tryFrom(fn, (error) => new UnexpectedError(error)); + } +} diff --git a/packages/fabric/core/src/result/result.spec.ts b/packages/fabric/core/src/result/result.spec.ts new file mode 100644 index 0000000..5628f2c --- /dev/null +++ b/packages/fabric/core/src/result/result.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, expectTypeOf, it, vitest } from "vitest"; +import { UnexpectedError } from "../error/unexpected-error.js"; +import { Result } from "./result.js"; + +describe("Result", () => { + describe("isOk", () => { + it("should return true if the result is ok", () => { + const result = Result.succeedWith(1) as Result; + + expect(result.isOk()).toBe(true); + + if (result.isOk()) { + expect(result.value).toEqual(1); + + expectTypeOf(result).toEqualTypeOf>(); + } + }); + }); + + describe("isError", () => { + it("should return true if the result is an error", () => { + const result = Result.failWith(new UnexpectedError()) as Result< + number, + UnexpectedError + >; + + expect(result.isError()).toBe(true); + + if (result.isError()) { + expect(result.value).toBeInstanceOf(UnexpectedError); + + expectTypeOf(result).toEqualTypeOf>(); + } + }); + }); + + describe("Map", () => { + it("should return the result of the last function", () => { + const x = 0; + + const result = Result.succeedWith(x + 1).map((x) => x * 2); + + expect(result.unwrapOrThrow()).toEqual(2); + + expectTypeOf(result).toEqualTypeOf>(); + }); + + it("should not execute the function if the result is an error", () => { + const fn = vitest.fn(); + const result = Result.failWith(new UnexpectedError()).map(fn); + + expect(result.isError()).toBe(true); + + expect(fn).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/fabric/core/src/result/result.ts b/packages/fabric/core/src/result/result.ts index a7c8d7f..72e25a2 100644 --- a/packages/fabric/core/src/result/result.ts +++ b/packages/fabric/core/src/result/result.ts @@ -1,11 +1,151 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { isError } from "../error/is-error.js"; import { TaggedError } from "../error/tagged-error.js"; -import { UnexpectedError } from "../error/unexpected-error.js"; /** - * Un Result representa el resultado de una operación - * que puede ser un valor de tipo `TValue` o un error `TError`. + * A Result represents the outcome of an operation + * that can be either a value of type `TValue` or an error `TError`. */ -export type Result< - TValue, - TError extends TaggedError = UnexpectedError, -> = TValue | TError; +export class Result { + static succeedWith(value: T): Result { + return new Result(value); + } + + static failWith(error: T): Result { + return new Result(error); + } + + static ok(): Result; + static ok(value: T): Result; + static ok(value?: any) { + return new Result(value ?? undefined); + } + + static tryFrom( + fn: () => T, + errorMapper: (error: any) => TError, + ): Result { + try { + return Result.succeedWith(fn()); + } catch (error) { + return Result.failWith(errorMapper(error)); + } + } + + private constructor(readonly value: TValue | TError) {} + + /** + * Unwrap the value of the result. + * If the result is an error, it will throw the error. + */ + unwrapOrThrow(): TValue { + if (isError(this.value)) { + throw this.value; + } + + return this.value as TValue; + } + + /** + * Throw the error if the result is an error. + * otherwise, do nothing. + */ + orThrow(): void { + if (isError(this.value)) { + throw this.value; + } + } + + unwrapErrorOrThrow(): TError { + if (!isError(this.value)) { + throw new Error("Result is not an error"); + } + + return this.value; + } + + /** + * Check if the result is a success. + */ + isOk(): this is Result { + return !isError(this.value); + } + + /** + * Check if the result is an error. + */ + isError(): this is Result { + return isError(this.value); + } + + /** + * Map a function over the value of the result. + */ + map( + fn: (value: TValue) => TMappedValue, + ): Result { + if (!isError(this.value)) { + return Result.succeedWith(fn(this.value as TValue)); + } + + return this as any; + } + + /** + * Maps a function over the value of the result and flattens the result. + */ + flatMap( + fn: (value: TValue) => Result, + ): Result { + if (!isError(this.value)) { + return fn(this.value as TValue) as any; + } + + return this as any; + } + + /** + * Try to map a function over the value of the result. + * If the function throws an error, the result will be a failure. + */ + tryMap( + fn: (value: TValue) => TMappedValue, + errMapper: (error: any) => TError, + ): Result { + if (!isError(this.value)) { + try { + return Result.succeedWith(fn(this.value as TValue)); + } catch (error) { + return Result.failWith(errMapper(error)); + } + } + + return this as any; + } + + /** + * Map a function over the error of the result. + */ + mapError( + fn: (error: TError) => TMappedError, + ): Result { + if (isError(this.value)) { + return Result.failWith(fn(this.value as TError)); + } + + return this as unknown as Result; + } + + /** + * Taps a function if the result is a success. + * This is useful for side effects that do not modify the result. + */ + tap(fn: (value: TValue) => void): Result { + if (!isError(this.value)) { + fn(this.value as TValue); + } + + return this; + } +} diff --git a/packages/fabric/core/src/run/index.ts b/packages/fabric/core/src/run/index.ts new file mode 100644 index 0000000..2392331 --- /dev/null +++ b/packages/fabric/core/src/run/index.ts @@ -0,0 +1 @@ +export * from "./run.js"; diff --git a/packages/fabric/core/src/run/run.spec.ts b/packages/fabric/core/src/run/run.spec.ts new file mode 100644 index 0000000..a573da7 --- /dev/null +++ b/packages/fabric/core/src/run/run.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { UnexpectedError } from "../error/unexpected-error.js"; +import { Result } from "../result/result.js"; +import { Run } from "./run.js"; + +describe("Run", () => { + describe("In Sequence", () => { + it("should pipe the results of multiple async functions", async () => { + const result = await Run.seq( + async () => Result.succeedWith(1), + async (x) => Result.succeedWith(x + 1), + async (x) => Result.succeedWith(x * 2), + ); + + expect(result.unwrapOrThrow()).toEqual(4); + }); + + it("should return the first error if one of the functions fails", async () => { + const result = await Run.seq( + async () => Result.succeedWith(1), + async () => Result.failWith(new UnexpectedError()), + async (x) => Result.succeedWith(x * 2), + ); + + expect(result.isError()).toBe(true); + }); + }); +}); diff --git a/packages/fabric/core/src/run/run.ts b/packages/fabric/core/src/run/run.ts new file mode 100644 index 0000000..9872572 --- /dev/null +++ b/packages/fabric/core/src/run/run.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TaggedError } from "../error/tagged-error.js"; +import { AsyncResult } from "../result/async-result.js"; + +export namespace Run { + // prettier-ignore + export async function seq< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + ): AsyncResult; + // prettier-ignore + export async function seq< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + T3, TE3 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + fn3: (value: T2) => AsyncResult, + ): AsyncResult; + export async function seq( + ...fns: ((...args: any[]) => AsyncResult)[] + ): AsyncResult { + let result = await fns[0](); + + for (let i = 1; i < fns.length; i++) { + if (result.isError()) { + return result; + } + + result = await fns[i](result.unwrapOrThrow()); + } + + return result; + } + + // prettier-ignore + export async function seqUNSAFE< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + ): Promise; + // prettier-ignore + export async function seqUNSAFE< + T1,TE1 extends TaggedError, + T2,TE2 extends TaggedError, + T3,TE3 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + fn3: (value: T2) => AsyncResult, + ): Promise; + export async function seqUNSAFE( + ...fns: ((...args: any[]) => AsyncResult)[] + ): Promise { + const result = await (seq as any)(...fns); + + if (result.isError()) { + throw result.unwrapOrThrow(); + } + + return result.unwrapOrThrow(); + } + + export async function UNSAFE( + fn: () => AsyncResult, + ): Promise { + return (await fn()).unwrapOrThrow(); + } +} diff --git a/packages/fabric/core/src/time/posix-date.ts b/packages/fabric/core/src/time/posix-date.ts index ac794f4..6c6245e 100644 --- a/packages/fabric/core/src/time/posix-date.ts +++ b/packages/fabric/core/src/time/posix-date.ts @@ -1,9 +1,38 @@ +import { isRecord } from "../record/is-record.js"; import { TaggedVariant } from "../variant/variant.js"; export class PosixDate { - constructor(public readonly timestamp: number) {} + constructor(public readonly timestamp: number = Date.now()) {} + + public toJSON(): PosixDateJSON { + return { + type: "posix-date", + timestamp: this.timestamp, + }; + } + + public static fromJson(json: PosixDateJSON): PosixDate { + return new PosixDate(json.timestamp); + } + + public static isPosixDateJSON(value: unknown): value is PosixDateJSON { + if ( + isRecord(value) && + "type" in value && + "timestamp" in value && + value["type"] === "posix-date" && + typeof value["timestamp"] === "number" + ) + return true; + return false; + } } export interface TimeZone extends TaggedVariant<"TimeZone"> { timestamp: number; } + +export interface PosixDateJSON { + type: "posix-date"; + timestamp: number; +} diff --git a/packages/fabric/domain/src/models/fields/embedded.ts b/packages/fabric/domain/src/models/fields/embedded.ts new file mode 100644 index 0000000..59b83c4 --- /dev/null +++ b/packages/fabric/domain/src/models/fields/embedded.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars +export interface EmbeddedFieldOptions extends BaseField {} + +export interface EmbeddedField + extends TaggedVariant<"EmbeddedField">, + EmbeddedFieldOptions {} + +export function createEmbeddedField< + K = any, + T extends EmbeddedFieldOptions = EmbeddedFieldOptions, +>(opts: T = {} as T): EmbeddedField & T { + return { + [VariantTag]: "EmbeddedField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts index b7f67b9..76e1ee8 100644 --- a/packages/fabric/domain/src/models/fields/field-to-type.ts +++ b/packages/fabric/domain/src/models/fields/field-to-type.ts @@ -1,10 +1,13 @@ +import { PosixDate } from "@fabric/core"; import { Decimal } from "decimal.js"; import { UUID } from "../../types/uuid.js"; import { DecimalField } from "./decimal.js"; +import { EmbeddedField } from "./embedded.js"; import { FloatField } from "./float.js"; import { IntegerField } from "./integer.js"; import { ReferenceField } from "./reference-field.js"; import { StringField } from "./string-field.js"; +import { TimestampField } from "./timestamp.js"; import { UUIDField } from "./uuid-field.js"; /** @@ -18,6 +21,8 @@ export type FieldToType = : TField extends ReferenceField ? MaybeOptional : TField extends DecimalField ? MaybeOptional : TField extends FloatField ? MaybeOptional + : TField extends TimestampField ? MaybeOptional + : TField extends EmbeddedField ? MaybeOptional : never; //prettier-ignore diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index 0f1b30a..cde17d4 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -1,8 +1,10 @@ import { createDecimalField, DecimalField } from "./decimal.js"; +import { createEmbeddedField, EmbeddedField } from "./embedded.js"; import { createFloatField, FloatField } from "./float.js"; import { createIntegerField, IntegerField } from "./integer.js"; import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createStringField, StringField } from "./string-field.js"; +import { createTimestampField, TimestampField } from "./timestamp.js"; import { createUUIDField, UUIDField } from "./uuid-field.js"; export * from "./base-field.js"; export * from "./field-to-type.js"; @@ -14,7 +16,9 @@ export type FieldDefinition = | IntegerField | FloatField | DecimalField - | ReferenceField; + | ReferenceField + | TimestampField + | EmbeddedField; export namespace Field { export const string = createStringField; @@ -23,4 +27,6 @@ export namespace Field { export const reference = createReferenceField; export const decimal = createDecimalField; export const float = createFloatField; + export const timestamp = createTimestampField; + export const embedded = createEmbeddedField; } diff --git a/packages/fabric/domain/src/models/fields/reference-field.spec.ts b/packages/fabric/domain/src/models/fields/reference-field.spec.ts index d5e75b4..2adfb93 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.spec.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { defineModel } from "../model.js"; import { Field } from "./index.js"; import { - InvalidReferenceField, + InvalidReferenceFieldError, validateReferenceField, } from "./reference-field.js"; @@ -26,26 +26,18 @@ describe("Validate Reference Field", () => { Field.reference({ targetModel: "foo", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should not return an error if the target model is in the schema", () => { - const result = validateReferenceField( + validateReferenceField( schema, Field.reference({ targetModel: "User", }), - ); - - if (isError(result)) { - throw result.reason; - } + ).unwrapOrThrow(); }); it("should return an error if the target key is not in the target model", () => { @@ -55,13 +47,9 @@ describe("Validate Reference Field", () => { targetModel: "User", targetKey: "foo", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should return error if the target key is not unique", () => { @@ -71,13 +59,9 @@ describe("Validate Reference Field", () => { targetModel: "User", targetKey: "otherNotUnique", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should not return an error if the target key is in the target model and is unique", () => { diff --git a/packages/fabric/domain/src/models/fields/reference-field.ts b/packages/fabric/domain/src/models/fields/reference-field.ts index b8272ec..a2180de 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.ts @@ -27,16 +27,20 @@ export function getTargetKey(field: ReferenceField): string { export function validateReferenceField( schema: ModelSchema, field: ReferenceField, -): Result { +): Result { if (!schema[field.targetModel]) { - return new InvalidReferenceField( - `The target model '${field.targetModel}' is not in the schema.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target model '${field.targetModel}' is not in the schema.`, + ), ); } if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) { - return new InvalidReferenceField( - `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, + ), ); } @@ -44,13 +48,17 @@ export function validateReferenceField( field.targetKey && !schema[field.targetModel].fields[field.targetKey].isUnique ) { - return new InvalidReferenceField( - `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, + ), ); } + + return Result.ok(); } -export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> { +export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> { constructor(readonly reason: string) { super("InvalidReferenceField"); } diff --git a/packages/fabric/domain/src/models/fields/timestamp.ts b/packages/fabric/domain/src/models/fields/timestamp.ts new file mode 100644 index 0000000..41b1eab --- /dev/null +++ b/packages/fabric/domain/src/models/fields/timestamp.ts @@ -0,0 +1,18 @@ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TimestampFieldOptions extends BaseField {} + +export interface TimestampField + extends TaggedVariant<"TimestampField">, + TimestampFieldOptions {} + +export function createTimestampField( + opts: T = {} as T, +): TimestampField & T { + return { + [VariantTag]: "TimestampField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts index 8150980..ca5ce05 100644 --- a/packages/fabric/domain/src/models/model-schema.ts +++ b/packages/fabric/domain/src/models/model-schema.ts @@ -1,7 +1,7 @@ -import { Model } from "./model.js"; +import { Collection } from "./model.js"; -export type ModelSchema = Record; +export type ModelSchema = Record; -export type ModelSchemaFromModels = { +export type ModelSchemaFromModels = { [K in TModels["name"]]: Extract; }; diff --git a/packages/fabric/domain/src/models/model.ts b/packages/fabric/domain/src/models/model.ts index a79321b..a2a4ec8 100644 --- a/packages/fabric/domain/src/models/model.ts +++ b/packages/fabric/domain/src/models/model.ts @@ -48,7 +48,7 @@ export function defineCollection< } as const; } -export type ModelToType = { +export type ModelToType = { [K in Keyof]: FieldToType; }; diff --git a/packages/fabric/domain/src/models/query/filter-options.ts b/packages/fabric/domain/src/models/query/filter-options.ts index 9a86305..98a5ed1 100644 --- a/packages/fabric/domain/src/models/query/filter-options.ts +++ b/packages/fabric/domain/src/models/query/filter-options.ts @@ -16,9 +16,9 @@ export type SingleFilterOption = { export type MultiFilterOption = SingleFilterOption[]; -export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type"); -export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value"); -export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator"); +export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type"; +export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value"; +export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator"; export type LikeFilterOption = T extends string ? { diff --git a/packages/fabric/domain/src/models/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts index 40304e6..d8aa630 100644 --- a/packages/fabric/domain/src/models/query/query-builder.ts +++ b/packages/fabric/domain/src/models/query/query-builder.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../../errors/query-error.js"; import { StorageDriver } from "../../storage/storage-driver.js"; @@ -41,19 +42,23 @@ export class QueryBuilder implements StoreQuery { }); } + select(): AsyncResult; select>( - keys?: K[], - ): AsyncResult[], StoreQueryError> { - return this.driver.select(this.schema[this.query.from], { + keys: K[], + ): AsyncResult[], StoreQueryError>; + select>(keys?: K[]): AsyncResult { + return this.driver.select(this.schema, { ...this.query, keys, }); } + selectOne(): AsyncResult; selectOne>( - keys?: K[], - ): AsyncResult, StoreQueryError> { - return this.driver.selectOne(this.schema[this.query.from], { + keys: K, + ): AsyncResult, StoreQueryError>; + selectOne>(keys?: K[]): AsyncResult { + return this.driver.selectOne(this.schema, { ...this.query, keys, }); diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts index 7e30408..4be04f3 100644 --- a/packages/fabric/domain/src/models/state-store.spec.ts +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -1,4 +1,4 @@ -import { isError } from "@fabric/core"; +import { isError, Run } from "@fabric/core"; import { SQLiteStorageDriver } from "@fabric/store-sqlite"; import { afterEach, @@ -58,9 +58,7 @@ describe("State Store", () => { if (isError(insertResult)) throw insertResult; - const result = await store.from("users").select(); - - if (isError(result)) throw result; + const result = (await store.from("users").select()).unwrapOrThrow(); expectTypeOf(result).toEqualTypeOf< { @@ -83,34 +81,39 @@ describe("State Store", () => { it("should query with a where clause", async () => { const newUUID = UUIDGeneratorMock.generate(); - await store.insertInto("users", { - name: "test", - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - }); - await store.insertInto("users", { - name: "anotherName", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }); - await store.insertInto("users", { - name: "anotherName2", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }); + await Run.seqUNSAFE( + () => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName2", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + ); - const result = await store - .from("users") - .where({ - name: isLike("te*"), - }) - .select(); - - if (isError(result)) throw result; + const result = await Run.UNSAFE(() => + store + .from("users") + .where({ + name: isLike("te%"), + }) + .select(), + ); expectTypeOf(result).toEqualTypeOf< { diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index 292bc7e..fd6473c 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -1,4 +1,5 @@ import { AsyncResult } from "@fabric/core"; +import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { StorageDriver } from "../storage/storage-driver.js"; import { ModelSchemaFromModels } from "./model-schema.js"; @@ -20,8 +21,8 @@ export class StateStore { }, {} as ModelSchemaFromModels); } - async migrate(): AsyncResult { - await this.driver.sync(this.schema); + migrate(): AsyncResult { + return this.driver.sync(this.schema); } async insertInto>( diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 6fc6c03..0e414b2 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -20,7 +20,7 @@ export interface StorageDriver { * Run a select query against the store. */ select( - model: Collection, + model: ModelSchema, query: QueryDefinition, ): AsyncResult; @@ -28,7 +28,7 @@ export interface StorageDriver { * Run a select query against the store. */ selectOne( - model: Collection, + model: ModelSchema, query: QueryDefinition, ): AsyncResult; diff --git a/packages/fabric/domain/src/utils/json-utils.ts b/packages/fabric/domain/src/utils/json-utils.ts new file mode 100644 index 0000000..22d3340 --- /dev/null +++ b/packages/fabric/domain/src/utils/json-utils.ts @@ -0,0 +1,14 @@ +import { PosixDate } from "@fabric/core"; + +export namespace JSONUtils { + export function reviver(key: string, value: unknown) { + if (PosixDate.isPosixDateJSON(value)) { + return PosixDate.fromJson(value); + } + return value; + } + + export function parse(json: string): T { + return JSON.parse(json, reviver); + } +} diff --git a/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts index 904cc84..5b122f5 100644 --- a/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts +++ b/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts @@ -15,7 +15,7 @@ describe("sortByDependencies", () => { const result = sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }); + }).unwrapOrThrow(); expect(result).toEqual([ { id: 3, name: "C", dependencies: [] }, @@ -35,7 +35,7 @@ describe("sortByDependencies", () => { sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }), + }).unwrapErrorOrThrow(), ).toBeInstanceOf(CircularDependencyError); }); @@ -45,7 +45,7 @@ describe("sortByDependencies", () => { const result = sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }); + }).unwrapOrThrow(); expect(result).toEqual([]); }); diff --git a/packages/fabric/domain/src/utils/sort-by-dependencies.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.ts index 8a7d2c9..78c0a00 100644 --- a/packages/fabric/domain/src/utils/sort-by-dependencies.ts +++ b/packages/fabric/domain/src/utils/sort-by-dependencies.ts @@ -34,14 +34,15 @@ export function sortByDependencies( visited.add(key); sorted.push(key); }; - try { - graph.forEach((deps, key) => { - visit(key, []); - }); - } catch (e) { - return e as CircularDependencyError; - } - return sorted.map( - (key) => array.find((element) => keyGetter(element) === key) as T, + return Result.tryFrom( + () => { + graph.forEach((deps, key) => { + visit(key, []); + }); + return sorted.map( + (key) => array.find((element) => keyGetter(element) === key) as T, + ); + }, + (e) => e as CircularDependencyError, ); } diff --git a/packages/fabric/store-sqlite/src/model-to-sql.spec.ts b/packages/fabric/store-sqlite/src/model-to-sql.spec.ts new file mode 100644 index 0000000..8038dec --- /dev/null +++ b/packages/fabric/store-sqlite/src/model-to-sql.spec.ts @@ -0,0 +1,22 @@ +import { defineCollection, Field } from "@fabric/domain"; +import { describe, expect, it } from "vitest"; +import { modelToSql } from "./model-to-sql.js"; + +describe("ModelToSQL", () => { + const model = defineCollection("something", { + id: Field.uuid({ isPrimaryKey: true }), + name: Field.string(), + age: Field.integer(), + // isTrue: Field.boolean(), + date: Field.timestamp(), + reference: Field.reference({ targetModel: "somethingElse" }), + }); + + it("should generate SQL for a model", () => { + const result = modelToSql(model); + + expect(result).toEqual( + `CREATE TABLE something (id TEXT PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL, date NUMERIC NOT NULL, reference TEXT NOT NULL REFERENCES somethingElse(id))`, + ); + }); +}); diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index ede39f4..4f2bee6 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Variant, VariantTag } from "@fabric/core"; -import { FieldDefinition, getTargetKey, Model } from "@fabric/domain"; +import { Collection, FieldDefinition, getTargetKey } from "@fabric/domain"; +import { EmbeddedField } from "@fabric/domain/dist/models/fields/embedded.js"; +import { TimestampField } from "@fabric/domain/dist/models/fields/timestamp.js"; type FieldSQLDefinitionMap = { [K in FieldDefinition[VariantTag]]: ( @@ -19,7 +21,9 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { "TEXT", f.isPrimaryKey ? "PRIMARY KEY" : "", modifiersFromOpts(f), - ].join(" "); + ] + .filter((x) => x) + .join(" "); }, IntegerField: (n, f): string => { return [n, "INTEGER", modifiersFromOpts(f)].join(" "); @@ -29,8 +33,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { n, "TEXT", modifiersFromOpts(f), - ",", - `FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`, + `REFERENCES ${f.targetModel}(${getTargetKey(f)})`, ].join(" "); }, FloatField: (n, f): string => { @@ -39,6 +42,12 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { DecimalField: (n, f): string => { return [n, "REAL", modifiersFromOpts(f)].join(" "); }, + TimestampField: (n, f: TimestampField): string => { + return [n, "NUMERIC", modifiersFromOpts(f)].join(" "); + }, + EmbeddedField: (n, f: EmbeddedField): string => { + return [n, "TEXT", modifiersFromOpts(f)].join(" "); + }, }; function fieldDefinitionToSQL(name: string, field: FieldDefinition) { return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); @@ -48,17 +57,17 @@ function modifiersFromOpts(field: FieldDefinition) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { return; } - return [ - !field.isOptional ? "NOT NULL" : "", - field.isUnique ? "UNIQUE" : "", - ].join(" "); + return [!field.isOptional ? "NOT NULL" : "", field.isUnique ? "UNIQUE" : ""] + .filter((x) => x) + .join(" "); } export function modelToSql( - model: Model>, + model: Collection>, ) { const fields = Object.entries(model.fields) .map(([name, type]) => fieldDefinitionToSQL(name, type)) + .filter((x) => x) .join(", "); return `CREATE TABLE ${model.name} (${fields})`; diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sql-to-value.ts index 152748c..a75af59 100644 --- a/packages/fabric/store-sqlite/src/sql-to-value.ts +++ b/packages/fabric/store-sqlite/src/sql-to-value.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { VariantTag } from "@fabric/core"; +import { PosixDate, VariantTag } from "@fabric/core"; import { Collection, FieldDefinition, FieldToType } from "@fabric/domain"; export function transformRow(model: Collection) { @@ -36,4 +36,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { ReferenceField: (f, v) => v, FloatField: (f, v) => v, DecimalField: (f, v) => v, + TimestampField: (f, v) => new PosixDate(v), + EmbeddedField: (f, v: string) => JSON.parse(v), }; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 514c9a7..89bc226 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -5,28 +5,32 @@ import { SQLiteStorageDriver } from "./sqlite-driver.js"; describe("SQLite Store Driver", () => { const schema = { + demo: defineModel("demo", { + value: Field.float(), + owner: Field.reference({ targetModel: "users" }), + }), users: defineModel("users", { name: Field.string(), }), }; - let store: SQLiteStorageDriver; + let driver: SQLiteStorageDriver; beforeEach(() => { - store = new SQLiteStorageDriver(":memory:"); + driver = new SQLiteStorageDriver(":memory:"); }); afterEach(async () => { - const result = await store.close(); + const result = await driver.close(); if (isError(result)) throw result; }); it("should synchronize the store and insert a record", async () => { - const result = await store.sync(schema); + const result = await driver.sync(schema); if (isError(result)) throw result; - const insertResult = await store.insert(schema.users, { + const insertResult = await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", @@ -35,7 +39,9 @@ describe("SQLite Store Driver", () => { if (isError(insertResult)) throw insertResult; - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -43,19 +49,21 @@ describe("SQLite Store Driver", () => { }); it("should be update a record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - const err = await store.update(schema.users, "1", { name: "updated" }); + const err = await driver.update(schema.users, "1", { name: "updated" }); if (isError(err)) throw err; - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "updated", streamId: "1", streamVersion: 1n }, @@ -63,39 +71,43 @@ describe("SQLite Store Driver", () => { }); it("should be able to delete a record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.delete(schema.users, "1"); + await driver.delete(schema.users, "1"); - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([]); }); it("should be able to select records", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1n, }); - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -104,22 +116,24 @@ describe("SQLite Store Driver", () => { }); it("should be able to select one record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1n, }); - const record = await store.selectOne(schema.users, { from: "users" }); + const record = ( + await driver.selectOne(schema, { from: "users" }) + ).unwrapOrThrow(); expect(record).toEqual({ id: "1", @@ -130,25 +144,27 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - where: { name: isLike("te%") }, - }); + const result = ( + await driver.select(schema, { + from: "users", + where: { name: isLike("te%") }, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { @@ -161,25 +177,27 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause of a specific type", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - where: { streamVersion: 1n }, - }); + const result = ( + await driver.select(schema, { + from: "users", + where: { streamVersion: 1n }, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { @@ -198,26 +216,28 @@ describe("SQLite Store Driver", () => { }); it("should select with a limit and offset", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - limit: 1, - offset: 1, - }); + const result = ( + await driver.select(schema, { + from: "users", + limit: 1, + offset: 1, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 2071776..f5d4806 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -39,10 +39,6 @@ export class SQLiteStorageDriver implements StorageDriver { constructor(private path: string) { this.db = new Database(path); - - // Enable Write-Ahead Logging, which is faster and more reliable. - this.db.run("PRAGMA journal_mode = WAL;"); - this.db.run("PRAGMA foreign_keys = ON;"); } /** @@ -93,53 +89,65 @@ export class SQLiteStorageDriver implements StorageDriver { model: Model, record: Record, ): AsyncResult { - try { - const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, recordToSQLParams(model, record)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; + const stmt = await this.getOrCreatePreparedStatement(sql); + return await run(stmt, recordToSQLParams(model, record)); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); } /** * Run a select query against the store. */ async select( - collection: Collection, + schema: ModelSchema, query: QueryDefinition, ): AsyncResult { - try { - const [stmt, params] = await this.getSelectStatement(collection, query); - return await getAll(stmt, params, transformRow(collection)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - query, - }); - } + return AsyncResult.tryFrom( + async () => { + const [stmt, params] = await this.getSelectStatement( + schema[query.from], + query, + ); + return await getAll(stmt, params, transformRow(schema[query.from])); + }, + (err) => + new StoreQueryError(err.message, { + err, + query, + }), + ); } /** * Run a select query against the store. */ async selectOne( - collection: Collection, + schema: ModelSchema, query: QueryDefinition, ): AsyncResult { - try { - const [stmt, params] = await this.getSelectStatement(collection, query); - return await getOne(stmt, params, transformRow(collection)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - query, - }); - } + return AsyncResult.tryFrom( + async () => { + const [stmt, params] = await this.getSelectStatement( + schema[query.from], + query, + ); + return await getOne(stmt, params, transformRow(schema[query.from])); + }, + (err) => + new StoreQueryError(err.message, { + err, + query, + }), + ); } /** @@ -148,48 +156,56 @@ export class SQLiteStorageDriver implements StorageDriver { async sync( schema: ModelSchema, ): AsyncResult { - try { - await dbRun(this.db, "BEGIN TRANSACTION;"); - for (const modelKey in schema) { - const model = schema[modelKey]; - await dbRun(this.db, modelToSql(model)); - } - await dbRun(this.db, "COMMIT;"); - } catch (error: any) { - await dbRun(this.db, "ROLLBACK;"); - return new StoreQueryError(error.message, { - error, - schema, - }); - } + return AsyncResult.tryFrom( + async () => { + // Enable Write-Ahead Logging, which is faster and more reliable. + await dbRun(this.db, "PRAGMA journal_mode = WAL;"); + + // Enable foreign key constraints. + await dbRun(this.db, "PRAGMA foreign_keys = ON;"); + + // Begin a transaction to create the schema. + await dbRun(this.db, "BEGIN TRANSACTION;"); + for (const modelKey in schema) { + const model = schema[modelKey]; + await dbRun(this.db, modelToSql(model)); + } + await dbRun(this.db, "COMMIT;"); + }, + (error) => + new StoreQueryError(error.message, { + error, + schema, + }), + ); } /** * Drop the store. This is a destructive operation. */ async drop(): AsyncResult { - try { - if (this.path === ":memory:") { - return new StoreQueryError("Cannot drop in-memory database", {}); - } else { - await unlink(this.path); - } - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - }); - } + return AsyncResult.tryFrom( + async () => { + if (this.path === ":memory:") { + throw "Cannot drop in-memory database"; + } else { + await unlink(this.path); + } + }, + (error) => + new StoreQueryError(error.message, { + error, + }), + ); } async close(): AsyncResult { - try { + return AsyncResult.from(async () => { for (const stmt of this.cachedStatements.values()) { await finalize(stmt); } await dbClose(this.db); - } catch (error: any) { - return new UnexpectedError({ error }); - } + }); } /** @@ -200,21 +216,23 @@ export class SQLiteStorageDriver implements StorageDriver { id: string, record: Record, ): AsyncResult { - try { - const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; - const stmt = await this.getOrCreatePreparedStatement(sql); - const params = recordToSQLParams(model, { - ...record, - id, - }); - return await run(stmt, params); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; + const stmt = await this.getOrCreatePreparedStatement(sql); + const params = recordToSQLParams(model, { + ...record, + id, + }); + return await run(stmt, params); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); } /** @@ -222,16 +240,18 @@ export class SQLiteStorageDriver implements StorageDriver { */ async delete(model: Model, id: string): AsyncResult { - try { - const sql = `DELETE FROM ${model.name} WHERE id = :id`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, { ":id": id }); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - id, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`; + const stmt = await this.getOrCreatePreparedStatement(sql); + return await run(stmt, { [keyToParam("id")]: id }); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + id, + }), + ); } } diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts index 855c564..4a58620 100644 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Database, Statement } from "sqlite3"; -export function dbRun(db: Database, statement: string): Promise { +export function dbRun(db: Database, statement: string): Promise { return new Promise((resolve, reject) => { - db.run(statement, (err) => { + db.all(statement, (err, result) => { if (err) { reject(err); } else { - resolve(); + resolve(result); } }); }); diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/value-to-sql.ts index d55618a..e6a45b0 100644 --- a/packages/fabric/store-sqlite/src/value-to-sql.ts +++ b/packages/fabric/store-sqlite/src/value-to-sql.ts @@ -20,6 +20,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { ReferenceField: (f, v) => v, FloatField: (f, v) => v, DecimalField: (f, v) => v, + TimestampField: (f, v) => v.timestamp, + EmbeddedField: (f, v: string) => JSON.stringify(v), }; export function fieldValueToSQL(field: FieldDefinition, value: any) { From 6919a819b67edcbccae6ee5dff5c27520ca3798a Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 15:39:51 -0300 Subject: [PATCH 15/37] [fabric/domain] Refactor event store and event stream interfaces --- .../fabric/domain/src/events/event-store.ts | 42 +++++++------------ .../fabric/domain/src/events/event-stream.ts | 16 +++++++ packages/fabric/domain/src/events/event.ts | 18 +++----- packages/fabric/domain/src/events/index.ts | 2 + .../fabric/domain/src/events/stored-event.ts | 10 +++++ 5 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 packages/fabric/domain/src/events/event-stream.ts create mode 100644 packages/fabric/domain/src/events/stored-event.ts diff --git a/packages/fabric/domain/src/events/event-store.ts b/packages/fabric/domain/src/events/event-store.ts index c58102b..0e31fc1 100644 --- a/packages/fabric/domain/src/events/event-store.ts +++ b/packages/fabric/domain/src/events/event-store.ts @@ -1,36 +1,26 @@ -import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; +import { AsyncResult, PosixDate } from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; -import { UUID } from "../types/uuid.js"; -import { Event, StoredEvent } from "./event.js"; +import { EventsFromStream, EventStream } from "./event-stream.js"; +import { StoredEvent } from "./stored-event.js"; -export interface EventStore { - getStream( - streamId: UUID, - ): AsyncResult, StoreQueryError>; - - appendToStream( - streamId: UUID, - events: TEvent, - ): AsyncResult; -} - -export interface EventStream { - getCurrentVersion(): bigint; - - append(events: TEvent): AsyncResult, StoreQueryError>; - - subscribe(callback: (event: StoredEvent) => MaybePromise): void; - - getEvents( - opts?: EventFilterOptions, - ): AsyncResult[], StoreQueryError>; +export interface EventStore { + /** + * Store a new event in the event store. + */ + append< + TStreamKey extends TEventStream["name"], + T extends EventsFromStream, + >( + streamName: TStreamKey, + event: T, + ): AsyncResult, StoreQueryError>; } export interface EventFilterOptions { fromDate?: PosixDate; toDate?: PosixDate; - fromVersion?: number; - toVersion?: number; + fromVersion?: bigint; + toVersion?: bigint; limit?: number; offset?: number; } diff --git a/packages/fabric/domain/src/events/event-stream.ts b/packages/fabric/domain/src/events/event-stream.ts new file mode 100644 index 0000000..9b70954 --- /dev/null +++ b/packages/fabric/domain/src/events/event-stream.ts @@ -0,0 +1,16 @@ +import { AsyncResult } from "@fabric/core"; +import { UUID } from "../types/uuid.js"; +import { Event } from "./event.js"; +import { StoredEvent } from "./stored-event.js"; + +export interface EventStream< + TName extends string = string, + TEvent extends Event = Event, +> { + id: UUID; + name: TName; + append(event: Event): AsyncResult>; +} + +export type EventsFromStream = + T extends EventStream ? TEvent : never; diff --git a/packages/fabric/domain/src/events/event.ts b/packages/fabric/domain/src/events/event.ts index be843d1..e314103 100644 --- a/packages/fabric/domain/src/events/event.ts +++ b/packages/fabric/domain/src/events/event.ts @@ -1,20 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PosixDate, TaggedVariant } from "@fabric/core"; import { UUID } from "../types/uuid.js"; /** * An event is a tagged variant with a payload and a timestamp. */ -export interface Event - extends TaggedVariant { - streamId: UUID; - payload: TPayload; +export interface Event { + readonly type: TTag; + readonly id: UUID; + readonly streamId: UUID; + readonly payload: TPayload; } - -/** - * A stored event is an inmutable event, already stored, with it's version in the stream and timestamp. - */ -export type StoredEvent = Readonly & { - readonly version: number; - readonly timestamp: PosixDate; -}; diff --git a/packages/fabric/domain/src/events/index.ts b/packages/fabric/domain/src/events/index.ts index a28ecf2..7b181cc 100644 --- a/packages/fabric/domain/src/events/index.ts +++ b/packages/fabric/domain/src/events/index.ts @@ -1,2 +1,4 @@ export * from "./event-store.js"; +export * from "./event-stream.js"; export * from "./event.js"; +export * from "./stored-event.js"; diff --git a/packages/fabric/domain/src/events/stored-event.ts b/packages/fabric/domain/src/events/stored-event.ts new file mode 100644 index 0000000..e0a33eb --- /dev/null +++ b/packages/fabric/domain/src/events/stored-event.ts @@ -0,0 +1,10 @@ +import { PosixDate } from "@fabric/core"; +import { Event } from "./event.js"; + +/** + * A stored event is an inmutable event, already stored, with it's version in the stream and timestamp. + */ +export type StoredEvent = TEvent & { + readonly version: bigint; + readonly timestamp: PosixDate; +}; From 475ec309cb9cdb976cd3643565d70be83a504de3 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 15:41:12 -0300 Subject: [PATCH 16/37] [fabric/store-sqlite] Refactor SQLite wrapper: replace old functions with SQLiteDatabase class for improved management and caching of statements --- .../store-sqlite/src/sqlite-driver.spec.ts | 268 ++++++++++-------- .../fabric/store-sqlite/src/sqlite-driver.ts | 100 +++---- .../fabric/store-sqlite/src/sqlite-wrapper.ts | 97 ------- .../store-sqlite/src/sqlite/sqlite-wrapper.ts | 134 +++++++++ 4 files changed, 316 insertions(+), 283 deletions(-) delete mode 100644 packages/fabric/store-sqlite/src/sqlite-wrapper.ts create mode 100644 packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 89bc226..375165d 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -1,4 +1,4 @@ -import { isError } from "@fabric/core"; +import { Run } from "@fabric/core"; import { defineModel, Field, isLike } from "@fabric/domain"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { SQLiteStorageDriver } from "./sqlite-driver.js"; @@ -21,27 +21,24 @@ describe("SQLite Store Driver", () => { }); afterEach(async () => { - const result = await driver.close(); - if (isError(result)) throw result; + await Run.UNSAFE(() => driver.close()); }); it("should synchronize the store and insert a record", async () => { - const result = await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - if (isError(result)) throw result; + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); - const insertResult = await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - - if (isError(insertResult)) throw insertResult; - - const records = ( - await driver.select(schema, { from: "users" }) - ).unwrapOrThrow(); + const records = await Run.UNSAFE(() => + driver.select(schema, { from: "users" }), + ); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -49,21 +46,24 @@ describe("SQLite Store Driver", () => { }); it("should be update a record", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); - const err = await driver.update(schema.users, "1", { name: "updated" }); - if (isError(err)) throw err; + await Run.UNSAFE(() => + driver.update(schema.users, "1", { name: "updated" }), + ); - const records = ( - await driver.select(schema, { from: "users" }) - ).unwrapOrThrow(); + const records = await Run.UNSAFE(() => + driver.select(schema, { from: "users" }), + ); expect(records).toEqual([ { id: "1", name: "updated", streamId: "1", streamVersion: 1n }, @@ -71,43 +71,49 @@ describe("SQLite Store Driver", () => { }); it("should be able to delete a record", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); - await driver.delete(schema.users, "1"); + await Run.UNSAFE(() => driver.delete(schema.users, "1")); - const records = ( - await driver.select(schema, { from: "users" }) - ).unwrapOrThrow(); + const records = await Run.UNSAFE(() => + driver.select(schema, { from: "users" }), + ); expect(records).toEqual([]); }); it("should be able to select records", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - await driver.insert(schema.users, { - id: "2", - name: "test", - streamId: "2", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "2", + name: "test", + streamId: "2", + streamVersion: 1n, + }), + ); - const records = ( - await driver.select(schema, { from: "users" }) - ).unwrapOrThrow(); + const records = await Run.UNSAFE(() => + driver.select(schema, { from: "users" }), + ); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -116,24 +122,28 @@ describe("SQLite Store Driver", () => { }); it("should be able to select one record", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - await driver.insert(schema.users, { - id: "2", - name: "test", - streamId: "2", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "2", + name: "test", + streamId: "2", + streamVersion: 1n, + }), + ); - const record = ( - await driver.selectOne(schema, { from: "users" }) - ).unwrapOrThrow(); + const record = await Run.UNSAFE(() => + driver.selectOne(schema, { from: "users" }), + ); expect(record).toEqual({ id: "1", @@ -144,27 +154,31 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - await driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }), + ); - const result = ( - await driver.select(schema, { + const result = await Run.UNSAFE(() => + driver.select(schema, { from: "users", where: { name: isLike("te%") }, - }) - ).unwrapOrThrow(); + }), + ); expect(result).toEqual([ { @@ -177,27 +191,31 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause of a specific type", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - await driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }), + ); - const result = ( - await driver.select(schema, { + const result = await Run.UNSAFE(() => + driver.select(schema, { from: "users", where: { streamVersion: 1n }, - }) - ).unwrapOrThrow(); + }), + ); expect(result).toEqual([ { @@ -216,28 +234,32 @@ describe("SQLite Store Driver", () => { }); it("should select with a limit and offset", async () => { - await driver.sync(schema); + await Run.UNSAFE(() => driver.sync(schema)); - await driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - await driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "1", + name: "test", + streamId: "1", + streamVersion: 1n, + }), + ); + await Run.UNSAFE(() => + driver.insert(schema.users, { + id: "2", + name: "jamón", + streamId: "2", + streamVersion: 1n, + }), + ); - const result = ( - await driver.select(schema, { + const result = await Run.UNSAFE(() => + driver.select(schema, { from: "users", limit: 1, offset: 1, - }) - ).unwrapOrThrow(); + }), + ); expect(result).toEqual([ { diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index f5d4806..1ceee8f 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -11,7 +11,6 @@ import { StorageDriver, StoreQueryError, } from "@fabric/domain"; -import { Database, Statement } from "sqlite3"; import { filterToParams, filterToSQL } from "./filter-to-sql.js"; import { modelToSql } from "./model-to-sql.js"; import { @@ -22,44 +21,19 @@ import { recordToSQLSet, } from "./record-utils.js"; import { transformRow } from "./sql-to-value.js"; -import { - dbClose, - dbRun, - finalize, - getAll, - getOne, - prepare, - run, -} from "./sqlite-wrapper.js"; +import { SQLiteDatabase } from "./sqlite/sqlite-wrapper.js"; export class SQLiteStorageDriver implements StorageDriver { - private db: Database; - - private cachedStatements = new Map(); + private db: SQLiteDatabase; constructor(private path: string) { - this.db = new Database(path); + this.db = new SQLiteDatabase(path); } - /** - * Get a statement from the cache or prepare a new one. - */ - private async getOrCreatePreparedStatement(sql: string): Promise { - if (this.cachedStatements.has(sql)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- We know it's there. - return this.cachedStatements.get(sql)!; - } - - const stmt = await prepare(this.db, sql); - this.cachedStatements.set(sql, stmt); - - return stmt; - } - - private async getSelectStatement( + private getSelectStatement( collection: Collection, query: QueryDefinition, - ): Promise<[Statement, Record]> { + ): [string, Record] { const selectFields = query.keys ? query.keys.join(", ") : "*"; const queryFilter = filterToSQL(query.where); @@ -75,7 +49,7 @@ export class SQLiteStorageDriver implements StorageDriver { ].join(" "); return [ - await this.getOrCreatePreparedStatement(sql), + sql, { ...filterToParams(collection, query.where), }, @@ -91,9 +65,10 @@ export class SQLiteStorageDriver implements StorageDriver { ): AsyncResult { return AsyncResult.tryFrom( async () => { - const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, recordToSQLParams(model, record)); + await this.db.runPrepared( + `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`, + recordToSQLParams(model, record), + ); }, (error) => new StoreQueryError(error.message, { @@ -113,11 +88,15 @@ export class SQLiteStorageDriver implements StorageDriver { ): AsyncResult { return AsyncResult.tryFrom( async () => { - const [stmt, params] = await this.getSelectStatement( + const [sql, params] = this.getSelectStatement( schema[query.from], query, ); - return await getAll(stmt, params, transformRow(schema[query.from])); + return this.db.allPrepared( + sql, + params, + transformRow(schema[query.from]), + ); }, (err) => new StoreQueryError(err.message, { @@ -136,11 +115,15 @@ export class SQLiteStorageDriver implements StorageDriver { ): AsyncResult { return AsyncResult.tryFrom( async () => { - const [stmt, params] = await this.getSelectStatement( + const [stmt, params] = this.getSelectStatement( schema[query.from], query, ); - return await getOne(stmt, params, transformRow(schema[query.from])); + return await this.db.onePrepared( + stmt, + params, + transformRow(schema[query.from]), + ); }, (err) => new StoreQueryError(err.message, { @@ -158,19 +141,12 @@ export class SQLiteStorageDriver implements StorageDriver { ): AsyncResult { return AsyncResult.tryFrom( async () => { - // Enable Write-Ahead Logging, which is faster and more reliable. - await dbRun(this.db, "PRAGMA journal_mode = WAL;"); - - // Enable foreign key constraints. - await dbRun(this.db, "PRAGMA foreign_keys = ON;"); - - // Begin a transaction to create the schema. - await dbRun(this.db, "BEGIN TRANSACTION;"); - for (const modelKey in schema) { - const model = schema[modelKey]; - await dbRun(this.db, modelToSql(model)); - } - await dbRun(this.db, "COMMIT;"); + await this.db.withTransaction(async () => { + for (const modelKey in schema) { + const model = schema[modelKey]; + await this.db.runPrepared(modelToSql(model)); + } + }); }, (error) => new StoreQueryError(error.message, { @@ -201,10 +177,7 @@ export class SQLiteStorageDriver implements StorageDriver { async close(): AsyncResult { return AsyncResult.from(async () => { - for (const stmt of this.cachedStatements.values()) { - await finalize(stmt); - } - await dbClose(this.db); + this.db.close(); }); } @@ -218,13 +191,14 @@ export class SQLiteStorageDriver implements StorageDriver { ): AsyncResult { return AsyncResult.tryFrom( async () => { - const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; - const stmt = await this.getOrCreatePreparedStatement(sql); const params = recordToSQLParams(model, { ...record, id, }); - return await run(stmt, params); + await this.db.runPrepared( + `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`, + params, + ); }, (error) => new StoreQueryError(error.message, { @@ -238,13 +212,13 @@ export class SQLiteStorageDriver implements StorageDriver { /** * Delete a record from the store. */ - async delete(model: Model, id: string): AsyncResult { return AsyncResult.tryFrom( async () => { - const sql = `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, { [keyToParam("id")]: id }); + await this.db.runPrepared( + `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`, + { $id: id }, + ); }, (error) => new StoreQueryError(error.message, { diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts deleted file mode 100644 index 4a58620..0000000 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Database, Statement } from "sqlite3"; - -export function dbRun(db: Database, statement: string): Promise { - return new Promise((resolve, reject) => { - db.all(statement, (err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); -} - -export function dbClose(db: Database): Promise { - return new Promise((resolve, reject) => { - db.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -export function prepare(db: Database, statement: string): Promise { - return new Promise((resolve, reject) => { - const stmt = db.prepare(statement, (err) => { - if (err) { - reject(err); - } else { - resolve(stmt); - } - }); - }); -} - -export function run( - stmt: Statement, - params: Record, -): Promise { - return new Promise((resolve, reject) => { - stmt.run(params, (err: Error | null) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} - -export function getAll( - stmt: Statement, - params: Record, - transformer: (row: any) => any, -): Promise[]> { - return new Promise((resolve, reject) => { - stmt.all(params, (err: Error | null, rows: Record[]) => { - if (err) { - reject(err); - } else { - resolve(rows.map(transformer)); - } - }); - }); -} - -export function getOne( - stmt: Statement, - params: Record, - transformer: (row: any) => any, -): Promise> { - return new Promise((resolve, reject) => { - stmt.all(params, (err: Error | null, rows: Record[]) => { - if (err) { - reject(err); - } else { - resolve(rows.map(transformer)[0]); - } - }); - }); -} - -export function finalize(stmt: Statement): Promise { - return new Promise((resolve, reject) => { - stmt.finalize((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); -} diff --git a/packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts new file mode 100644 index 0000000..7424a0f --- /dev/null +++ b/packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts @@ -0,0 +1,134 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { MaybePromise } from "@fabric/core"; +import SQLite from "sqlite3"; + +export class SQLiteDatabase { + db: SQLite.Database; + + private cachedStatements = new Map(); + + constructor(private readonly path: string) { + this.db = new SQLite.Database(path); + } + + async init() { + await this.run("PRAGMA journal_mode = WAL"); + await this.run("PRAGMA foreign_keys = ON"); + } + + async close() { + await this.finalizeStatements(); + await new Promise((resolve, reject) => { + this.db.close((err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + async withTransaction(fn: () => MaybePromise) { + try { + await this.run("BEGIN TRANSACTION"); + await fn(); + await this.run("COMMIT"); + } catch { + await this.run("ROLLBACK"); + } + } + + run(sql: string, params?: Record) { + return new Promise((resolve, reject) => { + this.db.run(sql, params, (err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + runPrepared(sql: string, params?: Record) { + const cachedStmt = this.getCachedStatement(sql); + + return new Promise((resolve, reject) => { + cachedStmt.run(params, (err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + + allPrepared( + sql: string, + params?: Record, + transformer?: (row: any) => any, + ) { + const cachedStmt = this.getCachedStatement(sql); + + return new Promise((resolve, reject) => { + cachedStmt.all( + params, + (err: Error | null, rows: Record[]) => { + if (err) { + reject(err); + } else { + resolve(transformer ? rows.map(transformer) : rows); + } + }, + ); + }); + } + + onePrepared( + sql: string, + params?: Record, + transformer?: (row: any) => any, + ) { + const cachedStmt = this.getCachedStatement(sql); + + return new Promise((resolve, reject) => { + cachedStmt.all( + params, + (err: Error | null, rows: Record[]) => { + if (err) { + reject(err); + } else { + resolve(transformer ? rows.map(transformer)[0] : rows[0]); + } + }, + ); + }); + } + + private getCachedStatement(sql: string) { + let cached = this.cachedStatements.get(sql); + + if (!cached) { + const stmt = this.db.prepare(sql); + this.cachedStatements.set(sql, stmt); + cached = stmt; + } + return cached; + } + + private async finalizeStatements() { + for (const stmt of this.cachedStatements.values()) { + await new Promise((resolve, reject) => { + stmt.finalize((err: Error | null) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + } + } +} From 69caa775d12aa7d9f3da4d35b37cdac73fe3f400 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 15:47:48 -0300 Subject: [PATCH 17/37] Rename store-sqlite package to sqlite-store and update imports accordingly --- packages/fabric/domain/package.json | 2 +- packages/fabric/domain/src/models/state-store.spec.ts | 2 +- packages/fabric/store-sqlite/package.json | 2 +- packages/fabric/store-sqlite/src/sqlite-driver.ts | 8 ++++---- .../store-sqlite/src/{ => sqlite}/filter-to-sql.spec.ts | 0 .../fabric/store-sqlite/src/{ => sqlite}/filter-to-sql.ts | 0 .../store-sqlite/src/{ => sqlite}/model-to-sql.spec.ts | 0 .../fabric/store-sqlite/src/{ => sqlite}/model-to-sql.ts | 6 ++---- .../fabric/store-sqlite/src/{ => sqlite}/record-utils.ts | 0 .../fabric/store-sqlite/src/{ => sqlite}/sql-to-value.ts | 0 .../fabric/store-sqlite/src/{ => sqlite}/value-to-sql.ts | 0 yarn.lock | 6 +++--- 12 files changed, 12 insertions(+), 14 deletions(-) rename packages/fabric/store-sqlite/src/{ => sqlite}/filter-to-sql.spec.ts (100%) rename packages/fabric/store-sqlite/src/{ => sqlite}/filter-to-sql.ts (100%) rename packages/fabric/store-sqlite/src/{ => sqlite}/model-to-sql.spec.ts (100%) rename packages/fabric/store-sqlite/src/{ => sqlite}/model-to-sql.ts (88%) rename packages/fabric/store-sqlite/src/{ => sqlite}/record-utils.ts (100%) rename packages/fabric/store-sqlite/src/{ => sqlite}/sql-to-value.ts (100%) rename packages/fabric/store-sqlite/src/{ => sqlite}/value-to-sql.ts (100%) diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index c44c9e6..0309310 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -9,7 +9,7 @@ "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "@fabric/store-sqlite": "workspace:^", + "@fabric/sqlite-store": "workspace:^", "typescript": "^5.6.2", "vitest": "^2.1.1" }, diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts index 4be04f3..f42c899 100644 --- a/packages/fabric/domain/src/models/state-store.spec.ts +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -1,5 +1,5 @@ import { isError, Run } from "@fabric/core"; -import { SQLiteStorageDriver } from "@fabric/store-sqlite"; +import { SQLiteStorageDriver } from "@fabric/sqlite-store"; import { afterEach, beforeEach, diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/store-sqlite/package.json index 813d1f6..b7f2c6f 100644 --- a/packages/fabric/store-sqlite/package.json +++ b/packages/fabric/store-sqlite/package.json @@ -1,5 +1,5 @@ { - "name": "@fabric/store-sqlite", + "name": "@fabric/sqlite-store", "type": "module", "module": "dist/index.js", "main": "dist/index.js", diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 1ceee8f..2bb4e16 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -11,16 +11,16 @@ import { StorageDriver, StoreQueryError, } from "@fabric/domain"; -import { filterToParams, filterToSQL } from "./filter-to-sql.js"; -import { modelToSql } from "./model-to-sql.js"; +import { filterToParams, filterToSQL } from "./sqlite/filter-to-sql.js"; +import { modelToSql } from "./sqlite/model-to-sql.js"; import { keyToParam, recordToSQLKeyParams, recordToSQLKeys, recordToSQLParams, recordToSQLSet, -} from "./record-utils.js"; -import { transformRow } from "./sql-to-value.js"; +} from "./sqlite/record-utils.js"; +import { transformRow } from "./sqlite/sql-to-value.js"; import { SQLiteDatabase } from "./sqlite/sqlite-wrapper.js"; export class SQLiteStorageDriver implements StorageDriver { diff --git a/packages/fabric/store-sqlite/src/filter-to-sql.spec.ts b/packages/fabric/store-sqlite/src/sqlite/filter-to-sql.spec.ts similarity index 100% rename from packages/fabric/store-sqlite/src/filter-to-sql.spec.ts rename to packages/fabric/store-sqlite/src/sqlite/filter-to-sql.spec.ts diff --git a/packages/fabric/store-sqlite/src/filter-to-sql.ts b/packages/fabric/store-sqlite/src/sqlite/filter-to-sql.ts similarity index 100% rename from packages/fabric/store-sqlite/src/filter-to-sql.ts rename to packages/fabric/store-sqlite/src/sqlite/filter-to-sql.ts diff --git a/packages/fabric/store-sqlite/src/model-to-sql.spec.ts b/packages/fabric/store-sqlite/src/sqlite/model-to-sql.spec.ts similarity index 100% rename from packages/fabric/store-sqlite/src/model-to-sql.spec.ts rename to packages/fabric/store-sqlite/src/sqlite/model-to-sql.spec.ts diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/sqlite/model-to-sql.ts similarity index 88% rename from packages/fabric/store-sqlite/src/model-to-sql.ts rename to packages/fabric/store-sqlite/src/sqlite/model-to-sql.ts index 4f2bee6..5b5129d 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/sqlite/model-to-sql.ts @@ -1,8 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Variant, VariantTag } from "@fabric/core"; import { Collection, FieldDefinition, getTargetKey } from "@fabric/domain"; -import { EmbeddedField } from "@fabric/domain/dist/models/fields/embedded.js"; -import { TimestampField } from "@fabric/domain/dist/models/fields/timestamp.js"; type FieldSQLDefinitionMap = { [K in FieldDefinition[VariantTag]]: ( @@ -42,10 +40,10 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { DecimalField: (n, f): string => { return [n, "REAL", modifiersFromOpts(f)].join(" "); }, - TimestampField: (n, f: TimestampField): string => { + TimestampField: (n, f): string => { return [n, "NUMERIC", modifiersFromOpts(f)].join(" "); }, - EmbeddedField: (n, f: EmbeddedField): string => { + EmbeddedField: (n, f): string => { return [n, "TEXT", modifiersFromOpts(f)].join(" "); }, }; diff --git a/packages/fabric/store-sqlite/src/record-utils.ts b/packages/fabric/store-sqlite/src/sqlite/record-utils.ts similarity index 100% rename from packages/fabric/store-sqlite/src/record-utils.ts rename to packages/fabric/store-sqlite/src/sqlite/record-utils.ts diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sqlite/sql-to-value.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sql-to-value.ts rename to packages/fabric/store-sqlite/src/sqlite/sql-to-value.ts diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/sqlite/value-to-sql.ts similarity index 100% rename from packages/fabric/store-sqlite/src/value-to-sql.ts rename to packages/fabric/store-sqlite/src/sqlite/value-to-sql.ts diff --git a/yarn.lock b/yarn.lock index 80b024b..b8b8d60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -419,16 +419,16 @@ __metadata: resolution: "@fabric/domain@workspace:packages/fabric/domain" dependencies: "@fabric/core": "workspace:^" - "@fabric/store-sqlite": "workspace:^" + "@fabric/sqlite-store": "workspace:^" decimal.js: "npm:^10.4.3" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown linkType: soft -"@fabric/store-sqlite@workspace:^, @fabric/store-sqlite@workspace:packages/fabric/store-sqlite": +"@fabric/sqlite-store@workspace:^, @fabric/sqlite-store@workspace:packages/fabric/store-sqlite": version: 0.0.0-use.local - resolution: "@fabric/store-sqlite@workspace:packages/fabric/store-sqlite" + resolution: "@fabric/sqlite-store@workspace:packages/fabric/store-sqlite" dependencies: "@fabric/core": "workspace:^" "@fabric/domain": "workspace:^" From 010e3eecfc92897c21586bfd2e3b5571f3504e7f Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 16:59:09 -0300 Subject: [PATCH 18/37] Standardize exports and add exports field for improved module resolution --- packages/fabric/core/package.json | 10 +++------- packages/fabric/domain/package.json | 8 ++++++-- packages/fabric/store-sqlite/package.json | 7 +++++-- packages/templates/lib/package.json | 7 +++++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/fabric/core/package.json b/packages/fabric/core/package.json index fd94849..6d67351 100644 --- a/packages/fabric/core/package.json +++ b/packages/fabric/core/package.json @@ -1,14 +1,10 @@ { "name": "@fabric/core", "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", "exports": { - ".": "./dist/index.js", - "./domain": "./dist/domain.js", - "./validation": "./dist/validation.js", - "./validation/fields": "./dist/validation/fields/index.js" + ".": "./dist/index.js" }, "files": [ "dist" diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index 0309310..cc2aa68 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -1,8 +1,12 @@ { "name": "@fabric/domain", "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js", + "./mocks": "./dist/mocks.js" + }, "files": [ "dist" ], diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/store-sqlite/package.json index b7f2c6f..6aea475 100644 --- a/packages/fabric/store-sqlite/package.json +++ b/packages/fabric/store-sqlite/package.json @@ -1,8 +1,11 @@ { "name": "@fabric/sqlite-store", "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, "files": [ "dist" ], diff --git a/packages/templates/lib/package.json b/packages/templates/lib/package.json index 8338fb0..93f66cf 100644 --- a/packages/templates/lib/package.json +++ b/packages/templates/lib/package.json @@ -1,8 +1,11 @@ { "name": "@ulthar/lib-template", "type": "module", - "module": "dist/index.js", - "main": "dist/index.js", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, "files": [ "dist" ], From d443e9e3955790ec36a8071468b3e1f37448b24d Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 17:01:25 -0300 Subject: [PATCH 19/37] Remove storage-driver interface; Add state-store as driver-specific implementation --- packages/fabric/domain/src/index.ts | 1 - .../fabric/domain/src/models/model-schema.ts | 6 +- .../fabric/domain/src/models/query/index.ts | 1 - .../domain/src/models/query/query-builder.ts | 66 ----- .../fabric/domain/src/models/query/query.ts | 18 +- .../domain/src/models/state-store.spec.ts | 136 --------- .../fabric/domain/src/models/state-store.ts | 43 +-- packages/fabric/domain/src/storage/index.ts | 1 - .../domain/src/storage/storage-driver.ts | 65 ----- packages/fabric/store-sqlite/src/index.ts | 2 +- .../store-sqlite/src/sqlite-driver.spec.ts | 273 ------------------ .../fabric/store-sqlite/src/sqlite-driver.ts | 231 --------------- .../fabric/store-sqlite/src/state/index.ts | 1 + .../store-sqlite/src/state/query-builder.ts | 127 ++++++++ .../src/state/state-store.spec.ts | 213 ++++++++++++++ .../store-sqlite/src/state/state-store.ts | 144 +++++++++ 16 files changed, 508 insertions(+), 820 deletions(-) delete mode 100644 packages/fabric/domain/src/models/query/query-builder.ts delete mode 100644 packages/fabric/domain/src/models/state-store.spec.ts delete mode 100644 packages/fabric/domain/src/storage/index.ts delete mode 100644 packages/fabric/domain/src/storage/storage-driver.ts delete mode 100644 packages/fabric/store-sqlite/src/sqlite-driver.spec.ts delete mode 100644 packages/fabric/store-sqlite/src/sqlite-driver.ts create mode 100644 packages/fabric/store-sqlite/src/state/index.ts create mode 100644 packages/fabric/store-sqlite/src/state/query-builder.ts create mode 100644 packages/fabric/store-sqlite/src/state/state-store.spec.ts create mode 100644 packages/fabric/store-sqlite/src/state/state-store.ts diff --git a/packages/fabric/domain/src/index.ts b/packages/fabric/domain/src/index.ts index af3bf8e..00fedea 100644 --- a/packages/fabric/domain/src/index.ts +++ b/packages/fabric/domain/src/index.ts @@ -4,6 +4,5 @@ export * from "./files/index.js"; export * from "./models/index.js"; export * from "./security/index.js"; export * from "./services/index.js"; -export * from "./storage/index.js"; export * from "./types/index.js"; export * from "./use-case/index.js"; diff --git a/packages/fabric/domain/src/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts index ca5ce05..8150980 100644 --- a/packages/fabric/domain/src/models/model-schema.ts +++ b/packages/fabric/domain/src/models/model-schema.ts @@ -1,7 +1,7 @@ -import { Collection } from "./model.js"; +import { Model } from "./model.js"; -export type ModelSchema = Record; +export type ModelSchema = Record; -export type ModelSchemaFromModels = { +export type ModelSchemaFromModels = { [K in TModels["name"]]: Extract; }; diff --git a/packages/fabric/domain/src/models/query/index.ts b/packages/fabric/domain/src/models/query/index.ts index 1aa861d..ff7581f 100644 --- a/packages/fabric/domain/src/models/query/index.ts +++ b/packages/fabric/domain/src/models/query/index.ts @@ -1,4 +1,3 @@ export * from "./filter-options.js"; export * from "./order-by-options.js"; -export * from "./query-builder.js"; export * from "./query.js"; diff --git a/packages/fabric/domain/src/models/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts deleted file mode 100644 index d8aa630..0000000 --- a/packages/fabric/domain/src/models/query/query-builder.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { AsyncResult, Keyof } from "@fabric/core"; -import { StoreQueryError } from "../../errors/query-error.js"; -import { StorageDriver } from "../../storage/storage-driver.js"; -import { ModelSchema } from "../model-schema.js"; -import { FilterOptions } from "./filter-options.js"; -import { OrderByOptions } from "./order-by-options.js"; -import { - QueryDefinition, - SelectableQuery, - StoreLimitableQuery, - StoreQuery, - StoreSortableQuery, -} from "./query.js"; - -export class QueryBuilder implements StoreQuery { - constructor( - private driver: StorageDriver, - private schema: ModelSchema, - private query: QueryDefinition, - ) {} - - where(where: FilterOptions): StoreSortableQuery { - return new QueryBuilder(this.driver, this.schema, { - ...this.query, - where, - }); - } - - orderBy(opts: OrderByOptions): StoreLimitableQuery { - return new QueryBuilder(this.driver, this.schema, { - ...this.query, - orderBy: opts, - }); - } - - limit(limit: number, offset?: number | undefined): SelectableQuery { - return new QueryBuilder(this.driver, this.schema, { - ...this.query, - limit, - offset, - }); - } - - select(): AsyncResult; - select>( - keys: K[], - ): AsyncResult[], StoreQueryError>; - select>(keys?: K[]): AsyncResult { - return this.driver.select(this.schema, { - ...this.query, - keys, - }); - } - - selectOne(): AsyncResult; - selectOne>( - keys: K, - ): AsyncResult, StoreQueryError>; - selectOne>(keys?: K[]): AsyncResult { - return this.driver.selectOne(this.schema, { - ...this.query, - keys, - }); - } -} diff --git a/packages/fabric/domain/src/models/query/query.ts b/packages/fabric/domain/src/models/query/query.ts index febc5d8..8f25a4d 100644 --- a/packages/fabric/domain/src/models/query/query.ts +++ b/packages/fabric/domain/src/models/query/query.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AsyncResult, Keyof } from "@fabric/core"; +import { AsyncResult, Keyof, Optional } from "@fabric/core"; import { StoreQueryError } from "../../errors/query-error.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; @@ -14,10 +14,10 @@ export interface StoreQuery { keys: K[], ): AsyncResult[], StoreQueryError>; - selectOne(): AsyncResult; + selectOne(): AsyncResult, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult, StoreQueryError>; + ): AsyncResult>, StoreQueryError>; } export interface StoreSortableQuery { @@ -29,10 +29,10 @@ export interface StoreSortableQuery { keys: K[], ): AsyncResult[], StoreQueryError>; - selectOne(): AsyncResult; + selectOne(): AsyncResult, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult, StoreQueryError>; + ): AsyncResult>, StoreQueryError>; } export interface StoreLimitableQuery { @@ -43,10 +43,10 @@ export interface StoreLimitableQuery { keys: K[], ): AsyncResult[], StoreQueryError>; - selectOne(): AsyncResult; + selectOne(): AsyncResult, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult, StoreQueryError>; + ): AsyncResult>, StoreQueryError>; } export interface SelectableQuery { @@ -55,10 +55,10 @@ export interface SelectableQuery { keys: K[], ): AsyncResult[], StoreQueryError>; - selectOne(): AsyncResult; + selectOne(): AsyncResult, StoreQueryError>; selectOne>( keys: K[], - ): AsyncResult, StoreQueryError>; + ): AsyncResult>, StoreQueryError>; } export interface QueryDefinition { diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts deleted file mode 100644 index f42c899..0000000 --- a/packages/fabric/domain/src/models/state-store.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { isError, Run } from "@fabric/core"; -import { SQLiteStorageDriver } from "@fabric/sqlite-store"; -import { - afterEach, - beforeEach, - describe, - expect, - expectTypeOf, - it, -} from "vitest"; -import { UUIDGeneratorMock } from "../services/uuid-generator.mock.js"; -import { UUID } from "../types/uuid.js"; -import { Field } from "./fields/index.js"; -import { defineModel } from "./model.js"; -import { isLike } from "./query/filter-options.js"; -import { StateStore } from "./state-store.js"; - -describe("State Store", () => { - const models = [ - defineModel("users", { - name: Field.string(), - }), - ]; - - let driver: SQLiteStorageDriver; - let store: StateStore<(typeof models)[number]>; - - beforeEach(async () => { - driver = new SQLiteStorageDriver(":memory:"); - store = new StateStore(driver, models); - const migrationResult = await store.migrate(); - if (isError(migrationResult)) throw migrationResult; - }); - - afterEach(async () => { - await driver.close(); - }); - - it("should insert a record", async () => { - const newUUID = UUIDGeneratorMock.generate(); - const insertResult = await store.insertInto("users", { - name: "test", - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - }); - if (isError(insertResult)) throw insertResult; - }); - - it("should query with a basic select", async () => { - const newUUID = UUIDGeneratorMock.generate(); - const insertResult = await store.insertInto("users", { - name: "test", - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - }); - - if (isError(insertResult)) throw insertResult; - - const result = (await store.from("users").select()).unwrapOrThrow(); - - expectTypeOf(result).toEqualTypeOf< - { - id: UUID; - streamId: UUID; - streamVersion: bigint; - name: string; - }[] - >(); - - expect(result).toEqual([ - { - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - name: "test", - }, - ]); - }); - - it("should query with a where clause", async () => { - const newUUID = UUIDGeneratorMock.generate(); - - await Run.seqUNSAFE( - () => - store.insertInto("users", { - name: "test", - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - }), - () => - store.insertInto("users", { - name: "anotherName", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }), - () => - store.insertInto("users", { - name: "anotherName2", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }), - ); - - const result = await Run.UNSAFE(() => - store - .from("users") - .where({ - name: isLike("te%"), - }) - .select(), - ); - - expectTypeOf(result).toEqualTypeOf< - { - id: UUID; - streamId: UUID; - streamVersion: bigint; - name: string; - }[] - >(); - - expect(result).toEqual([ - { - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - name: "test", - }, - ]); - }); -}); diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index fd6473c..c94359a 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -1,42 +1,19 @@ import { AsyncResult } from "@fabric/core"; -import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; -import { StorageDriver } from "../storage/storage-driver.js"; import { ModelSchemaFromModels } from "./model-schema.js"; import { Model, ModelToType } from "./model.js"; -import { QueryBuilder } from "./query/query-builder.js"; import { StoreQuery } from "./query/query.js"; -export class StateStore { - private schema: ModelSchemaFromModels; - constructor( - private driver: StorageDriver, - models: TModel[], - ) { - this.schema = models.reduce((acc, model: TModel) => { - return { - ...acc, - [model.name]: model, - }; - }, {} as ModelSchemaFromModels); - } - - migrate(): AsyncResult { - return this.driver.sync(this.schema); - } - - async insertInto>( - collection: T, - record: ModelToType[T]>, - ): AsyncResult { - return this.driver.insert(this.schema[collection], record); - } - +export interface ReadonlyStateStore { from>( collection: T, - ): StoreQuery[T]>> { - return new QueryBuilder(this.driver, this.schema, { - from: collection, - }) as StoreQuery[T]>>; - } + ): StoreQuery[T]>>; +} + +export interface WritableStateStore + extends ReadonlyStateStore { + insertInto>( + collection: T, + record: ModelToType[T]>, + ): AsyncResult; } diff --git a/packages/fabric/domain/src/storage/index.ts b/packages/fabric/domain/src/storage/index.ts deleted file mode 100644 index 1d74826..0000000 --- a/packages/fabric/domain/src/storage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./storage-driver.js"; diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts deleted file mode 100644 index 0e414b2..0000000 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import { AsyncResult, UnexpectedError } from "@fabric/core"; -import { CircularDependencyError } from "../errors/circular-dependency-error.js"; -import { StoreQueryError } from "../errors/query-error.js"; -import { ModelSchema } from "../models/model-schema.js"; -import { Collection } from "../models/model.js"; -import { QueryDefinition } from "../models/query/query.js"; - -export interface StorageDriver { - /** - * Insert data into the store - */ - insert( - model: Collection, - record: Record, - ): AsyncResult; - - /** - * Run a select query against the store. - */ - select( - model: ModelSchema, - query: QueryDefinition, - ): AsyncResult; - - /** - * Run a select query against the store. - */ - selectOne( - model: ModelSchema, - query: QueryDefinition, - ): AsyncResult; - - /** - * Sincronice the store with the schema. - */ - sync( - schema: ModelSchema, - ): AsyncResult; - - /** - * Drop the store. This is a destructive operation. - */ - drop(): AsyncResult; - - /** - * Close the store. - */ - close(): AsyncResult; - - /** - * Update a record in the store. - */ - update( - model: Collection, - id: string, - record: Record, - ): AsyncResult; - - /** - * Delete a record from the store. - */ - delete(model: Collection, id: string): AsyncResult; -} diff --git a/packages/fabric/store-sqlite/src/index.ts b/packages/fabric/store-sqlite/src/index.ts index af380ab..5dae270 100644 --- a/packages/fabric/store-sqlite/src/index.ts +++ b/packages/fabric/store-sqlite/src/index.ts @@ -1 +1 @@ -export * from "./sqlite-driver.js"; +export * from "./state/index.js"; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts deleted file mode 100644 index 375165d..0000000 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Run } from "@fabric/core"; -import { defineModel, Field, isLike } from "@fabric/domain"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { SQLiteStorageDriver } from "./sqlite-driver.js"; - -describe("SQLite Store Driver", () => { - const schema = { - demo: defineModel("demo", { - value: Field.float(), - owner: Field.reference({ targetModel: "users" }), - }), - users: defineModel("users", { - name: Field.string(), - }), - }; - - let driver: SQLiteStorageDriver; - - beforeEach(() => { - driver = new SQLiteStorageDriver(":memory:"); - }); - - afterEach(async () => { - await Run.UNSAFE(() => driver.close()); - }); - - it("should synchronize the store and insert a record", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - - const records = await Run.UNSAFE(() => - driver.select(schema, { from: "users" }), - ); - - expect(records).toEqual([ - { id: "1", name: "test", streamId: "1", streamVersion: 1n }, - ]); - }); - - it("should be update a record", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - - await Run.UNSAFE(() => - driver.update(schema.users, "1", { name: "updated" }), - ); - - const records = await Run.UNSAFE(() => - driver.select(schema, { from: "users" }), - ); - - expect(records).toEqual([ - { id: "1", name: "updated", streamId: "1", streamVersion: 1n }, - ]); - }); - - it("should be able to delete a record", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - - await Run.UNSAFE(() => driver.delete(schema.users, "1")); - - const records = await Run.UNSAFE(() => - driver.select(schema, { from: "users" }), - ); - - expect(records).toEqual([]); - }); - - it("should be able to select records", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "2", - name: "test", - streamId: "2", - streamVersion: 1n, - }), - ); - - const records = await Run.UNSAFE(() => - driver.select(schema, { from: "users" }), - ); - - expect(records).toEqual([ - { id: "1", name: "test", streamId: "1", streamVersion: 1n }, - { id: "2", name: "test", streamId: "2", streamVersion: 1n }, - ]); - }); - - it("should be able to select one record", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "2", - name: "test", - streamId: "2", - streamVersion: 1n, - }), - ); - - const record = await Run.UNSAFE(() => - driver.selectOne(schema, { from: "users" }), - ); - - expect(record).toEqual({ - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }); - }); - - it("should select a record with a where clause", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }), - ); - - const result = await Run.UNSAFE(() => - driver.select(schema, { - from: "users", - where: { name: isLike("te%") }, - }), - ); - - expect(result).toEqual([ - { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }, - ]); - }); - - it("should select a record with a where clause of a specific type", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }), - ); - - const result = await Run.UNSAFE(() => - driver.select(schema, { - from: "users", - where: { streamVersion: 1n }, - }), - ); - - expect(result).toEqual([ - { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }, - { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }, - ]); - }); - - it("should select with a limit and offset", async () => { - await Run.UNSAFE(() => driver.sync(schema)); - - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "1", - name: "test", - streamId: "1", - streamVersion: 1n, - }), - ); - await Run.UNSAFE(() => - driver.insert(schema.users, { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }), - ); - - const result = await Run.UNSAFE(() => - driver.select(schema, { - from: "users", - limit: 1, - offset: 1, - }), - ); - - expect(result).toEqual([ - { - id: "2", - name: "jamón", - streamId: "2", - streamVersion: 1n, - }, - ]); - }); -}); diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts deleted file mode 100644 index 2bb4e16..0000000 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ /dev/null @@ -1,231 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { AsyncResult, UnexpectedError } from "@fabric/core"; -import { unlink } from "fs/promises"; - -import { - CircularDependencyError, - Collection, - Model, - ModelSchema, - QueryDefinition, - StorageDriver, - StoreQueryError, -} from "@fabric/domain"; -import { filterToParams, filterToSQL } from "./sqlite/filter-to-sql.js"; -import { modelToSql } from "./sqlite/model-to-sql.js"; -import { - keyToParam, - recordToSQLKeyParams, - recordToSQLKeys, - recordToSQLParams, - recordToSQLSet, -} from "./sqlite/record-utils.js"; -import { transformRow } from "./sqlite/sql-to-value.js"; -import { SQLiteDatabase } from "./sqlite/sqlite-wrapper.js"; - -export class SQLiteStorageDriver implements StorageDriver { - private db: SQLiteDatabase; - - constructor(private path: string) { - this.db = new SQLiteDatabase(path); - } - - private getSelectStatement( - collection: Collection, - query: QueryDefinition, - ): [string, Record] { - const selectFields = query.keys ? query.keys.join(", ") : "*"; - - const queryFilter = filterToSQL(query.where); - const limit = query.limit ? `LIMIT ${query.limit}` : ""; - const offset = query.offset ? `OFFSET ${query.offset}` : ""; - - const sql = [ - `SELECT ${selectFields}`, - `FROM ${query.from}`, - queryFilter, - limit, - offset, - ].join(" "); - - return [ - sql, - { - ...filterToParams(collection, query.where), - }, - ]; - } - - /** - * Insert data into the store - */ - async insert( - model: Model, - record: Record, - ): AsyncResult { - return AsyncResult.tryFrom( - async () => { - await this.db.runPrepared( - `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`, - recordToSQLParams(model, record), - ); - }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }), - ); - } - - /** - * Run a select query against the store. - */ - async select( - schema: ModelSchema, - query: QueryDefinition, - ): AsyncResult { - return AsyncResult.tryFrom( - async () => { - const [sql, params] = this.getSelectStatement( - schema[query.from], - query, - ); - return this.db.allPrepared( - sql, - params, - transformRow(schema[query.from]), - ); - }, - (err) => - new StoreQueryError(err.message, { - err, - query, - }), - ); - } - - /** - * Run a select query against the store. - */ - async selectOne( - schema: ModelSchema, - query: QueryDefinition, - ): AsyncResult { - return AsyncResult.tryFrom( - async () => { - const [stmt, params] = this.getSelectStatement( - schema[query.from], - query, - ); - return await this.db.onePrepared( - stmt, - params, - transformRow(schema[query.from]), - ); - }, - (err) => - new StoreQueryError(err.message, { - err, - query, - }), - ); - } - - /** - * Sincronice the store with the schema. - */ - async sync( - schema: ModelSchema, - ): AsyncResult { - return AsyncResult.tryFrom( - async () => { - await this.db.withTransaction(async () => { - for (const modelKey in schema) { - const model = schema[modelKey]; - await this.db.runPrepared(modelToSql(model)); - } - }); - }, - (error) => - new StoreQueryError(error.message, { - error, - schema, - }), - ); - } - - /** - * Drop the store. This is a destructive operation. - */ - async drop(): AsyncResult { - return AsyncResult.tryFrom( - async () => { - if (this.path === ":memory:") { - throw "Cannot drop in-memory database"; - } else { - await unlink(this.path); - } - }, - (error) => - new StoreQueryError(error.message, { - error, - }), - ); - } - - async close(): AsyncResult { - return AsyncResult.from(async () => { - this.db.close(); - }); - } - - /** - * Update a record in the store. - */ - async update( - model: Model, - id: string, - record: Record, - ): AsyncResult { - return AsyncResult.tryFrom( - async () => { - const params = recordToSQLParams(model, { - ...record, - id, - }); - await this.db.runPrepared( - `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`, - params, - ); - }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }), - ); - } - - /** - * Delete a record from the store. - */ - async delete(model: Model, id: string): AsyncResult { - return AsyncResult.tryFrom( - async () => { - await this.db.runPrepared( - `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`, - { $id: id }, - ); - }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - id, - }), - ); - } -} diff --git a/packages/fabric/store-sqlite/src/state/index.ts b/packages/fabric/store-sqlite/src/state/index.ts new file mode 100644 index 0000000..381f7cc --- /dev/null +++ b/packages/fabric/store-sqlite/src/state/index.ts @@ -0,0 +1 @@ +export * from "./state-store.js"; diff --git a/packages/fabric/store-sqlite/src/state/query-builder.ts b/packages/fabric/store-sqlite/src/state/query-builder.ts new file mode 100644 index 0000000..60b3438 --- /dev/null +++ b/packages/fabric/store-sqlite/src/state/query-builder.ts @@ -0,0 +1,127 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AsyncResult, Keyof, Optional } from "@fabric/core"; +import { + Collection, + FilterOptions, + ModelSchema, + OrderByOptions, + QueryDefinition, + SelectableQuery, + StoreLimitableQuery, + StoreQuery, + StoreQueryError, + StoreSortableQuery, +} from "@fabric/domain"; +import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.js"; +import { transformRow } from "../sqlite/sql-to-value.js"; +import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js"; + +export class QueryBuilder implements StoreQuery { + constructor( + private db: SQLiteDatabase, + private schema: ModelSchema, + private query: QueryDefinition, + ) {} + + where(where: FilterOptions): StoreSortableQuery { + return new QueryBuilder(this.db, this.schema, { + ...this.query, + where, + }); + } + + orderBy(opts: OrderByOptions): StoreLimitableQuery { + return new QueryBuilder(this.db, this.schema, { + ...this.query, + orderBy: opts, + }); + } + + limit(limit: number, offset?: number | undefined): SelectableQuery { + return new QueryBuilder(this.db, this.schema, { + ...this.query, + limit, + offset, + }); + } + + select(): AsyncResult; + select>( + keys: K[], + ): AsyncResult[], StoreQueryError>; + select>(keys?: K[]): AsyncResult { + return AsyncResult.tryFrom( + async () => { + const [sql, params] = getSelectStatement(this.schema[this.query.from], { + ...this.query, + keys, + }); + return this.db.allPrepared( + sql, + params, + transformRow(this.schema[this.query.from]), + ); + }, + (err) => + new StoreQueryError(err.message, { + err, + query: this.query, + }), + ); + } + + selectOne(): AsyncResult, StoreQueryError>; + selectOne>( + keys: K, + ): AsyncResult>, StoreQueryError>; + selectOne>(keys?: K[]): AsyncResult { + return AsyncResult.tryFrom( + async () => { + const [stmt, params] = getSelectStatement( + this.schema[this.query.from], + { + ...this.query, + keys, + limit: 1, + }, + ); + return await this.db.onePrepared( + stmt, + params, + transformRow(this.schema[this.query.from]), + ); + }, + (err) => + new StoreQueryError(err.message, { + err, + query: this.query, + }), + ); + } +} + +export function getSelectStatement( + collection: Collection, + query: QueryDefinition, +): [string, Record] { + const selectFields = query.keys ? query.keys.join(", ") : "*"; + + const queryFilter = filterToSQL(query.where); + const limit = query.limit ? `LIMIT ${query.limit}` : ""; + const offset = query.offset ? `OFFSET ${query.offset}` : ""; + + const sql = [ + `SELECT ${selectFields}`, + `FROM ${query.from}`, + queryFilter, + limit, + offset, + ].join(" "); + + return [ + sql, + { + ...filterToParams(collection, query.where), + }, + ]; +} diff --git a/packages/fabric/store-sqlite/src/state/state-store.spec.ts b/packages/fabric/store-sqlite/src/state/state-store.spec.ts new file mode 100644 index 0000000..1818958 --- /dev/null +++ b/packages/fabric/store-sqlite/src/state/state-store.spec.ts @@ -0,0 +1,213 @@ +import { Run } from "@fabric/core"; +import { defineModel, Field, isLike, UUID } from "@fabric/domain"; +import { UUIDGeneratorMock } from "@fabric/domain/mocks"; +import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, +} from "vitest"; +import { SQLiteStateStore } from "./state-store.js"; + +describe("State Store", () => { + const models = [ + defineModel("demo", { + value: Field.float(), + owner: Field.reference({ targetModel: "users" }), + }), + defineModel("users", { + name: Field.string(), + }), + ]; + + let store: SQLiteStateStore<(typeof models)[number]>; + + beforeEach(async () => { + store = new SQLiteStateStore(":memory:", models); + await Run.UNSAFE(() => store.migrate()); + }); + + afterEach(async () => { + await Run.UNSAFE(() => store.close()); + }); + + it("should insert a record", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + await Run.UNSAFE(() => + store.insertInto("users", { + id: newUUID, + name: "test", + streamId: newUUID, + streamVersion: 1n, + }), + ); + }); + + it("should select all records", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + await Run.UNSAFE(() => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + ); + + const result = await Run.UNSAFE(() => store.from("users").select()); + + expectTypeOf(result).toEqualTypeOf< + { + id: UUID; + streamId: UUID; + streamVersion: bigint; + name: string; + }[] + >(); + + expect(result).toEqual([ + { + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + name: "test", + }, + ]); + }); + + it("should select records with a filter", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + await Run.seqUNSAFE( + () => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName2", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + ); + + const result = await Run.UNSAFE(() => + store + .from("users") + .where({ + name: isLike("te%"), + }) + .select(), + ); + + expectTypeOf(result).toEqualTypeOf< + { + id: UUID; + streamId: UUID; + streamVersion: bigint; + name: string; + }[] + >(); + + expect(result).toEqual([ + { + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + name: "test", + }, + ]); + }); + + it("should update a record", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + await Run.UNSAFE(() => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + ); + + await Run.UNSAFE(() => + store.update("users", newUUID, { + name: "updated", + }), + ); + + const result = await Run.UNSAFE(() => + store.from("users").where({ id: newUUID }).selectOne(), + ); + + expect(result).toEqual({ + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + name: "updated", + }); + }); + + it("should delete a record", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + await Run.UNSAFE(() => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + ); + + await Run.UNSAFE(() => store.delete("users", newUUID)); + + const result = await Run.UNSAFE(() => + store.from("users").where({ id: newUUID }).selectOne(), + ); + + expect(result).toBeUndefined(); + }); + + //test for inserting into a collection with a reference + + it("should insert a record with a reference", async () => { + const newUUID = UUIDGeneratorMock.generate(); + const ownerUUID = UUIDGeneratorMock.generate(); + + await Run.UNSAFE(() => + store.insertInto("users", { + id: ownerUUID, + name: "test", + streamId: ownerUUID, + streamVersion: 1n, + }), + ); + + await Run.UNSAFE(() => + store.insertInto("demo", { + id: newUUID, + value: 1.0, + owner: ownerUUID, + streamId: newUUID, + streamVersion: 1n, + }), + ); + }); +}); diff --git a/packages/fabric/store-sqlite/src/state/state-store.ts b/packages/fabric/store-sqlite/src/state/state-store.ts new file mode 100644 index 0000000..e178e7f --- /dev/null +++ b/packages/fabric/store-sqlite/src/state/state-store.ts @@ -0,0 +1,144 @@ +import { AsyncResult, UnexpectedError } from "@fabric/core"; +import { + Model, + ModelSchemaFromModels, + ModelToType, + StoreQuery, + StoreQueryError, + UUID, + WritableStateStore, +} from "@fabric/domain"; +import { modelToSql } from "../sqlite/model-to-sql.js"; +import { + keyToParam, + recordToSQLKeyParams, + recordToSQLKeys, + recordToSQLParams, + recordToSQLSet, +} from "../sqlite/record-utils.js"; +import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js"; +import { QueryBuilder } from "./query-builder.js"; + +export class SQLiteStateStore + implements WritableStateStore +{ + private schema: ModelSchemaFromModels; + private db: SQLiteDatabase; + + constructor( + private dbPath: string, + models: TModel[], + ) { + this.schema = models.reduce((acc, model: TModel) => { + return { + ...acc, + [model.name]: model, + }; + }, {} as ModelSchemaFromModels); + + this.db = new SQLiteDatabase(dbPath); + } + + async insertInto>( + collection: T, + record: ModelToType[T]>, + ): AsyncResult { + const model = this.schema[collection]; + + return AsyncResult.tryFrom( + async () => { + await this.db.runPrepared( + `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`, + recordToSQLParams(model, record), + ); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); + } + + from>( + collection: T, + ): StoreQuery[T]>> { + return new QueryBuilder(this.db, this.schema, { + from: collection, + }) as StoreQuery[T]>>; + } + + update>( + collection: T, + id: UUID, + record: Partial[T]>>, + ): AsyncResult { + const model = this.schema[collection]; + + return AsyncResult.tryFrom( + async () => { + const params = recordToSQLParams(model, { + ...record, + id, + }); + await this.db.runPrepared( + `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`, + params, + ); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); + } + + delete>( + collection: T, + id: UUID, + ): AsyncResult { + const model = this.schema[collection]; + + return AsyncResult.tryFrom( + async () => { + await this.db.runPrepared( + `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`, + { $id: id }, + ); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + id, + }), + ); + } + + migrate(): AsyncResult { + return AsyncResult.tryFrom( + async () => { + await this.db.init(); + await this.db.withTransaction(async () => { + for (const modelKey in this.schema) { + const model = + this.schema[modelKey as keyof ModelSchemaFromModels]; + await this.db.runPrepared(modelToSql(model)); + } + }); + }, + (error) => + new StoreQueryError(error.message, { + error, + schema: this.schema, + }), + ); + } + + async close(): AsyncResult { + return AsyncResult.from(() => this.db.close()); + } +} From d62b58803323953c90c49025d2c7f46517c8c9cb Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 17:07:59 -0300 Subject: [PATCH 20/37] Rename store-sqlite -> sqlite-store to match package name --- packages/fabric/{store-sqlite => sqlite-store}/README.md | 0 packages/fabric/{store-sqlite => sqlite-store}/package.json | 0 packages/fabric/{store-sqlite => sqlite-store}/src/index.ts | 0 .../src/sqlite/filter-to-sql.spec.ts | 0 .../src/sqlite/filter-to-sql.ts | 0 .../src/sqlite/model-to-sql.spec.ts | 0 .../{store-sqlite => sqlite-store}/src/sqlite/model-to-sql.ts | 0 .../{store-sqlite => sqlite-store}/src/sqlite/record-utils.ts | 0 .../{store-sqlite => sqlite-store}/src/sqlite/sql-to-value.ts | 0 .../src/sqlite/sqlite-wrapper.ts | 0 .../{store-sqlite => sqlite-store}/src/sqlite/value-to-sql.ts | 0 .../fabric/{store-sqlite => sqlite-store}/src/state/index.ts | 0 .../{store-sqlite => sqlite-store}/src/state/query-builder.ts | 0 .../src/state/state-store.spec.ts | 0 .../{store-sqlite => sqlite-store}/src/state/state-store.ts | 0 .../fabric/{store-sqlite => sqlite-store}/tsconfig.build.json | 0 packages/fabric/{store-sqlite => sqlite-store}/tsconfig.json | 0 .../fabric/{store-sqlite => sqlite-store}/vitest.config.ts | 0 yarn.lock | 4 ++-- 19 files changed, 2 insertions(+), 2 deletions(-) rename packages/fabric/{store-sqlite => sqlite-store}/README.md (100%) rename packages/fabric/{store-sqlite => sqlite-store}/package.json (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/index.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/filter-to-sql.spec.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/filter-to-sql.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/model-to-sql.spec.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/model-to-sql.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/record-utils.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/sql-to-value.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/sqlite-wrapper.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/sqlite/value-to-sql.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/state/index.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/state/query-builder.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/state/state-store.spec.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/src/state/state-store.ts (100%) rename packages/fabric/{store-sqlite => sqlite-store}/tsconfig.build.json (100%) rename packages/fabric/{store-sqlite => sqlite-store}/tsconfig.json (100%) rename packages/fabric/{store-sqlite => sqlite-store}/vitest.config.ts (100%) diff --git a/packages/fabric/store-sqlite/README.md b/packages/fabric/sqlite-store/README.md similarity index 100% rename from packages/fabric/store-sqlite/README.md rename to packages/fabric/sqlite-store/README.md diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/sqlite-store/package.json similarity index 100% rename from packages/fabric/store-sqlite/package.json rename to packages/fabric/sqlite-store/package.json diff --git a/packages/fabric/store-sqlite/src/index.ts b/packages/fabric/sqlite-store/src/index.ts similarity index 100% rename from packages/fabric/store-sqlite/src/index.ts rename to packages/fabric/sqlite-store/src/index.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/filter-to-sql.spec.ts b/packages/fabric/sqlite-store/src/sqlite/filter-to-sql.spec.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/filter-to-sql.spec.ts rename to packages/fabric/sqlite-store/src/sqlite/filter-to-sql.spec.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/filter-to-sql.ts b/packages/fabric/sqlite-store/src/sqlite/filter-to-sql.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/filter-to-sql.ts rename to packages/fabric/sqlite-store/src/sqlite/filter-to-sql.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/model-to-sql.spec.ts b/packages/fabric/sqlite-store/src/sqlite/model-to-sql.spec.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/model-to-sql.spec.ts rename to packages/fabric/sqlite-store/src/sqlite/model-to-sql.spec.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/model-to-sql.ts b/packages/fabric/sqlite-store/src/sqlite/model-to-sql.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/model-to-sql.ts rename to packages/fabric/sqlite-store/src/sqlite/model-to-sql.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/record-utils.ts b/packages/fabric/sqlite-store/src/sqlite/record-utils.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/record-utils.ts rename to packages/fabric/sqlite-store/src/sqlite/record-utils.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/sql-to-value.ts b/packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/sql-to-value.ts rename to packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts b/packages/fabric/sqlite-store/src/sqlite/sqlite-wrapper.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/sqlite-wrapper.ts rename to packages/fabric/sqlite-store/src/sqlite/sqlite-wrapper.ts diff --git a/packages/fabric/store-sqlite/src/sqlite/value-to-sql.ts b/packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts similarity index 100% rename from packages/fabric/store-sqlite/src/sqlite/value-to-sql.ts rename to packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts diff --git a/packages/fabric/store-sqlite/src/state/index.ts b/packages/fabric/sqlite-store/src/state/index.ts similarity index 100% rename from packages/fabric/store-sqlite/src/state/index.ts rename to packages/fabric/sqlite-store/src/state/index.ts diff --git a/packages/fabric/store-sqlite/src/state/query-builder.ts b/packages/fabric/sqlite-store/src/state/query-builder.ts similarity index 100% rename from packages/fabric/store-sqlite/src/state/query-builder.ts rename to packages/fabric/sqlite-store/src/state/query-builder.ts diff --git a/packages/fabric/store-sqlite/src/state/state-store.spec.ts b/packages/fabric/sqlite-store/src/state/state-store.spec.ts similarity index 100% rename from packages/fabric/store-sqlite/src/state/state-store.spec.ts rename to packages/fabric/sqlite-store/src/state/state-store.spec.ts diff --git a/packages/fabric/store-sqlite/src/state/state-store.ts b/packages/fabric/sqlite-store/src/state/state-store.ts similarity index 100% rename from packages/fabric/store-sqlite/src/state/state-store.ts rename to packages/fabric/sqlite-store/src/state/state-store.ts diff --git a/packages/fabric/store-sqlite/tsconfig.build.json b/packages/fabric/sqlite-store/tsconfig.build.json similarity index 100% rename from packages/fabric/store-sqlite/tsconfig.build.json rename to packages/fabric/sqlite-store/tsconfig.build.json diff --git a/packages/fabric/store-sqlite/tsconfig.json b/packages/fabric/sqlite-store/tsconfig.json similarity index 100% rename from packages/fabric/store-sqlite/tsconfig.json rename to packages/fabric/sqlite-store/tsconfig.json diff --git a/packages/fabric/store-sqlite/vitest.config.ts b/packages/fabric/sqlite-store/vitest.config.ts similarity index 100% rename from packages/fabric/store-sqlite/vitest.config.ts rename to packages/fabric/sqlite-store/vitest.config.ts diff --git a/yarn.lock b/yarn.lock index b8b8d60..a15bb70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -426,9 +426,9 @@ __metadata: languageName: unknown linkType: soft -"@fabric/sqlite-store@workspace:^, @fabric/sqlite-store@workspace:packages/fabric/store-sqlite": +"@fabric/sqlite-store@workspace:^, @fabric/sqlite-store@workspace:packages/fabric/sqlite-store": version: 0.0.0-use.local - resolution: "@fabric/sqlite-store@workspace:packages/fabric/store-sqlite" + resolution: "@fabric/sqlite-store@workspace:packages/fabric/sqlite-store" dependencies: "@fabric/core": "workspace:^" "@fabric/domain": "workspace:^" From 559a3f3c22a08ee4110ff27ad9f75dd68646906a Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sat, 12 Oct 2024 21:33:05 -0300 Subject: [PATCH 21/37] Update packages and vitest configurations --- package.json | 10 +- packages/fabric/core/package.json | 14 +- packages/fabric/core/vitest.config.ts | 2 +- packages/fabric/domain/package.json | 10 +- packages/fabric/domain/vitest.config.ts | 2 +- packages/fabric/sqlite-store/package.json | 9 +- packages/fabric/sqlite-store/vitest.config.ts | 2 +- packages/templates/domain/package.json | 9 +- packages/templates/domain/vitest.config.ts | 2 +- packages/templates/lib/package.json | 9 +- packages/templates/lib/vitest.config.ts | 2 +- yarn.lock | 634 ++++++++++++------ 12 files changed, 483 insertions(+), 222 deletions(-) diff --git a/package.json b/package.json index bf7f334..39a0741 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,18 @@ "apps/**/*" ], "devDependencies": { - "@eslint/js": "^9.10.0", + "@eslint/js": "^9.12.0", "@types/eslint": "^9.6.1", "@types/eslint__js": "^8.42.3", "cross-env": "^7.0.3", - "eslint": "^9.10.0", + "eslint": "^9.12.0", "husky": "^9.1.6", "lint-staged": "^15.2.10", "prettier": "^3.3.3", "tsx": "^4.19.1", - "typescript": "^5.6.2", - "typescript-eslint": "^8.6.0", - "zx": "^8.1.7" + "typescript": "^5.6.3", + "typescript-eslint": "^8.8.1", + "zx": "^8.1.9" }, "scripts": { "lint": "eslint . --fix --report-unused-disable-directives", diff --git a/packages/fabric/core/package.json b/packages/fabric/core/package.json index 6d67351..4c1e33b 100644 --- a/packages/fabric/core/package.json +++ b/packages/fabric/core/package.json @@ -1,5 +1,7 @@ { "name": "@fabric/core", + "private": true, + "sideEffects": false, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -9,19 +11,15 @@ "files": [ "dist" ], - "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "@types/validator": "^13.12.2", - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.6.3", + "vitest": "^2.1.2" }, "scripts": { "test": "vitest", + "coverage": "vitest run --coverage", "build": "tsc -p tsconfig.build.json" - }, - "sideEffects": false, - "dependencies": { - "validator": "^13.12.0" } } diff --git a/packages/fabric/core/vitest.config.ts b/packages/fabric/core/vitest.config.ts index f1362e1..820dbd6 100644 --- a/packages/fabric/core/vitest.config.ts +++ b/packages/fabric/core/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { - exclude: ["**/index.ts"], + exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], }, passWithNoTests: true, }, diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index cc2aa68..0c0e6df 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -1,5 +1,7 @@ { "name": "@fabric/domain", + "private": true, + "sideEffects": false, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -10,12 +12,11 @@ "files": [ "dist" ], - "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "@fabric/sqlite-store": "workspace:^", - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.6.3", + "vitest": "^2.1.2" }, "dependencies": { "@fabric/core": "workspace:^", @@ -23,6 +24,7 @@ }, "scripts": { "test": "vitest", + "coverage": "vitest run --coverage", "build": "tsc -p tsconfig.build.json" } } diff --git a/packages/fabric/domain/vitest.config.ts b/packages/fabric/domain/vitest.config.ts index f1362e1..820dbd6 100644 --- a/packages/fabric/domain/vitest.config.ts +++ b/packages/fabric/domain/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { - exclude: ["**/index.ts"], + exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], }, passWithNoTests: true, }, diff --git a/packages/fabric/sqlite-store/package.json b/packages/fabric/sqlite-store/package.json index 6aea475..e75e379 100644 --- a/packages/fabric/sqlite-store/package.json +++ b/packages/fabric/sqlite-store/package.json @@ -1,5 +1,7 @@ { "name": "@fabric/sqlite-store", + "private": true, + "sideEffects": false, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -9,11 +11,11 @@ "files": [ "dist" ], - "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.6.3", + "vitest": "^2.1.2" }, "dependencies": { "@fabric/core": "workspace:^", @@ -22,6 +24,7 @@ }, "scripts": { "test": "vitest", + "coverage": "vitest run --coverage", "build": "tsc -p tsconfig.build.json" } } diff --git a/packages/fabric/sqlite-store/vitest.config.ts b/packages/fabric/sqlite-store/vitest.config.ts index f1362e1..820dbd6 100644 --- a/packages/fabric/sqlite-store/vitest.config.ts +++ b/packages/fabric/sqlite-store/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { - exclude: ["**/index.ts"], + exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], }, passWithNoTests: true, }, diff --git a/packages/templates/domain/package.json b/packages/templates/domain/package.json index 191826f..a4f22df 100644 --- a/packages/templates/domain/package.json +++ b/packages/templates/domain/package.json @@ -1,16 +1,18 @@ { "name": "@ulthar/template-domain", + "private": true, + "sideEffects": false, "type": "module", "module": "dist/index.js", "main": "dist/index.js", "files": [ "dist" ], - "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.6.3", + "vitest": "^2.1.2" }, "dependencies": { "@fabric/core": "workspace:^", @@ -18,6 +20,7 @@ }, "scripts": { "test": "vitest", + "coverage": "vitest run --coverage", "build": "tsc -p tsconfig.build.json" } } diff --git a/packages/templates/domain/vitest.config.ts b/packages/templates/domain/vitest.config.ts index f1362e1..820dbd6 100644 --- a/packages/templates/domain/vitest.config.ts +++ b/packages/templates/domain/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { - exclude: ["**/index.ts"], + exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], }, passWithNoTests: true, }, diff --git a/packages/templates/lib/package.json b/packages/templates/lib/package.json index 93f66cf..b75d100 100644 --- a/packages/templates/lib/package.json +++ b/packages/templates/lib/package.json @@ -1,5 +1,7 @@ { "name": "@ulthar/lib-template", + "private": true, + "sideEffects": false, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -9,17 +11,18 @@ "files": [ "dist" ], - "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { - "typescript": "^5.6.2", - "vitest": "^2.1.1" + "@vitest/coverage-v8": "^2.1.2", + "typescript": "^5.6.3", + "vitest": "^2.1.2" }, "dependencies": { "@fabric/core": "workspace:^" }, "scripts": { "test": "vitest", + "coverage": "vitest run --coverage", "build": "tsc -p tsconfig.build.json" } } diff --git a/packages/templates/lib/vitest.config.ts b/packages/templates/lib/vitest.config.ts index f1362e1..820dbd6 100644 --- a/packages/templates/lib/vitest.config.ts +++ b/packages/templates/lib/vitest.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { coverage: { - exclude: ["**/index.ts"], + exclude: ["dist/**", "vitest.config.ts", "**/index.ts", "**/*.spec.ts"], }, passWithNoTests: true, }, diff --git a/yarn.lock b/yarn.lock index a15bb70..b0bc5f4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,59 @@ __metadata: version: 8 cacheKey: 10c0 +"@ampproject/remapping@npm:^2.3.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/81d63cca5443e0f0c72ae18b544cc28c7c0ec2cea46e7cb888bb0e0f411a1191d0d6b7af798d54e30777d8d1488b2ec0732aac2be342d3d7d3ffd271c6f489ed + languageName: node + linkType: hard + +"@babel/helper-string-parser@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-string-parser@npm:7.25.7" + checksum: 10c0/73ef2ceb81f8294678a0afe8ab0103729c0370cac2e830e0d5128b03be5f6a2635838af31d391d763e3c5a4460ed96f42fd7c9b552130670d525be665913bc4c + languageName: node + linkType: hard + +"@babel/helper-validator-identifier@npm:^7.25.7": + version: 7.25.7 + resolution: "@babel/helper-validator-identifier@npm:7.25.7" + checksum: 10c0/07438e5bf01ab2882a15027fdf39ac3b0ba1b251774a5130917907014684e2f70fef8fd620137ca062c4c4eedc388508d2ea7a3a7d9936a32785f4fe116c68c0 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.25.4": + version: 7.25.8 + resolution: "@babel/parser@npm:7.25.8" + dependencies: + "@babel/types": "npm:^7.25.8" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/a1a13845b7e8dda4c970791814a4bbf60004969882f18f470e260ad822d2e1f8941948f851e9335895563610f240fa6c98481ce8019865e469502bbf21daafa4 + languageName: node + linkType: hard + +"@babel/types@npm:^7.25.4, @babel/types@npm:^7.25.8": + version: 7.25.8 + resolution: "@babel/types@npm:7.25.8" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.7" + "@babel/helper-validator-identifier": "npm:^7.25.7" + to-fast-properties: "npm:^2.0.0" + checksum: 10c0/55ca2d6df6426c98db2769ce884ce5e9de83a512ea2dd7bcf56c811984dc14351cacf42932a723630c5afcff2455809323decd645820762182f10b7b5252b59f + languageName: node + linkType: hard + +"@bcoe/v8-coverage@npm:^0.2.3": + version: 0.2.3 + resolution: "@bcoe/v8-coverage@npm:0.2.3" + checksum: 10c0/6b80ae4cb3db53f486da2dc63b6e190a74c8c3cca16bb2733f234a0b6a9382b09b146488ae08e2b22cf00f6c83e20f3e040a2f7894f05c045c946d6a090b1d52 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -363,6 +416,13 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^0.6.0": + version: 0.6.0 + resolution: "@eslint/core@npm:0.6.0" + checksum: 10c0/fffdb3046ad6420f8cb9204b6466fdd8632a9baeebdaf2a97d458a4eac0e16653ba50d82d61835d7d771f6ced0ec942ec482b2fbccc300e45f2cbf784537f240 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.1.0": version: 3.1.0 resolution: "@eslint/eslintrc@npm:3.1.0" @@ -380,10 +440,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:9.10.0, @eslint/js@npm:^9.10.0": - version: 9.10.0 - resolution: "@eslint/js@npm:9.10.0" - checksum: 10c0/2ac45a002dc1ccf25be46ea61001ada8d77248d1313ab4e53f3735e5ae00738a757874e41f62ad6fbd49df7dffeece66e5f53ff0d7b78a99ce4c68e8fea66753 +"@eslint/js@npm:9.12.0, @eslint/js@npm:^9.12.0": + version: 9.12.0 + resolution: "@eslint/js@npm:9.12.0" + checksum: 10c0/325650a59a1ce3d97c69441501ebaf415607248bacbe8c8ca35adc7cb73b524f592f266a75772f496b06f3239e3ee1996722a242148085f0ee5fb3dd7065897c languageName: node linkType: hard @@ -394,12 +454,12 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.1.0": - version: 0.1.0 - resolution: "@eslint/plugin-kit@npm:0.1.0" +"@eslint/plugin-kit@npm:^0.2.0": + version: 0.2.0 + resolution: "@eslint/plugin-kit@npm:0.2.0" dependencies: levn: "npm:^0.4.1" - checksum: 10c0/fae97cd4efc1c32501c286abba1b5409848ce8c989e1ca6a5bb057a304a2cd721e6e957f6bc35ce95cfd0871e822ed42df3c759fecdad72c30e70802e26f83c7 + checksum: 10c0/00b92bc52ad09b0e2bbbb30591c02a895f0bec3376759562590e8a57a13d096b22f8c8773b6bf791a7cf2ea614123b3d592fd006c51ac5fd0edbb90ea6d8760c languageName: node linkType: hard @@ -407,10 +467,9 @@ __metadata: version: 0.0.0-use.local resolution: "@fabric/core@workspace:packages/fabric/core" dependencies: - "@types/validator": "npm:^13.12.2" - typescript: "npm:^5.6.2" - validator: "npm:^13.12.0" - vitest: "npm:^2.1.1" + "@vitest/coverage-v8": "npm:^2.1.2" + typescript: "npm:^5.6.3" + vitest: "npm:^2.1.2" languageName: unknown linkType: soft @@ -419,22 +478,23 @@ __metadata: resolution: "@fabric/domain@workspace:packages/fabric/domain" dependencies: "@fabric/core": "workspace:^" - "@fabric/sqlite-store": "workspace:^" + "@vitest/coverage-v8": "npm:^2.1.2" decimal.js: "npm:^10.4.3" - typescript: "npm:^5.6.2" - vitest: "npm:^2.1.1" + typescript: "npm:^5.6.3" + vitest: "npm:^2.1.2" languageName: unknown linkType: soft -"@fabric/sqlite-store@workspace:^, @fabric/sqlite-store@workspace:packages/fabric/sqlite-store": +"@fabric/sqlite-store@workspace:packages/fabric/sqlite-store": version: 0.0.0-use.local resolution: "@fabric/sqlite-store@workspace:packages/fabric/sqlite-store" dependencies: "@fabric/core": "workspace:^" "@fabric/domain": "workspace:^" + "@vitest/coverage-v8": "npm:^2.1.2" sqlite3: "npm:^5.1.7" - typescript: "npm:^5.6.2" - vitest: "npm:^2.1.1" + typescript: "npm:^5.6.3" + vitest: "npm:^2.1.2" languageName: unknown linkType: soft @@ -445,6 +505,23 @@ __metadata: languageName: node linkType: hard +"@humanfs/core@npm:^0.19.0": + version: 0.19.0 + resolution: "@humanfs/core@npm:0.19.0" + checksum: 10c0/f87952d5caba6ae427a620eff783c5d0b6cef0cfc256dec359cdaa636c5f161edb8d8dad576742b3de7f0b2f222b34aad6870248e4b7d2177f013426cbcda232 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.5": + version: 0.16.5 + resolution: "@humanfs/node@npm:0.16.5" + dependencies: + "@humanfs/core": "npm:^0.19.0" + "@humanwhocodes/retry": "npm:^0.3.0" + checksum: 10c0/41c365ab09e7c9eaeed373d09243195aef616d6745608a36fc3e44506148c28843872f85e69e2bf5f1e992e194286155a1c1cecfcece6a2f43875e37cd243935 + languageName: node + linkType: hard + "@humanwhocodes/module-importer@npm:^1.0.1": version: 1.0.1 resolution: "@humanwhocodes/module-importer@npm:1.0.1" @@ -459,6 +536,13 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/retry@npm:^0.3.1": + version: 0.3.1 + resolution: "@humanwhocodes/retry@npm:0.3.1" + checksum: 10c0/f0da1282dfb45e8120480b9e2e275e2ac9bbe1cf016d046fdad8e27cc1285c45bb9e711681237944445157b430093412b4446c1ab3fc4bb037861b5904101d3b + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -473,13 +557,55 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.5.0": +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a + languageName: node + linkType: hard + +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": "npm:^1.2.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10c0/1be4fd4a6b0f41337c4f5fdf4afc3bd19e39c3691924817108b82ffcb9c9e609c273f936932b9fba4b3a298ce2eb06d9bff4eb1cc3bd81c4f4ee1b4917e25feb + languageName: node + linkType: hard + +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e + languageName: node + linkType: hard + +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10c0/2a5aa7b4b5c3464c895c802d8ae3f3d2b92fcbe84ad12f8d0bfbb1f5ad006717e7577ee1fd2eac00c088abe486c7adb27976f45d2941ff6b0b92b2c3302c60f4 + languageName: node + linkType: hard + +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.1.0" + "@jridgewell/sourcemap-codec": "npm:^1.4.14" + checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -497,7 +623,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": +"@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -701,6 +827,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:^1.0.6": + version: 1.0.6 + resolution: "@types/estree@npm:1.0.6" + checksum: 10c0/cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a + languageName: node + linkType: hard + "@types/fs-extra@npm:>=11": version: 11.0.4 resolution: "@types/fs-extra@npm:11.0.4" @@ -711,7 +844,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.15": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db @@ -736,22 +869,15 @@ __metadata: languageName: node linkType: hard -"@types/validator@npm:^13.12.2": - version: 13.12.2 - resolution: "@types/validator@npm:13.12.2" - checksum: 10c0/64f1326c768947d756ab5bcd73f3f11a6f07dc76292aea83890d0390a9b9acb374f8df6b24af2c783271f276d3d613b78fc79491fe87edee62108d54be2e3c31 - languageName: node - linkType: hard - -"@typescript-eslint/eslint-plugin@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.6.0" +"@typescript-eslint/eslint-plugin@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.8.1" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.6.0" - "@typescript-eslint/type-utils": "npm:8.6.0" - "@typescript-eslint/utils": "npm:8.6.0" - "@typescript-eslint/visitor-keys": "npm:8.6.0" + "@typescript-eslint/scope-manager": "npm:8.8.1" + "@typescript-eslint/type-utils": "npm:8.8.1" + "@typescript-eslint/utils": "npm:8.8.1" + "@typescript-eslint/visitor-keys": "npm:8.8.1" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -762,66 +888,66 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/c777f01535b896d3092f9886a67ccf9e50bf9e0f581ffab607c5e95dbf3092299b0d9f3e6041b134d69059a6fa5691785940b81015f73bb9a0e9d1605f6442ea + checksum: 10c0/020a0a482202b34c6665a56ec5902e38ae1870b2600ec1b2092de352b23099dde553781ee8323974f63962ebe164a6304f0019e937afb5cf7854b0e0163ad1ca languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/parser@npm:8.6.0" +"@typescript-eslint/parser@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/parser@npm:8.8.1" dependencies: - "@typescript-eslint/scope-manager": "npm:8.6.0" - "@typescript-eslint/types": "npm:8.6.0" - "@typescript-eslint/typescript-estree": "npm:8.6.0" - "@typescript-eslint/visitor-keys": "npm:8.6.0" + "@typescript-eslint/scope-manager": "npm:8.8.1" + "@typescript-eslint/types": "npm:8.8.1" + "@typescript-eslint/typescript-estree": "npm:8.8.1" + "@typescript-eslint/visitor-keys": "npm:8.8.1" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/3f280d289b486359194d422d89df9896b3f10a6d45cdf851d1d5f3200489271a31ab503c127cb5656f9b0ad6d795dd708b960f21fb105750aac19f41f8f815d1 + checksum: 10c0/2afd147ccec6754316d6837d6108a5d822eb6071e1a7355073288c232530bc3e49901d3f08755ce02d497110c531f3b3658eb46d0ff875a69d4f360b5f938cb4 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/scope-manager@npm:8.6.0" +"@typescript-eslint/scope-manager@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/scope-manager@npm:8.8.1" dependencies: - "@typescript-eslint/types": "npm:8.6.0" - "@typescript-eslint/visitor-keys": "npm:8.6.0" - checksum: 10c0/37092ef70171c06854ac67ebfb2255063890c1c6133654e6b15b6adb6d2ab83de4feafd1599f4d02ed71a018226fcb3a389021758ec045e1904fb1798e90b4fe + "@typescript-eslint/types": "npm:8.8.1" + "@typescript-eslint/visitor-keys": "npm:8.8.1" + checksum: 10c0/6f697baf087aedc3f0f228ff964fd108a9dd33fe4e5cc6c914be6367c324cee55629e099832668042bedfec8cdc72c6ef2ca960ee26966dbcc75753059a1352f languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/type-utils@npm:8.6.0" +"@typescript-eslint/type-utils@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/type-utils@npm:8.8.1" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.6.0" - "@typescript-eslint/utils": "npm:8.6.0" + "@typescript-eslint/typescript-estree": "npm:8.8.1" + "@typescript-eslint/utils": "npm:8.8.1" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.3.0" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/914b4637caa40c102117655a9b4451e0db611a61309ed39d6c57522655463c059f4dfd4e2d7ffdefcc9ab7533be21fb877b740c58f5be11f3530aa29f3d2cb62 + checksum: 10c0/6edfc2b9fca5233dd922141f080377b677db1093ec3e702a3ab52d58f77b91c0fb69479d4d42f125536b8fc0ffa85c07c7de2f17cc4c6fa1df1226ec01e5608c languageName: node linkType: hard -"@typescript-eslint/types@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/types@npm:8.6.0" - checksum: 10c0/e7051d212252f7d1905b5527b211e335db4ec5bb1d3a52d73c8d2de6ddf5cbc981f2c92ca9ffcef35f7447bda635ea1ccce5f884ade7f243d14f2a254982c698 +"@typescript-eslint/types@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/types@npm:8.8.1" + checksum: 10c0/4b44857332a0b1bfafbeccb8be157f8266d9e226ac723f6af1272b9b670b49444423ddac733655163eb3b90e8c88393a68ab2d7f326f5775371eaf4b9ca31d7b languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/typescript-estree@npm:8.6.0" +"@typescript-eslint/typescript-estree@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.8.1" dependencies: - "@typescript-eslint/types": "npm:8.6.0" - "@typescript-eslint/visitor-keys": "npm:8.6.0" + "@typescript-eslint/types": "npm:8.8.1" + "@typescript-eslint/visitor-keys": "npm:8.8.1" debug: "npm:^4.3.4" fast-glob: "npm:^3.3.2" is-glob: "npm:^4.0.3" @@ -831,31 +957,31 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/33ab8c03221a797865301f09d1d198c67f8b0e3dbf0d13e41f699dc2740242303a9fcfd7b38302cef318541fdedd832fd6e8ba34a5041a57e9114fa134045385 + checksum: 10c0/e3b9bc1e925c07833237044271cdc9bd8bdba3e2143dcfc5bf3bf481c89731b666a6fad25333a4b1980ac2f4c6f5e6e42c71206f73f3704e319f6b3b67463a6a languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/utils@npm:8.6.0" +"@typescript-eslint/utils@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/utils@npm:8.8.1" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.6.0" - "@typescript-eslint/types": "npm:8.6.0" - "@typescript-eslint/typescript-estree": "npm:8.6.0" + "@typescript-eslint/scope-manager": "npm:8.8.1" + "@typescript-eslint/types": "npm:8.8.1" + "@typescript-eslint/typescript-estree": "npm:8.8.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - checksum: 10c0/5b615106342dfdf09f5a73e2554cc0c4d979c262a9a4548eb76ec7045768e0ff0bf0316cf8a5eb5404689cd476fcd335fc84f90eb985557559e42aeee33d687e + checksum: 10c0/954a2e85ae56a3ebefb6e41fb33c59ffa886963860536e9729a35ecea55eefdc58858c7aa126048c4a61f4fd9997b4f7601e7884ed2b3e4e7a46c9e4617a9f29 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.6.0": - version: 8.6.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.6.0" +"@typescript-eslint/visitor-keys@npm:8.8.1": + version: 8.8.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.8.1" dependencies: - "@typescript-eslint/types": "npm:8.6.0" + "@typescript-eslint/types": "npm:8.8.1" eslint-visitor-keys: "npm:^3.4.3" - checksum: 10c0/9bd5d5daee9de7e009fdd1b64b1eca685a699d1b2639373bc279c97e25e769fff56fffef708ef66a2b19bc8bb201d36daf9e7084f0e0872178bfcf9d923b41f3 + checksum: 10c0/6f917090b61277bd443aa851c532c4a9cc91ad57aedf185c5dff0c530f158cce84ef815833bd8deffa87f0bbf7a9f1abd1e02e30af2463c4e7f27c0c08f59080 languageName: node linkType: hard @@ -864,8 +990,9 @@ __metadata: resolution: "@ulthar/lib-template@workspace:packages/templates/lib" dependencies: "@fabric/core": "workspace:^" - typescript: "npm:^5.6.2" - vitest: "npm:^2.1.1" + "@vitest/coverage-v8": "npm:^2.1.2" + typescript: "npm:^5.6.3" + vitest: "npm:^2.1.2" languageName: unknown linkType: soft @@ -875,32 +1002,59 @@ __metadata: dependencies: "@fabric/core": "workspace:^" "@fabric/domain": "workspace:^" - typescript: "npm:^5.6.2" - vitest: "npm:^2.1.1" + "@vitest/coverage-v8": "npm:^2.1.2" + typescript: "npm:^5.6.3" + vitest: "npm:^2.1.2" languageName: unknown linkType: soft -"@vitest/expect@npm:2.1.1": - version: 2.1.1 - resolution: "@vitest/expect@npm:2.1.1" +"@vitest/coverage-v8@npm:^2.1.2": + version: 2.1.2 + resolution: "@vitest/coverage-v8@npm:2.1.2" dependencies: - "@vitest/spy": "npm:2.1.1" - "@vitest/utils": "npm:2.1.1" - chai: "npm:^5.1.1" + "@ampproject/remapping": "npm:^2.3.0" + "@bcoe/v8-coverage": "npm:^0.2.3" + debug: "npm:^4.3.6" + istanbul-lib-coverage: "npm:^3.2.2" + istanbul-lib-report: "npm:^3.0.1" + istanbul-lib-source-maps: "npm:^5.0.6" + istanbul-reports: "npm:^3.1.7" + magic-string: "npm:^0.30.11" + magicast: "npm:^0.3.4" + std-env: "npm:^3.7.0" + test-exclude: "npm:^7.0.1" tinyrainbow: "npm:^1.2.0" - checksum: 10c0/2a467bcd37378b653040cca062a665f382087eb9f69cff670848a0c207a8458f27211c408c75b7e563e069a2e6d533c78f24e1a317c259646b948813342dbf3d + peerDependencies: + "@vitest/browser": 2.1.2 + vitest: 2.1.2 + peerDependenciesMeta: + "@vitest/browser": + optional: true + checksum: 10c0/46fbe7f9f9c6b40a6477286d0296733578a3801c8ffe972cb16878eefd5fb13a5b87b66597c280e3f5d50084649b27d8a1024b97b56dbdfcc73770dc2758bdef languageName: node linkType: hard -"@vitest/mocker@npm:2.1.1": - version: 2.1.1 - resolution: "@vitest/mocker@npm:2.1.1" +"@vitest/expect@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/expect@npm:2.1.2" + dependencies: + "@vitest/spy": "npm:2.1.2" + "@vitest/utils": "npm:2.1.2" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: 10c0/57233a60685f81ff5cb615156ac164608488c584cb62d7cc63d7ac28674e4c954133d4bb0948e88241c0f07d31803c0d1efd88562c4cac8e1bc5a2b24367ec0f + languageName: node + linkType: hard + +"@vitest/mocker@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/mocker@npm:2.1.2" dependencies: "@vitest/spy": "npm:^2.1.0-beta.1" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.11" peerDependencies: - "@vitest/spy": 2.1.1 + "@vitest/spy": 2.1.2 msw: ^2.3.5 vite: ^5.0.0 peerDependenciesMeta: @@ -908,41 +1062,50 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/e0681bb75bf7255ce49f720d193c9c795a64d42fef13c7af5c157514ebce88a5b89dbf702aa0929d4cefaed3db73351bd3ade3ccabecc09a23a872d9c55be50d + checksum: 10c0/24824666d3045bdbbff77481b033d58fd07db6247846c6090cae44b75080e691f743f850300f27f9b0a790c9e3c918848a400cf7c024c9633084c1ad6311d201 languageName: node linkType: hard -"@vitest/pretty-format@npm:2.1.1, @vitest/pretty-format@npm:^2.1.1": - version: 2.1.1 - resolution: "@vitest/pretty-format@npm:2.1.1" +"@vitest/pretty-format@npm:2.1.2, @vitest/pretty-format@npm:^2.1.2": + version: 2.1.2 + resolution: "@vitest/pretty-format@npm:2.1.2" dependencies: tinyrainbow: "npm:^1.2.0" - checksum: 10c0/21057465a794a037a7af2c48397531eadf9b2d8a7b4d1ee5af9081cf64216cd0039b9e06317319df79aa2240fed1dbb6767b530deae2bd4b42d6fb974297e97d + checksum: 10c0/e2c35dc424450f46794ff420b050e2ce77b3f3d2bdf2509c1adf51d327eeb5cc4ea42fc44919d63b3afdbfcc6da7d7e82962193d0a543c81e0f35ccdfc808835 languageName: node linkType: hard -"@vitest/runner@npm:2.1.1": - version: 2.1.1 - resolution: "@vitest/runner@npm:2.1.1" +"@vitest/runner@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/runner@npm:2.1.2" dependencies: - "@vitest/utils": "npm:2.1.1" + "@vitest/utils": "npm:2.1.2" pathe: "npm:^1.1.2" - checksum: 10c0/a6d1424d6224d8a60ed0bbf7cdacb165ef36bc71cc957ad2c11ed1989fa5106636173369f0d8e1fa3f319a965091e52c8ce21203fce4bafe772632ccc2bd65a6 + checksum: 10c0/c6008703ef7b9033b219690a84003c9c078e9de7ace63cefe7c9cd455667d5081c328645e3a538e23fcc221170901d1d1bb0430c4402391d74f8ffab8db62f81 languageName: node linkType: hard -"@vitest/snapshot@npm:2.1.1": - version: 2.1.1 - resolution: "@vitest/snapshot@npm:2.1.1" +"@vitest/snapshot@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/snapshot@npm:2.1.2" dependencies: - "@vitest/pretty-format": "npm:2.1.1" + "@vitest/pretty-format": "npm:2.1.2" magic-string: "npm:^0.30.11" pathe: "npm:^1.1.2" - checksum: 10c0/e9dadee87a2f489883dec0360b55b2776d2a07e460bf2430b34867cd4e9f34b09b3e219a23bc8c3e1359faefdd166072d3305b66a0bea475c7d616470b7d841c + checksum: 10c0/a05805e9eb9d460830d9f30fbdd488fee4e8bb87dc55e71f5c3541fcd4ef4d333f5c020fd26e8554771157e4e8037d164a63ab5ac0046f7640aca0b8b3fbc837 languageName: node linkType: hard -"@vitest/spy@npm:2.1.1, @vitest/spy@npm:^2.1.0-beta.1": +"@vitest/spy@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/spy@npm:2.1.2" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: 10c0/28781abb8c33274bfcf7ab85d4ce47f1583b0a11575fecbdce7b88dac5df5de62c5e11b0b55043e610c0712258b66aee2a8ff2f55068352c61b4a5d2aa9d50ca + languageName: node + linkType: hard + +"@vitest/spy@npm:^2.1.0-beta.1": version: 2.1.1 resolution: "@vitest/spy@npm:2.1.1" dependencies: @@ -951,14 +1114,14 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:2.1.1": - version: 2.1.1 - resolution: "@vitest/utils@npm:2.1.1" +"@vitest/utils@npm:2.1.2": + version: 2.1.2 + resolution: "@vitest/utils@npm:2.1.2" dependencies: - "@vitest/pretty-format": "npm:2.1.1" + "@vitest/pretty-format": "npm:2.1.2" loupe: "npm:^3.1.1" tinyrainbow: "npm:^1.2.0" - checksum: 10c0/b724c7f23591860bd24cd8e6d0cd803405f4fbff746db160a948290742144463287566a05ca400deb56817603b5185c4429707947869c3d453805860b5e3a3e5 + checksum: 10c0/ab1fac69f34c32eb229c4e5f14bec37f16211a77ba16b0e178678d5a67fd74a209c365df0cf7d27bfd6fd2572d563a6b28269d13f958dc083175a6ae2c269085 languageName: node linkType: hard @@ -1398,7 +1561,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:~4.3.6": +"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:~4.3.6": version: 4.3.7 resolution: "debug@npm:4.3.7" dependencies: @@ -1698,13 +1861,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^8.0.2": - version: 8.0.2 - resolution: "eslint-scope@npm:8.0.2" +"eslint-scope@npm:^8.1.0": + version: 8.1.0 + resolution: "eslint-scope@npm:8.1.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10c0/477f820647c8755229da913025b4567347fd1f0bf7cbdf3a256efff26a7e2e130433df052bd9e3d014025423dc00489bea47eb341002b15553673379c1a7dc36 + checksum: 10c0/ae1df7accae9ea90465c2ded70f7064d6d1f2962ef4cc87398855c4f0b3a5ab01063e0258d954bb94b184f6759febe04c3118195cab5c51978a7229948ba2875 languageName: node linkType: hard @@ -1722,27 +1885,37 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^9.10.0": - version: 9.10.0 - resolution: "eslint@npm:9.10.0" +"eslint-visitor-keys@npm:^4.1.0": + version: 4.1.0 + resolution: "eslint-visitor-keys@npm:4.1.0" + checksum: 10c0/5483ef114c93a136aa234140d7aa3bd259488dae866d35cb0d0b52e6a158f614760a57256ac8d549acc590a87042cb40f6951815caa821e55dc4fd6ef4c722eb + languageName: node + linkType: hard + +"eslint@npm:^9.12.0": + version: 9.12.0 + resolution: "eslint@npm:9.12.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.2.0" "@eslint-community/regexpp": "npm:^4.11.0" "@eslint/config-array": "npm:^0.18.0" + "@eslint/core": "npm:^0.6.0" "@eslint/eslintrc": "npm:^3.1.0" - "@eslint/js": "npm:9.10.0" - "@eslint/plugin-kit": "npm:^0.1.0" + "@eslint/js": "npm:9.12.0" + "@eslint/plugin-kit": "npm:^0.2.0" + "@humanfs/node": "npm:^0.16.5" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@humanwhocodes/retry": "npm:^0.3.0" - "@nodelib/fs.walk": "npm:^1.2.8" + "@humanwhocodes/retry": "npm:^0.3.1" + "@types/estree": "npm:^1.0.6" + "@types/json-schema": "npm:^7.0.15" ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" cross-spawn: "npm:^7.0.2" debug: "npm:^4.3.2" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^8.0.2" - eslint-visitor-keys: "npm:^4.0.0" - espree: "npm:^10.1.0" + eslint-scope: "npm:^8.1.0" + eslint-visitor-keys: "npm:^4.1.0" + espree: "npm:^10.2.0" esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" @@ -1752,13 +1925,11 @@ __metadata: ignore: "npm:^5.2.0" imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" json-stable-stringify-without-jsonify: "npm:^1.0.1" lodash.merge: "npm:^4.6.2" minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" text-table: "npm:^0.2.0" peerDependencies: jiti: "*" @@ -1767,11 +1938,11 @@ __metadata: optional: true bin: eslint: bin/eslint.js - checksum: 10c0/7357f3995b15043eea83c8c0ab16c385ce3f28925c1b11cfcd6b2ede8faab3d91ede84a68173dd5f6e3e176e177984e6218de58b7b8388e53e2881f1ec07c836 + checksum: 10c0/67cf6ea3ea28dcda7dd54aac33e2d4028eb36991d13defb0d2339c3eaa877d5dddd12cd4416ddc701a68bcde9e0bb9e65524c2e4e9914992c724f5b51e949dda languageName: node linkType: hard -"espree@npm:^10.0.1, espree@npm:^10.1.0": +"espree@npm:^10.0.1": version: 10.1.0 resolution: "espree@npm:10.1.0" dependencies: @@ -1782,6 +1953,17 @@ __metadata: languageName: node linkType: hard +"espree@npm:^10.2.0": + version: 10.2.0 + resolution: "espree@npm:10.2.0" + dependencies: + acorn: "npm:^8.12.0" + acorn-jsx: "npm:^5.3.2" + eslint-visitor-keys: "npm:^4.1.0" + checksum: 10c0/2b6bfb683e7e5ab2e9513949879140898d80a2d9867ea1db6ff5b0256df81722633b60a7523a7c614f05a39aeea159dd09ad2a0e90c0e218732fc016f9086215 + languageName: node + linkType: hard + "esquery@npm:^1.5.0": version: 1.6.0 resolution: "esquery@npm:1.6.0" @@ -2088,7 +2270,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.4.1": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -2153,6 +2335,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:^2.0.0": + version: 2.0.2 + resolution: "html-escaper@npm:2.0.2" + checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -2367,13 +2556,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: 10c0/cf7d4ac35fb96bab6a1d2c3598fe5ebb29aafb52c0aaa482b5a3ed9d8ba3edc11631e3ec2637660c44b3ce0e61a08d54946e8af30dec0b60a7c27296c68ffd05 - languageName: node - linkType: hard - "is-stream@npm:^3.0.0": version: 3.0.0 resolution: "is-stream@npm:3.0.0" @@ -2395,6 +2577,45 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2": + version: 3.2.2 + resolution: "istanbul-lib-coverage@npm:3.2.2" + checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b + languageName: node + linkType: hard + +"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1": + version: 3.0.1 + resolution: "istanbul-lib-report@npm:3.0.1" + dependencies: + istanbul-lib-coverage: "npm:^3.0.0" + make-dir: "npm:^4.0.0" + supports-color: "npm:^7.1.0" + checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7 + languageName: node + linkType: hard + +"istanbul-lib-source-maps@npm:^5.0.6": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" + dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" + debug: "npm:^4.1.1" + istanbul-lib-coverage: "npm:^3.0.0" + checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f + languageName: node + linkType: hard + +"istanbul-reports@npm:^3.1.7": + version: 3.1.7 + resolution: "istanbul-reports@npm:3.1.7" + dependencies: + html-escaper: "npm:^2.0.0" + istanbul-lib-report: "npm:^3.0.0" + checksum: 10c0/a379fadf9cf8dc5dfe25568115721d4a7eb82fbd50b005a6672aff9c6989b20cc9312d7865814e0859cd8df58cbf664482e1d3604be0afde1f7fc3ccc1394a51 + languageName: node + linkType: hard + "jackspeak@npm:^3.1.2": version: 3.4.3 resolution: "jackspeak@npm:3.4.3" @@ -2570,6 +2791,26 @@ __metadata: languageName: node linkType: hard +"magicast@npm:^0.3.4": + version: 0.3.5 + resolution: "magicast@npm:0.3.5" + dependencies: + "@babel/parser": "npm:^7.25.4" + "@babel/types": "npm:^7.25.4" + source-map-js: "npm:^1.2.0" + checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64 + languageName: node + linkType: hard + +"make-dir@npm:^4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: "npm:^7.5.3" + checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68 + languageName: node + linkType: hard + "make-fetch-happen@npm:^13.0.0": version: 13.0.1 resolution: "make-fetch-happen@npm:13.0.1" @@ -3369,7 +3610,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.6.0": +"semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.6.0": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -3702,6 +3943,17 @@ __metadata: languageName: node linkType: hard +"test-exclude@npm:^7.0.1": + version: 7.0.1 + resolution: "test-exclude@npm:7.0.1" + dependencies: + "@istanbuljs/schema": "npm:^0.1.2" + glob: "npm:^10.4.1" + minimatch: "npm:^9.0.4" + checksum: 10c0/6d67b9af4336a2e12b26a68c83308c7863534c65f27ed4ff7068a56f5a58f7ac703e8fc80f698a19bb154fd8f705cdf7ec347d9512b2c522c737269507e7b263 + languageName: node + linkType: hard + "text-table@npm:^0.2.0": version: 0.2.0 resolution: "text-table@npm:0.2.0" @@ -3744,6 +3996,13 @@ __metadata: languageName: node linkType: hard +"to-fast-properties@npm:^2.0.0": + version: 2.0.0 + resolution: "to-fast-properties@npm:2.0.0" + checksum: 10c0/b214d21dbfb4bce3452b6244b336806ffea9c05297148d32ebb428d5c43ce7545bdfc65a1ceb58c9ef4376a65c0cb2854d645f33961658b3e3b4f84910ddcdd7 + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -3796,37 +4055,37 @@ __metadata: languageName: node linkType: hard -"typescript-eslint@npm:^8.6.0": - version: 8.6.0 - resolution: "typescript-eslint@npm:8.6.0" +"typescript-eslint@npm:^8.8.1": + version: 8.8.1 + resolution: "typescript-eslint@npm:8.8.1" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.6.0" - "@typescript-eslint/parser": "npm:8.6.0" - "@typescript-eslint/utils": "npm:8.6.0" + "@typescript-eslint/eslint-plugin": "npm:8.8.1" + "@typescript-eslint/parser": "npm:8.8.1" + "@typescript-eslint/utils": "npm:8.8.1" peerDependenciesMeta: typescript: optional: true - checksum: 10c0/d009170af1cffece3a63784c3f6d6f5074fd42d198540f3140dd0fed4f37b1888d59abb5992624099834cae2ea4863b6c526b5f11ecbfd105f41a87e300305db + checksum: 10c0/d6793697fce239ef8838ced6e1e59940c30579c8f62c49bc605fdeda9f3f7a5c24bfddd997b142f8c411859dc0b9985ecdae569814dd4f8e6775e1899d55e9cc languageName: node linkType: hard -"typescript@npm:^5.6.2": - version: 5.6.2 - resolution: "typescript@npm:5.6.2" +"typescript@npm:^5.6.3": + version: 5.6.3 + resolution: "typescript@npm:5.6.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/3ed8297a8c7c56b7fec282532503d1ac795239d06e7c4966b42d4330c6cf433a170b53bcf93a130a7f14ccc5235de5560df4f1045eb7f3550b46ebed16d3c5e5 + checksum: 10c0/44f61d3fb15c35359bc60399cb8127c30bae554cd555b8e2b46d68fa79d680354b83320ad419ff1b81a0bdf324197b29affe6cc28988cd6a74d4ac60c94f9799 languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.6.2#optional!builtin": - version: 5.6.2 - resolution: "typescript@patch:typescript@npm%3A5.6.2#optional!builtin::version=5.6.2&hash=8c6c40" +"typescript@patch:typescript@npm%3A^5.6.3#optional!builtin": + version: 5.6.3 + resolution: "typescript@patch:typescript@npm%3A5.6.3#optional!builtin::version=5.6.3&hash=8c6c40" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/94eb47e130d3edd964b76da85975601dcb3604b0c848a36f63ac448d0104e93819d94c8bdf6b07c00120f2ce9c05256b8b6092d23cf5cf1c6fa911159e4d572f + checksum: 10c0/7c9d2e07c81226d60435939618c91ec2ff0b75fbfa106eec3430f0fcf93a584bc6c73176676f532d78c3594fe28a54b36eb40b3d75593071a7ec91301533ace7 languageName: node linkType: hard @@ -3834,18 +4093,18 @@ __metadata: version: 0.0.0-use.local resolution: "ulthar-framework@workspace:." dependencies: - "@eslint/js": "npm:^9.10.0" + "@eslint/js": "npm:^9.12.0" "@types/eslint": "npm:^9.6.1" "@types/eslint__js": "npm:^8.42.3" cross-env: "npm:^7.0.3" - eslint: "npm:^9.10.0" + eslint: "npm:^9.12.0" husky: "npm:^9.1.6" lint-staged: "npm:^15.2.10" prettier: "npm:^3.3.3" tsx: "npm:^4.19.1" - typescript: "npm:^5.6.2" - typescript-eslint: "npm:^8.6.0" - zx: "npm:^8.1.7" + typescript: "npm:^5.6.3" + typescript-eslint: "npm:^8.8.1" + zx: "npm:^8.1.9" languageName: unknown linkType: soft @@ -3908,16 +4167,9 @@ __metadata: languageName: node linkType: hard -"validator@npm:^13.12.0": - version: 13.12.0 - resolution: "validator@npm:13.12.0" - checksum: 10c0/21d48a7947c9e8498790550f56cd7971e0e3d724c73388226b109c1bac2728f4f88caddfc2f7ed4b076f9b0d004316263ac786a17e9c4edf075741200718cd32 - languageName: node - linkType: hard - -"vite-node@npm:2.1.1": - version: 2.1.1 - resolution: "vite-node@npm:2.1.1" +"vite-node@npm:2.1.2": + version: 2.1.2 + resolution: "vite-node@npm:2.1.2" dependencies: cac: "npm:^6.7.14" debug: "npm:^4.3.6" @@ -3925,7 +4177,7 @@ __metadata: vite: "npm:^5.0.0" bin: vite-node: vite-node.mjs - checksum: 10c0/8a8b958df3d48af915e07e7efb042ee4c036ca0b73d2c411dc29254fd3533ada0807ce5096d8339894d3e786418b7d1a9c4ae02718c6aca11b5098de2b14c336 + checksum: 10c0/7bef84ee757373cc3d171aba51299389e31cb39265df7beef3bb4b70edf1f99425577cd70b9048d357653a0247e8c20f2aa62579d57b2cfc8d74cd6945828b7f languageName: node linkType: hard @@ -3972,17 +4224,17 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^2.1.1": - version: 2.1.1 - resolution: "vitest@npm:2.1.1" +"vitest@npm:^2.1.2": + version: 2.1.2 + resolution: "vitest@npm:2.1.2" dependencies: - "@vitest/expect": "npm:2.1.1" - "@vitest/mocker": "npm:2.1.1" - "@vitest/pretty-format": "npm:^2.1.1" - "@vitest/runner": "npm:2.1.1" - "@vitest/snapshot": "npm:2.1.1" - "@vitest/spy": "npm:2.1.1" - "@vitest/utils": "npm:2.1.1" + "@vitest/expect": "npm:2.1.2" + "@vitest/mocker": "npm:2.1.2" + "@vitest/pretty-format": "npm:^2.1.2" + "@vitest/runner": "npm:2.1.2" + "@vitest/snapshot": "npm:2.1.2" + "@vitest/spy": "npm:2.1.2" + "@vitest/utils": "npm:2.1.2" chai: "npm:^5.1.1" debug: "npm:^4.3.6" magic-string: "npm:^0.30.11" @@ -3993,13 +4245,13 @@ __metadata: tinypool: "npm:^1.0.0" tinyrainbow: "npm:^1.2.0" vite: "npm:^5.0.0" - vite-node: "npm:2.1.1" + vite-node: "npm:2.1.2" why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.1.1 - "@vitest/ui": 2.1.1 + "@vitest/browser": 2.1.2 + "@vitest/ui": 2.1.2 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -4017,7 +4269,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/77a67092338613376dadd8f6f6872383db8409402ce400ac1de48efd87a7214183e798484a3eb2310221c03554e37a00f9fdbc91e49194e7c68e009a5589f494 + checksum: 10c0/79301678bb0207f4bfb16e155e1744ed0b9866c4e1913bb43a3821c01fcda033b7263dac8da87946f90dff6f8f1578a4f94f860409edf0332c3dbfa3a8202803 languageName: node linkType: hard @@ -4134,9 +4386,9 @@ __metadata: languageName: node linkType: hard -"zx@npm:^8.1.7": - version: 8.1.7 - resolution: "zx@npm:8.1.7" +"zx@npm:^8.1.9": + version: 8.1.9 + resolution: "zx@npm:8.1.9" dependencies: "@types/fs-extra": "npm:>=11" "@types/node": "npm:>=20" @@ -4147,6 +4399,6 @@ __metadata: optional: true bin: zx: build/cli.js - checksum: 10c0/9641c0f07e5963928981709d5f909c9920cd8dce058c8a268c085ae257cc796d2d40b0b06fbdcad6788dfac75e26b40f01943706e8b6bea4b01e030ae1d488bf + checksum: 10c0/b11f0e75b8ed618ededc88ecba1d01bc1415bb6966bd68539f8b47bb13d7bf43bf7b64a80798b0cafb308742a19c14df2b08a09ca60703f227bc1a6122547556 languageName: node linkType: hard From 6b46677be975915c2afeae0d0acb134a9c272c7d Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:46:02 -0300 Subject: [PATCH 22/37] [fabric/core] Refactor AsyncResult to accept MaybePromise in tryFrom and from functions --- packages/fabric/core/src/result/async-result.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/fabric/core/src/result/async-result.ts b/packages/fabric/core/src/result/async-result.ts index 22bc787..266bedf 100644 --- a/packages/fabric/core/src/result/async-result.ts +++ b/packages/fabric/core/src/result/async-result.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { TaggedError } from "../error/tagged-error.js"; import { UnexpectedError } from "../error/unexpected-error.js"; +import { MaybePromise } from "../types/maybe-promise.js"; import { Result } from "./result.js"; /** @@ -14,7 +15,7 @@ export type AsyncResult< export namespace AsyncResult { export async function tryFrom( - fn: () => Promise, + fn: () => MaybePromise, errorMapper: (error: any) => TError, ): AsyncResult { try { @@ -25,8 +26,8 @@ export namespace AsyncResult { } export async function from( - fn: () => Promise, - ): AsyncResult { - return tryFrom(fn, (error) => new UnexpectedError(error)); + fn: () => MaybePromise, + ): AsyncResult { + return tryFrom(fn, (error) => new UnexpectedError(error) as never); } } From 417110722716f86aeb473b580602830633d74959 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:48:43 -0300 Subject: [PATCH 23/37] [fabric/core] Add Run.seq overload to handle 4 params --- packages/fabric/core/src/run/run.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/fabric/core/src/run/run.ts b/packages/fabric/core/src/run/run.ts index 9872572..6935632 100644 --- a/packages/fabric/core/src/run/run.ts +++ b/packages/fabric/core/src/run/run.ts @@ -21,6 +21,18 @@ export namespace Run { fn2: (value: T1) => AsyncResult, fn3: (value: T2) => AsyncResult, ): AsyncResult; + // prettier-ignore + export async function seq< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + T3, TE3 extends TaggedError, + T4, TE4 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + fn3: (value: T2) => AsyncResult, + fn4: (value: T3) => AsyncResult, + ): AsyncResult; export async function seq( ...fns: ((...args: any[]) => AsyncResult)[] ): AsyncResult { From b71ecb5de128b3873d8d14dbb28c261a0c5bf7bd Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:49:12 -0300 Subject: [PATCH 24/37] [fabric/core] Add EmptyRecord type to define an empty object structure --- packages/fabric/core/src/types/record.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/fabric/core/src/types/record.ts diff --git a/packages/fabric/core/src/types/record.ts b/packages/fabric/core/src/types/record.ts new file mode 100644 index 0000000..c156343 --- /dev/null +++ b/packages/fabric/core/src/types/record.ts @@ -0,0 +1 @@ +export type EmptyRecord = Record; From 3afdb5d230f8b14c5c67ee9585e8c322ae0f440f Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:50:01 -0300 Subject: [PATCH 25/37] [fabric/domain] Remove validator dependency and simplify isInMemoryFile function --- packages/fabric/domain/src/files/is-in-memory-file.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/fabric/domain/src/files/is-in-memory-file.ts b/packages/fabric/domain/src/files/is-in-memory-file.ts index c3a07f0..09c4049 100644 --- a/packages/fabric/domain/src/files/is-in-memory-file.ts +++ b/packages/fabric/domain/src/files/is-in-memory-file.ts @@ -1,19 +1,14 @@ import { isRecord } from "@fabric/core"; -import validator from "validator"; import { InMemoryFile } from "./in-memory-file.js"; -const { isBase64, isMimeType } = validator; - export function isInMemoryFile(value: unknown): value is InMemoryFile { try { return ( isRecord(value) && "data" in value && typeof value.data === "string" && - isBase64(value.data.split(",")[1]) && "mimeType" in value && typeof value.mimeType === "string" && - isMimeType(value.mimeType) && "name" in value && typeof value.name === "string" && "sizeInBytes" in value && From 9a63ba22f12e2245ad754505f55b274b56e46d83 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:50:43 -0300 Subject: [PATCH 26/37] [fabric/domain] Add 'utils' export --- packages/fabric/domain/src/index.ts | 1 + packages/fabric/domain/src/utils/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/fabric/domain/src/index.ts b/packages/fabric/domain/src/index.ts index 00fedea..97addcc 100644 --- a/packages/fabric/domain/src/index.ts +++ b/packages/fabric/domain/src/index.ts @@ -6,3 +6,4 @@ export * from "./security/index.js"; export * from "./services/index.js"; export * from "./types/index.js"; export * from "./use-case/index.js"; +export * from "./utils/index.js"; diff --git a/packages/fabric/domain/src/utils/index.ts b/packages/fabric/domain/src/utils/index.ts index 245b7df..7bca11f 100644 --- a/packages/fabric/domain/src/utils/index.ts +++ b/packages/fabric/domain/src/utils/index.ts @@ -1 +1,2 @@ +export * from "./json-utils.js"; export * from "./sort-by-dependencies.js"; From a6a303f256e9feb8a638f5c30dd5d78e8bf23f8a Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:55:39 -0300 Subject: [PATCH 27/37] [fabric/domain] Refactor EventStore interface and remove EventStream --- .../fabric/domain/src/events/event-store.ts | 26 +++++++++++++------ .../fabric/domain/src/events/event-stream.ts | 16 ------------ packages/fabric/domain/src/events/event.ts | 5 ++++ packages/fabric/domain/src/events/index.ts | 1 - 4 files changed, 23 insertions(+), 25 deletions(-) delete mode 100644 packages/fabric/domain/src/events/event-stream.ts diff --git a/packages/fabric/domain/src/events/event-store.ts b/packages/fabric/domain/src/events/event-store.ts index 0e31fc1..549031f 100644 --- a/packages/fabric/domain/src/events/event-store.ts +++ b/packages/fabric/domain/src/events/event-store.ts @@ -1,21 +1,31 @@ -import { AsyncResult, PosixDate } from "@fabric/core"; +import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; -import { EventsFromStream, EventStream } from "./event-stream.js"; +import { UUID } from "../types/uuid.js"; +import { Event, EventFromKey } from "./event.js"; import { StoredEvent } from "./stored-event.js"; -export interface EventStore { +export interface EventStore { /** * Store a new event in the event store. */ - append< - TStreamKey extends TEventStream["name"], - T extends EventsFromStream, - >( - streamName: TStreamKey, + append( event: T, ): AsyncResult, StoreQueryError>; + + getEventsFromStream( + streamId: UUID, + ): AsyncResult[], StoreQueryError>; + + subscribe( + events: TEventKey[], + subscriber: EventSubscriber>, + ): void; } +export type EventSubscriber = ( + event: StoredEvent, +) => MaybePromise; + export interface EventFilterOptions { fromDate?: PosixDate; toDate?: PosixDate; diff --git a/packages/fabric/domain/src/events/event-stream.ts b/packages/fabric/domain/src/events/event-stream.ts deleted file mode 100644 index 9b70954..0000000 --- a/packages/fabric/domain/src/events/event-stream.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AsyncResult } from "@fabric/core"; -import { UUID } from "../types/uuid.js"; -import { Event } from "./event.js"; -import { StoredEvent } from "./stored-event.js"; - -export interface EventStream< - TName extends string = string, - TEvent extends Event = Event, -> { - id: UUID; - name: TName; - append(event: Event): AsyncResult>; -} - -export type EventsFromStream = - T extends EventStream ? TEvent : never; diff --git a/packages/fabric/domain/src/events/event.ts b/packages/fabric/domain/src/events/event.ts index e314103..d71ef0a 100644 --- a/packages/fabric/domain/src/events/event.ts +++ b/packages/fabric/domain/src/events/event.ts @@ -10,3 +10,8 @@ export interface Event { readonly streamId: UUID; readonly payload: TPayload; } + +export type EventFromKey< + TEvents extends Event, + TKey extends TEvents["type"], +> = Extract; diff --git a/packages/fabric/domain/src/events/index.ts b/packages/fabric/domain/src/events/index.ts index 7b181cc..a6fc9bd 100644 --- a/packages/fabric/domain/src/events/index.ts +++ b/packages/fabric/domain/src/events/index.ts @@ -1,4 +1,3 @@ export * from "./event-store.js"; -export * from "./event-stream.js"; export * from "./event.js"; export * from "./stored-event.js"; From 4ea00f515b05eddaf620af5712d24424ce6276be Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Mon, 14 Oct 2024 09:56:45 -0300 Subject: [PATCH 28/37] [fabric/sqlite-store] Implement SQLiteEventStore --- .../src/events/event-store.spec.ts | 77 ++++++++ .../sqlite-store/src/events/event-store.ts | 176 ++++++++++++++++++ .../fabric/sqlite-store/src/events/index.ts | 1 + .../{sqlite-wrapper.ts => sqlite-database.ts} | 12 +- .../sqlite-store/src/state/query-builder.ts | 2 +- .../sqlite-store/src/state/state-store.ts | 4 +- 6 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 packages/fabric/sqlite-store/src/events/event-store.spec.ts create mode 100644 packages/fabric/sqlite-store/src/events/event-store.ts create mode 100644 packages/fabric/sqlite-store/src/events/index.ts rename packages/fabric/sqlite-store/src/sqlite/{sqlite-wrapper.ts => sqlite-database.ts} (90%) diff --git a/packages/fabric/sqlite-store/src/events/event-store.spec.ts b/packages/fabric/sqlite-store/src/events/event-store.spec.ts new file mode 100644 index 0000000..4ca0888 --- /dev/null +++ b/packages/fabric/sqlite-store/src/events/event-store.spec.ts @@ -0,0 +1,77 @@ +import { PosixDate, Run } from "@fabric/core"; +import { Event } from "@fabric/domain"; +import { UUIDGeneratorMock } from "@fabric/domain/mocks"; +import { afterEach, beforeEach, describe, expect, it, vitest } from "vitest"; +import { SQLiteEventStore } from "./event-store.js"; + +describe("Event Store", () => { + type UserCreated = Event<"UserCreated", { name: string }>; + type UserUpdated = Event<"UserUpdated", { name: string }>; + type UserDeleted = Event<"UserDeleted", void>; + + type UserEvents = UserCreated | UserUpdated | UserDeleted; + + let store: SQLiteEventStore; + + beforeEach(async () => { + store = new SQLiteEventStore(":memory:"); + await Run.UNSAFE(() => store.migrate()); + }); + + afterEach(async () => { + await Run.UNSAFE(() => store.close()); + }); + + it("Should append an event", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + const userCreated: UserCreated = { + type: "UserCreated", + id: newUUID, + streamId: newUUID, + payload: { name: "test" }, + }; + + await Run.UNSAFE(() => store.append(userCreated)); + + const events = await Run.UNSAFE(() => store.getEventsFromStream(newUUID)); + + expect(events).toHaveLength(1); + + expect(events[0]).toEqual({ + id: newUUID, + streamId: newUUID, + type: "UserCreated", + version: BigInt(1), + timestamp: expect.any(PosixDate), + payload: { name: "test" }, + }); + }); + + it("should notify subscribers on append", async () => { + const newUUID = UUIDGeneratorMock.generate(); + + const userCreated: UserCreated = { + type: "UserCreated", + id: newUUID, + streamId: newUUID, + payload: { name: "test" }, + }; + + const subscriber = vitest.fn(); + + store.subscribe(["UserCreated"], subscriber); + + await Run.UNSAFE(() => store.append(userCreated)); + + expect(subscriber).toHaveBeenCalledTimes(1); + expect(subscriber).toHaveBeenCalledWith({ + id: newUUID, + streamId: newUUID, + type: "UserCreated", + version: BigInt(1), + timestamp: expect.any(PosixDate), + payload: { name: "test" }, + }); + }); +}); diff --git a/packages/fabric/sqlite-store/src/events/event-store.ts b/packages/fabric/sqlite-store/src/events/event-store.ts new file mode 100644 index 0000000..8700f30 --- /dev/null +++ b/packages/fabric/sqlite-store/src/events/event-store.ts @@ -0,0 +1,176 @@ +import { AsyncResult, MaybePromise, PosixDate, Run } from "@fabric/core"; +import { + Event, + EventFromKey, + EventStore, + EventSubscriber, + JSONUtils, + StoredEvent, + StoreQueryError, + UUID, +} from "@fabric/domain"; +import { SQLiteDatabase } from "../sqlite/sqlite-database.js"; + +export class SQLiteEventStore + implements EventStore +{ + private db: SQLiteDatabase; + + private streamVersions = new Map(); + + private eventSubscribers = new Map< + TEvents["type"], + EventSubscriber[] + >(); + + constructor(private readonly dbPath: string) { + this.db = new SQLiteDatabase(dbPath); + } + + async migrate(): AsyncResult { + return AsyncResult.tryFrom( + async () => { + await this.db.init(); + await this.db.run( + `CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + streamId TEXT NOT NULL, + version INTEGER NOT NULL, + timestamp NUMERIC NOT NULL, + payload TEXT NOT NULL, + UNIQUE(streamId, version) + )`, + ); + }, + (error) => new StoreQueryError(error.message, { error }), + ); + } + + async getEventsFromStream( + streamId: UUID, + ): AsyncResult[], StoreQueryError> { + return AsyncResult.tryFrom( + async () => { + const events = await this.db.allPrepared( + `SELECT * FROM events WHERE streamId = $id`, + { + $id: streamId, + }, + (event) => ({ + id: event.id, + streamId: event.streamId, + type: event.type, + version: BigInt(event.version), + timestamp: new PosixDate(event.timestamp), + payload: JSONUtils.parse(event.payload), + }), + ); + return events; + }, + (error) => new StoreQueryError(error.message, { error }), + ); + } + + async append( + event: T, + ): AsyncResult, StoreQueryError> { + return Run.seq( + () => this.getLastVersion(event.streamId), + (version) => + AsyncResult.from(() => { + this.streamVersions.set(event.streamId, version + 1n); + return version; + }), + (version) => this.storeEvent(event.streamId, version + 1n, event), + (storedEvent) => + AsyncResult.from(async () => { + await this.notifySubscribers(storedEvent); + return storedEvent; + }), + ); + } + + private async notifySubscribers( + event: StoredEvent, + ): AsyncResult { + return AsyncResult.from(async () => { + const subscribers = this.eventSubscribers.get(event.type) || []; + await Promise.all(subscribers.map((subscriber) => subscriber(event))); + }); + } + + private async getLastVersion( + streamId: UUID, + ): AsyncResult { + return AsyncResult.tryFrom( + async () => { + const { lastVersion } = await this.db.onePrepared( + `SELECT max(version) as lastVersion FROM events WHERE streamId = $id`, + { + $id: streamId, + }, + ); + + return !lastVersion ? 0n : BigInt(lastVersion); + }, + (error) => + new StoreQueryError(`Error getting last version:${error.message}`, { + error, + }), + ); + } + + subscribe( + events: TEventKey[], + subscriber: ( + event: StoredEvent>, + ) => MaybePromise, + ): void { + events.forEach((event) => { + const subscribers = this.eventSubscribers.get(event) || []; + const newSubscribers = [ + ...subscribers, + subscriber, + ] as EventSubscriber[]; + this.eventSubscribers.set(event, newSubscribers); + }); + } + + close(): AsyncResult { + return AsyncResult.tryFrom( + () => this.db.close(), + (error) => new StoreQueryError(error.message, { error }), + ); + } + + private storeEvent( + streamId: UUID, + version: bigint, + event: T, + ): AsyncResult, StoreQueryError> { + return AsyncResult.tryFrom( + async () => { + const storedEvent: StoredEvent = { + ...event, + version: version, + timestamp: new PosixDate(), + }; + await this.db.runPrepared( + `INSERT INTO events (id, streamId, type, version, timestamp, payload) + VALUES ($id, $streamId, $type, $version, $timestamp, $payload)`, + { + $id: storedEvent.id, + $streamId: streamId, + $type: storedEvent.type, + $version: storedEvent.version.toString(), + $timestamp: storedEvent.timestamp.timestamp, + $payload: JSON.stringify(storedEvent.payload), + }, + ); + return storedEvent; + }, + (error) => new StoreQueryError("Error appending event", { error }), + ); + } +} diff --git a/packages/fabric/sqlite-store/src/events/index.ts b/packages/fabric/sqlite-store/src/events/index.ts new file mode 100644 index 0000000..84a156c --- /dev/null +++ b/packages/fabric/sqlite-store/src/events/index.ts @@ -0,0 +1 @@ +export * from "./event-store.js"; diff --git a/packages/fabric/sqlite-store/src/sqlite/sqlite-wrapper.ts b/packages/fabric/sqlite-store/src/sqlite/sqlite-database.ts similarity index 90% rename from packages/fabric/sqlite-store/src/sqlite/sqlite-wrapper.ts rename to packages/fabric/sqlite-store/src/sqlite/sqlite-database.ts index 7424a0f..7daecb7 100644 --- a/packages/fabric/sqlite-store/src/sqlite/sqlite-wrapper.ts +++ b/packages/fabric/sqlite-store/src/sqlite/sqlite-database.ts @@ -79,7 +79,11 @@ export class SQLiteDatabase { if (err) { reject(err); } else { - resolve(transformer ? rows.map(transformer) : rows); + try { + resolve(transformer ? rows.map(transformer) : rows); + } catch (e) { + reject(e); + } } }, ); @@ -100,7 +104,11 @@ export class SQLiteDatabase { if (err) { reject(err); } else { - resolve(transformer ? rows.map(transformer)[0] : rows[0]); + try { + resolve(transformer ? rows.map(transformer)[0] : rows[0]); + } catch (e) { + reject(e); + } } }, ); diff --git a/packages/fabric/sqlite-store/src/state/query-builder.ts b/packages/fabric/sqlite-store/src/state/query-builder.ts index 60b3438..ef29134 100644 --- a/packages/fabric/sqlite-store/src/state/query-builder.ts +++ b/packages/fabric/sqlite-store/src/state/query-builder.ts @@ -14,7 +14,7 @@ import { } from "@fabric/domain"; import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.js"; import { transformRow } from "../sqlite/sql-to-value.js"; -import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js"; +import { SQLiteDatabase } from "../sqlite/sqlite-database.js"; export class QueryBuilder implements StoreQuery { constructor( diff --git a/packages/fabric/sqlite-store/src/state/state-store.ts b/packages/fabric/sqlite-store/src/state/state-store.ts index e178e7f..2c097c1 100644 --- a/packages/fabric/sqlite-store/src/state/state-store.ts +++ b/packages/fabric/sqlite-store/src/state/state-store.ts @@ -16,7 +16,7 @@ import { recordToSQLParams, recordToSQLSet, } from "../sqlite/record-utils.js"; -import { SQLiteDatabase } from "../sqlite/sqlite-wrapper.js"; +import { SQLiteDatabase } from "../sqlite/sqlite-database.js"; import { QueryBuilder } from "./query-builder.js"; export class SQLiteStateStore @@ -26,7 +26,7 @@ export class SQLiteStateStore private db: SQLiteDatabase; constructor( - private dbPath: string, + private readonly dbPath: string, models: TModel[], ) { this.schema = models.reduce((acc, model: TModel) => { From 38e23ba095145347851fb3a7832cc41cfd99c53f Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 07:45:11 -0300 Subject: [PATCH 29/37] Refactor event interface to use VariantTag for event types --- packages/fabric/core/src/variant/variant.ts | 2 +- .../fabric/domain/src/events/event-store.ts | 14 +++++-- packages/fabric/domain/src/events/event.ts | 7 ++-- .../src/events/event-store.spec.ts | 8 ++-- .../sqlite-store/src/events/event-store.ts | 40 +++++++++++-------- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/packages/fabric/core/src/variant/variant.ts b/packages/fabric/core/src/variant/variant.ts index 12d5b47..928fdd4 100644 --- a/packages/fabric/core/src/variant/variant.ts +++ b/packages/fabric/core/src/variant/variant.ts @@ -7,7 +7,7 @@ export interface TaggedVariant { export type VariantFromTag< TVariant extends TaggedVariant, - TTag extends TVariant[typeof VariantTag], + TTag extends TVariant[VariantTag], > = Extract; export namespace Variant { diff --git a/packages/fabric/domain/src/events/event-store.ts b/packages/fabric/domain/src/events/event-store.ts index 549031f..62e09ee 100644 --- a/packages/fabric/domain/src/events/event-store.ts +++ b/packages/fabric/domain/src/events/event-store.ts @@ -1,7 +1,13 @@ -import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; +import { + AsyncResult, + MaybePromise, + PosixDate, + VariantFromTag, + VariantTag, +} from "@fabric/core"; import { StoreQueryError } from "../errors/query-error.js"; import { UUID } from "../types/uuid.js"; -import { Event, EventFromKey } from "./event.js"; +import { Event } from "./event.js"; import { StoredEvent } from "./stored-event.js"; export interface EventStore { @@ -16,9 +22,9 @@ export interface EventStore { streamId: UUID, ): AsyncResult[], StoreQueryError>; - subscribe( + subscribe( events: TEventKey[], - subscriber: EventSubscriber>, + subscriber: EventSubscriber>, ): void; } diff --git a/packages/fabric/domain/src/events/event.ts b/packages/fabric/domain/src/events/event.ts index d71ef0a..8b8a417 100644 --- a/packages/fabric/domain/src/events/event.ts +++ b/packages/fabric/domain/src/events/event.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { VariantTag } from "@fabric/core"; import { UUID } from "../types/uuid.js"; /** * An event is a tagged variant with a payload and a timestamp. */ export interface Event { - readonly type: TTag; + readonly [VariantTag]: TTag; readonly id: UUID; readonly streamId: UUID; readonly payload: TPayload; @@ -13,5 +14,5 @@ export interface Event { export type EventFromKey< TEvents extends Event, - TKey extends TEvents["type"], -> = Extract; + TKey extends TEvents[VariantTag], +> = Extract; diff --git a/packages/fabric/sqlite-store/src/events/event-store.spec.ts b/packages/fabric/sqlite-store/src/events/event-store.spec.ts index 4ca0888..4093c78 100644 --- a/packages/fabric/sqlite-store/src/events/event-store.spec.ts +++ b/packages/fabric/sqlite-store/src/events/event-store.spec.ts @@ -26,7 +26,7 @@ describe("Event Store", () => { const newUUID = UUIDGeneratorMock.generate(); const userCreated: UserCreated = { - type: "UserCreated", + _tag: "UserCreated", id: newUUID, streamId: newUUID, payload: { name: "test" }, @@ -41,7 +41,7 @@ describe("Event Store", () => { expect(events[0]).toEqual({ id: newUUID, streamId: newUUID, - type: "UserCreated", + _tag: "UserCreated", version: BigInt(1), timestamp: expect.any(PosixDate), payload: { name: "test" }, @@ -52,7 +52,7 @@ describe("Event Store", () => { const newUUID = UUIDGeneratorMock.generate(); const userCreated: UserCreated = { - type: "UserCreated", + _tag: "UserCreated", id: newUUID, streamId: newUUID, payload: { name: "test" }, @@ -68,7 +68,7 @@ describe("Event Store", () => { expect(subscriber).toHaveBeenCalledWith({ id: newUUID, streamId: newUUID, - type: "UserCreated", + _tag: "UserCreated", version: BigInt(1), timestamp: expect.any(PosixDate), payload: { name: "test" }, diff --git a/packages/fabric/sqlite-store/src/events/event-store.ts b/packages/fabric/sqlite-store/src/events/event-store.ts index 8700f30..298a38d 100644 --- a/packages/fabric/sqlite-store/src/events/event-store.ts +++ b/packages/fabric/sqlite-store/src/events/event-store.ts @@ -1,4 +1,10 @@ -import { AsyncResult, MaybePromise, PosixDate, Run } from "@fabric/core"; +import { + AsyncResult, + MaybePromise, + PosixDate, + Run, + VariantTag, +} from "@fabric/core"; import { Event, EventFromKey, @@ -19,7 +25,7 @@ export class SQLiteEventStore private streamVersions = new Map(); private eventSubscribers = new Map< - TEvents["type"], + TEvents[VariantTag], EventSubscriber[] >(); @@ -34,7 +40,7 @@ export class SQLiteEventStore await this.db.run( `CREATE TABLE IF NOT EXISTS events ( id TEXT PRIMARY KEY, - type TEXT NOT NULL, + _tag TEXT NOT NULL, streamId TEXT NOT NULL, version INTEGER NOT NULL, timestamp NUMERIC NOT NULL, @@ -57,13 +63,13 @@ export class SQLiteEventStore { $id: streamId, }, - (event) => ({ - id: event.id, - streamId: event.streamId, - type: event.type, - version: BigInt(event.version), - timestamp: new PosixDate(event.timestamp), - payload: JSONUtils.parse(event.payload), + (e) => ({ + id: e.id, + streamId: e.streamId, + _tag: e._tag, + version: BigInt(e.version), + timestamp: new PosixDate(e.timestamp), + payload: JSONUtils.parse(e.payload), }), ); return events; @@ -95,7 +101,7 @@ export class SQLiteEventStore event: StoredEvent, ): AsyncResult { return AsyncResult.from(async () => { - const subscribers = this.eventSubscribers.get(event.type) || []; + const subscribers = this.eventSubscribers.get(event[VariantTag]) || []; await Promise.all(subscribers.map((subscriber) => subscriber(event))); }); } @@ -121,13 +127,13 @@ export class SQLiteEventStore ); } - subscribe( - events: TEventKey[], + subscribe( + eventNames: TEventKey[], subscriber: ( event: StoredEvent>, ) => MaybePromise, ): void { - events.forEach((event) => { + eventNames.forEach((event) => { const subscribers = this.eventSubscribers.get(event) || []; const newSubscribers = [ ...subscribers, @@ -157,12 +163,12 @@ export class SQLiteEventStore timestamp: new PosixDate(), }; await this.db.runPrepared( - `INSERT INTO events (id, streamId, type, version, timestamp, payload) - VALUES ($id, $streamId, $type, $version, $timestamp, $payload)`, + `INSERT INTO events (id, streamId, _tag, version, timestamp, payload) + VALUES ($id, $streamId, $_tag, $version, $timestamp, $payload)`, { $id: storedEvent.id, $streamId: streamId, - $type: storedEvent.type, + $_tag: storedEvent[VariantTag], $version: storedEvent.version.toString(), $timestamp: storedEvent.timestamp.timestamp, $payload: JSON.stringify(storedEvent.payload), From 1886c52ecedc95088c78b55b73d8ff951fcd95ae Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 07:59:35 -0300 Subject: [PATCH 30/37] [fabric/core] Enhance VariantMatcher to support return type parameterization in match function --- packages/fabric/core/src/variant/match.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/fabric/core/src/variant/match.ts b/packages/fabric/core/src/variant/match.ts index e4f6932..5d14799 100644 --- a/packages/fabric/core/src/variant/match.ts +++ b/packages/fabric/core/src/variant/match.ts @@ -1,17 +1,21 @@ import { Fn } from "../types/fn.js"; import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js"; -export type VariantMatcher> = { - [K in TVariant[VariantTag]]: Fn>; +export type VariantMatcher, T> = { + [K in TVariant[VariantTag]]: Fn, T>; }; export function match>( v: TVariant, ) { return { - case>( - cases: TMatcher, - ): ReturnType { + case< + const TReturnType, + const TMatcher extends VariantMatcher< + TVariant, + TReturnType + > = VariantMatcher, + >(cases: TMatcher): TReturnType { if (!(v[VariantTag] in cases)) { throw new Error("Non-exhaustive pattern match"); } From 7a56c34941cfad10bb6dfe1243fe12900010e66d Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 14:58:22 -0300 Subject: [PATCH 31/37] [fabric/core] Refactor TaggedError and UnexpectedError to accept custom error messages --- packages/fabric/core/src/error/tagged-error.ts | 7 ++++--- packages/fabric/core/src/error/unexpected-error.ts | 9 ++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/fabric/core/src/error/tagged-error.ts b/packages/fabric/core/src/error/tagged-error.ts index 876c87e..c01f0bf 100644 --- a/packages/fabric/core/src/error/tagged-error.ts +++ b/packages/fabric/core/src/error/tagged-error.ts @@ -3,14 +3,15 @@ import { TaggedVariant, VariantTag } from "../variant/index.js"; /** * A TaggedError is a tagged variant with an error message. */ -export class TaggedError +export abstract class TaggedError extends Error implements TaggedVariant { readonly [VariantTag]: Tag; - constructor(tag: Tag) { - super(); + constructor(tag: Tag, message?: string) { + super(message); this[VariantTag] = tag; + this.name = tag; } } diff --git a/packages/fabric/core/src/error/unexpected-error.ts b/packages/fabric/core/src/error/unexpected-error.ts index ba9fa23..a9ee30f 100644 --- a/packages/fabric/core/src/error/unexpected-error.ts +++ b/packages/fabric/core/src/error/unexpected-error.ts @@ -8,12 +8,7 @@ import { TaggedError } from "./tagged-error.js"; * we must be prepared to handle. */ export class UnexpectedError extends TaggedError<"UnexpectedError"> { - constructor(readonly context: Record = {}) { - super("UnexpectedError"); - this.message = "An unexpected error occurred"; - } - - toString() { - return `UnexpectedError: ${this.message}\n${JSON.stringify(this.context, null, 2)}`; + constructor(message?: string) { + super("UnexpectedError", message); } } From 0ac04a839ff0d00dc4e75a6e1964db8fad7d7654 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:10:08 -0300 Subject: [PATCH 32/37] [fabric/core] Add export for Record type in core types --- packages/fabric/core/src/types/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fabric/core/src/types/index.ts b/packages/fabric/core/src/types/index.ts index 20f6f8c..5112de5 100644 --- a/packages/fabric/core/src/types/index.ts +++ b/packages/fabric/core/src/types/index.ts @@ -3,3 +3,4 @@ export * from "./fn.js"; export * from "./keyof.js"; export * from "./maybe-promise.js"; export * from "./optional.js"; +export * from "./record.js"; From 76af85a496404e2b180745d92e53fd1ce7e6a196 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:10:37 -0300 Subject: [PATCH 33/37] [fabric/core] Add ensureValue utility function and export utils --- packages/fabric/core/src/index.ts | 1 + packages/fabric/core/src/utils/ensure-value.ts | 8 ++++++++ packages/fabric/core/src/utils/index.ts | 1 + 3 files changed, 10 insertions(+) create mode 100644 packages/fabric/core/src/utils/ensure-value.ts create mode 100644 packages/fabric/core/src/utils/index.ts diff --git a/packages/fabric/core/src/index.ts b/packages/fabric/core/src/index.ts index b17c448..8baf34e 100644 --- a/packages/fabric/core/src/index.ts +++ b/packages/fabric/core/src/index.ts @@ -5,4 +5,5 @@ export * from "./result/index.js"; export * from "./run/index.js"; export * from "./time/index.js"; export * from "./types/index.js"; +export * from "./utils/index.js"; export * from "./variant/index.js"; diff --git a/packages/fabric/core/src/utils/ensure-value.ts b/packages/fabric/core/src/utils/ensure-value.ts new file mode 100644 index 0000000..11bb5e8 --- /dev/null +++ b/packages/fabric/core/src/utils/ensure-value.ts @@ -0,0 +1,8 @@ +import { UnexpectedError } from "../error/unexpected-error.js"; + +export function ensureValue(value?: T): T { + if (!value) { + throw new UnexpectedError("Value is undefined"); + } + return value; +} diff --git a/packages/fabric/core/src/utils/index.ts b/packages/fabric/core/src/utils/index.ts new file mode 100644 index 0000000..2a01b7e --- /dev/null +++ b/packages/fabric/core/src/utils/index.ts @@ -0,0 +1 @@ +export * from "./ensure-value.js"; From 758f8d933ac006cbd6a6d95982e7583d854a1996 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:12:01 -0300 Subject: [PATCH 34/37] [fabric/domain] Add optional deletedAt field to DefaultModelFields --- packages/fabric/domain/src/models/model.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fabric/domain/src/models/model.ts b/packages/fabric/domain/src/models/model.ts index a2a4ec8..61a7893 100644 --- a/packages/fabric/domain/src/models/model.ts +++ b/packages/fabric/domain/src/models/model.ts @@ -19,6 +19,7 @@ export const DefaultModelFields = { isUnsigned: true, hasArbitraryPrecision: true, }), + deletedAt: Field.timestamp({ isOptional: true }), }; export interface Model< From 6a0be50ef7173a3dacae6fc3fd73e5b76e9433bb Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:12:37 -0300 Subject: [PATCH 35/37] [fabric/domain] Simplify StoreQueryError constructor --- .../fabric/domain/src/errors/query-error.ts | 9 ++----- .../sqlite-store/src/events/event-store.ts | 13 ++++----- .../sqlite-store/src/state/query-builder.ts | 12 ++------- .../sqlite-store/src/state/state-store.ts | 27 +++---------------- 4 files changed, 13 insertions(+), 48 deletions(-) diff --git a/packages/fabric/domain/src/errors/query-error.ts b/packages/fabric/domain/src/errors/query-error.ts index fca7f63..ca36ae8 100644 --- a/packages/fabric/domain/src/errors/query-error.ts +++ b/packages/fabric/domain/src/errors/query-error.ts @@ -1,12 +1,7 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { TaggedError } from "@fabric/core"; export class StoreQueryError extends TaggedError<"StoreQueryError"> { - constructor( - public message: string, - public context: any, - ) { - super("StoreQueryError"); + constructor(public message: string) { + super("StoreQueryError", message); } } diff --git a/packages/fabric/sqlite-store/src/events/event-store.ts b/packages/fabric/sqlite-store/src/events/event-store.ts index 298a38d..b54e659 100644 --- a/packages/fabric/sqlite-store/src/events/event-store.ts +++ b/packages/fabric/sqlite-store/src/events/event-store.ts @@ -49,7 +49,7 @@ export class SQLiteEventStore )`, ); }, - (error) => new StoreQueryError(error.message, { error }), + (error) => new StoreQueryError(error.message), ); } @@ -74,7 +74,7 @@ export class SQLiteEventStore ); return events; }, - (error) => new StoreQueryError(error.message, { error }), + (error) => new StoreQueryError(error.message), ); } @@ -120,10 +120,7 @@ export class SQLiteEventStore return !lastVersion ? 0n : BigInt(lastVersion); }, - (error) => - new StoreQueryError(`Error getting last version:${error.message}`, { - error, - }), + (error) => new StoreQueryError(error.message), ); } @@ -146,7 +143,7 @@ export class SQLiteEventStore close(): AsyncResult { return AsyncResult.tryFrom( () => this.db.close(), - (error) => new StoreQueryError(error.message, { error }), + (error) => new StoreQueryError(error.message), ); } @@ -176,7 +173,7 @@ export class SQLiteEventStore ); return storedEvent; }, - (error) => new StoreQueryError("Error appending event", { error }), + (error) => new StoreQueryError(error.message), ); } } diff --git a/packages/fabric/sqlite-store/src/state/query-builder.ts b/packages/fabric/sqlite-store/src/state/query-builder.ts index ef29134..858f725 100644 --- a/packages/fabric/sqlite-store/src/state/query-builder.ts +++ b/packages/fabric/sqlite-store/src/state/query-builder.ts @@ -62,11 +62,7 @@ export class QueryBuilder implements StoreQuery { transformRow(this.schema[this.query.from]), ); }, - (err) => - new StoreQueryError(err.message, { - err, - query: this.query, - }), + (err) => new StoreQueryError(err.message), ); } @@ -91,11 +87,7 @@ export class QueryBuilder implements StoreQuery { transformRow(this.schema[this.query.from]), ); }, - (err) => - new StoreQueryError(err.message, { - err, - query: this.query, - }), + (err) => new StoreQueryError(err.message), ); } } diff --git a/packages/fabric/sqlite-store/src/state/state-store.ts b/packages/fabric/sqlite-store/src/state/state-store.ts index 2c097c1..ccb02cc 100644 --- a/packages/fabric/sqlite-store/src/state/state-store.ts +++ b/packages/fabric/sqlite-store/src/state/state-store.ts @@ -52,12 +52,7 @@ export class SQLiteStateStore recordToSQLParams(model, record), ); }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }), + (error) => new StoreQueryError(error.message), ); } @@ -87,12 +82,7 @@ export class SQLiteStateStore params, ); }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }), + (error) => new StoreQueryError(error.message), ); } @@ -109,12 +99,7 @@ export class SQLiteStateStore { $id: id }, ); }, - (error) => - new StoreQueryError(error.message, { - error, - collectionName: model.name, - id, - }), + (error) => new StoreQueryError(error.message), ); } @@ -130,11 +115,7 @@ export class SQLiteStateStore } }); }, - (error) => - new StoreQueryError(error.message, { - error, - schema: this.schema, - }), + (error) => new StoreQueryError(error.message), ); } From c9a061419c872cbb66a6aa491400290a26f7a0c9 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:13:59 -0300 Subject: [PATCH 36/37] [fabric/sqlite-store] Add null handling for value conversion and update tests for deletedAt field --- .../sqlite-store/src/sqlite/sql-to-value.ts | 3 +++ .../sqlite-store/src/sqlite/value-to-sql.ts | 3 +++ .../sqlite-store/src/state/state-store.spec.ts | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts b/packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts index a75af59..ce8d123 100644 --- a/packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts +++ b/packages/fabric/sqlite-store/src/sqlite/sql-to-value.ts @@ -14,6 +14,9 @@ export function transformRow(model: Collection) { } function valueFromSQL(field: FieldDefinition, value: any): any { + if (value === null) { + return null; + } const r = FieldSQLInsertMap[field[VariantTag]]; return r(field as any, value); } diff --git a/packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts b/packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts index e6a45b0..c5feeae 100644 --- a/packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts +++ b/packages/fabric/sqlite-store/src/sqlite/value-to-sql.ts @@ -25,6 +25,9 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { }; export function fieldValueToSQL(field: FieldDefinition, value: any) { + if (value === null) { + return null; + } const r = FieldSQLInsertMap[field[VariantTag]] as any; return r(field as any, value); } diff --git a/packages/fabric/sqlite-store/src/state/state-store.spec.ts b/packages/fabric/sqlite-store/src/state/state-store.spec.ts index 1818958..bd1b298 100644 --- a/packages/fabric/sqlite-store/src/state/state-store.spec.ts +++ b/packages/fabric/sqlite-store/src/state/state-store.spec.ts @@ -1,4 +1,4 @@ -import { Run } from "@fabric/core"; +import { PosixDate, Run } from "@fabric/core"; import { defineModel, Field, isLike, UUID } from "@fabric/domain"; import { UUIDGeneratorMock } from "@fabric/domain/mocks"; import { @@ -42,6 +42,7 @@ describe("State Store", () => { name: "test", streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), ); }); @@ -55,6 +56,7 @@ describe("State Store", () => { id: newUUID, streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), ); @@ -66,6 +68,7 @@ describe("State Store", () => { streamId: UUID; streamVersion: bigint; name: string; + deletedAt: PosixDate | null; }[] >(); @@ -75,6 +78,7 @@ describe("State Store", () => { streamId: newUUID, streamVersion: 1n, name: "test", + deletedAt: null, }, ]); }); @@ -89,6 +93,7 @@ describe("State Store", () => { id: newUUID, streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), () => store.insertInto("users", { @@ -96,6 +101,7 @@ describe("State Store", () => { id: UUIDGeneratorMock.generate(), streamId: UUIDGeneratorMock.generate(), streamVersion: 1n, + deletedAt: null, }), () => store.insertInto("users", { @@ -103,6 +109,7 @@ describe("State Store", () => { id: UUIDGeneratorMock.generate(), streamId: UUIDGeneratorMock.generate(), streamVersion: 1n, + deletedAt: null, }), ); @@ -121,6 +128,7 @@ describe("State Store", () => { streamId: UUID; streamVersion: bigint; name: string; + deletedAt: PosixDate | null; }[] >(); @@ -130,6 +138,7 @@ describe("State Store", () => { streamId: newUUID, streamVersion: 1n, name: "test", + deletedAt: null, }, ]); }); @@ -143,6 +152,7 @@ describe("State Store", () => { id: newUUID, streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), ); @@ -161,6 +171,7 @@ describe("State Store", () => { streamId: newUUID, streamVersion: 1n, name: "updated", + deletedAt: null, }); }); @@ -173,6 +184,7 @@ describe("State Store", () => { id: newUUID, streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), ); @@ -197,6 +209,7 @@ describe("State Store", () => { name: "test", streamId: ownerUUID, streamVersion: 1n, + deletedAt: null, }), ); @@ -207,6 +220,7 @@ describe("State Store", () => { owner: ownerUUID, streamId: newUUID, streamVersion: 1n, + deletedAt: null, }), ); }); From 8c6f043f865b9894e545d9e79a7c5294b3397e99 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Tue, 15 Oct 2024 15:16:10 -0300 Subject: [PATCH 37/37] [fabric/domain] Add Projection interface to handle model projection from events --- .../fabric/domain/src/projections/projection.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 packages/fabric/domain/src/projections/projection.ts diff --git a/packages/fabric/domain/src/projections/projection.ts b/packages/fabric/domain/src/projections/projection.ts new file mode 100644 index 0000000..801b963 --- /dev/null +++ b/packages/fabric/domain/src/projections/projection.ts @@ -0,0 +1,13 @@ +import { VariantTag } from "@fabric/core"; +import { Event } from "../events/event.js"; +import { StoredEvent } from "../events/stored-event.js"; +import { Model, ModelToType } from "../models/model.js"; + +export interface Projection { + model: TModel; + events: TEvents[VariantTag][]; + projection: ( + event: StoredEvent, + model?: ModelToType, + ) => ModelToType; +}