From 4fff9f91f5fcadb935242b051b04394d5e0ca4d8 Mon Sep 17 00:00:00 2001 From: Pablo Baleztena Date: Thu, 10 Oct 2024 13:42:30 -0300 Subject: [PATCH] [fabric/domain] Improve Result & AsyncResult types; Refactor model types; Add timestamp and embedded field support --- .../fabric/core/src/error/tagged-error.ts | 2 +- packages/fabric/core/src/index.ts | 1 + .../fabric/core/src/result/async-result.ts | 29 ++- .../fabric/core/src/result/result.spec.ts | 57 +++++ packages/fabric/core/src/result/result.ts | 154 +++++++++++++- packages/fabric/core/src/run/index.ts | 1 + packages/fabric/core/src/run/run.spec.ts | 28 +++ packages/fabric/core/src/run/run.ts | 75 +++++++ packages/fabric/core/src/time/posix-date.ts | 31 ++- .../domain/src/models/fields/embedded.ts | 20 ++ .../domain/src/models/fields/field-to-type.ts | 5 + .../fabric/domain/src/models/fields/index.ts | 8 +- .../src/models/fields/reference-field.spec.ts | 34 +-- .../src/models/fields/reference-field.ts | 24 ++- .../domain/src/models/fields/timestamp.ts | 18 ++ .../fabric/domain/src/models/model-schema.ts | 6 +- packages/fabric/domain/src/models/model.ts | 2 +- .../domain/src/models/query/filter-options.ts | 6 +- .../domain/src/models/query/query-builder.ts | 17 +- .../domain/src/models/state-store.spec.ts | 63 +++--- .../fabric/domain/src/models/state-store.ts | 5 +- .../domain/src/storage/storage-driver.ts | 4 +- .../fabric/domain/src/utils/json-utils.ts | 14 ++ .../src/utils/sort-by-dependencies.spec.ts | 6 +- .../domain/src/utils/sort-by-dependencies.ts | 19 +- .../store-sqlite/src/model-to-sql.spec.ts | 22 ++ .../fabric/store-sqlite/src/model-to-sql.ts | 27 ++- .../fabric/store-sqlite/src/sql-to-value.ts | 4 +- .../store-sqlite/src/sqlite-driver.spec.ts | 108 ++++++---- .../fabric/store-sqlite/src/sqlite-driver.ts | 200 ++++++++++-------- .../fabric/store-sqlite/src/sqlite-wrapper.ts | 6 +- .../fabric/store-sqlite/src/value-to-sql.ts | 2 + 32 files changed, 745 insertions(+), 253 deletions(-) create mode 100644 packages/fabric/core/src/result/result.spec.ts create mode 100644 packages/fabric/core/src/run/index.ts create mode 100644 packages/fabric/core/src/run/run.spec.ts create mode 100644 packages/fabric/core/src/run/run.ts create mode 100644 packages/fabric/domain/src/models/fields/embedded.ts create mode 100644 packages/fabric/domain/src/models/fields/timestamp.ts create mode 100644 packages/fabric/domain/src/utils/json-utils.ts create mode 100644 packages/fabric/store-sqlite/src/model-to-sql.spec.ts diff --git a/packages/fabric/core/src/error/tagged-error.ts b/packages/fabric/core/src/error/tagged-error.ts index 01175a1..876c87e 100644 --- a/packages/fabric/core/src/error/tagged-error.ts +++ b/packages/fabric/core/src/error/tagged-error.ts @@ -3,7 +3,7 @@ import { TaggedVariant, VariantTag } from "../variant/index.js"; /** * A TaggedError is a tagged variant with an error message. */ -export class TaggedError +export class TaggedError extends Error implements TaggedVariant { diff --git a/packages/fabric/core/src/index.ts b/packages/fabric/core/src/index.ts index dbc3c9e..b17c448 100644 --- a/packages/fabric/core/src/index.ts +++ b/packages/fabric/core/src/index.ts @@ -2,6 +2,7 @@ export * from "./array/index.js"; export * from "./error/index.js"; export * from "./record/index.js"; export * from "./result/index.js"; +export * from "./run/index.js"; export * from "./time/index.js"; export * from "./types/index.js"; export * from "./variant/index.js"; diff --git a/packages/fabric/core/src/result/async-result.ts b/packages/fabric/core/src/result/async-result.ts index be0600e..22bc787 100644 --- a/packages/fabric/core/src/result/async-result.ts +++ b/packages/fabric/core/src/result/async-result.ts @@ -1,11 +1,32 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { TaggedError } from "../error/tagged-error.js"; +import { UnexpectedError } from "../error/unexpected-error.js"; import { Result } from "./result.js"; /** - * Un AsyncResult representa el resultado de una operación asíncrona que puede - * resolver en un valor de tipo `TValue` o en un error de tipo `TError`. + * An AsyncResult represents the result of an asynchronous operation that can + * resolve to a value of type `TValue` or an error of type `TError`. */ export type AsyncResult< - TValue, - TError extends TaggedError = never, + TValue = any, + TError extends TaggedError = never, > = Promise>; + +export namespace AsyncResult { + export async function tryFrom( + fn: () => Promise, + errorMapper: (error: any) => TError, + ): AsyncResult { + try { + return Result.succeedWith(await fn()); + } catch (error) { + return Result.failWith(errorMapper(error)); + } + } + + export async function from( + fn: () => Promise, + ): AsyncResult { + return tryFrom(fn, (error) => new UnexpectedError(error)); + } +} diff --git a/packages/fabric/core/src/result/result.spec.ts b/packages/fabric/core/src/result/result.spec.ts new file mode 100644 index 0000000..5628f2c --- /dev/null +++ b/packages/fabric/core/src/result/result.spec.ts @@ -0,0 +1,57 @@ +import { describe, expect, expectTypeOf, it, vitest } from "vitest"; +import { UnexpectedError } from "../error/unexpected-error.js"; +import { Result } from "./result.js"; + +describe("Result", () => { + describe("isOk", () => { + it("should return true if the result is ok", () => { + const result = Result.succeedWith(1) as Result; + + expect(result.isOk()).toBe(true); + + if (result.isOk()) { + expect(result.value).toEqual(1); + + expectTypeOf(result).toEqualTypeOf>(); + } + }); + }); + + describe("isError", () => { + it("should return true if the result is an error", () => { + const result = Result.failWith(new UnexpectedError()) as Result< + number, + UnexpectedError + >; + + expect(result.isError()).toBe(true); + + if (result.isError()) { + expect(result.value).toBeInstanceOf(UnexpectedError); + + expectTypeOf(result).toEqualTypeOf>(); + } + }); + }); + + describe("Map", () => { + it("should return the result of the last function", () => { + const x = 0; + + const result = Result.succeedWith(x + 1).map((x) => x * 2); + + expect(result.unwrapOrThrow()).toEqual(2); + + expectTypeOf(result).toEqualTypeOf>(); + }); + + it("should not execute the function if the result is an error", () => { + const fn = vitest.fn(); + const result = Result.failWith(new UnexpectedError()).map(fn); + + expect(result.isError()).toBe(true); + + expect(fn).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/fabric/core/src/result/result.ts b/packages/fabric/core/src/result/result.ts index a7c8d7f..72e25a2 100644 --- a/packages/fabric/core/src/result/result.ts +++ b/packages/fabric/core/src/result/result.ts @@ -1,11 +1,151 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { isError } from "../error/is-error.js"; import { TaggedError } from "../error/tagged-error.js"; -import { UnexpectedError } from "../error/unexpected-error.js"; /** - * Un Result representa el resultado de una operación - * que puede ser un valor de tipo `TValue` o un error `TError`. + * A Result represents the outcome of an operation + * that can be either a value of type `TValue` or an error `TError`. */ -export type Result< - TValue, - TError extends TaggedError = UnexpectedError, -> = TValue | TError; +export class Result { + static succeedWith(value: T): Result { + return new Result(value); + } + + static failWith(error: T): Result { + return new Result(error); + } + + static ok(): Result; + static ok(value: T): Result; + static ok(value?: any) { + return new Result(value ?? undefined); + } + + static tryFrom( + fn: () => T, + errorMapper: (error: any) => TError, + ): Result { + try { + return Result.succeedWith(fn()); + } catch (error) { + return Result.failWith(errorMapper(error)); + } + } + + private constructor(readonly value: TValue | TError) {} + + /** + * Unwrap the value of the result. + * If the result is an error, it will throw the error. + */ + unwrapOrThrow(): TValue { + if (isError(this.value)) { + throw this.value; + } + + return this.value as TValue; + } + + /** + * Throw the error if the result is an error. + * otherwise, do nothing. + */ + orThrow(): void { + if (isError(this.value)) { + throw this.value; + } + } + + unwrapErrorOrThrow(): TError { + if (!isError(this.value)) { + throw new Error("Result is not an error"); + } + + return this.value; + } + + /** + * Check if the result is a success. + */ + isOk(): this is Result { + return !isError(this.value); + } + + /** + * Check if the result is an error. + */ + isError(): this is Result { + return isError(this.value); + } + + /** + * Map a function over the value of the result. + */ + map( + fn: (value: TValue) => TMappedValue, + ): Result { + if (!isError(this.value)) { + return Result.succeedWith(fn(this.value as TValue)); + } + + return this as any; + } + + /** + * Maps a function over the value of the result and flattens the result. + */ + flatMap( + fn: (value: TValue) => Result, + ): Result { + if (!isError(this.value)) { + return fn(this.value as TValue) as any; + } + + return this as any; + } + + /** + * Try to map a function over the value of the result. + * If the function throws an error, the result will be a failure. + */ + tryMap( + fn: (value: TValue) => TMappedValue, + errMapper: (error: any) => TError, + ): Result { + if (!isError(this.value)) { + try { + return Result.succeedWith(fn(this.value as TValue)); + } catch (error) { + return Result.failWith(errMapper(error)); + } + } + + return this as any; + } + + /** + * Map a function over the error of the result. + */ + mapError( + fn: (error: TError) => TMappedError, + ): Result { + if (isError(this.value)) { + return Result.failWith(fn(this.value as TError)); + } + + return this as unknown as Result; + } + + /** + * Taps a function if the result is a success. + * This is useful for side effects that do not modify the result. + */ + tap(fn: (value: TValue) => void): Result { + if (!isError(this.value)) { + fn(this.value as TValue); + } + + return this; + } +} diff --git a/packages/fabric/core/src/run/index.ts b/packages/fabric/core/src/run/index.ts new file mode 100644 index 0000000..2392331 --- /dev/null +++ b/packages/fabric/core/src/run/index.ts @@ -0,0 +1 @@ +export * from "./run.js"; diff --git a/packages/fabric/core/src/run/run.spec.ts b/packages/fabric/core/src/run/run.spec.ts new file mode 100644 index 0000000..a573da7 --- /dev/null +++ b/packages/fabric/core/src/run/run.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { UnexpectedError } from "../error/unexpected-error.js"; +import { Result } from "../result/result.js"; +import { Run } from "./run.js"; + +describe("Run", () => { + describe("In Sequence", () => { + it("should pipe the results of multiple async functions", async () => { + const result = await Run.seq( + async () => Result.succeedWith(1), + async (x) => Result.succeedWith(x + 1), + async (x) => Result.succeedWith(x * 2), + ); + + expect(result.unwrapOrThrow()).toEqual(4); + }); + + it("should return the first error if one of the functions fails", async () => { + const result = await Run.seq( + async () => Result.succeedWith(1), + async () => Result.failWith(new UnexpectedError()), + async (x) => Result.succeedWith(x * 2), + ); + + expect(result.isError()).toBe(true); + }); + }); +}); diff --git a/packages/fabric/core/src/run/run.ts b/packages/fabric/core/src/run/run.ts new file mode 100644 index 0000000..9872572 --- /dev/null +++ b/packages/fabric/core/src/run/run.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TaggedError } from "../error/tagged-error.js"; +import { AsyncResult } from "../result/async-result.js"; + +export namespace Run { + // prettier-ignore + export async function seq< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + ): AsyncResult; + // prettier-ignore + export async function seq< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + T3, TE3 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + fn3: (value: T2) => AsyncResult, + ): AsyncResult; + export async function seq( + ...fns: ((...args: any[]) => AsyncResult)[] + ): AsyncResult { + let result = await fns[0](); + + for (let i = 1; i < fns.length; i++) { + if (result.isError()) { + return result; + } + + result = await fns[i](result.unwrapOrThrow()); + } + + return result; + } + + // prettier-ignore + export async function seqUNSAFE< + T1, TE1 extends TaggedError, + T2, TE2 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + ): Promise; + // prettier-ignore + export async function seqUNSAFE< + T1,TE1 extends TaggedError, + T2,TE2 extends TaggedError, + T3,TE3 extends TaggedError, + >( + fn1: () => AsyncResult, + fn2: (value: T1) => AsyncResult, + fn3: (value: T2) => AsyncResult, + ): Promise; + export async function seqUNSAFE( + ...fns: ((...args: any[]) => AsyncResult)[] + ): Promise { + const result = await (seq as any)(...fns); + + if (result.isError()) { + throw result.unwrapOrThrow(); + } + + return result.unwrapOrThrow(); + } + + export async function UNSAFE( + fn: () => AsyncResult, + ): Promise { + return (await fn()).unwrapOrThrow(); + } +} diff --git a/packages/fabric/core/src/time/posix-date.ts b/packages/fabric/core/src/time/posix-date.ts index ac794f4..6c6245e 100644 --- a/packages/fabric/core/src/time/posix-date.ts +++ b/packages/fabric/core/src/time/posix-date.ts @@ -1,9 +1,38 @@ +import { isRecord } from "../record/is-record.js"; import { TaggedVariant } from "../variant/variant.js"; export class PosixDate { - constructor(public readonly timestamp: number) {} + constructor(public readonly timestamp: number = Date.now()) {} + + public toJSON(): PosixDateJSON { + return { + type: "posix-date", + timestamp: this.timestamp, + }; + } + + public static fromJson(json: PosixDateJSON): PosixDate { + return new PosixDate(json.timestamp); + } + + public static isPosixDateJSON(value: unknown): value is PosixDateJSON { + if ( + isRecord(value) && + "type" in value && + "timestamp" in value && + value["type"] === "posix-date" && + typeof value["timestamp"] === "number" + ) + return true; + return false; + } } export interface TimeZone extends TaggedVariant<"TimeZone"> { timestamp: number; } + +export interface PosixDateJSON { + type: "posix-date"; + timestamp: number; +} diff --git a/packages/fabric/domain/src/models/fields/embedded.ts b/packages/fabric/domain/src/models/fields/embedded.ts new file mode 100644 index 0000000..59b83c4 --- /dev/null +++ b/packages/fabric/domain/src/models/fields/embedded.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars +export interface EmbeddedFieldOptions extends BaseField {} + +export interface EmbeddedField + extends TaggedVariant<"EmbeddedField">, + EmbeddedFieldOptions {} + +export function createEmbeddedField< + K = any, + T extends EmbeddedFieldOptions = EmbeddedFieldOptions, +>(opts: T = {} as T): EmbeddedField & T { + return { + [VariantTag]: "EmbeddedField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts index b7f67b9..76e1ee8 100644 --- a/packages/fabric/domain/src/models/fields/field-to-type.ts +++ b/packages/fabric/domain/src/models/fields/field-to-type.ts @@ -1,10 +1,13 @@ +import { PosixDate } from "@fabric/core"; import { Decimal } from "decimal.js"; import { UUID } from "../../types/uuid.js"; import { DecimalField } from "./decimal.js"; +import { EmbeddedField } from "./embedded.js"; import { FloatField } from "./float.js"; import { IntegerField } from "./integer.js"; import { ReferenceField } from "./reference-field.js"; import { StringField } from "./string-field.js"; +import { TimestampField } from "./timestamp.js"; import { UUIDField } from "./uuid-field.js"; /** @@ -18,6 +21,8 @@ export type FieldToType = : TField extends ReferenceField ? MaybeOptional : TField extends DecimalField ? MaybeOptional : TField extends FloatField ? MaybeOptional + : TField extends TimestampField ? MaybeOptional + : TField extends EmbeddedField ? MaybeOptional : never; //prettier-ignore diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index 0f1b30a..cde17d4 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -1,8 +1,10 @@ import { createDecimalField, DecimalField } from "./decimal.js"; +import { createEmbeddedField, EmbeddedField } from "./embedded.js"; import { createFloatField, FloatField } from "./float.js"; import { createIntegerField, IntegerField } from "./integer.js"; import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createStringField, StringField } from "./string-field.js"; +import { createTimestampField, TimestampField } from "./timestamp.js"; import { createUUIDField, UUIDField } from "./uuid-field.js"; export * from "./base-field.js"; export * from "./field-to-type.js"; @@ -14,7 +16,9 @@ export type FieldDefinition = | IntegerField | FloatField | DecimalField - | ReferenceField; + | ReferenceField + | TimestampField + | EmbeddedField; export namespace Field { export const string = createStringField; @@ -23,4 +27,6 @@ export namespace Field { export const reference = createReferenceField; export const decimal = createDecimalField; export const float = createFloatField; + export const timestamp = createTimestampField; + export const embedded = createEmbeddedField; } diff --git a/packages/fabric/domain/src/models/fields/reference-field.spec.ts b/packages/fabric/domain/src/models/fields/reference-field.spec.ts index d5e75b4..2adfb93 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.spec.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.spec.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import { defineModel } from "../model.js"; import { Field } from "./index.js"; import { - InvalidReferenceField, + InvalidReferenceFieldError, validateReferenceField, } from "./reference-field.js"; @@ -26,26 +26,18 @@ describe("Validate Reference Field", () => { Field.reference({ targetModel: "foo", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should not return an error if the target model is in the schema", () => { - const result = validateReferenceField( + validateReferenceField( schema, Field.reference({ targetModel: "User", }), - ); - - if (isError(result)) { - throw result.reason; - } + ).unwrapOrThrow(); }); it("should return an error if the target key is not in the target model", () => { @@ -55,13 +47,9 @@ describe("Validate Reference Field", () => { targetModel: "User", targetKey: "foo", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should return error if the target key is not unique", () => { @@ -71,13 +59,9 @@ describe("Validate Reference Field", () => { targetModel: "User", targetKey: "otherNotUnique", }), - ); + ).unwrapErrorOrThrow(); - if (!isError(result)) { - throw "Expected an error"; - } - - expect(result).toBeInstanceOf(InvalidReferenceField); + expect(result).toBeInstanceOf(InvalidReferenceFieldError); }); it("should not return an error if the target key is in the target model and is unique", () => { diff --git a/packages/fabric/domain/src/models/fields/reference-field.ts b/packages/fabric/domain/src/models/fields/reference-field.ts index b8272ec..a2180de 100644 --- a/packages/fabric/domain/src/models/fields/reference-field.ts +++ b/packages/fabric/domain/src/models/fields/reference-field.ts @@ -27,16 +27,20 @@ export function getTargetKey(field: ReferenceField): string { export function validateReferenceField( schema: ModelSchema, field: ReferenceField, -): Result { +): Result { if (!schema[field.targetModel]) { - return new InvalidReferenceField( - `The target model '${field.targetModel}' is not in the schema.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target model '${field.targetModel}' is not in the schema.`, + ), ); } if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) { - return new InvalidReferenceField( - `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, + ), ); } @@ -44,13 +48,17 @@ export function validateReferenceField( field.targetKey && !schema[field.targetModel].fields[field.targetKey].isUnique ) { - return new InvalidReferenceField( - `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, + return Result.failWith( + new InvalidReferenceFieldError( + `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, + ), ); } + + return Result.ok(); } -export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> { +export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> { constructor(readonly reason: string) { super("InvalidReferenceField"); } diff --git a/packages/fabric/domain/src/models/fields/timestamp.ts b/packages/fabric/domain/src/models/fields/timestamp.ts new file mode 100644 index 0000000..41b1eab --- /dev/null +++ b/packages/fabric/domain/src/models/fields/timestamp.ts @@ -0,0 +1,18 @@ +import { TaggedVariant, VariantTag } from "@fabric/core"; +import { BaseField } from "./base-field.js"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface TimestampFieldOptions extends BaseField {} + +export interface TimestampField + extends TaggedVariant<"TimestampField">, + TimestampFieldOptions {} + +export function createTimestampField( + opts: T = {} as T, +): TimestampField & T { + return { + [VariantTag]: "TimestampField", + ...opts, + } as const; +} diff --git a/packages/fabric/domain/src/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts index 8150980..ca5ce05 100644 --- a/packages/fabric/domain/src/models/model-schema.ts +++ b/packages/fabric/domain/src/models/model-schema.ts @@ -1,7 +1,7 @@ -import { Model } from "./model.js"; +import { Collection } from "./model.js"; -export type ModelSchema = Record; +export type ModelSchema = Record; -export type ModelSchemaFromModels = { +export type ModelSchemaFromModels = { [K in TModels["name"]]: Extract; }; diff --git a/packages/fabric/domain/src/models/model.ts b/packages/fabric/domain/src/models/model.ts index a79321b..a2a4ec8 100644 --- a/packages/fabric/domain/src/models/model.ts +++ b/packages/fabric/domain/src/models/model.ts @@ -48,7 +48,7 @@ export function defineCollection< } as const; } -export type ModelToType = { +export type ModelToType = { [K in Keyof]: FieldToType; }; diff --git a/packages/fabric/domain/src/models/query/filter-options.ts b/packages/fabric/domain/src/models/query/filter-options.ts index 9a86305..98a5ed1 100644 --- a/packages/fabric/domain/src/models/query/filter-options.ts +++ b/packages/fabric/domain/src/models/query/filter-options.ts @@ -16,9 +16,9 @@ export type SingleFilterOption = { export type MultiFilterOption = SingleFilterOption[]; -export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type"); -export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value"); -export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator"); +export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type"; +export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value"; +export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator"; export type LikeFilterOption = T extends string ? { diff --git a/packages/fabric/domain/src/models/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts index 40304e6..d8aa630 100644 --- a/packages/fabric/domain/src/models/query/query-builder.ts +++ b/packages/fabric/domain/src/models/query/query-builder.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../../errors/query-error.js"; import { StorageDriver } from "../../storage/storage-driver.js"; @@ -41,19 +42,23 @@ export class QueryBuilder implements StoreQuery { }); } + select(): AsyncResult; select>( - keys?: K[], - ): AsyncResult[], StoreQueryError> { - return this.driver.select(this.schema[this.query.from], { + keys: K[], + ): AsyncResult[], StoreQueryError>; + select>(keys?: K[]): AsyncResult { + return this.driver.select(this.schema, { ...this.query, keys, }); } + selectOne(): AsyncResult; selectOne>( - keys?: K[], - ): AsyncResult, StoreQueryError> { - return this.driver.selectOne(this.schema[this.query.from], { + keys: K, + ): AsyncResult, StoreQueryError>; + selectOne>(keys?: K[]): AsyncResult { + return this.driver.selectOne(this.schema, { ...this.query, keys, }); diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts index 7e30408..4be04f3 100644 --- a/packages/fabric/domain/src/models/state-store.spec.ts +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -1,4 +1,4 @@ -import { isError } from "@fabric/core"; +import { isError, Run } from "@fabric/core"; import { SQLiteStorageDriver } from "@fabric/store-sqlite"; import { afterEach, @@ -58,9 +58,7 @@ describe("State Store", () => { if (isError(insertResult)) throw insertResult; - const result = await store.from("users").select(); - - if (isError(result)) throw result; + const result = (await store.from("users").select()).unwrapOrThrow(); expectTypeOf(result).toEqualTypeOf< { @@ -83,34 +81,39 @@ describe("State Store", () => { it("should query with a where clause", async () => { const newUUID = UUIDGeneratorMock.generate(); - await store.insertInto("users", { - name: "test", - id: newUUID, - streamId: newUUID, - streamVersion: 1n, - }); - await store.insertInto("users", { - name: "anotherName", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }); - await store.insertInto("users", { - name: "anotherName2", - id: UUIDGeneratorMock.generate(), - streamId: UUIDGeneratorMock.generate(), - streamVersion: 1n, - }); + await Run.seqUNSAFE( + () => + store.insertInto("users", { + name: "test", + id: newUUID, + streamId: newUUID, + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + () => + store.insertInto("users", { + name: "anotherName2", + id: UUIDGeneratorMock.generate(), + streamId: UUIDGeneratorMock.generate(), + streamVersion: 1n, + }), + ); - const result = await store - .from("users") - .where({ - name: isLike("te*"), - }) - .select(); - - if (isError(result)) throw result; + const result = await Run.UNSAFE(() => + store + .from("users") + .where({ + name: isLike("te%"), + }) + .select(), + ); expectTypeOf(result).toEqualTypeOf< { diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index 292bc7e..fd6473c 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -1,4 +1,5 @@ import { AsyncResult } from "@fabric/core"; +import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { StorageDriver } from "../storage/storage-driver.js"; import { ModelSchemaFromModels } from "./model-schema.js"; @@ -20,8 +21,8 @@ export class StateStore { }, {} as ModelSchemaFromModels); } - async migrate(): AsyncResult { - await this.driver.sync(this.schema); + migrate(): AsyncResult { + return this.driver.sync(this.schema); } async insertInto>( diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 6fc6c03..0e414b2 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -20,7 +20,7 @@ export interface StorageDriver { * Run a select query against the store. */ select( - model: Collection, + model: ModelSchema, query: QueryDefinition, ): AsyncResult; @@ -28,7 +28,7 @@ export interface StorageDriver { * Run a select query against the store. */ selectOne( - model: Collection, + model: ModelSchema, query: QueryDefinition, ): AsyncResult; diff --git a/packages/fabric/domain/src/utils/json-utils.ts b/packages/fabric/domain/src/utils/json-utils.ts new file mode 100644 index 0000000..22d3340 --- /dev/null +++ b/packages/fabric/domain/src/utils/json-utils.ts @@ -0,0 +1,14 @@ +import { PosixDate } from "@fabric/core"; + +export namespace JSONUtils { + export function reviver(key: string, value: unknown) { + if (PosixDate.isPosixDateJSON(value)) { + return PosixDate.fromJson(value); + } + return value; + } + + export function parse(json: string): T { + return JSON.parse(json, reviver); + } +} diff --git a/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts index 904cc84..5b122f5 100644 --- a/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts +++ b/packages/fabric/domain/src/utils/sort-by-dependencies.spec.ts @@ -15,7 +15,7 @@ describe("sortByDependencies", () => { const result = sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }); + }).unwrapOrThrow(); expect(result).toEqual([ { id: 3, name: "C", dependencies: [] }, @@ -35,7 +35,7 @@ describe("sortByDependencies", () => { sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }), + }).unwrapErrorOrThrow(), ).toBeInstanceOf(CircularDependencyError); }); @@ -45,7 +45,7 @@ describe("sortByDependencies", () => { const result = sortByDependencies(array, { keyGetter: (element) => element.name, depGetter: (element) => element.dependencies, - }); + }).unwrapOrThrow(); expect(result).toEqual([]); }); diff --git a/packages/fabric/domain/src/utils/sort-by-dependencies.ts b/packages/fabric/domain/src/utils/sort-by-dependencies.ts index 8a7d2c9..78c0a00 100644 --- a/packages/fabric/domain/src/utils/sort-by-dependencies.ts +++ b/packages/fabric/domain/src/utils/sort-by-dependencies.ts @@ -34,14 +34,15 @@ export function sortByDependencies( visited.add(key); sorted.push(key); }; - try { - graph.forEach((deps, key) => { - visit(key, []); - }); - } catch (e) { - return e as CircularDependencyError; - } - return sorted.map( - (key) => array.find((element) => keyGetter(element) === key) as T, + return Result.tryFrom( + () => { + graph.forEach((deps, key) => { + visit(key, []); + }); + return sorted.map( + (key) => array.find((element) => keyGetter(element) === key) as T, + ); + }, + (e) => e as CircularDependencyError, ); } diff --git a/packages/fabric/store-sqlite/src/model-to-sql.spec.ts b/packages/fabric/store-sqlite/src/model-to-sql.spec.ts new file mode 100644 index 0000000..8038dec --- /dev/null +++ b/packages/fabric/store-sqlite/src/model-to-sql.spec.ts @@ -0,0 +1,22 @@ +import { defineCollection, Field } from "@fabric/domain"; +import { describe, expect, it } from "vitest"; +import { modelToSql } from "./model-to-sql.js"; + +describe("ModelToSQL", () => { + const model = defineCollection("something", { + id: Field.uuid({ isPrimaryKey: true }), + name: Field.string(), + age: Field.integer(), + // isTrue: Field.boolean(), + date: Field.timestamp(), + reference: Field.reference({ targetModel: "somethingElse" }), + }); + + it("should generate SQL for a model", () => { + const result = modelToSql(model); + + 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))`, + ); + }); +}); diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index ede39f4..4f2bee6 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -1,6 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Variant, VariantTag } from "@fabric/core"; -import { FieldDefinition, getTargetKey, Model } from "@fabric/domain"; +import { Collection, FieldDefinition, getTargetKey } from "@fabric/domain"; +import { EmbeddedField } from "@fabric/domain/dist/models/fields/embedded.js"; +import { TimestampField } from "@fabric/domain/dist/models/fields/timestamp.js"; type FieldSQLDefinitionMap = { [K in FieldDefinition[VariantTag]]: ( @@ -19,7 +21,9 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { "TEXT", f.isPrimaryKey ? "PRIMARY KEY" : "", modifiersFromOpts(f), - ].join(" "); + ] + .filter((x) => x) + .join(" "); }, IntegerField: (n, f): string => { return [n, "INTEGER", modifiersFromOpts(f)].join(" "); @@ -29,8 +33,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { n, "TEXT", modifiersFromOpts(f), - ",", - `FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`, + `REFERENCES ${f.targetModel}(${getTargetKey(f)})`, ].join(" "); }, FloatField: (n, f): string => { @@ -39,6 +42,12 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { DecimalField: (n, f): string => { return [n, "REAL", modifiersFromOpts(f)].join(" "); }, + TimestampField: (n, f: TimestampField): string => { + return [n, "NUMERIC", modifiersFromOpts(f)].join(" "); + }, + EmbeddedField: (n, f: EmbeddedField): string => { + return [n, "TEXT", modifiersFromOpts(f)].join(" "); + }, }; function fieldDefinitionToSQL(name: string, field: FieldDefinition) { return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); @@ -48,17 +57,17 @@ function modifiersFromOpts(field: FieldDefinition) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { return; } - return [ - !field.isOptional ? "NOT NULL" : "", - field.isUnique ? "UNIQUE" : "", - ].join(" "); + return [!field.isOptional ? "NOT NULL" : "", field.isUnique ? "UNIQUE" : ""] + .filter((x) => x) + .join(" "); } export function modelToSql( - model: Model>, + model: Collection>, ) { const fields = Object.entries(model.fields) .map(([name, type]) => fieldDefinitionToSQL(name, type)) + .filter((x) => x) .join(", "); return `CREATE TABLE ${model.name} (${fields})`; diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sql-to-value.ts index 152748c..a75af59 100644 --- a/packages/fabric/store-sqlite/src/sql-to-value.ts +++ b/packages/fabric/store-sqlite/src/sql-to-value.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { VariantTag } from "@fabric/core"; +import { PosixDate, VariantTag } from "@fabric/core"; import { Collection, FieldDefinition, FieldToType } from "@fabric/domain"; export function transformRow(model: Collection) { @@ -36,4 +36,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { ReferenceField: (f, v) => v, FloatField: (f, v) => v, DecimalField: (f, v) => v, + TimestampField: (f, v) => new PosixDate(v), + EmbeddedField: (f, v: string) => JSON.parse(v), }; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 514c9a7..89bc226 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -5,28 +5,32 @@ import { SQLiteStorageDriver } from "./sqlite-driver.js"; describe("SQLite Store Driver", () => { const schema = { + demo: defineModel("demo", { + value: Field.float(), + owner: Field.reference({ targetModel: "users" }), + }), users: defineModel("users", { name: Field.string(), }), }; - let store: SQLiteStorageDriver; + let driver: SQLiteStorageDriver; beforeEach(() => { - store = new SQLiteStorageDriver(":memory:"); + driver = new SQLiteStorageDriver(":memory:"); }); afterEach(async () => { - const result = await store.close(); + const result = await driver.close(); if (isError(result)) throw result; }); it("should synchronize the store and insert a record", async () => { - const result = await store.sync(schema); + const result = await driver.sync(schema); if (isError(result)) throw result; - const insertResult = await store.insert(schema.users, { + const insertResult = await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", @@ -35,7 +39,9 @@ describe("SQLite Store Driver", () => { if (isError(insertResult)) throw insertResult; - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -43,19 +49,21 @@ describe("SQLite Store Driver", () => { }); it("should be update a record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - const err = await store.update(schema.users, "1", { name: "updated" }); + const err = await driver.update(schema.users, "1", { name: "updated" }); if (isError(err)) throw err; - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "updated", streamId: "1", streamVersion: 1n }, @@ -63,39 +71,43 @@ describe("SQLite Store Driver", () => { }); it("should be able to delete a record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.delete(schema.users, "1"); + await driver.delete(schema.users, "1"); - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([]); }); it("should be able to select records", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1n, }); - const records = await store.select(schema.users, { from: "users" }); + const records = ( + await driver.select(schema, { from: "users" }) + ).unwrapOrThrow(); expect(records).toEqual([ { id: "1", name: "test", streamId: "1", streamVersion: 1n }, @@ -104,22 +116,24 @@ describe("SQLite Store Driver", () => { }); it("should be able to select one record", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1n, }); - const record = await store.selectOne(schema.users, { from: "users" }); + const record = ( + await driver.selectOne(schema, { from: "users" }) + ).unwrapOrThrow(); expect(record).toEqual({ id: "1", @@ -130,25 +144,27 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - where: { name: isLike("te%") }, - }); + const result = ( + await driver.select(schema, { + from: "users", + where: { name: isLike("te%") }, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { @@ -161,25 +177,27 @@ describe("SQLite Store Driver", () => { }); it("should select a record with a where clause of a specific type", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - where: { streamVersion: 1n }, - }); + const result = ( + await driver.select(schema, { + from: "users", + where: { streamVersion: 1n }, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { @@ -198,26 +216,28 @@ describe("SQLite Store Driver", () => { }); it("should select with a limit and offset", async () => { - await store.sync(schema); + await driver.sync(schema); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1n, }); - await store.insert(schema.users, { + await driver.insert(schema.users, { id: "2", name: "jamón", streamId: "2", streamVersion: 1n, }); - const result = await store.select(schema.users, { - from: "users", - limit: 1, - offset: 1, - }); + const result = ( + await driver.select(schema, { + from: "users", + limit: 1, + offset: 1, + }) + ).unwrapOrThrow(); expect(result).toEqual([ { diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 2071776..f5d4806 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -39,10 +39,6 @@ export class SQLiteStorageDriver implements StorageDriver { constructor(private path: string) { this.db = new Database(path); - - // Enable Write-Ahead Logging, which is faster and more reliable. - this.db.run("PRAGMA journal_mode = WAL;"); - this.db.run("PRAGMA foreign_keys = ON;"); } /** @@ -93,53 +89,65 @@ export class SQLiteStorageDriver implements StorageDriver { model: Model, record: Record, ): AsyncResult { - try { - const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, recordToSQLParams(model, record)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; + const stmt = await this.getOrCreatePreparedStatement(sql); + return await run(stmt, recordToSQLParams(model, record)); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); } /** * Run a select query against the store. */ async select( - collection: Collection, + schema: ModelSchema, query: QueryDefinition, ): AsyncResult { - try { - const [stmt, params] = await this.getSelectStatement(collection, query); - return await getAll(stmt, params, transformRow(collection)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - query, - }); - } + return AsyncResult.tryFrom( + async () => { + const [stmt, params] = await this.getSelectStatement( + schema[query.from], + query, + ); + return await getAll(stmt, params, transformRow(schema[query.from])); + }, + (err) => + new StoreQueryError(err.message, { + err, + query, + }), + ); } /** * Run a select query against the store. */ async selectOne( - collection: Collection, + schema: ModelSchema, query: QueryDefinition, ): AsyncResult { - try { - const [stmt, params] = await this.getSelectStatement(collection, query); - return await getOne(stmt, params, transformRow(collection)); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - query, - }); - } + return AsyncResult.tryFrom( + async () => { + const [stmt, params] = await this.getSelectStatement( + schema[query.from], + query, + ); + return await getOne(stmt, params, transformRow(schema[query.from])); + }, + (err) => + new StoreQueryError(err.message, { + err, + query, + }), + ); } /** @@ -148,48 +156,56 @@ export class SQLiteStorageDriver implements StorageDriver { async sync( schema: ModelSchema, ): AsyncResult { - try { - await dbRun(this.db, "BEGIN TRANSACTION;"); - for (const modelKey in schema) { - const model = schema[modelKey]; - await dbRun(this.db, modelToSql(model)); - } - await dbRun(this.db, "COMMIT;"); - } catch (error: any) { - await dbRun(this.db, "ROLLBACK;"); - return new StoreQueryError(error.message, { - error, - schema, - }); - } + return AsyncResult.tryFrom( + async () => { + // Enable Write-Ahead Logging, which is faster and more reliable. + await dbRun(this.db, "PRAGMA journal_mode = WAL;"); + + // Enable foreign key constraints. + await dbRun(this.db, "PRAGMA foreign_keys = ON;"); + + // Begin a transaction to create the schema. + await dbRun(this.db, "BEGIN TRANSACTION;"); + for (const modelKey in schema) { + const model = schema[modelKey]; + await dbRun(this.db, modelToSql(model)); + } + await dbRun(this.db, "COMMIT;"); + }, + (error) => + new StoreQueryError(error.message, { + error, + schema, + }), + ); } /** * Drop the store. This is a destructive operation. */ async drop(): AsyncResult { - try { - if (this.path === ":memory:") { - return new StoreQueryError("Cannot drop in-memory database", {}); - } else { - await unlink(this.path); - } - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - }); - } + return AsyncResult.tryFrom( + async () => { + if (this.path === ":memory:") { + throw "Cannot drop in-memory database"; + } else { + await unlink(this.path); + } + }, + (error) => + new StoreQueryError(error.message, { + error, + }), + ); } async close(): AsyncResult { - try { + return AsyncResult.from(async () => { for (const stmt of this.cachedStatements.values()) { await finalize(stmt); } await dbClose(this.db); - } catch (error: any) { - return new UnexpectedError({ error }); - } + }); } /** @@ -200,21 +216,23 @@ export class SQLiteStorageDriver implements StorageDriver { id: string, record: Record, ): AsyncResult { - try { - const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; - const stmt = await this.getOrCreatePreparedStatement(sql); - const params = recordToSQLParams(model, { - ...record, - id, - }); - return await run(stmt, params); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - record, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; + const stmt = await this.getOrCreatePreparedStatement(sql); + const params = recordToSQLParams(model, { + ...record, + id, + }); + return await run(stmt, params); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + record, + }), + ); } /** @@ -222,16 +240,18 @@ export class SQLiteStorageDriver implements StorageDriver { */ async delete(model: Model, id: string): AsyncResult { - try { - const sql = `DELETE FROM ${model.name} WHERE id = :id`; - const stmt = await this.getOrCreatePreparedStatement(sql); - return await run(stmt, { ":id": id }); - } catch (error: any) { - return new StoreQueryError(error.message, { - error, - collectionName: model.name, - id, - }); - } + return AsyncResult.tryFrom( + async () => { + const sql = `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`; + const stmt = await this.getOrCreatePreparedStatement(sql); + return await run(stmt, { [keyToParam("id")]: id }); + }, + (error) => + new StoreQueryError(error.message, { + error, + collectionName: model.name, + id, + }), + ); } } diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts index 855c564..4a58620 100644 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Database, Statement } from "sqlite3"; -export function dbRun(db: Database, statement: string): Promise { +export function dbRun(db: Database, statement: string): Promise { return new Promise((resolve, reject) => { - db.run(statement, (err) => { + db.all(statement, (err, result) => { if (err) { reject(err); } else { - resolve(); + resolve(result); } }); }); diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/value-to-sql.ts index d55618a..e6a45b0 100644 --- a/packages/fabric/store-sqlite/src/value-to-sql.ts +++ b/packages/fabric/store-sqlite/src/value-to-sql.ts @@ -20,6 +20,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = { ReferenceField: (f, v) => v, FloatField: (f, v) => v, DecimalField: (f, v) => v, + TimestampField: (f, v) => v.timestamp, + EmbeddedField: (f, v: string) => JSON.stringify(v), }; export function fieldValueToSQL(field: FieldDefinition, value: any) {