Format files and configure workspace formatting in vscode

This commit is contained in:
Pablo Baleztena 2024-10-16 01:53:38 -03:00
parent a79683d9d4
commit 4cc99aec91
45 changed files with 230 additions and 204 deletions

View File

@ -17,5 +17,8 @@
}, },
"typescript.preferences.importModuleSpecifierEnding": "js", "typescript.preferences.importModuleSpecifierEnding": "js",
"cSpell.words": ["autodocs", "Syntropy"], "cSpell.words": ["autodocs", "Syntropy"],
"typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"] "typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"],
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
} }

View File

@ -1,17 +1,17 @@
/** /**
* Get the element type of an array. * Get the element type of an array.
*/ */
export type ArrayElement<T extends readonly unknown[]> = export type ArrayElement<T extends readonly unknown[]> = T extends
T extends readonly (infer U)[] ? U : never; readonly (infer U)[] ? U : never;
/** /**
* Get the first element type of a tuple. * Get the first element type of a tuple.
*/ */
export type TupleFirstElement<T extends readonly unknown[]> = export type TupleFirstElement<T extends readonly unknown[]> = T extends
T extends readonly [infer U, ...unknown[]] ? U : never; readonly [infer U, ...unknown[]] ? U : never;
/** /**
* Get the LAST element type of a tuple. * Get the LAST element type of a tuple.
*/ */
export type TupleLastElement<T extends readonly unknown[]> = export type TupleLastElement<T extends readonly unknown[]> = T extends
T extends readonly [...unknown[], infer U] ? U : never; readonly [...unknown[], infer U] ? U : never;

View File

