Compare commits

..

2 Commits

23 changed files with 312 additions and 144 deletions

View File

@ -1,32 +0,0 @@
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;
}

View File

@ -1,9 +1,21 @@
import { UUID } from "../../types/uuid.js"; import { UUID } from "../../types/uuid.js";
import { IntegerField } from "./integer.js";
import { StringField } from "./string-field.js"; import { StringField } from "./string-field.js";
import { UUIDField } from "./uuid-field.js"; import { UUIDField } from "./uuid-field.js";
/**
* Converts a field definition to its corresponding TypeScript type.
*/
export type FieldToType<TField> = TField extends StringField export type FieldToType<TField> = TField extends StringField
? string ? ToOptional<TField, string>
: TField extends UUIDField : TField extends UUIDField
? UUID ? ToOptional<TField, UUID>
: TField extends IntegerField
? TField["hasArbitraryPrecision"] extends true
? ToOptional<TField, bigint>
: ToOptional<TField, number>
: never; : never;
type ToOptional<TField, TType> = TField extends { isOptional: true }
? TType | null
: TType;

View File

@ -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 { 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 type FieldDefinition = StringField | UUIDField; export type FieldDefinition =
| StringField
| UUIDField
| IntegerField
| ReferenceField;
export namespace Field { export namespace Field {
export const string = createStringField; export const string = createStringField;
export const uuid = createUUIDField; export const uuid = createUUIDField;
export const integer = createIntegerField;
export const reference = createReferenceField;
} }

View File

@ -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<T extends IntegerFieldOptions>(
opts: T = {} as T,
): IntegerField & T {
return {
[VariantTag]: "IntegerField",
...opts,
} as const;
}

View File

@ -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.",
);
});
});

View File

@ -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<T extends ReferenceFieldOptions>(
opts: T = {} as T,
): ReferenceField & T {
return {
[VariantTag]: "ReferenceField",
...opts,
} as const;
}
export function validateReferenceField(
schema: ModelSchema,
modelName: string,
fieldName: string,
field: ReferenceField,
): Result<void, InvalidReferenceField> {
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}`;
}
}

View File

@ -1,15 +1,19 @@
import { TaggedVariant, VariantTag } from "../../../variant/variant.js"; import { TaggedVariant, VariantTag } from "../../../variant/variant.js";
import { BaseField } from "./base-field.js"; import { BaseField } from "./base-field.js";
export interface UUIDOptions extends BaseField { export interface UUIDFieldOptions extends BaseField {
isPrimaryKey?: boolean; 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<T extends UUIDFieldOptions>(
opts: T = {} as T,
): UUIDField & T {
return { return {
[VariantTag]: "UUIDField", [VariantTag]: "UUIDField",
...opts, ...opts,
}; } as const;
} }

View File

@ -1,4 +1,4 @@
export * from "./create-model.js";
export * from "./fields/index.js"; export * from "./fields/index.js";
export * from "./model-to-type.js"; export * from "./model-schema.js";
export * from "./relations/index.js"; export * from "./model.js";
export * from "./types/index.js";

View File

@ -0,0 +1,3 @@
import { Model } from "./model.js";
export type ModelSchema = Record<string, Model>;

View File

@ -1,6 +0,0 @@
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]>;
};

View File

@ -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<typeof User>;
expectTypeOf<User>().toEqualTypeOf<{
id: UUID;
streamId: UUID;
streamVersion: bigint;
name: string;
password: string;
phone: string | null;
}>();
});
});

View File

@ -0,0 +1,23 @@
import { Field, FieldDefinition } from "./fields/index.js";
export type CustomModelFields = Record<string, FieldDefinition>;
export const DefaultModelFields = {
id: Field.uuid({ isPrimaryKey: true }),
streamId: Field.uuid({ isIndexed: true }),
streamVersion: Field.integer({
isUnsigned: true,
hasArbitraryPrecision: true,
}),
};
export type Model<TFields extends CustomModelFields = CustomModelFields> =
typeof DefaultModelFields & TFields;
export function defineModel<TFields extends CustomModelFields>(
fields: TFields,
): Model<TFields> {
return {
...fields,
...DefaultModelFields,
} as const;
}

View File

@ -1,5 +0,0 @@
import { OneToOneRelation } from "./one-to-one.js";
export type RelationDefinition = OneToOneRelation<string, string>;
export namespace Relations {}

View File

@ -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<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,
};
}

View File

@ -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<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,
};
}

View File

@ -0,0 +1,2 @@
export * from "./model-field-names.js";
export * from "./model-to-type.js";

View File

@ -0,0 +1,3 @@
import { CustomModelFields } from "../model.js";
export type ModelFieldNames<TModel extends CustomModelFields> = keyof TModel;

View File

@ -0,0 +1,6 @@
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]>;
};

View File

@ -1,9 +1,5 @@
import { import { ModelSchema } from "../../domain/index.js";
ModelDefinition, import { ModelToType } from "../../domain/models/types/model-to-type.js";
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 { AsyncResult } from "../../result/async-result.js";
import { Keyof } from "../../types/index.js"; import { Keyof } from "../../types/index.js";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
@ -19,9 +15,9 @@ import {
} from "./query.js"; } from "./query.js";
export class QueryBuilder< export class QueryBuilder<
TModels extends ModelDefinition, TModels extends ModelSchema,
TEntityName extends ModelName<TModels>, TEntityName extends Keyof<TModels>,
T = ModelToType<ModelFromName<TModels, TEntityName>>, T = ModelToType<TModels[TEntityName]>,
> implements StoreQuery<T> > implements StoreQuery<T>
{ {
constructor( constructor(

View File

@ -1,16 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { ModelSchema } from "../domain/index.js";
import { import { ModelToType } from "../domain/models/types/model-to-type.js";
ModelDefinition, import { Keyof } from "../types/keyof.js";
ModelFromName,
ModelName,
} from "../domain/models/create-model.js";
import { ModelToType } from "../domain/models/model-to-type.js";
import { StoreQuery } from "./query/query.js"; import { StoreQuery } from "./query/query.js";
export interface StateStore< export interface StateStore<TModels extends ModelSchema> {
TModels extends ModelDefinition<string, Record<string, any>>, from<TEntityName extends Keyof<TModels>>(
> {
from<TEntityName extends ModelName<TModels>>(
entityName: TEntityName, entityName: TEntityName,
): StoreQuery<ModelToType<ModelFromName<TModels, TEntityName>>>; ): StoreQuery<ModelToType<TModels[TEntityName]>>;
} }

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 { UnexpectedError } from "../error/unexpected-error.js";
import { AsyncResult } from "../result/async-result.js"; import { AsyncResult } from "../result/async-result.js";
import { CircularDependencyError } from "./errors/circular-dependency-error.js"; import { CircularDependencyError } from "./errors/circular-dependency-error.js";
@ -30,7 +30,7 @@ export interface StorageDriver {
* Sincronice the store with the schema. * Sincronice the store with the schema.
*/ */
sync( sync(
schema: ModelDefinition[], schema: ModelSchema,
): AsyncResult<void, StoreQueryError | CircularDependencyError>; ): AsyncResult<void, StoreQueryError | CircularDependencyError>;
/** /**

View File

@ -0,0 +1 @@
export * from "./sqlite-driver.js";