From dca326d0c5c41d470daa7be30af5a246f74f7233 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Sun, 15 Sep 2024 21:16:06 -0300 Subject: [PATCH] (WIP) Add state-store utils --- eslint.config.js | 5 + .../fabric/core/src/domain/events/event.ts | 2 + .../core/src/domain/models/create-model.ts | 32 +++++++ .../src/domain/models/fields/base-field.ts | 5 + .../src/domain/models/fields/field-to-type.ts | 9 ++ .../core/src/domain/models/fields/index.ts | 9 ++ .../src/domain/models/fields/string-field.ts | 20 ++++ .../src/domain/models/fields/uuid-field.ts | 15 +++ .../core/src/domain/models/model-to-type.ts | 6 ++ .../core/src/domain/models/relations/index.ts | 5 + .../domain/models/relations/many-to-many.ts | 0 .../domain/models/relations/one-to-many.ts | 34 +++++++ .../src/domain/models/relations/one-to-one.ts | 33 +++++++ .../fabric/core/src/domain/security/index.ts | 2 +- .../core/src/domain/security/policy-map.ts | 7 -- .../fabric/core/src/domain/security/policy.ts | 7 ++ .../domain/use-case/use-case-definition.ts | 66 ++++++------- packages/fabric/core/src/index.ts | 1 + packages/fabric/core/src/storage/driver.ts | 54 +++++++++++ .../core/src/storage/query/filter-options.ts | 93 +++++++++++++++++++ .../src/storage/query/order-by-options.ts | 3 + .../core/src/storage/query/query-builder.ts | 74 +++++++++++++++ .../fabric/core/src/storage/query/query.ts | 56 +++++++++++ .../fabric/core/src/storage/state-store.ts | 16 ++++ .../utils/sort-by-dependencies.spec.ts | 54 +++++++++++ .../src/storage/utils/sort-by-dependencies.ts | 55 +++++++++++ packages/fabric/core/src/types/enum.ts | 1 + packages/fabric/core/src/types/index.ts | 2 + packages/fabric/core/src/types/keyof.ts | 4 + 29 files changed, 622 insertions(+), 48 deletions(-) create mode 100644 packages/fabric/core/src/domain/models/create-model.ts create mode 100644 packages/fabric/core/src/domain/models/fields/base-field.ts create mode 100644 packages/fabric/core/src/domain/models/fields/field-to-type.ts create mode 100644 packages/fabric/core/src/domain/models/fields/index.ts create mode 100644 packages/fabric/core/src/domain/models/fields/string-field.ts create mode 100644 packages/fabric/core/src/domain/models/fields/uuid-field.ts create mode 100644 packages/fabric/core/src/domain/models/model-to-type.ts create mode 100644 packages/fabric/core/src/domain/models/relations/index.ts create mode 100644 packages/fabric/core/src/domain/models/relations/many-to-many.ts create mode 100644 packages/fabric/core/src/domain/models/relations/one-to-many.ts create mode 100644 packages/fabric/core/src/domain/models/relations/one-to-one.ts delete mode 100644 packages/fabric/core/src/domain/security/policy-map.ts create mode 100644 packages/fabric/core/src/domain/security/policy.ts create mode 100644 packages/fabric/core/src/storage/driver.ts create mode 100644 packages/fabric/core/src/storage/query/filter-options.ts create mode 100644 packages/fabric/core/src/storage/query/order-by-options.ts create mode 100644 packages/fabric/core/src/storage/query/query-builder.ts create mode 100644 packages/fabric/core/src/storage/query/query.ts create mode 100644 packages/fabric/core/src/storage/state-store.ts create mode 100644 packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts create mode 100644 packages/fabric/core/src/storage/utils/sort-by-dependencies.ts create mode 100644 packages/fabric/core/src/types/enum.ts create mode 100644 packages/fabric/core/src/types/keyof.ts diff --git a/eslint.config.js b/eslint.config.js index 96133b5..7acab71 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,4 +7,9 @@ export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.strict, ...tseslint.configs.stylistic, + { + rules: { + "@typescript-eslint/no-namespace": "off", + }, + }, ); diff --git a/packages/fabric/core/src/domain/events/event.ts b/packages/fabric/core/src/domain/events/event.ts index 9001dfe..1663011 100644 --- a/packages/fabric/core/src/domain/events/event.ts +++ b/packages/fabric/core/src/domain/events/event.ts @@ -1,10 +1,12 @@ import { TaggedVariant } from "../../variant/variant.js"; +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; timestamp: number; } diff --git a/packages/fabric/core/src/domain/models/create-model.ts b/packages/fabric/core/src/domain/models/create-model.ts new file mode 100644 index 0000000..1b22e5d --- /dev/null +++ b/packages/fabric/core/src/domain/models/create-model.ts @@ -0,0 +1,32 @@ +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/fields/base-field.ts b/packages/fabric/core/src/domain/models/fields/base-field.ts new file mode 100644 index 0000000..6ef73a4 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/base-field.ts @@ -0,0 +1,5 @@ +export interface BaseField { + isOptional?: boolean; + isUnique?: boolean; + isIndexed?: boolean; +} 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 new file mode 100644 index 0000000..0a7eed5 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/field-to-type.ts @@ -0,0 +1,9 @@ +import { UUID } from "../../types/uuid.js"; +import { StringField } from "./string-field.js"; +import { UUIDField } from "./uuid-field.js"; + +export type FieldToType = TField extends StringField + ? string + : TField extends UUIDField + ? UUID + : never; diff --git a/packages/fabric/core/src/domain/models/fields/index.ts b/packages/fabric/core/src/domain/models/fields/index.ts new file mode 100644 index 0000000..67b0fa4 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/index.ts @@ -0,0 +1,9 @@ +import { createStringField, StringField } from "./string-field.js"; +import { createUUIDField } from "./uuid-field.js"; + +export type FieldDefinition = StringField; + +export namespace Field { + export const string = createStringField; + export const uuid = createUUIDField; +} diff --git a/packages/fabric/core/src/domain/models/fields/string-field.ts b/packages/fabric/core/src/domain/models/fields/string-field.ts new file mode 100644 index 0000000..d615dd9 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/string-field.ts @@ -0,0 +1,20 @@ +import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { BaseField } from "./base-field.js"; + +export interface StringFieldOptions extends BaseField { + maxLength?: number; + minLength?: number; +} + +export interface StringField + extends TaggedVariant<"StringField">, + StringFieldOptions {} + +export function createStringField( + opts: T, +): StringField & T { + return { + [VariantTag]: "StringField", + ...opts, + } as const; +} diff --git a/packages/fabric/core/src/domain/models/fields/uuid-field.ts b/packages/fabric/core/src/domain/models/fields/uuid-field.ts new file mode 100644 index 0000000..0e55212 --- /dev/null +++ b/packages/fabric/core/src/domain/models/fields/uuid-field.ts @@ -0,0 +1,15 @@ +import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; +import { BaseField } from "./base-field.js"; + +export interface UUIDOptions extends BaseField { + isPrimaryKey?: boolean; +} + +export interface UUIDField extends TaggedVariant<"UUID_FIELD">, UUIDOptions {} + +export function createUUIDField(opts: UUIDOptions): UUIDField { + return { + [VariantTag]: "UUID_FIELD", + ...opts, + }; +} diff --git a/packages/fabric/core/src/domain/models/model-to-type.ts b/packages/fabric/core/src/domain/models/model-to-type.ts new file mode 100644 index 0000000..9a61e72 --- /dev/null +++ b/packages/fabric/core/src/domain/models/model-to-type.ts @@ -0,0 +1,6 @@ +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/relations/index.ts b/packages/fabric/core/src/domain/models/relations/index.ts new file mode 100644 index 0000000..96aa0a5 --- /dev/null +++ b/packages/fabric/core/src/domain/models/relations/index.ts @@ -0,0 +1,5 @@ +import { OneToOneRelation } from "./one-to-one.js"; + +export type RelationDefinition = OneToOneRelation; + +export namespace Relations {} diff --git a/packages/fabric/core/src/domain/models/relations/many-to-many.ts b/packages/fabric/core/src/domain/models/relations/many-to-many.ts new file mode 100644 index 0000000..e69de29 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 new file mode 100644 index 0000000..2d72989 --- /dev/null +++ b/packages/fabric/core/src/domain/models/relations/one-to-many.ts @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..15f003d --- /dev/null +++ b/packages/fabric/core/src/domain/models/relations/one-to-one.ts @@ -0,0 +1,33 @@ +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/security/index.ts b/packages/fabric/core/src/domain/security/index.ts index 3c864c3..9856aaf 100644 --- a/packages/fabric/core/src/domain/security/index.ts +++ b/packages/fabric/core/src/domain/security/index.ts @@ -1 +1 @@ -export * from "./policy-map.js"; +export * from "./policy.js"; diff --git a/packages/fabric/core/src/domain/security/policy-map.ts b/packages/fabric/core/src/domain/security/policy-map.ts deleted file mode 100644 index df9a5de..0000000 --- a/packages/fabric/core/src/domain/security/policy-map.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * A PolicyMap maps user types to their security policies. - */ -export type PolicyMap< - UserType extends string, - PolicyType extends string, -> = Record; diff --git a/packages/fabric/core/src/domain/security/policy.ts b/packages/fabric/core/src/domain/security/policy.ts new file mode 100644 index 0000000..21ecbc1 --- /dev/null +++ b/packages/fabric/core/src/domain/security/policy.ts @@ -0,0 +1,7 @@ +/** + * A Policy maps permissions to which user types are allowed to perform them. + */ +export type Policy< + TUserType extends string, + TPolicyType extends string, +> = Record; 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 cdce112..c16f76e 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 @@ -1,51 +1,37 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { TaggedError } from "../../error/tagged-error.js"; import { UseCase } from "./use-case.js"; export type UseCaseDefinition< + TDependencies = any, + TPayload = any, + TOutput = any, + TErrors extends TaggedError = any, +> = BasicUseCaseDefinition; + +interface BasicUseCaseDefinition< TDependencies, TPayload, TOutput, TErrors extends TaggedError, -> = TPayload extends undefined - ? { - /** - * The use case name. - */ - name: string; +> { + /** + * The use case name. + */ + name: string; - /** - * Whether the use case requires authentication or not. - */ - isAuthRequired?: boolean; + /** + * Whether the use case requires authentication or not. + */ + isAuthRequired: boolean; - /** - * The required permissions to execute the use case. - */ - requiredPermissions?: string[]; + /** + * The required permissions to execute the use case. + **/ + requiredPermissions?: string[]; - /** - * The use case function. - */ - useCase: UseCase; - } - : { - /** - * The use case name. - */ - name: string; - - /** - * Whether the use case requires authentication or not. - */ - isAuthRequired?: boolean; - - /** - * The required permissions to execute the use case. - */ - requiredPermissions?: string[]; - - /** - * The use case function. - */ - useCase: UseCase; - }; + /** + * The use case function. + */ + useCase: UseCase; +} diff --git a/packages/fabric/core/src/index.ts b/packages/fabric/core/src/index.ts index 1a38f60..83a7a19 100644 --- a/packages/fabric/core/src/index.ts +++ b/packages/fabric/core/src/index.ts @@ -3,5 +3,6 @@ export * from "./domain/index.js"; export * from "./error/index.js"; export * from "./record/index.js"; export * from "./result/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/driver.ts b/packages/fabric/core/src/storage/driver.ts new file mode 100644 index 0000000..36f6503 --- /dev/null +++ b/packages/fabric/core/src/storage/driver.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ModelDefinition } from "../domain/models/create-model.js"; +import { TaggedError } from "../error/tagged-error.js"; +import { AsyncResult } from "../result/async-result.js"; +import { QueryDefinition } from "./query/query.js"; +import { CircularDependencyError } from "./utils/sort-by-dependencies.js"; + +export interface StorageDriver { + /** + * Insert data into the store + */ + insert(collectionName: string, record: any): AsyncResult; + + /** + * Run a select query against the store. + */ + select(query: QueryDefinition): AsyncResult; + + /** + * Run a select query against the store. + */ + selectOne(query: QueryDefinition): AsyncResult; + + /** + * Sincronice the store with the schema. + */ + sync( + schema: ModelDefinition[], + ): AsyncResult; + + /** + * Drop the store. This is a destructive operation. + */ + drop(): AsyncResult; + + /** + * Update a record in the store. + */ + update( + collectionName: string, + id: string, + record: Record, + ): AsyncResult; +} + +export class QueryError extends TaggedError<"QueryError"> { + constructor( + public message: string, + public context: any, + ) { + super("QueryError"); + } +} diff --git a/packages/fabric/core/src/storage/query/filter-options.ts b/packages/fabric/core/src/storage/query/filter-options.ts new file mode 100644 index 0000000..7a6630e --- /dev/null +++ b/packages/fabric/core/src/storage/query/filter-options.ts @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type FilterOptions = + | SingleFilterOption + | MultiFilterOption; + +export type SingleFilterOption = { + [K in keyof T]?: + | T[K] + | LikeFilterOption + | ComparisonFilterOption + | InFilterOption; +}; + +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 type LikeFilterOption = T extends string + ? { + [FILTER_OPTION_TYPE_SYMBOL]: "like"; + [FILTER_OPTION_VALUE_SYMBOL]: string; + } + : never; + +export interface InFilterOption { + [FILTER_OPTION_TYPE_SYMBOL]: "in"; + [FILTER_OPTION_VALUE_SYMBOL]: T[]; +} + +export interface ComparisonFilterOption { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison"; + [FILTER_OPTION_OPERATOR_SYMBOL]: ComparisonOperator; + [FILTER_OPTION_VALUE_SYMBOL]: T; +} + +export type ComparisonOperator = "<" | ">" | "<=" | ">=" | "<>"; + +export function isGreaterThan(value: any): ComparisonFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison", + [FILTER_OPTION_OPERATOR_SYMBOL]: ">", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isLessThan(value: any): ComparisonFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison", + [FILTER_OPTION_OPERATOR_SYMBOL]: "<", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isGreaterOrEqualTo(value: any): ComparisonFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison", + [FILTER_OPTION_OPERATOR_SYMBOL]: ">=", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isLessOrEqualTo(value: any): ComparisonFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison", + [FILTER_OPTION_OPERATOR_SYMBOL]: "<=", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isNotEqualTo(value: any): ComparisonFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "comparison", + [FILTER_OPTION_OPERATOR_SYMBOL]: "<>", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isLike(value: string): LikeFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "like", + [FILTER_OPTION_VALUE_SYMBOL]: value, + }; +} + +export function isIn(values: T[]): InFilterOption { + return { + [FILTER_OPTION_TYPE_SYMBOL]: "in", + [FILTER_OPTION_VALUE_SYMBOL]: values, + }; +} diff --git a/packages/fabric/core/src/storage/query/order-by-options.ts b/packages/fabric/core/src/storage/query/order-by-options.ts new file mode 100644 index 0000000..dca1ec4 --- /dev/null +++ b/packages/fabric/core/src/storage/query/order-by-options.ts @@ -0,0 +1,3 @@ +export type OrderByOptions = { + [K in keyof T]?: "ASC" | "DESC"; +}; diff --git a/packages/fabric/core/src/storage/query/query-builder.ts b/packages/fabric/core/src/storage/query/query-builder.ts new file mode 100644 index 0000000..cc4ba86 --- /dev/null +++ b/packages/fabric/core/src/storage/query/query-builder.ts @@ -0,0 +1,74 @@ +import { + ModelDefinition, + ModelFromName, + ModelName, +} from "../../domain/models/create-model.js"; +import { ModelToType } from "../../domain/models/model-to-type.js"; +import { AsyncResult } from "../../result/async-result.js"; +import { Keyof } from "../../types/index.js"; +import { QueryError, StorageDriver } from "../driver.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< + TModels extends ModelDefinition, + TEntityName extends ModelName, + T = ModelToType>, +> implements StoreQuery +{ + constructor( + private driver: StorageDriver, + private query: QueryDefinition, + ) {} + + where(where: FilterOptions): StoreSortableQuery { + this.query = { + ...this.query, + where, + }; + return this; + } + + orderBy(opts: OrderByOptions): StoreLimitableQuery { + this.query = { + ...this.query, + orderBy: opts, + }; + return this; + } + + limit(limit: number, offset?: number | undefined): SelectableQuery { + this.query = { + ...this.query, + limit, + offset, + }; + + return this; + } + + select>( + keys?: K[], + ): AsyncResult[], QueryError> { + return this.driver.select({ + ...this.query, + keys, + }); + } + + selectOne>( + keys?: K[], + ): AsyncResult, QueryError> { + return this.driver.selectOne({ + ...this.query, + keys, + }); + } +} diff --git a/packages/fabric/core/src/storage/query/query.ts b/packages/fabric/core/src/storage/query/query.ts new file mode 100644 index 0000000..10a5b19 --- /dev/null +++ b/packages/fabric/core/src/storage/query/query.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { AsyncResult } from "../../result/async-result.js"; +import { Keyof } from "../../types/keyof.js"; +import { QueryError } from "../driver.js"; +import { FilterOptions } from "./filter-options.js"; +import { OrderByOptions } from "./order-by-options.js"; + +export interface StoreQuery { + where(where: FilterOptions): StoreSortableQuery; + orderBy(opts: OrderByOptions): StoreLimitableQuery; + limit(limit: number, offset?: number): SelectableQuery; + + select(): AsyncResult; + select>(keys: K[]): AsyncResult[], QueryError>; + + selectOne(): AsyncResult; + selectOne>(keys: K[]): AsyncResult, QueryError>; +} + +export interface StoreSortableQuery { + orderBy(opts: OrderByOptions): StoreLimitableQuery; + limit(limit: number, offset?: number): SelectableQuery; + + select(): AsyncResult; + select>(keys: K[]): AsyncResult[], QueryError>; + + selectOne(): AsyncResult; + selectOne>(keys: K[]): AsyncResult, QueryError>; +} + +export interface StoreLimitableQuery { + limit(limit: number, offset?: number): SelectableQuery; + + select(): AsyncResult; + select>(keys: K[]): AsyncResult[], QueryError>; + + selectOne(): AsyncResult; + selectOne>(keys: K[]): AsyncResult, QueryError>; +} + +export interface SelectableQuery { + select(): AsyncResult; + select>(keys: K[]): AsyncResult[], QueryError>; + + selectOne(): AsyncResult; + selectOne>(keys: K[]): AsyncResult, QueryError>; +} + +export interface QueryDefinition { + from: K; + where?: FilterOptions; + orderBy?: OrderByOptions; + limit?: number; + offset?: number; + keys?: string[]; +} diff --git a/packages/fabric/core/src/storage/state-store.ts b/packages/fabric/core/src/storage/state-store.ts new file mode 100644 index 0000000..9e472f4 --- /dev/null +++ b/packages/fabric/core/src/storage/state-store.ts @@ -0,0 +1,16 @@ +/* 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 { StoreQuery } from "./query/query.js"; + +export interface StateStore< + TModels extends ModelDefinition>, +> { + from>( + entityName: TEntityName, + ): StoreQuery>>; +} diff --git a/packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts b/packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts new file mode 100644 index 0000000..9ac3ad5 --- /dev/null +++ b/packages/fabric/core/src/storage/utils/sort-by-dependencies.spec.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, it } from "vitest"; +import { + CircularDependencyError, + sortByDependencies, +} from "./sort-by-dependencies.js"; + +describe("sortByDependencies", () => { + it("should sort an array of objects by their dependencies", () => { + const array = [ + { id: 1, name: "A", dependencies: ["C", "D"] }, + { id: 2, name: "B", dependencies: ["A", "D"] }, + { id: 3, name: "C", dependencies: [] }, + { id: 4, name: "D", dependencies: [] }, + ]; + + const result = sortByDependencies(array, { + keyGetter: (element) => element.name, + depGetter: (element) => element.dependencies, + }); + + expect(result).toEqual([ + { id: 3, name: "C", dependencies: [] }, + { id: 4, name: "D", dependencies: [] }, + { id: 1, name: "A", dependencies: ["C", "D"] }, + { id: 2, name: "B", dependencies: ["A", "D"] }, + ]); + }); + + it("should throw a CircularDependencyError when circular dependencies are detected", () => { + const array = [ + { id: 1, name: "A", dependencies: ["B"] }, + { id: 2, name: "B", dependencies: ["A"] }, + ]; + + expect( + sortByDependencies(array, { + keyGetter: (element) => element.name, + depGetter: (element) => element.dependencies, + }), + ).toBeInstanceOf(CircularDependencyError); + }); + + it("should return an empty array when the input array is empty", () => { + const array: any[] = []; + + const result = sortByDependencies(array, { + keyGetter: (element) => element.name, + depGetter: (element) => element.dependencies, + }); + + expect(result).toEqual([]); + }); +}); diff --git a/packages/fabric/core/src/storage/utils/sort-by-dependencies.ts b/packages/fabric/core/src/storage/utils/sort-by-dependencies.ts new file mode 100644 index 0000000..11b28fc --- /dev/null +++ b/packages/fabric/core/src/storage/utils/sort-by-dependencies.ts @@ -0,0 +1,55 @@ +import { TaggedError } from "../../error/tagged-error.js"; +import { Result } from "../../result/result.js"; + +export function sortByDependencies( + array: T[], + { + keyGetter, + depGetter, + }: { + keyGetter: (element: T) => string; + depGetter: (element: T) => string[]; + }, +): Result { + const graph = new Map(); + const visited = new Set(); + const sorted: string[] = []; + array.forEach((element) => { + const key = keyGetter(element); + const deps = depGetter(element); + graph.set(key, deps); + }); + const visit = (key: string, ancestors: string[]) => { + if (visited.has(key)) { + return; + } + ancestors.push(key); + const deps = graph.get(key) || []; + deps.forEach((dep) => { + if (ancestors.includes(dep)) { + throw new CircularDependencyError(key, dep); + } + visit(dep, ancestors.slice()); + }); + 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, + ); +} + +export class CircularDependencyError extends TaggedError<"CircularDependencyError"> { + context: { key: string; dep: string }; + constructor(key: string, dep: string) { + super("CircularDependencyError"); + this.context = { key, dep }; + } +} diff --git a/packages/fabric/core/src/types/enum.ts b/packages/fabric/core/src/types/enum.ts new file mode 100644 index 0000000..0237c2e --- /dev/null +++ b/packages/fabric/core/src/types/enum.ts @@ -0,0 +1 @@ +export type EnumToValues> = T[keyof T]; diff --git a/packages/fabric/core/src/types/index.ts b/packages/fabric/core/src/types/index.ts index aa45ed2..2e9bdd3 100644 --- a/packages/fabric/core/src/types/index.ts +++ b/packages/fabric/core/src/types/index.ts @@ -1,2 +1,4 @@ +export * from "./enum.js"; export * from "./fn.js"; +export * from "./keyof.js"; export * from "./optional.js"; diff --git a/packages/fabric/core/src/types/keyof.ts b/packages/fabric/core/src/types/keyof.ts new file mode 100644 index 0000000..5a7e13d --- /dev/null +++ b/packages/fabric/core/src/types/keyof.ts @@ -0,0 +1,4 @@ +/** + * Only string keys are allowed in the keyof type + */ +export type Keyof = Extract;