Compare commits
5 Commits
80c34e4649
...
27dbd44741
| Author | SHA1 | Date | |
|---|---|---|---|
| 27dbd44741 | |||
| 09f045daf6 | |||
| 290544dc9a | |||
| 9092b032b3 | |||
| 3f91e35790 |
41
packages/fabric/core/src/variant/variant.spec.ts
Normal file
41
packages/fabric/core/src/variant/variant.spec.ts
Normal file
@ -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<SuccessVariant>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Variant.is(errorVariant, "error")) {
|
||||||
|
expectTypeOf(errorVariant).toEqualTypeOf<ErrorVariant>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for a non-matching tag", () => {
|
||||||
|
expect(Variant.is(successVariant, "error")).toBe(false);
|
||||||
|
expect(Variant.is(errorVariant, "success")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -9,3 +9,15 @@ export type VariantFromTag<
|
|||||||
TVariant extends TaggedVariant<string>,
|
TVariant extends TaggedVariant<string>,
|
||||||
TTag extends TVariant[typeof VariantTag],
|
TTag extends TVariant[typeof VariantTag],
|
||||||
> = Extract<TVariant, { [VariantTag]: TTag }>;
|
> = Extract<TVariant, { [VariantTag]: TTag }>;
|
||||||
|
|
||||||
|
export namespace Variant {
|
||||||
|
export function is<
|
||||||
|
TVariant extends TaggedVariant<string>,
|
||||||
|
TTag extends TVariant[VariantTag],
|
||||||
|
>(
|
||||||
|
variant: TVariant,
|
||||||
|
tag: TTag,
|
||||||
|
): variant is Extract<TVariant, { [VariantTag]: TTag }> {
|
||||||
|
return variant[VariantTag] === tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
|
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
import { Event, StoredEvent } from "../events/event.js";
|
|
||||||
import { UUID } from "../types/uuid.js";
|
import { UUID } from "../types/uuid.js";
|
||||||
|
import { Event, StoredEvent } from "./event.js";
|
||||||
|
|
||||||
export interface EventStore<TEvent extends Event = Event> {
|
export interface EventStore<TEvent extends Event = Event> {
|
||||||
getStream<TEventStreamEvent extends TEvent>(
|
getStream<TEventStreamEvent extends TEvent>(
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./event-store.js";
|
||||||
export * from "./event.js";
|
export * from "./event.js";
|
||||||
|
|||||||
@ -2,7 +2,6 @@ export * from "./errors/index.js";
|
|||||||
export * from "./events/index.js";
|
export * from "./events/index.js";
|
||||||
export * from "./files/index.js";
|
export * from "./files/index.js";
|
||||||
export * from "./models/index.js";
|
export * from "./models/index.js";
|
||||||
export * from "./query/index.js";
|
|
||||||
export * from "./security/index.js";
|
export * from "./security/index.js";
|
||||||
export * from "./storage/index.js";
|
export * from "./storage/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { createReferenceField, ReferenceField } from "./reference-field.js";
|
|||||||
import { createStringField, StringField } from "./string-field.js";
|
import { createStringField, StringField } from "./string-field.js";
|
||||||
import { createUUIDField, UUIDField } from "./uuid-field.js";
|
import { createUUIDField, UUIDField } from "./uuid-field.js";
|
||||||
export * from "./base-field.js";
|
export * from "./base-field.js";
|
||||||
|
export * from "./reference-field.js";
|
||||||
|
|
||||||
export type FieldDefinition =
|
export type FieldDefinition =
|
||||||
| StringField
|
| StringField
|
||||||
|
|||||||
@ -9,22 +9,22 @@ import {
|
|||||||
|
|
||||||
describe("Validate Reference Field", () => {
|
describe("Validate Reference Field", () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
user: defineModel({
|
User: defineModel("User", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
password: Field.string(),
|
password: Field.string(),
|
||||||
phone: Field.string({ isOptional: true }),
|
|
||||||
otherUnique: Field.integer({ isUnique: true }),
|
otherUnique: Field.integer({ isUnique: true }),
|
||||||
otherNotUnique: Field.uuid(),
|
otherNotUnique: Field.uuid(),
|
||||||
|
otherUser: Field.reference({
|
||||||
|
targetModel: "User",
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should return an error when the target model is not in the schema", () => {
|
it("should return an error when the target model is not in the schema", () => {
|
||||||
const result = validateReferenceField(
|
const result = validateReferenceField(
|
||||||
schema,
|
schema,
|
||||||
"post",
|
|
||||||
"authorId",
|
|
||||||
Field.reference({
|
Field.reference({
|
||||||
model: "foo",
|
targetModel: "foo",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -33,33 +33,26 @@ describe("Validate Reference Field", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(InvalidReferenceField);
|
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", () => {
|
it("should not return an error if the target model is in the schema", () => {
|
||||||
const result = validateReferenceField(
|
const result = validateReferenceField(
|
||||||
schema,
|
schema,
|
||||||
"post",
|
|
||||||
"authorId",
|
|
||||||
Field.reference({
|
Field.reference({
|
||||||
model: "user",
|
targetModel: "User",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isError(result)) {
|
if (isError(result)) {
|
||||||
throw result.toString();
|
throw result.reason;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return an error if the target key is not in the target model", () => {
|
it("should return an error if the target key is not in the target model", () => {
|
||||||
const result = validateReferenceField(
|
const result = validateReferenceField(
|
||||||
schema,
|
schema,
|
||||||
"post",
|
|
||||||
"authorId",
|
|
||||||
Field.reference({
|
Field.reference({
|
||||||
model: "user",
|
targetModel: "User",
|
||||||
targetKey: "foo",
|
targetKey: "foo",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -69,34 +62,13 @@ describe("Validate Reference Field", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(InvalidReferenceField);
|
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", () => {
|
it("should return error if the target key is not unique", () => {
|
||||||
const result = validateReferenceField(
|
const result = validateReferenceField(
|
||||||
schema,
|
schema,
|
||||||
"post",
|
|
||||||
"authorId",
|
|
||||||
Field.reference({
|
Field.reference({
|
||||||
model: "user",
|
targetModel: "User",
|
||||||
targetKey: "otherNotUnique",
|
targetKey: "otherNotUnique",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -106,8 +78,19 @@ describe("Validate Reference Field", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(InvalidReferenceField);
|
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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ModelSchema } from "../model-schema.js";
|
|||||||
import { BaseField } from "./base-field.js";
|
import { BaseField } from "./base-field.js";
|
||||||
|
|
||||||
export interface ReferenceFieldOptions extends BaseField {
|
export interface ReferenceFieldOptions extends BaseField {
|
||||||
model: string;
|
targetModel: string;
|
||||||
targetKey?: string;
|
targetKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,47 +20,38 @@ export function createReferenceField<T extends ReferenceFieldOptions>(
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTargetKey(field: ReferenceField): string {
|
||||||
|
return field.targetKey || "id";
|
||||||
|
}
|
||||||
|
|
||||||
export function validateReferenceField(
|
export function validateReferenceField(
|
||||||
schema: ModelSchema,
|
schema: ModelSchema,
|
||||||
modelName: string,
|
|
||||||
fieldName: string,
|
|
||||||
field: ReferenceField,
|
field: ReferenceField,
|
||||||
): Result<void, InvalidReferenceField> {
|
): Result<void, InvalidReferenceField> {
|
||||||
if (!schema[field.model]) {
|
if (!schema[field.targetModel]) {
|
||||||
return new InvalidReferenceField(
|
return new InvalidReferenceField(
|
||||||
modelName,
|
`The target model '${field.targetModel}' is not in the schema.`,
|
||||||
fieldName,
|
|
||||||
`The target model '${field.model}' 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(
|
return new InvalidReferenceField(
|
||||||
modelName,
|
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
|
||||||
fieldName,
|
|
||||||
`The target key '${field.targetKey}' is not in the target model '${field.model}'.`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.targetKey && !schema[field.model][field.targetKey].isUnique) {
|
if (
|
||||||
|
field.targetKey &&
|
||||||
|
!schema[field.targetModel].fields[field.targetKey].isUnique
|
||||||
|
) {
|
||||||
return new InvalidReferenceField(
|
return new InvalidReferenceField(
|
||||||
modelName,
|
`The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
|
||||||
fieldName,
|
|
||||||
`The target key '${field.model}'.'${field.targetKey}' is not unique.`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> {
|
export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> {
|
||||||
constructor(
|
constructor(readonly reason: string) {
|
||||||
readonly modelName: string,
|
|
||||||
readonly fieldName: string,
|
|
||||||
readonly reason: string,
|
|
||||||
) {
|
|
||||||
super("InvalidReferenceField");
|
super("InvalidReferenceField");
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `InvalidReferenceField: ${this.modelName}.${this.fieldName}. ${this.reason}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./fields/index.js";
|
export * from "./fields/index.js";
|
||||||
export * from "./model-schema.js";
|
export * from "./model-schema.js";
|
||||||
export * from "./model.js";
|
export * from "./model.js";
|
||||||
export * from "./types/index.js";
|
export * from "./query/index.js";
|
||||||
|
export * from "./state-store.js";
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { describe, expectTypeOf, it } from "vitest";
|
import { describe, expectTypeOf, it } from "vitest";
|
||||||
import { UUID } from "../types/uuid.js";
|
import { UUID } from "../types/uuid.js";
|
||||||
import { Field } from "./fields/index.js";
|
import { Field } from "./fields/index.js";
|
||||||
import { defineModel } from "./model.js";
|
import { defineModel, ModelToType } from "./model.js";
|
||||||
import { ModelToType } from "./types/model-to-type.js";
|
|
||||||
|
|
||||||
describe("CreateModel", () => {
|
describe("CreateModel", () => {
|
||||||
it("should create a model and it's interface type", () => {
|
it("should create a model and it's interface type", () => {
|
||||||
const User = defineModel({
|
const User = defineModel("User", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
password: Field.string(),
|
password: Field.string(),
|
||||||
phone: Field.string({ isOptional: true }),
|
phone: Field.string({ isOptional: true }),
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { Keyof } from "@fabric/core";
|
||||||
|
import { FieldToType } from "./fields/field-to-type.js";
|
||||||
import { Field, FieldDefinition } from "./fields/index.js";
|
import { Field, FieldDefinition } from "./fields/index.js";
|
||||||
|
|
||||||
export type CustomModelFields = Record<string, FieldDefinition>;
|
export type CustomModelFields = Record<string, FieldDefinition>;
|
||||||
@ -10,14 +12,34 @@ export const DefaultModelFields = {
|
|||||||
hasArbitraryPrecision: true,
|
hasArbitraryPrecision: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
export type Model<TFields extends CustomModelFields = CustomModelFields> =
|
export interface Model<
|
||||||
typeof DefaultModelFields & TFields;
|
TName extends string = string,
|
||||||
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
|
> {
|
||||||
|
name: TName;
|
||||||
|
fields: typeof DefaultModelFields & TFields;
|
||||||
|
}
|
||||||
|
|
||||||
export function defineModel<TFields extends CustomModelFields>(
|
export function defineModel<
|
||||||
fields: TFields,
|
TName extends string,
|
||||||
): Model<TFields> {
|
TFields extends CustomModelFields,
|
||||||
|
>(name: TName, fields: TFields): Model<TName, TFields> {
|
||||||
return {
|
return {
|
||||||
...fields,
|
name,
|
||||||
...DefaultModelFields,
|
fields: { ...DefaultModelFields, ...fields },
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ModelToType<TModel extends Model> = {
|
||||||
|
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModelFieldNames<TModel extends CustomModelFields> = Keyof<
|
||||||
|
TModel["fields"]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ModelAddressableFields<TModel extends Model> = {
|
||||||
|
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends { isUnique: true }
|
||||||
|
? K
|
||||||
|
: never;
|
||||||
|
}[Keyof<TModel["fields"]>];
|
||||||
|
|||||||
@ -14,9 +14,9 @@ export type SingleFilterOption<T = any> = {
|
|||||||
|
|
||||||
export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
|
export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
|
||||||
|
|
||||||
export const FILTER_OPTION_TYPE_SYMBOL = Symbol("$type");
|
export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type");
|
||||||
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("$value");
|
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value");
|
||||||
export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("$operator");
|
export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator");
|
||||||
|
|
||||||
export type LikeFilterOption<T> = T extends string
|
export type LikeFilterOption<T> = T extends string
|
||||||
? {
|
? {
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import { AsyncResult, Keyof } from "@fabric/core";
|
import { AsyncResult, Keyof } from "@fabric/core";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../../errors/query-error.js";
|
||||||
import { ModelToType } from "../models/index.js";
|
import { StorageDriver } from "../../storage/storage-driver.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 { FilterOptions } from "./filter-options.js";
|
||||||
import { OrderByOptions } from "./order-by-options.js";
|
import { OrderByOptions } from "./order-by-options.js";
|
||||||
import {
|
import {
|
||||||
@ -14,44 +11,32 @@ import {
|
|||||||
StoreSortableQuery,
|
StoreSortableQuery,
|
||||||
} from "./query.js";
|
} from "./query.js";
|
||||||
|
|
||||||
export class QueryBuilder<
|
export class QueryBuilder<T> implements StoreQuery<T> {
|
||||||
TModels extends ModelSchema,
|
|
||||||
TEntityName extends Keyof<TModels>,
|
|
||||||
T = ModelToType<TModels[TEntityName]>,
|
|
||||||
> implements StoreQuery<T>
|
|
||||||
{
|
|
||||||
constructor(
|
constructor(
|
||||||
private driver: StorageDriver,
|
private driver: StorageDriver,
|
||||||
private query: QueryDefinition<TEntityName>,
|
private query: QueryDefinition,
|
||||||
) {}
|
) {}
|
||||||
aggregate<K extends AggregateOptions<T>>(): SelectableQuery<K> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
||||||
this.query = {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
where,
|
where,
|
||||||
};
|
});
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
|
||||||
this.query = {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
orderBy: opts,
|
orderBy: opts,
|
||||||
};
|
});
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
|
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
|
||||||
this.query = {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
};
|
});
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { AsyncResult, Keyof } from "@fabric/core";
|
import { AsyncResult, Keyof } from "@fabric/core";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../../errors/query-error.js";
|
||||||
import { AggregateOptions } from "./aggregate-options.js";
|
|
||||||
import { FilterOptions } from "./filter-options.js";
|
import { FilterOptions } from "./filter-options.js";
|
||||||
import { OrderByOptions } from "./order-by-options.js";
|
import { OrderByOptions } from "./order-by-options.js";
|
||||||
|
|
||||||
@ -10,8 +9,6 @@ export interface StoreQuery<T> {
|
|||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
aggregate<K extends AggregateOptions<T>>(opts: K): SelectableQuery<K>;
|
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): AsyncResult<T[], StoreQueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys: K[],
|
keys: K[],
|
||||||
5
packages/fabric/domain/src/models/state-store.ts
Normal file
5
packages/fabric/domain/src/models/state-store.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { StorageDriver } from "../storage/storage-driver.js";
|
||||||
|
|
||||||
|
export class StateStore {
|
||||||
|
constructor(private driver: StorageDriver) {}
|
||||||
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./model-field-names.js";
|
|
||||||
export * from "./model-to-type.js";
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { CustomModelFields } from "../model.js";
|
|
||||||
|
|
||||||
export type ModelFieldNames<TModel extends CustomModelFields> = keyof TModel;
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { FieldToType } from "../fields/field-to-type.js";
|
|
||||||
import { Model } from "../model.js";
|
|
||||||
|
|
||||||
export type ModelToType<TModel extends Model> = {
|
|
||||||
[K in keyof TModel]: FieldToType<TModel[K]>;
|
|
||||||
};
|
|
||||||
@ -1,3 +1 @@
|
|||||||
export * from "./event-store.js";
|
|
||||||
export * from "./state-store.js";
|
|
||||||
export * from "./storage-driver.js";
|
export * from "./storage-driver.js";
|
||||||
|
|||||||
@ -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<TModels extends ModelSchema> {
|
|
||||||
constructor(private driver: StorageDriver) {}
|
|
||||||
|
|
||||||
from<TEntityName extends Keyof<TModels>>(
|
|
||||||
entityName: TEntityName,
|
|
||||||
): StoreQuery<ModelToType<TModels[TEntityName]>> {
|
|
||||||
return new QueryBuilder(this.driver, {
|
|
||||||
from: entityName,
|
|
||||||
}) as StoreQuery<ModelToType<TModels[TEntityName]>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core";
|
|||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
import { ModelSchema } from "../models/model-schema.js";
|
import { ModelSchema } from "../models/model-schema.js";
|
||||||
import { QueryDefinition } from "../query/query.js";
|
import { QueryDefinition } from "../models/query/query.js";
|
||||||
|
|
||||||
export interface StorageDriver {
|
export interface StorageDriver {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,47 +1,60 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { VariantTag } from "@fabric/core";
|
import { Variant, VariantTag } from "@fabric/core";
|
||||||
import { BaseField, FieldDefinition, Model } from "@fabric/domain";
|
import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
|
||||||
|
|
||||||
type FieldMap = {
|
type FieldMap = {
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
[K in FieldDefinition[VariantTag]]: (
|
||||||
|
name: string,
|
||||||
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
||||||
) => string;
|
) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldMap: FieldMap = {
|
const FieldMap: FieldMap = {
|
||||||
StringField: (f) => {
|
StringField: (n, f) => {
|
||||||
return "TEXT" + modifiersFromOpts(f);
|
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
UUIDField: (f) => {
|
UUIDField: (n, f) => {
|
||||||
return [
|
return [
|
||||||
|
n,
|
||||||
"TEXT",
|
"TEXT",
|
||||||
f.isPrimaryKey ? "PRIMARY KEY" : "",
|
f.isPrimaryKey ? "PRIMARY KEY" : "",
|
||||||
modifiersFromOpts(f),
|
modifiersFromOpts(f),
|
||||||
].join(" ");
|
].join(" ");
|
||||||
},
|
},
|
||||||
IntegerField: function (): string {
|
IntegerField: function (n, f): string {
|
||||||
throw new Error("Function not implemented.");
|
return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
ReferenceField: function (): string {
|
ReferenceField: function (n, f): string {
|
||||||
throw new Error("Function not implemented.");
|
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 [
|
return [
|
||||||
!options.isOptional ? "NOT NULL" : "",
|
!field.isOptional ? "NOT NULL" : "",
|
||||||
options.isUnique ? "UNIQUE" : "",
|
field.isUnique ? "UNIQUE" : "",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldDefinitionToSQL(field: FieldDefinition) {
|
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||||
return FieldMap[field[VariantTag]](field as any);
|
return FieldMap[field[VariantTag]](name, field as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function modelToSql(
|
export function modelToSql(
|
||||||
model: Model<string, Record<string, FieldDefinition>>,
|
model: Model<string, Record<string, FieldDefinition>>,
|
||||||
) {
|
) {
|
||||||
return Object.entries(model.fields)
|
const fields = Object.entries(model.fields)
|
||||||
.map(([name, type]) => `${name} ${fieldDefinitionToSQL(type)}`)
|
.map(([name, type]) => fieldDefinitionToSQL(name, type))
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
|
return `CREATE TABLE ${model.name} (${fields})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||||
import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
||||||
|
|
||||||
describe("SQLite Store Driver", () => {
|
describe("SQLite Store Driver", () => {
|
||||||
const model = createModel({
|
const schema = {
|
||||||
name: "test",
|
users: defineModel("users", {
|
||||||
fields: {
|
|
||||||
id: Field.uuid({}),
|
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
},
|
}),
|
||||||
});
|
};
|
||||||
|
|
||||||
let store: SQLiteStorageDriver;
|
let store: SQLiteStorageDriver;
|
||||||
|
|
||||||
@ -23,71 +22,115 @@ describe("SQLite Store Driver", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to synchronize the store and insert a record", async () => {
|
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;
|
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 () => {
|
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;
|
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 () => {
|
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;
|
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([]);
|
expect(records).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to select records", async () => {
|
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;
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
await store.insert("users", {
|
||||||
await store.insert("test", { id: "2", name: "test" });
|
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([
|
expect(records).toEqual([
|
||||||
{ id: "1", name: "test" },
|
{ id: "1", name: "test", streamId: "1", streamVersion: 1 },
|
||||||
{ id: "2", name: "test" },
|
{ id: "2", name: "test", streamId: "2", streamVersion: 1 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should be able to select one record", async () => {
|
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;
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
await store.insert("users", {
|
||||||
await store.insert("test", { id: "2", name: "test" });
|
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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { unlink } from "fs/promises";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CircularDependencyError,
|
CircularDependencyError,
|
||||||
|
ModelSchema,
|
||||||
QueryDefinition,
|
QueryDefinition,
|
||||||
StorageDriver,
|
StorageDriver,
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
@ -35,6 +36,7 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
|
|
||||||
// Enable Write-Ahead Logging, which is faster and more reliable.
|
// 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;");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -109,13 +111,13 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
* Sincronice the store with the schema.
|
* Sincronice the store with the schema.
|
||||||
*/
|
*/
|
||||||
async sync(
|
async sync(
|
||||||
schema: ModelDefinition[],
|
schema: ModelSchema,
|
||||||
): AsyncResult<void, StoreQueryError | CircularDependencyError> {
|
): AsyncResult<void, StoreQueryError | CircularDependencyError> {
|
||||||
try {
|
try {
|
||||||
await dbRun(this.db, "BEGIN TRANSACTION;");
|
await dbRun(this.db, "BEGIN TRANSACTION;");
|
||||||
for (const model of schema) {
|
for (const modelKey in schema) {
|
||||||
const query = `CREATE TABLE ${model.name} (${modelToSql(model)});`;
|
const model = schema[modelKey];
|
||||||
await dbRun(this.db, query);
|
await dbRun(this.db, modelToSql(model));
|
||||||
}
|
}
|
||||||
await dbRun(this.db, "COMMIT;");
|
await dbRun(this.db, "COMMIT;");
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user