(WIP) Add state-store utils
This commit is contained in:
parent
c4483f073e
commit
dca326d0c5
@ -7,4 +7,9 @@ export default tseslint.config(
|
|||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.strict,
|
...tseslint.configs.strict,
|
||||||
...tseslint.configs.stylistic,
|
...tseslint.configs.stylistic,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-namespace": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import { TaggedVariant } from "../../variant/variant.js";
|
import { TaggedVariant } from "../../variant/variant.js";
|
||||||
|
import { UUID } from "../types/uuid.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event is a tagged variant with a payload and a timestamp.
|
* An event is a tagged variant with a payload and a timestamp.
|
||||||
*/
|
*/
|
||||||
export interface Event<TTag extends string, TPayload>
|
export interface Event<TTag extends string, TPayload>
|
||||||
extends TaggedVariant<TTag> {
|
extends TaggedVariant<TTag> {
|
||||||
|
streamId: UUID;
|
||||||
payload: TPayload;
|
payload: TPayload;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
32
packages/fabric/core/src/domain/models/create-model.ts
Normal file
32
packages/fabric/core/src/domain/models/create-model.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { FieldDefinition } from "./fields/index.js";
|
||||||
|
|
||||||
|
export interface ModelDefinition<
|
||||||
|
TName extends string = string,
|
||||||
|
TFields extends Record<string, FieldDefinition> = Record<
|
||||||
|
string,
|
||||||
|
FieldDefinition
|
||||||
|
>,
|
||||||
|
> {
|
||||||
|
name: TName;
|
||||||
|
fields: TFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelName<
|
||||||
|
TModel extends ModelDefinition<string, Record<string, FieldDefinition>>,
|
||||||
|
> = TModel["name"];
|
||||||
|
|
||||||
|
export type ModelFromName<
|
||||||
|
TModels extends ModelDefinition<string, Record<string, FieldDefinition>>,
|
||||||
|
TName extends ModelName<TModels>,
|
||||||
|
> = Extract<TModels, { name: TName }>;
|
||||||
|
|
||||||
|
export type ModelFieldNames<
|
||||||
|
TModel extends ModelDefinition<string, Record<string, FieldDefinition>>,
|
||||||
|
> = keyof TModel["fields"];
|
||||||
|
|
||||||
|
export function createModel<
|
||||||
|
TName extends string,
|
||||||
|
TFields extends Record<string, FieldDefinition>,
|
||||||
|
>(opts: ModelDefinition<TName, TFields>): ModelDefinition<TName, TFields> {
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export interface BaseField {
|
||||||
|
isOptional?: boolean;
|
||||||
|
isUnique?: boolean;
|
||||||
|
isIndexed?: boolean;
|
||||||
|
}
|
||||||
@ -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> = TField extends StringField
|
||||||
|
? string
|
||||||
|
: TField extends UUIDField
|
||||||
|
? UUID
|
||||||
|
: never;
|
||||||
9
packages/fabric/core/src/domain/models/fields/index.ts
Normal file
9
packages/fabric/core/src/domain/models/fields/index.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -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<T extends StringFieldOptions>(
|
||||||
|
opts: T,
|
||||||
|
): StringField & T {
|
||||||
|
return {
|
||||||
|
[VariantTag]: "StringField",
|
||||||
|
...opts,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
15
packages/fabric/core/src/domain/models/fields/uuid-field.ts
Normal file
15
packages/fabric/core/src/domain/models/fields/uuid-field.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
6
packages/fabric/core/src/domain/models/model-to-type.ts
Normal file
6
packages/fabric/core/src/domain/models/model-to-type.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { ModelDefinition } from "./create-model.js";
|
||||||
|
import { FieldToType } from "./fields/field-to-type.js";
|
||||||
|
|
||||||
|
export type ModelToType<TModel extends ModelDefinition> = {
|
||||||
|
[K in keyof TModel["fields"]]: FieldToType<TModel["fields"][K]>;
|
||||||
|
};
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { OneToOneRelation } from "./one-to-one.js";
|
||||||
|
|
||||||
|
export type RelationDefinition = OneToOneRelation<string, string>;
|
||||||
|
|
||||||
|
export namespace Relations {}
|
||||||
@ -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<TOwner, TTarget> {}
|
||||||
|
|
||||||
|
export function createOneToManyRelation<
|
||||||
|
TOwner extends string,
|
||||||
|
TTarget extends string,
|
||||||
|
>(
|
||||||
|
opts: OneToManyRelationOptions<TOwner, TTarget>,
|
||||||
|
): OneToManyRelation<TOwner, TTarget> {
|
||||||
|
return {
|
||||||
|
[VariantTag]: "ONE_TO_MANY_RELATION",
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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<TOwner extends string, TTarget extends string>
|
||||||
|
extends TaggedVariant<"ONE_TO_ONE_RELATION">,
|
||||||
|
OneToOneRelationOptions<TOwner, TTarget> {}
|
||||||
|
|
||||||
|
export function createOneToOneRelation<
|
||||||
|
TOwner extends string,
|
||||||
|
TTarget extends string,
|
||||||
|
>(
|
||||||
|
opts: OneToOneRelationOptions<TOwner, TTarget>,
|
||||||
|
): OneToOneRelation<TOwner, TTarget> {
|
||||||
|
return {
|
||||||
|
[VariantTag]: "ONE_TO_ONE_RELATION",
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./policy-map.js";
|
export * from "./policy.js";
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
/**
|
|
||||||
* A PolicyMap maps user types to their security policies.
|
|
||||||
*/
|
|
||||||
export type PolicyMap<
|
|
||||||
UserType extends string,
|
|
||||||
PolicyType extends string,
|
|
||||||
> = Record<UserType, PolicyType[]>;
|
|
||||||
7
packages/fabric/core/src/domain/security/policy.ts
Normal file
7
packages/fabric/core/src/domain/security/policy.ts
Normal file
@ -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<TPolicyType, TUserType[]>;
|
||||||
@ -1,51 +1,37 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { TaggedError } from "../../error/tagged-error.js";
|
import { TaggedError } from "../../error/tagged-error.js";
|
||||||
import { UseCase } from "./use-case.js";
|
import { UseCase } from "./use-case.js";
|
||||||
|
|
||||||
export type UseCaseDefinition<
|
export type UseCaseDefinition<
|
||||||
|
TDependencies = any,
|
||||||
|
TPayload = any,
|
||||||
|
TOutput = any,
|
||||||
|
TErrors extends TaggedError<string> = any,
|
||||||
|
> = BasicUseCaseDefinition<TDependencies, TPayload, TOutput, TErrors>;
|
||||||
|
|
||||||
|
interface BasicUseCaseDefinition<
|
||||||
TDependencies,
|
TDependencies,
|
||||||
TPayload,
|
TPayload,
|
||||||
TOutput,
|
TOutput,
|
||||||
TErrors extends TaggedError<string>,
|
TErrors extends TaggedError<string>,
|
||||||
> = TPayload extends undefined
|
> {
|
||||||
? {
|
/**
|
||||||
/**
|
* The use case name.
|
||||||
* The use case name.
|
*/
|
||||||
*/
|
name: string;
|
||||||
name: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the use case requires authentication or not.
|
* Whether the use case requires authentication or not.
|
||||||
*/
|
*/
|
||||||
isAuthRequired?: boolean;
|
isAuthRequired: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The required permissions to execute the use case.
|
* The required permissions to execute the use case.
|
||||||
*/
|
**/
|
||||||
requiredPermissions?: string[];
|
requiredPermissions?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The use case function.
|
* The use case function.
|
||||||
*/
|
*/
|
||||||
useCase: UseCase<TDependencies, TPayload, TOutput, TErrors>;
|
useCase: UseCase<TDependencies, TPayload, TOutput, TErrors>;
|
||||||
}
|
}
|
||||||
: {
|
|
||||||
/**
|
|
||||||
* 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<TDependencies, TPayload, TOutput, TErrors>;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -3,5 +3,6 @@ export * from "./domain/index.js";
|
|||||||
export * from "./error/index.js";
|
export * from "./error/index.js";
|
||||||
export * from "./record/index.js";
|
export * from "./record/index.js";
|
||||||
export * from "./result/index.js";
|
export * from "./result/index.js";
|
||||||
|
export * from "./time/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./variant/index.js";
|
export * from "./variant/index.js";
|
||||||
|
|||||||
54
packages/fabric/core/src/storage/driver.ts
Normal file
54
packages/fabric/core/src/storage/driver.ts
Normal file
@ -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<void, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a select query against the store.
|
||||||
|
*/
|
||||||
|
select(query: QueryDefinition): AsyncResult<any[], QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a select query against the store.
|
||||||
|
*/
|
||||||
|
selectOne(query: QueryDefinition): AsyncResult<any, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sincronice the store with the schema.
|
||||||
|
*/
|
||||||
|
sync(
|
||||||
|
schema: ModelDefinition[],
|
||||||
|
): AsyncResult<void, QueryError | CircularDependencyError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the store. This is a destructive operation.
|
||||||
|
*/
|
||||||
|
drop(): AsyncResult<void, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a record in the store.
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
collectionName: string,
|
||||||
|
id: string,
|
||||||
|
record: Record<string, any>,
|
||||||
|
): AsyncResult<void, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryError extends TaggedError<"QueryError"> {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public context: any,
|
||||||
|
) {
|
||||||
|
super("QueryError");
|
||||||
|
}
|
||||||
|
}
|
||||||
93
packages/fabric/core/src/storage/query/filter-options.ts
Normal file
93
packages/fabric/core/src/storage/query/filter-options.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export type FilterOptions<T = any> =
|
||||||
|
| SingleFilterOption<T>
|
||||||
|
| MultiFilterOption<T>;
|
||||||
|
|
||||||
|
export type SingleFilterOption<T = any> = {
|
||||||
|
[K in keyof T]?:
|
||||||
|
| T[K]
|
||||||
|
| LikeFilterOption<T[K]>
|
||||||
|
| ComparisonFilterOption<T[K]>
|
||||||
|
| InFilterOption<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
|
||||||
|
|
||||||
|
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> = T extends string
|
||||||
|
? {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "like";
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: string;
|
||||||
|
}
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export interface InFilterOption<T> {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "in";
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ComparisonFilterOption<T> {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison";
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: ComparisonOperator;
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComparisonOperator = "<" | ">" | "<=" | ">=" | "<>";
|
||||||
|
|
||||||
|
export function isGreaterThan(value: any): ComparisonFilterOption<any> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison",
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: ">",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLessThan(value: any): ComparisonFilterOption<any> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison",
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: "<",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGreaterOrEqualTo(value: any): ComparisonFilterOption<any> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison",
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: ">=",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLessOrEqualTo(value: any): ComparisonFilterOption<any> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison",
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: "<=",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNotEqualTo(value: any): ComparisonFilterOption<any> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "comparison",
|
||||||
|
[FILTER_OPTION_OPERATOR_SYMBOL]: "<>",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLike(value: string): LikeFilterOption<string> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "like",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIn<T>(values: T[]): InFilterOption<T> {
|
||||||
|
return {
|
||||||
|
[FILTER_OPTION_TYPE_SYMBOL]: "in",
|
||||||
|
[FILTER_OPTION_VALUE_SYMBOL]: values,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export type OrderByOptions<T> = {
|
||||||
|
[K in keyof T]?: "ASC" | "DESC";
|
||||||
|
};
|
||||||
74
packages/fabric/core/src/storage/query/query-builder.ts
Normal file
74
packages/fabric/core/src/storage/query/query-builder.ts
Normal file
@ -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<TModels>,
|
||||||
|
T = ModelToType<ModelFromName<TModels, TEntityName>>,
|
||||||
|
> implements StoreQuery<T>
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
private driver: StorageDriver,
|
||||||
|
private query: QueryDefinition<TEntityName>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
||||||
|
this.query = {
|
||||||
|
...this.query,
|
||||||
|
where,
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
|
||||||
|
this.query = {
|
||||||
|
...this.query,
|
||||||
|
orderBy: opts,
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
|
||||||
|
this.query = {
|
||||||
|
...this.query,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
select<K extends Keyof<T>>(
|
||||||
|
keys?: K[],
|
||||||
|
): AsyncResult<Pick<T, K>[], QueryError> {
|
||||||
|
return this.driver.select({
|
||||||
|
...this.query,
|
||||||
|
keys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOne<K extends Keyof<T>>(
|
||||||
|
keys?: K[],
|
||||||
|
): AsyncResult<Pick<T, K>, QueryError> {
|
||||||
|
return this.driver.selectOne({
|
||||||
|
...this.query,
|
||||||
|
keys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
56
packages/fabric/core/src/storage/query/query.ts
Normal file
56
packages/fabric/core/src/storage/query/query.ts
Normal file
@ -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<T> {
|
||||||
|
where(where: FilterOptions<T>): StoreSortableQuery<T>;
|
||||||
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
|
select(): AsyncResult<T[], QueryError>;
|
||||||
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
|
|
||||||
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreSortableQuery<T> {
|
||||||
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
|
select(): AsyncResult<T[], QueryError>;
|
||||||
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
|
|
||||||
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreLimitableQuery<T> {
|
||||||
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
|
select(): AsyncResult<T[], QueryError>;
|
||||||
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
|
|
||||||
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectableQuery<T> {
|
||||||
|
select(): AsyncResult<T[], QueryError>;
|
||||||
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
|
|
||||||
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryDefinition<K extends string = string> {
|
||||||
|
from: K;
|
||||||
|
where?: FilterOptions<any>;
|
||||||
|
orderBy?: OrderByOptions<any>;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
keys?: string[];
|
||||||
|
}
|
||||||
16
packages/fabric/core/src/storage/state-store.ts
Normal file
16
packages/fabric/core/src/storage/state-store.ts
Normal file
@ -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<string, Record<string, any>>,
|
||||||
|
> {
|
||||||
|
from<TEntityName extends ModelName<TModels>>(
|
||||||
|
entityName: TEntityName,
|
||||||
|
): StoreQuery<ModelToType<ModelFromName<TModels, TEntityName>>>;
|
||||||
|
}
|
||||||
@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
import { TaggedError } from "../../error/tagged-error.js";
|
||||||
|
import { Result } from "../../result/result.js";
|
||||||
|
|
||||||
|
export function sortByDependencies<T>(
|
||||||
|
array: T[],
|
||||||
|
{
|
||||||
|
keyGetter,
|
||||||
|
depGetter,
|
||||||
|
}: {
|
||||||
|
keyGetter: (element: T) => string;
|
||||||
|
depGetter: (element: T) => string[];
|
||||||
|
},
|
||||||
|
): Result<T[], CircularDependencyError> {
|
||||||
|
const graph = new Map<string, string[]>();
|
||||||
|
const visited = new Set<string>();
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/fabric/core/src/types/enum.ts
Normal file
1
packages/fabric/core/src/types/enum.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type EnumToValues<T extends Record<string, string>> = T[keyof T];
|
||||||
@ -1,2 +1,4 @@
|
|||||||
|
export * from "./enum.js";
|
||||||
export * from "./fn.js";
|
export * from "./fn.js";
|
||||||
|
export * from "./keyof.js";
|
||||||
export * from "./optional.js";
|
export * from "./optional.js";
|
||||||
|
|||||||
4
packages/fabric/core/src/types/keyof.ts
Normal file
4
packages/fabric/core/src/types/keyof.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Only string keys are allowed in the keyof type
|
||||||
|
*/
|
||||||
|
export type Keyof<T> = Extract<keyof T, string>;
|
||||||
Loading…
Reference in New Issue
Block a user