@ -3,10 +3,8 @@ import { type TaggedVariant, VariantTag } from "../variant/index.ts";
/** /**
* A TaggedError is a tagged variant with an error message. * A TaggedError is a tagged variant with an error message.
*/ */
export abstract class TaggedError<Tag extends string = string> export abstract class TaggedError<Tag extends string = string> extends Error
extends Error implements TaggedVariant<Tag> {
implements TaggedVariant<Tag>
{
readonly [VariantTag]: Tag; readonly [VariantTag]: Tag;
constructor(tag: Tag, message?: string) { constructor(tag: Tag, message?: string) {

View File

@ -10,13 +10,13 @@ import { Result } from "./result.ts";
*/ */
export type AsyncResult< export type AsyncResult<
TValue = any, TValue = any,
TError extends TaggedError = never TError extends TaggedError = never,
> = Promise<Result<TValue, TError>>; > = Promise<Result<TValue, TError>>;
export namespace AsyncResult { export namespace AsyncResult {
export async function tryFrom<T, TError extends TaggedError>( export async function tryFrom<T, TError extends TaggedError>(
fn: () => MaybePromise<T>, fn: () => MaybePromise<T>,
errorMapper: (error: any) => TError errorMapper: (error: any) => TError,
): AsyncResult<T, TError> { ): AsyncResult<T, TError> {
try { try {
return Result.succeedWith(await fn()); return Result.succeedWith(await fn());

View File

@ -24,7 +24,7 @@ export class Result<TValue, TError extends TaggedError = never> {
static tryFrom<T, TError extends TaggedError>( static tryFrom<T, TError extends TaggedError>(
fn: () => T, fn: () => T,
errorMapper: (error: any) => TError errorMapper: (error: any) => TError,
): Result<T, TError> { ): Result<T, TError> {
try { try {
return Result.succeedWith(fn()); return Result.succeedWith(fn());
@ -83,7 +83,7 @@ export class Result<TValue, TError extends TaggedError = never> {
* Map a function over the value of the result. * Map a function over the value of the result.
*/ */
map<TMappedValue>( map<TMappedValue>(
fn: (value: TValue) => TMappedValue fn: (value: TValue) => TMappedValue,
): Result<TMappedValue, TError> { ): Result<TMappedValue, TError> {
if (!isError(this.value)) { if (!isError(this.value)) {
return Result.succeedWith(fn(this.value as TValue)); return Result.succeedWith(fn(this.value as TValue));
@ -96,7 +96,7 @@ export class Result<TValue, TError extends TaggedError = never> {
* Maps a function over the value of the result and flattens the result. * Maps a function over the value of the result and flattens the result.
*/ */
flatMap<TMappedValue, TMappedError extends TaggedError>( flatMap<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => Result<TMappedValue, TMappedError> fn: (value: TValue) => Result<TMappedValue, TMappedError>,
): Result<TMappedValue, TError | TMappedError> { ): Result<TMappedValue, TError | TMappedError> {
if (!isError(this.value)) { if (!isError(this.value)) {
return fn(this.value as TValue) as any; return fn(this.value as TValue) as any;
@ -111,7 +111,7 @@ export class Result<TValue, TError extends TaggedError = never> {
*/ */
tryMap<TMappedValue>( tryMap<TMappedValue>(
fn: (value: TValue) => TMappedValue, fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError errMapper: (error: any) => TError,
): Result<TMappedValue, TError> { ): Result<TMappedValue, TError> {
if (!isError(this.value)) { if (!isError(this.value)) {
try { try {
@ -128,7 +128,7 @@ export class Result<TValue, TError extends TaggedError = never> {
* Map a function over the error of the result. * Map a function over the error of the result.
*/ */
mapError<TMappedError extends TaggedError>( mapError<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError fn: (error: TError) => TMappedError,
): Result<TValue, TMappedError> { ): Result<TValue, TMappedError> {
if (isError(this.value)) { if (isError(this.value)) {
return Result.failWith(fn(this.value as TError)); return Result.failWith(fn(this.value as TError));

View File

@ -10,7 +10,7 @@ describe("Run", () => {
const result = await Run.seq( const result = await Run.seq(
async () => Result.succeedWith(1), async () => Result.succeedWith(1),
async (x) => Result.succeedWith(x + 1), async (x) => Result.succeedWith(x + 1),
async (x) => Result.succeedWith(x * 2) async (x) => Result.succeedWith(x * 2),
); );
expect(result.unwrapOrThrow()).toEqual(4); expect(result.unwrapOrThrow()).toEqual(4);
@ -20,7 +20,7 @@ describe("Run", () => {
const result = await Run.seq( const result = await Run.seq(
async () => Result.succeedWith(1), async () => Result.succeedWith(1),
async () => Result.failWith(new UnexpectedError()), async () => Result.failWith(new UnexpectedError()),
async (x) => Result.succeedWith(x * 2) async (x) => Result.succeedWith(x * 2),
); );
expect(result.isError()).toBe(true); expect(result.isError()).toBe(true);

View File

@ -5,17 +5,22 @@ import type { AsyncResult } from "../result/async-result.ts";
export namespace Run { export namespace Run {
// prettier-ignore // prettier-ignore
export async function seq< export async function seq<
T1, TE1 extends TaggedError, T1,
T2, TE2 extends TaggedError, TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
>( >(
fn1: () => AsyncResult<T1, TE1>, fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>, fn2: (value: T1) => AsyncResult<T2, TE2>,
): AsyncResult<T2, TE1 | TE2>; ): AsyncResult<T2, TE1 | TE2>;
// prettier-ignore // prettier-ignore
export async function seq< export async function seq<
T1, TE1 extends TaggedError, T1,
T2, TE2 extends TaggedError, TE1 extends TaggedError,
T3, TE3 extends TaggedError, T2,
TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
>( >(
fn1: () => AsyncResult<T1, TE1>, fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>, fn2: (value: T1) => AsyncResult<T2, TE2>,
@ -23,10 +28,14 @@ export namespace Run {
): AsyncResult<T3, TE1 | TE2 | TE3>; ): AsyncResult<T3, TE1 | TE2 | TE3>;
// prettier-ignore // prettier-ignore
export async function seq< export async function seq<
T1, TE1 extends TaggedError, T1,
T2, TE2 extends TaggedError, TE1 extends TaggedError,
T3, TE3 extends TaggedError, T2,
T4, TE4 extends TaggedError, TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
T4,
TE4 extends TaggedError,
>( >(
fn1: () => AsyncResult<T1, TE1>, fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>, fn2: (value: T1) => AsyncResult<T2, TE2>,
@ -51,17 +60,22 @@ export namespace Run {
// prettier-ignore // prettier-ignore
export async function seqUNSAFE< export async function seqUNSAFE<
T1, TE1 extends TaggedError, T1,
T2, TE2 extends TaggedError, TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
>( >(
fn1: () => AsyncResult<T1, TE1>, fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>, fn2: (value: T1) => AsyncResult<T2, TE2>,
): Promise<T2>; ): Promise<T2>;
// prettier-ignore // prettier-ignore
export async function seqUNSAFE< export async function seqUNSAFE<
T1,TE1 extends TaggedError, T1,
T2,TE2 extends TaggedError, TE1 extends TaggedError,
T3,TE3 extends TaggedError, T2,
TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
>( >(
fn1: () => AsyncResult<T1, TE1>, fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>, fn2: (value: T1) => AsyncResult<T2, TE2>,
@ -80,7 +94,7 @@ export namespace Run {
} }
export async function UNSAFE<T, TError extends TaggedError>( export async function UNSAFE<T, TError extends TaggedError>(
fn: () => AsyncResult<T, TError> fn: () => AsyncResult<T, TError>,
): Promise<T> { ): Promise<T> {
return (await fn()).unwrapOrThrow(); return (await fn()).unwrapOrThrow();
} }

View File

@ -22,8 +22,9 @@ export class PosixDate {
"timestamp" in value && "timestamp" in value &&
value["type"] === "posix-date" && value["type"] === "posix-date" &&
typeof value["timestamp"] === "number" typeof value["timestamp"] === "number"
) ) {
return true; return true;
}
return false; return false;
} }
} }

View File

@ -31,5 +31,5 @@ Deno.test(
V2: (v) => v.b, V2: (v) => v.b,
}) })
).toThrow("Non-exhaustive pattern match"); ).toThrow("Non-exhaustive pattern match");
} },
); );

View File

@ -1,12 +1,16 @@
import type { Fn } from "../types/fn.ts"; import type { Fn } from "../types/fn.ts";
import { type TaggedVariant, type VariantFromTag, VariantTag } from "./variant.ts"; import {
type TaggedVariant,
type VariantFromTag,
VariantTag,
} from "./variant.ts";
export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = { export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = {
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>; [K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>;
}; };
export function match<const TVariant extends TaggedVariant<string>>( export function match<const TVariant extends TaggedVariant<string>>(
v: TVariant v: TVariant,
) { ) {
return { return {
case< case<
@ -14,14 +18,14 @@ export function match<const TVariant extends TaggedVariant<string>>(
const TMatcher extends VariantMatcher< const TMatcher extends VariantMatcher<
TVariant, TVariant,
TReturnType TReturnType
> = VariantMatcher<TVariant, TReturnType> > = VariantMatcher<TVariant, TReturnType>,
>(cases: TMatcher): TReturnType { >(cases: TMatcher): TReturnType {
if (!(v[VariantTag] in cases)) { if (!(v[VariantTag] in cases)) {
throw new Error("Non-exhaustive pattern match"); throw new Error("Non-exhaustive pattern match");
} }
return cases[v[VariantTag] as TVariant[VariantTag]]( return cases[v[VariantTag] as TVariant[VariantTag]](
v as Extract<TVariant, { [VariantTag]: TVariant[VariantTag] }> v as Extract<TVariant, { [VariantTag]: TVariant[VariantTag] }>,
); );
}, },
}; };

View File

@ -1,6 +1,7 @@
import { TaggedError } from "@fabric/core"; import { TaggedError } from "@fabric/core";
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> { export class CircularDependencyError
extends TaggedError<"CircularDependencyError"> {
context: { key: string; dep: string }; context: { key: string; dep: string };
constructor(key: string, dep: string) { constructor(key: string, dep: string) {
super("CircularDependencyError"); super("CircularDependencyError");

View File

@ -15,21 +15,21 @@ export interface EventStore<TEvents extends Event> {
* Store a new event in the event store. * Store a new event in the event store.
*/ */
append<T extends TEvents>( append<T extends TEvents>(
event: T event: T,
): AsyncResult<StoredEvent<T>, StoreQueryError>; ): AsyncResult<StoredEvent<T>, StoreQueryError>;
getEventsFromStream( getEventsFromStream(
streamId: UUID streamId: UUID,
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>; ): AsyncResult<StoredEvent<TEvents>[], StoreQueryError>;
subscribe<TEventKey extends TEvents[VariantTag]>( subscribe<TEventKey extends TEvents[VariantTag]>(
events: TEventKey[], events: TEventKey[],
subscriber: EventSubscriber<VariantFromTag<TEvents, TEventKey>> subscriber: EventSubscriber<VariantFromTag<TEvents, TEventKey>>,
): void; ): void;
} }
export type EventSubscriber<TEvents extends Event = Event> = ( export type EventSubscriber<TEvents extends Event = Event> = (
event: StoredEvent<TEvents> event: StoredEvent<TEvents>,
) => MaybePromise<void>; ) => MaybePromise<void>;
export interface EventFilterOptions { export interface EventFilterOptions {

View File

@ -14,5 +14,5 @@ export interface Event<TTag extends string = string, TPayload = any> {
export type EventFromKey< export type EventFromKey<
TEvents extends Event, TEvents extends Event,
TKey extends TEvents[VariantTag] TKey extends TEvents[VariantTag],
> = Extract<TEvents, { [VariantTag]: TKey }>; > = Extract<TEvents, { [VariantTag]: TKey }>;

View File

@ -5,7 +5,7 @@ import type { MimeType } from "./mime-type.ts";
*/ */
export function isMimeType<T extends MimeType>( export function isMimeType<T extends MimeType>(
expectedMimeType: T, expectedMimeType: T,
actualMimeType: string actualMimeType: string,
): actualMimeType is T { ): actualMimeType is T {
return actualMimeType.match("^" + expectedMimeType + "$") !== null; return actualMimeType.match("^" + expectedMimeType + "$") !== null;
} }

View File

@ -8,11 +8,10 @@ export interface DecimalFieldOptions extends BaseField {
} }
export interface DecimalField export interface DecimalField
extends TaggedVariant<"DecimalField">, extends TaggedVariant<"DecimalField">, DecimalFieldOptions {}
DecimalFieldOptions {}
export function createDecimalField<T extends DecimalFieldOptions>( export function createDecimalField<T extends DecimalFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): DecimalField & T { ): DecimalField & T {
return { return {
[VariantTag]: "DecimalField", [VariantTag]: "DecimalField",

View File

@ -5,12 +5,11 @@ import type { BaseField } from "./base-field.ts";
export interface EmbeddedFieldOptions<T = any> extends BaseField {} export interface EmbeddedFieldOptions<T = any> extends BaseField {}
export interface EmbeddedField<T = any> export interface EmbeddedField<T = any>
extends TaggedVariant<"EmbeddedField">, extends TaggedVariant<"EmbeddedField">, EmbeddedFieldOptions<T> {}
EmbeddedFieldOptions<T> {}
export function createEmbeddedField< export function createEmbeddedField<
K = any, K = any,
T extends EmbeddedFieldOptions<K> = EmbeddedFieldOptions<K> T extends EmbeddedFieldOptions<K> = EmbeddedFieldOptions<K>,
>(opts: T = {} as T): EmbeddedField & T { >(opts: T = {} as T): EmbeddedField & T {
return { return {
[VariantTag]: "EmbeddedField", [VariantTag]: "EmbeddedField",

View File

@ -14,23 +14,24 @@ import type { UUIDField } from "./uuid-field.ts";
* Converts a field definition to its corresponding TypeScript type. * Converts a field definition to its corresponding TypeScript type.
*/ */
//prettier-ignore //prettier-ignore
export type FieldToType<TField> = export type FieldToType<TField> = TField extends StringField
TField extends StringField ? MaybeOptional<TField, string> ? MaybeOptional<TField, string>
: TField extends UUIDField ? MaybeOptional<TField, UUID> : TField extends UUIDField ? MaybeOptional<TField, UUID>
: TField extends IntegerField ? IntegerFieldToType<TField> : TField extends IntegerField ? IntegerFieldToType<TField>
: TField extends ReferenceField ? MaybeOptional<TField, UUID> : TField extends ReferenceField ? MaybeOptional<TField, UUID>
: TField extends DecimalField ? MaybeOptional<TField, Decimal> : TField extends DecimalField ? MaybeOptional<TField, Decimal>
: TField extends FloatField ? MaybeOptional<TField, number> : TField extends FloatField ? MaybeOptional<TField, number>
: TField extends TimestampField ? MaybeOptional<TField, PosixDate> : TField extends TimestampField ? MaybeOptional<TField, PosixDate>
: TField extends EmbeddedField<infer TSubModel> ? MaybeOptional<TField, TSubModel> : TField extends EmbeddedField<infer TSubModel>
? MaybeOptional<TField, TSubModel>
: never; : never;
//prettier-ignore //prettier-ignore
type IntegerFieldToType<TField extends IntegerField> = TField["hasArbitraryPrecision"] extends true type IntegerFieldToType<TField extends IntegerField> =
? MaybeOptional<TField, bigint> TField["hasArbitraryPrecision"] extends true ? MaybeOptional<TField, bigint>
: TField["hasArbitraryPrecision"] extends false : TField["hasArbitraryPrecision"] extends false
? MaybeOptional<TField, number> ? MaybeOptional<TField, number>
: MaybeOptional<TField, number | bigint>; : MaybeOptional<TField, number | bigint>;
type MaybeOptional<TField, TType> = TField extends { isOptional: true } type MaybeOptional<TField, TType> = TField extends { isOptional: true }
? TType | null ? TType | null

View File

@ -4,11 +4,10 @@ import type { BaseField } from "./base-field.ts";
export interface FloatFieldOptions extends BaseField {} export interface FloatFieldOptions extends BaseField {}
export interface FloatField export interface FloatField
extends TaggedVariant<"FloatField">, extends TaggedVariant<"FloatField">, FloatFieldOptions {}
FloatFieldOptions {}
export function createFloatField<T extends FloatFieldOptions>( export function createFloatField<T extends FloatFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): FloatField & T { ): FloatField & T {
return { return {
[VariantTag]: "FloatField", [VariantTag]: "FloatField",

View File

@ -7,11 +7,10 @@ export interface IntegerFieldOptions extends BaseField {
} }
export interface IntegerField export interface IntegerField
extends TaggedVariant<"IntegerField">, extends TaggedVariant<"IntegerField">, IntegerFieldOptions {}
IntegerFieldOptions {}
export function createIntegerField<T extends IntegerFieldOptions>( export function createIntegerField<T extends IntegerFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): IntegerField & T { ): IntegerField & T {
return { return {
[VariantTag]: "IntegerField", [VariantTag]: "IntegerField",

View File

@ -25,7 +25,7 @@ describe("Validate Reference Field", () => {
schema, schema,
Field.reference({ Field.reference({
targetModel: "foo", targetModel: "foo",
}) }),
).unwrapErrorOrThrow(); ).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError); expect(result).toBeInstanceOf(InvalidReferenceFieldError);
@ -36,7 +36,7 @@ describe("Validate Reference Field", () => {
schema, schema,
Field.reference({ Field.reference({
targetModel: "User", targetModel: "User",
}) }),
).unwrapOrThrow(); ).unwrapOrThrow();
}); });
@ -46,7 +46,7 @@ describe("Validate Reference Field", () => {
Field.reference({ Field.reference({
targetModel: "User", targetModel: "User",
targetKey: "foo", targetKey: "foo",
}) }),
).unwrapErrorOrThrow(); ).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError); expect(result).toBeInstanceOf(InvalidReferenceFieldError);
@ -58,7 +58,7 @@ describe("Validate Reference Field", () => {
Field.reference({ Field.reference({
targetModel: "User", targetModel: "User",
targetKey: "otherNotUnique", targetKey: "otherNotUnique",
}) }),
).unwrapErrorOrThrow(); ).unwrapErrorOrThrow();
expect(result).toBeInstanceOf(InvalidReferenceFieldError); expect(result).toBeInstanceOf(InvalidReferenceFieldError);
@ -70,7 +70,7 @@ describe("Validate Reference Field", () => {
Field.reference({ Field.reference({
targetModel: "User", targetModel: "User",
targetKey: "otherUnique", targetKey: "otherUnique",
}) }),
); );
if (isError(result)) { if (isError(result)) {

View File

@ -13,11 +13,10 @@ export interface ReferenceFieldOptions extends BaseField {
} }
export interface ReferenceField export interface ReferenceField
extends TaggedVariant<"ReferenceField">, extends TaggedVariant<"ReferenceField">, ReferenceFieldOptions {}
ReferenceFieldOptions {}
export function createReferenceField<T extends ReferenceFieldOptions>( export function createReferenceField<T extends ReferenceFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): ReferenceField & T { ): ReferenceField & T {
return { return {
[VariantTag]: "ReferenceField", [VariantTag]: "ReferenceField",
@ -31,21 +30,21 @@ export function getTargetKey(field: ReferenceField): string {
export function validateReferenceField( export function validateReferenceField(
schema: ModelSchema, schema: ModelSchema,
field: ReferenceField field: ReferenceField,
): Result<void, InvalidReferenceFieldError> { ): Result<void, InvalidReferenceFieldError> {
if (!schema[field.targetModel]) { if (!schema[field.targetModel]) {
return Result.failWith( return Result.failWith(
new InvalidReferenceFieldError( new InvalidReferenceFieldError(
`The target model '${field.targetModel}' is not in the schema.` `The target model '${field.targetModel}' is not in the schema.`,
) ),
); );
} }
if (field.targetKey && !schema[field.targetModel]!.fields[field.targetKey]) { if (field.targetKey && !schema[field.targetModel]!.fields[field.targetKey]) {
return Result.failWith( return Result.failWith(
new InvalidReferenceFieldError( new InvalidReferenceFieldError(
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.` `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
) ),
); );
} }
@ -55,15 +54,16 @@ export function validateReferenceField(
) { ) {
return Result.failWith( return Result.failWith(
new InvalidReferenceFieldError( new InvalidReferenceFieldError(
`The target key '${field.targetModel}'.'${field.targetKey}' is not unique.` `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
) ),
); );
} }
return Result.ok(); return Result.ok();
} }
export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> { export class InvalidReferenceFieldError
extends TaggedError<"InvalidReferenceField"> {
constructor(readonly reason: string) { constructor(readonly reason: string) {
super("InvalidReferenceField"); super("InvalidReferenceField");
} }

View File

@ -7,11 +7,10 @@ export interface StringFieldOptions extends BaseField {
} }
export interface StringField export interface StringField
extends TaggedVariant<"StringField">, extends TaggedVariant<"StringField">, StringFieldOptions {}
StringFieldOptions {}
export function createStringField<T extends StringFieldOptions>( export function createStringField<T extends StringFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): StringField & T { ): StringField & T {
return { return {
[VariantTag]: "StringField", [VariantTag]: "StringField",

View File

@ -4,11 +4,10 @@ import type { BaseField } from "./base-field.ts";
export interface TimestampFieldOptions extends BaseField {} export interface TimestampFieldOptions extends BaseField {}
export interface TimestampField export interface TimestampField
extends TaggedVariant<"TimestampField">, extends TaggedVariant<"TimestampField">, TimestampFieldOptions {}
TimestampFieldOptions {}
export function createTimestampField<T extends TimestampFieldOptions>( export function createTimestampField<T extends TimestampFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): TimestampField & T { ): TimestampField & T {
return { return {
[VariantTag]: "TimestampField", [VariantTag]: "TimestampField",

View File

@ -6,11 +6,10 @@ export interface UUIDFieldOptions extends BaseField {
} }
export interface UUIDField export interface UUIDField
extends TaggedVariant<"UUIDField">, extends TaggedVariant<"UUIDField">, UUIDFieldOptions {}
UUIDFieldOptions {}
export function createUUIDField<T extends UUIDFieldOptions>( export function createUUIDField<T extends UUIDFieldOptions>(
opts: T = {} as T opts: T = {} as T,
): UUIDField & T { ): UUIDField & T {
return { return {
[VariantTag]: "UUIDField", [VariantTag]: "UUIDField",

View File

@ -6,7 +6,7 @@ export type CustomModelFields = Record<string, FieldDefinition>;
export interface Collection< export interface Collection<
TName extends string = string, TName extends string = string,
TFields extends CustomModelFields = CustomModelFields TFields extends CustomModelFields = CustomModelFields,
> { > {
name: TName; name: TName;
fields: TFields; fields: TFields;
@ -24,14 +24,14 @@ export const DefaultModelFields = {
export interface Model< export interface Model<
TName extends string = string, TName extends string = string,
TFields extends CustomModelFields = CustomModelFields TFields extends CustomModelFields = CustomModelFields,
> extends Collection<TName, TFields> { > extends Collection<TName, TFields> {
fields: typeof DefaultModelFields & TFields; fields: typeof DefaultModelFields & TFields;
} }
export function defineModel< export function defineModel<
TName extends string, TName extends string,
TFields extends CustomModelFields TFields extends CustomModelFields,
>(name: TName, fields: TFields): Model<TName, TFields> { >(name: TName, fields: TFields): Model<TName, TFields> {
return { return {
name, name,
@ -41,7 +41,7 @@ export function defineModel<
export function defineCollection< export function defineCollection<
TName extends string, TName extends string,
TFields extends CustomModelFields TFields extends CustomModelFields,
>(name: TName, fields: TFields): Collection<TName, TFields> { >(name: TName, fields: TFields): Collection<TName, TFields> {
return { return {
name, name,

View File

@ -20,11 +20,10 @@ export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type";
export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value"; export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value";
export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator"; export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator";
export type LikeFilterOption<T> = T extends string export type LikeFilterOption<T> = T extends string ? {
? { [FILTER_OPTION_TYPE_SYMBOL]: "like";
[FILTER_OPTION_TYPE_SYMBOL]: "like"; [FILTER_OPTION_VALUE_SYMBOL]: string;
[FILTER_OPTION_VALUE_SYMBOL]: string; }
}
: never; : never;
export interface InFilterOption<T> { export interface InFilterOption<T> {

View File

@ -11,12 +11,12 @@ export interface StoreQuery<T> {
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
} }
@ -26,12 +26,12 @@ export interface StoreSortableQuery<T> {
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
} }
@ -40,24 +40,24 @@ export interface StoreLimitableQuery<T> {
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
} }
export interface SelectableQuery<T> { export interface SelectableQuery<T> {
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
} }

View File

@ -6,7 +6,7 @@ import type { StoreQuery } from "./query/query.ts";
export interface ReadonlyStateStore<TModel extends Model> { export interface ReadonlyStateStore<TModel extends Model> {
from<T extends keyof ModelSchemaFromModels<TModel>>( from<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T collection: T,
): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>>; ): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>>;
} }
@ -14,6 +14,6 @@ export interface WritableStateStore<TModel extends Model>
extends ReadonlyStateStore<TModel> { extends ReadonlyStateStore<TModel> {
insertInto<T extends keyof ModelSchemaFromModels<TModel>>( insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T, collection: T,
record: ModelToType<ModelSchemaFromModels<TModel>[T]> record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
): AsyncResult<void, StoreQueryError>; ): AsyncResult<void, StoreQueryError>;
} }

View File

@ -8,6 +8,6 @@ export interface Projection<TModel extends Model, TEvents extends Event> {
events: TEvents[VariantTag][]; events: TEvents[VariantTag][];
projection: ( projection: (
event: StoredEvent<TEvents>, event: StoredEvent<TEvents>,
model?: ModelToType<TModel> model?: ModelToType<TModel>,
) => ModelToType<TModel>; ) => ModelToType<TModel>;
} }

View File

@ -6,14 +6,14 @@ export type UseCaseDefinition<
TDependencies = any, TDependencies = any,
TPayload = any, TPayload = any,
TOutput = any, TOutput = any,
TErrors extends TaggedError<string> = any TErrors extends TaggedError<string> = any,
> = BasicUseCaseDefinition<TDependencies, TPayload, TOutput, TErrors>; > = BasicUseCaseDefinition<TDependencies, TPayload, TOutput, TErrors>;
interface BasicUseCaseDefinition< interface BasicUseCaseDefinition<
TDependencies, TDependencies,
TPayload, TPayload,
TOutput, TOutput,
TErrors extends TaggedError<string> TErrors extends TaggedError<string>,
> { > {
/** /**
* The use case name. * The use case name.

View File

@ -11,6 +11,6 @@ export type UseCase<
> = TPayload extends undefined > = TPayload extends undefined
? (dependencies: TDependencies) => AsyncResult<TOutput, TErrors> ? (dependencies: TDependencies) => AsyncResult<TOutput, TErrors>
: ( : (
dependencies: TDependencies, dependencies: TDependencies,
payload: TPayload, payload: TPayload,
) => AsyncResult<TOutput, TErrors>; ) => AsyncResult<TOutput, TErrors>;

View File

@ -35,7 +35,7 @@ describe("sortByDependencies", () => {
sortByDependencies(array, { sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}).unwrapErrorOrThrow() }).unwrapErrorOrThrow(),
).toBeInstanceOf(CircularDependencyError); ).toBeInstanceOf(CircularDependencyError);
}); });

View File

@ -9,7 +9,7 @@ export function sortByDependencies<T>(
}: { }: {
keyGetter: (element: T) => string; keyGetter: (element: T) => string;
depGetter: (element: T) => string[]; depGetter: (element: T) => string[];
} },
): Result<T[], CircularDependencyError> { ): Result<T[], CircularDependencyError> {
const graph = new Map<string, string[]>(); const graph = new Map<string, string[]>();
const visited = new Set<string>(); const visited = new Set<string>();
@ -40,9 +40,9 @@ export function sortByDependencies<T>(
visit(key, []); visit(key, []);
}); });
return sorted.map( return sorted.map(
(key) => array.find((element) => keyGetter(element) === key) as T (key) => array.find((element) => keyGetter(element) === key) as T,
); );
}, },
(e) => e as CircularDependencyError (e) => e as CircularDependencyError,
); );
} }

View File

@ -18,8 +18,7 @@ import {
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts"; import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
export class SQLiteEventStore<TEvents extends Event> export class SQLiteEventStore<TEvents extends Event>
implements EventStore<TEvents> implements EventStore<TEvents> {
{
private db: SQLiteDatabase; private db: SQLiteDatabase;
private streamVersions = new Map<UUID, bigint>(); private streamVersions = new Map<UUID, bigint>();
@ -46,15 +45,15 @@ export class SQLiteEventStore<TEvents extends Event>
timestamp NUMERIC NOT NULL, timestamp NUMERIC NOT NULL,
payload TEXT NOT NULL, payload TEXT NOT NULL,
UNIQUE(streamId, version) UNIQUE(streamId, version)
)` )`,
); );
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
getEventsFromStream( getEventsFromStream(
streamId: UUID streamId: UUID,
): AsyncResult<StoredEvent<TEvents>[], StoreQueryError> { ): AsyncResult<StoredEvent<TEvents>[], StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
() => { () => {
@ -70,16 +69,16 @@ export class SQLiteEventStore<TEvents extends Event>
version: BigInt(e.version), version: BigInt(e.version),
timestamp: new PosixDate(e.timestamp), timestamp: new PosixDate(e.timestamp),
payload: JSONUtils.parse(e.payload), payload: JSONUtils.parse(e.payload),
}) }),
); );
return events; return events;
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
append<T extends TEvents>( append<T extends TEvents>(
event: T event: T,
): AsyncResult<StoredEvent<T>, StoreQueryError> { ): AsyncResult<StoredEvent<T>, StoreQueryError> {
return Run.seq( return Run.seq(
() => this.getLastVersion(event.streamId), () => this.getLastVersion(event.streamId),
@ -93,7 +92,7 @@ export class SQLiteEventStore<TEvents extends Event>
AsyncResult.from(async () => { AsyncResult.from(async () => {
await this.notifySubscribers(storedEvent); await this.notifySubscribers(storedEvent);
return storedEvent; return storedEvent;
}) }),
); );
} }
@ -111,20 +110,20 @@ export class SQLiteEventStore<TEvents extends Event>
`SELECT max(version) as lastVersion FROM events WHERE streamId = $id`, `SELECT max(version) as lastVersion FROM events WHERE streamId = $id`,
{ {
$id: streamId, $id: streamId,
} },
); );
return !lastVersion ? 0n : BigInt(lastVersion); return !lastVersion ? 0n : BigInt(lastVersion);
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
subscribe<TEventKey extends TEvents[VariantTag]>( subscribe<TEventKey extends TEvents[VariantTag]>(
eventNames: TEventKey[], eventNames: TEventKey[],
subscriber: ( subscriber: (
event: StoredEvent<EventFromKey<TEvents, TEventKey>> event: StoredEvent<EventFromKey<TEvents, TEventKey>>,
) => MaybePromise<void> ) => MaybePromise<void>,
): void { ): void {
eventNames.forEach((event) => { eventNames.forEach((event) => {
const subscribers = this.eventSubscribers.get(event) || []; const subscribers = this.eventSubscribers.get(event) || [];
@ -139,14 +138,14 @@ export class SQLiteEventStore<TEvents extends Event>
close(): AsyncResult<void, StoreQueryError> { close(): AsyncResult<void, StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
() => this.db.close(), () => this.db.close(),
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
private storeEvent<T extends Event>( private storeEvent<T extends Event>(
streamId: UUID, streamId: UUID,
version: bigint, version: bigint,
event: T event: T,
): AsyncResult<StoredEvent<T>, StoreQueryError> { ): AsyncResult<StoredEvent<T>, StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
() => { () => {
@ -165,11 +164,11 @@ export class SQLiteEventStore<TEvents extends Event>
$version: storedEvent.version.toString(), $version: storedEvent.version.toString(),
$timestamp: storedEvent.timestamp.timestamp, $timestamp: storedEvent.timestamp.timestamp,
$payload: JSON.stringify(storedEvent.payload), $payload: JSON.stringify(storedEvent.payload),
} },
); );
return storedEvent; return storedEvent;
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
} }

View File

@ -135,7 +135,7 @@ describe("SQL where clause from filter options", () => {
const result = filterToSQL(opts); const result = filterToSQL(opts);
const params = filterToParams(col, opts); const params = filterToParams(col, opts);
expect(result).toEqual( expect(result).toEqual(
"WHERE (name IN ($where_name_0_0,$where_name_0_1) AND age > $where_age_0) OR (status <> $where_status_1 AND salary > $where_salary_1) OR (rating < $where_rating_2 AND quantity >= $where_quantity_2)" "WHERE (name IN ($where_name_0_0,$where_name_0_1) AND age > $where_age_0) OR (status <> $where_status_1 AND salary > $where_salary_1) OR (rating < $where_rating_2 AND quantity >= $where_quantity_2)",
); );
expect(params).toEqual({ expect(params).toEqual({
$where_name_0_0: "John", $where_name_0_0: "John",

View File

@ -16,20 +16,22 @@ import { fieldValueToSQL } from "./value-to-sql.ts";
export function filterToSQL(filterOptions?: FilterOptions) { export function filterToSQL(filterOptions?: FilterOptions) {
if (!filterOptions) return ""; if (!filterOptions) return "";
if (Array.isArray(filterOptions)) if (Array.isArray(filterOptions)) {
return `WHERE ${getWhereFromMultiOption(filterOptions)}`; return `WHERE ${getWhereFromMultiOption(filterOptions)}`;
}
return `WHERE ${getWhereFromSingleOption(filterOptions)}`; return `WHERE ${getWhereFromSingleOption(filterOptions)}`;
} }
export function filterToParams( export function filterToParams(
collection: Collection, collection: Collection,
filterOptions?: FilterOptions filterOptions?: FilterOptions,
) { ) {
if (!filterOptions) return {}; if (!filterOptions) return {};
if (Array.isArray(filterOptions)) if (Array.isArray(filterOptions)) {
return getParamsFromMultiFilterOption(collection, filterOptions); return getParamsFromMultiFilterOption(collection, filterOptions);
}
return getParamsFromSingleFilterOption(collection, filterOptions); return getParamsFromSingleFilterOption(collection, filterOptions);
} }
@ -38,14 +40,14 @@ function getWhereFromMultiOption(filterOptions: MultiFilterOption) {
return filterOptions return filterOptions
.map( .map(
(option, i) => (option, i) =>
`(${getWhereFromSingleOption(option, { postfix: `_${i}` })})` `(${getWhereFromSingleOption(option, { postfix: `_${i}` })})`,
) )
.join(" OR "); .join(" OR ");
} }
function getWhereFromSingleOption( function getWhereFromSingleOption(
filterOptions: SingleFilterOption, filterOptions: SingleFilterOption,
opts: { postfix?: string } = {} opts: { postfix?: string } = {},
) { ) {
return Object.entries(filterOptions) return Object.entries(filterOptions)
.map(([key, value]) => getWhereFromKeyValue(key, value, opts)) .map(([key, value]) => getWhereFromKeyValue(key, value, opts))
@ -61,7 +63,7 @@ function getWhereParamKey(key: string, opts: { postfix?: string } = {}) {
function getWhereFromKeyValue( function getWhereFromKeyValue(
key: string, key: string,
value: FilterValue, value: FilterValue,
opts: { postfix?: string } = {} opts: { postfix?: string } = {},
) { ) {
if (value == undefined) { if (value == undefined) {
return `${key} IS NULL`; return `${key} IS NULL`;
@ -73,19 +75,25 @@ function getWhereFromKeyValue(
} }
if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") { if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") {
return `${key} IN (${value[FILTER_OPTION_VALUE_SYMBOL].map( return `${key} IN (${
(_v: any, i: number) => value[FILTER_OPTION_VALUE_SYMBOL].map(
`${getWhereParamKey(key, { (_v: any, i: number) =>
postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`, `${
})}` getWhereParamKey(key, {
).join(",")})`; postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`,
})
}`,
).join(",")
})`;
} }
if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") { if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") {
return `${key} ${value[FILTER_OPTION_OPERATOR_SYMBOL]} ${getWhereParamKey( return `${key} ${value[FILTER_OPTION_OPERATOR_SYMBOL]} ${
key, getWhereParamKey(
opts key,
)}`; opts,
)
}`;
} }
} }
return `${key} = ${getWhereParamKey(key, opts)}`; return `${key} = ${getWhereParamKey(key, opts)}`;
@ -93,7 +101,7 @@ function getWhereFromKeyValue(
function getParamsFromMultiFilterOption( function getParamsFromMultiFilterOption(
collection: Collection, collection: Collection,
filterOptions: MultiFilterOption filterOptions: MultiFilterOption,
) { ) {
return filterOptions.reduce( return filterOptions.reduce(
(acc, filterOption, i) => ({ (acc, filterOption, i) => ({
@ -102,14 +110,14 @@ function getParamsFromMultiFilterOption(
postfix: `_${i}`, postfix: `_${i}`,
}), }),
}), }),
{} {},
); );
} }
function getParamsFromSingleFilterOption( function getParamsFromSingleFilterOption(
collection: Collection, collection: Collection,
filterOptions: SingleFilterOption, filterOptions: SingleFilterOption,
opts: { postfix?: string } = {} opts: { postfix?: string } = {},
) { ) {
return Object.entries(filterOptions) return Object.entries(filterOptions)
.filter(([, value]) => { .filter(([, value]) => {
@ -122,10 +130,10 @@ function getParamsFromSingleFilterOption(
collection.fields[key]!, collection.fields[key]!,
key, key,
value, value,
opts opts,
), ),
}), }),
{} {},
); );
} }
@ -147,7 +155,7 @@ function getParamsForFilterKeyValue(
field: FieldDefinition, field: FieldDefinition,
key: string, key: string,
value: FilterValue, value: FilterValue,
opts: { postfix?: string } = {} opts: { postfix?: string } = {},
) { ) {
if (typeof value === "object") { if (typeof value === "object") {
if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") { if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") {
@ -155,12 +163,14 @@ function getParamsForFilterKeyValue(
(acc: Record<string, any>, _: any, i: number) => { (acc: Record<string, any>, _: any, i: number) => {
return { return {
...acc, ...acc,
[getWhereParamKey(key, { [
postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`, getWhereParamKey(key, {
})]: value[FILTER_OPTION_VALUE_SYMBOL][i], postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`,
})
]: value[FILTER_OPTION_VALUE_SYMBOL][i],
}; };
}, },
{} {},
); );
} }
} }

View File

@ -16,7 +16,7 @@ describe("ModelToSQL", () => {
const result = modelToSql(model); const result = modelToSql(model);
expect(result).toEqual( expect(result).toEqual(
`CREATE TABLE something (id TEXT PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL, date NUMERIC NOT NULL, reference TEXT NOT NULL REFERENCES somethingElse(id))` `CREATE TABLE something (id TEXT PRIMARY KEY, name TEXT NOT NULL, age INTEGER NOT NULL, date NUMERIC NOT NULL, reference TEXT NOT NULL REFERENCES somethingElse(id))`,
); );
}); });
}); });

View File

@ -5,7 +5,7 @@ import { Collection, FieldDefinition, getTargetKey } from "@fabric/domain";
type FieldSQLDefinitionMap = { type FieldSQLDefinitionMap = {
[K in FieldDefinition[VariantTag]]: ( [K in FieldDefinition[VariantTag]]: (
name: string, name: string,
field: Extract<FieldDefinition, { [VariantTag]: K }> field: Extract<FieldDefinition, { [VariantTag]: K }>,
) => string; ) => string;
}; };
@ -61,7 +61,7 @@ function modifiersFromOpts(field: FieldDefinition) {
} }
export function modelToSql( export function modelToSql(
model: Collection<string, Record<string, FieldDefinition>> model: Collection<string, Record<string, FieldDefinition>>,
) { ) {
const fields = Object.entries(model.fields) const fields = Object.entries(model.fields)
.map(([name, type]) => fieldDefinitionToSQL(name, type)) .map(([name, type]) => fieldDefinitionToSQL(name, type))

View File

@ -29,7 +29,7 @@ export function recordToSQLParams(model: Model, record: Record<string, any>) {
...acc, ...acc,
[keyToParam(key)]: fieldValueToSQL(model.fields[key]!, record[key]), [keyToParam(key)]: fieldValueToSQL(model.fields[key]!, record[key]),
}), }),
{} {},
); );
} }

View File

@ -24,7 +24,7 @@ function valueFromSQL(field: FieldDefinition, value: any): any {
type FieldSQLInsertMap = { type FieldSQLInsertMap = {
[K in FieldDefinition[VariantTag]]: ( [K in FieldDefinition[VariantTag]]: (
field: Extract<FieldDefinition, { [VariantTag]: K }>, field: Extract<FieldDefinition, { [VariantTag]: K }>,
value: any value: any,
) => FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>>; ) => FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>>;
}; };
const FieldSQLInsertMap: FieldSQLInsertMap = { const FieldSQLInsertMap: FieldSQLInsertMap = {

View File

@ -44,7 +44,7 @@ export class SQLiteDatabase {
allPrepared( allPrepared(
sql: string, sql: string,
params?: Record<string, any>, params?: Record<string, any>,
transformer?: (row: any) => any transformer?: (row: any) => any,
) { ) {
const cachedStmt = this.getCachedStatement(sql); const cachedStmt = this.getCachedStatement(sql);
@ -56,7 +56,7 @@ export class SQLiteDatabase {
onePrepared( onePrepared(
sql: string, sql: string,
params?: Record<string, any>, params?: Record<string, any>,
transformer?: (row: any) => any transformer?: (row: any) => any,
) { ) {
const cachedStmt = this.getCachedStatement(sql); const cachedStmt = this.getCachedStatement(sql);

View File

@ -5,7 +5,7 @@ import { FieldDefinition, FieldToType } from "@fabric/domain";
type FieldSQLInsertMap = { type FieldSQLInsertMap = {
[K in FieldDefinition[VariantTag]]: ( [K in FieldDefinition[VariantTag]]: (
field: Extract<FieldDefinition, { [VariantTag]: K }>, field: Extract<FieldDefinition, { [VariantTag]: K }>,
value: FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>> value: FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>>,
) => any; ) => any;
}; };
const FieldSQLInsertMap: FieldSQLInsertMap = { const FieldSQLInsertMap: FieldSQLInsertMap = {

View File

@ -20,7 +20,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
constructor( constructor(
private db: SQLiteDatabase, private db: SQLiteDatabase,
private schema: ModelSchema, private schema: ModelSchema,
private query: QueryDefinition private query: QueryDefinition,
) {} ) {}
where(where: FilterOptions<T>): StoreSortableQuery<T> { where(where: FilterOptions<T>): StoreSortableQuery<T> {
@ -47,7 +47,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Pick<T, K>[], StoreQueryError>; ): AsyncResult<Pick<T, K>[], StoreQueryError>;
select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> { select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
@ -57,21 +57,21 @@ export class QueryBuilder<T> implements StoreQuery<T> {
{ {
...this.query, ...this.query,
keys: keys!, keys: keys!,
} },
); );
return this.db.allPrepared( return this.db.allPrepared(
sql, sql,
params, params,
transformRow(this.schema[this.query.from]!) transformRow(this.schema[this.query.from]!),
); );
}, },
(err) => new StoreQueryError(err.message) (err) => new StoreQueryError(err.message),
); );
} }
selectOne(): AsyncResult<Optional<T>, StoreQueryError>; selectOne(): AsyncResult<Optional<T>, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys: K[] keys: K[],
): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>; ): AsyncResult<Optional<Pick<T, K>>, StoreQueryError>;
selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> { selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
@ -82,22 +82,22 @@ export class QueryBuilder<T> implements StoreQuery<T> {
...this.query, ...this.query,
keys: keys!, keys: keys!,
limit: 1, limit: 1,
} },
); );
return await this.db.onePrepared( return await this.db.onePrepared(
stmt, stmt,
params, params,
transformRow(this.schema[this.query.from]!) transformRow(this.schema[this.query.from]!),
); );
}, },
(err) => new StoreQueryError(err.message) (err) => new StoreQueryError(err.message),
); );
} }
} }
export function getSelectStatement( export function getSelectStatement(
collection: Collection, collection: Collection,
query: QueryDefinition query: QueryDefinition,
): [string, Record<string, any>] { ): [string, Record<string, any>] {
const selectFields = query.keys ? query.keys.join(", ") : "*"; const selectFields = query.keys ? query.keys.join(", ") : "*";

View File

@ -110,7 +110,7 @@ describe("State Store", () => {
streamId: UUIDGeneratorMock.generate(), streamId: UUIDGeneratorMock.generate(),
streamVersion: 1n, streamVersion: 1n,
deletedAt: null, deletedAt: null,
}) }),
); );
const result = await Run.UNSAFE(() => const result = await Run.UNSAFE(() =>

View File

@ -20,8 +20,7 @@ import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
import { QueryBuilder } from "./query-builder.ts"; import { QueryBuilder } from "./query-builder.ts";
export class SQLiteStateStore<TModel extends Model> export class SQLiteStateStore<TModel extends Model>
implements WritableStateStore<TModel> implements WritableStateStore<TModel> {
{
private schema: ModelSchemaFromModels<TModel>; private schema: ModelSchemaFromModels<TModel>;
private db: SQLiteDatabase; private db: SQLiteDatabase;
@ -38,25 +37,27 @@ export class SQLiteStateStore<TModel extends Model>
insertInto<T extends keyof ModelSchemaFromModels<TModel>>( insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T, collection: T,
record: ModelToType<ModelSchemaFromModels<TModel>[T]> record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
const model = this.schema[collection]; const model = this.schema[collection];
return AsyncResult.tryFrom( return AsyncResult.tryFrom(
() => { () => {
this.db.runPrepared( this.db.runPrepared(
`INSERT INTO ${model.name} (${recordToSQLKeys( `INSERT INTO ${model.name} (${
record recordToSQLKeys(
)}) VALUES (${recordToSQLKeyParams(record)})`, record,
recordToSQLParams(model, record) )
}) VALUES (${recordToSQLKeyParams(record)})`,
recordToSQLParams(model, record),
); );
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
from<T extends keyof ModelSchemaFromModels<TModel>>( from<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T collection: T,
): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>> { ): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>> {
return new QueryBuilder(this.db, this.schema, { return new QueryBuilder(this.db, this.schema, {
from: collection, from: collection,
@ -66,7 +67,7 @@ export class SQLiteStateStore<TModel extends Model>
update<T extends keyof ModelSchemaFromModels<TModel>>( update<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T, collection: T,
id: UUID, id: UUID,
record: Partial<ModelToType<ModelSchemaFromModels<TModel>[T]>> record: Partial<ModelToType<ModelSchemaFromModels<TModel>[T]>>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
const model = this.schema[collection]; const model = this.schema[collection];
@ -77,19 +78,21 @@ export class SQLiteStateStore<TModel extends Model>
id, id,
}); });
this.db.runPrepared( this.db.runPrepared(
`UPDATE ${model.name} SET ${recordToSQLSet( `UPDATE ${model.name} SET ${
record recordToSQLSet(
)} WHERE id = ${keyToParam("id")}`, record,
params )
} WHERE id = ${keyToParam("id")}`,
params,
); );
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
delete<T extends keyof ModelSchemaFromModels<TModel>>( delete<T extends keyof ModelSchemaFromModels<TModel>>(
collection: T, collection: T,
id: UUID id: UUID,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
const model = this.schema[collection]; const model = this.schema[collection];
@ -97,10 +100,10 @@ export class SQLiteStateStore<TModel extends Model>
() => { () => {
this.db.runPrepared( this.db.runPrepared(
`DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`, `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`,
{ $id: id } { $id: id },
); );
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }
@ -116,7 +119,7 @@ export class SQLiteStateStore<TModel extends Model>
} }
}); });
}, },
(error) => new StoreQueryError(error.message) (error) => new StoreQueryError(error.message),
); );
} }