Compare commits

...

2 Commits

33 changed files with 746 additions and 253 deletions

View File

@ -3,7 +3,7 @@ import { TaggedVariant, VariantTag } from "../variant/index.js";
/** /**
* A TaggedError is a tagged variant with an error message. * A TaggedError is a tagged variant with an error message.
*/ */
export class TaggedError<Tag extends string> export class TaggedError<Tag extends string = string>
extends Error extends Error
implements TaggedVariant<Tag> implements TaggedVariant<Tag>
{ {

View File

@ -2,6 +2,7 @@ export * from "./array/index.js";
export * from "./error/index.js"; export * from "./error/index.js";
export * from "./record/index.js"; export * from "./record/index.js";
export * from "./result/index.js"; export * from "./result/index.js";
export * from "./run/index.js";
export * from "./time/index.js"; export * from "./time/index.js";
export * from "./types/index.js"; export * from "./types/index.js";
export * from "./variant/index.js"; export * from "./variant/index.js";

View File

@ -1,11 +1,32 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { TaggedError } from "../error/tagged-error.js"; import { TaggedError } from "../error/tagged-error.js";
import { UnexpectedError } from "../error/unexpected-error.js";
import { Result } from "./result.js"; import { Result } from "./result.js";
/** /**
* Un AsyncResult representa el resultado de una operación asíncrona que puede * An AsyncResult represents the result of an asynchronous operation that can
* resolver en un valor de tipo `TValue` o en un error de tipo `TError`. * resolve to a value of type `TValue` or an error of type `TError`.
*/ */
export type AsyncResult< export type AsyncResult<
TValue, TValue = any,
TError extends TaggedError<string> = never, TError extends TaggedError = never,
> = Promise<Result<TValue, TError>>; > = Promise<Result<TValue, TError>>;
export namespace AsyncResult {
export async function tryFrom<T, TError extends TaggedError>(
fn: () => Promise<T>,
errorMapper: (error: any) => TError,
): AsyncResult<T, TError> {
try {
return Result.succeedWith(await fn());
} catch (error) {
return Result.failWith(errorMapper(error));
}
}
export async function from<T>(
fn: () => Promise<T>,
): AsyncResult<T, UnexpectedError> {
return tryFrom(fn, (error) => new UnexpectedError(error));
}
}

View File

@ -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<number, UnexpectedError>;
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value).toEqual(1);
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
}
});
});
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<Result<never, UnexpectedError>>();
}
});
});
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<Result<number, never>>();
});
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();
});
});
});

View File

@ -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 { TaggedError } from "../error/tagged-error.js";
import { UnexpectedError } from "../error/unexpected-error.js";
/** /**
* Un Result representa el resultado de una operación * A Result represents the outcome of an operation
* que puede ser un valor de tipo `TValue` o un error `TError`. * that can be either a value of type `TValue` or an error `TError`.
*/ */
export type Result< export class Result<TValue, TError extends TaggedError = never> {
TValue, static succeedWith<T>(value: T): Result<T, never> {
TError extends TaggedError<string> = UnexpectedError, return new Result<T, never>(value);
> = TValue | TError; }
static failWith<T extends TaggedError>(error: T): Result<never, T> {
return new Result<never, T>(error);
}
static ok(): Result<void, never>;
static ok<T>(value: T): Result<T, never>;
static ok(value?: any) {
return new Result(value ?? undefined);
}
static tryFrom<T, TError extends TaggedError>(
fn: () => T,
errorMapper: (error: any) => TError,
): Result<T, TError> {
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<TValue, never> {
return !isError(this.value);
}
/**
* Check if the result is an error.
*/
isError(): this is Result<never, TError> {
return isError(this.value);
}
/**
* Map a function over the value of the result.
*/
map<TMappedValue>(
fn: (value: TValue) => TMappedValue,
): Result<TMappedValue, TError> {
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<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => Result<TMappedValue, TMappedError>,
): Result<TMappedValue, TError | TMappedError> {
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<TMappedValue>(
fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError,
): Result<TMappedValue, TError> {
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<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError,
): Result<TValue, TMappedError> {
if (isError(this.value)) {
return Result.failWith(fn(this.value as TError));
}
return this as unknown as Result<TValue, TMappedError>;
}
/**
* 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<TValue, TError> {
if (!isError(this.value)) {
fn(this.value as TValue);
}
return this;
}
}

View File

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

View File

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

View File

@ -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<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): AsyncResult<T2, TE1 | TE2>;
// prettier-ignore
export async function seq<
T1, TE1 extends TaggedError,
T2, TE2 extends TaggedError,
T3, TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): AsyncResult<T3, TE1 | TE2 | TE3>;
export async function seq(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): AsyncResult<any, any> {
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<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): Promise<T2>;
// prettier-ignore
export async function seqUNSAFE<
T1,TE1 extends TaggedError,
T2,TE2 extends TaggedError,
T3,TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): Promise<T2>;
export async function seqUNSAFE(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): Promise<any> {
const result = await (seq as any)(...fns);
if (result.isError()) {
throw result.unwrapOrThrow();
}
return result.unwrapOrThrow();
}
export async function UNSAFE<T, TError extends TaggedError>(
fn: () => AsyncResult<T, TError>,
): Promise<T> {
return (await fn()).unwrapOrThrow();
}
}

View File

@ -1,9 +1,38 @@
import { isRecord } from "../record/is-record.js";
import { TaggedVariant } from "../variant/variant.js"; import { TaggedVariant } from "../variant/variant.js";
export class PosixDate { 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"> { export interface TimeZone extends TaggedVariant<"TimeZone"> {
timestamp: number; timestamp: number;
} }
export interface PosixDateJSON {
type: "posix-date";
timestamp: number;
}

View File

@ -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<T = any> extends BaseField {}
export interface EmbeddedField<T = any>
extends TaggedVariant<"EmbeddedField">,
EmbeddedFieldOptions<T> {}
export function createEmbeddedField<
K = any,
T extends EmbeddedFieldOptions<K> = EmbeddedFieldOptions<K>,
>(opts: T = {} as T): EmbeddedField & T {
return {
[VariantTag]: "EmbeddedField",
...opts,
} as const;
}

View File

@ -1,10 +1,13 @@
import { PosixDate } from "@fabric/core";
import { Decimal } from "decimal.js"; import { Decimal } from "decimal.js";
import { UUID } from "../../types/uuid.js"; import { UUID } from "../../types/uuid.js";
import { DecimalField } from "./decimal.js"; import { DecimalField } from "./decimal.js";
import { EmbeddedField } from "./embedded.js";
import { FloatField } from "./float.js"; import { FloatField } from "./float.js";
import { IntegerField } from "./integer.js"; import { IntegerField } from "./integer.js";
import { ReferenceField } from "./reference-field.js"; import { ReferenceField } from "./reference-field.js";
import { StringField } from "./string-field.js"; import { StringField } from "./string-field.js";
import { TimestampField } from "./timestamp.js";
import { UUIDField } from "./uuid-field.js"; import { UUIDField } from "./uuid-field.js";
/** /**
@ -18,6 +21,8 @@ export type FieldToType<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 EmbeddedField<infer TSubModel> ? MaybeOptional<TField, TSubModel>
: never; : never;
//prettier-ignore //prettier-ignore

View File

@ -1,8 +1,10 @@
import { createDecimalField, DecimalField } from "./decimal.js"; import { createDecimalField, DecimalField } from "./decimal.js";
import { createEmbeddedField, EmbeddedField } from "./embedded.js";
import { createFloatField, FloatField } from "./float.js"; import { createFloatField, FloatField } from "./float.js";
import { createIntegerField, IntegerField } from "./integer.js"; import { createIntegerField, IntegerField } from "./integer.js";
import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createReferenceField, ReferenceField } from "./reference-field.js";
import { createStringField, StringField } from "./string-field.js"; import { createStringField, StringField } from "./string-field.js";
import { createTimestampField, TimestampField } from "./timestamp.js";
import { createUUIDField, UUIDField } from "./uuid-field.js"; import { createUUIDField, UUIDField } from "./uuid-field.js";
export * from "./base-field.js"; export * from "./base-field.js";
export * from "./field-to-type.js"; export * from "./field-to-type.js";
@ -14,7 +16,9 @@ export type FieldDefinition =
| IntegerField | IntegerField
| FloatField | FloatField
| DecimalField | DecimalField
| ReferenceField; | ReferenceField
| TimestampField
| EmbeddedField;
export namespace Field { export namespace Field {
export const string = createStringField; export const string = createStringField;
@ -23,4 +27,6 @@ export namespace Field {
export const reference = createReferenceField; export const reference = createReferenceField;
export const decimal = createDecimalField; export const decimal = createDecimalField;
export const float = createFloatField; export const float = createFloatField;
export const timestamp = createTimestampField;
export const embedded = createEmbeddedField;
} }

View File

@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
import { defineModel } from "../model.js"; import { defineModel } from "../model.js";
import { Field } from "./index.js"; import { Field } from "./index.js";
import { import {
InvalidReferenceField, InvalidReferenceFieldError,
validateReferenceField, validateReferenceField,
} from "./reference-field.js"; } from "./reference-field.js";
@ -26,26 +26,18 @@ describe("Validate Reference Field", () => {
Field.reference({ Field.reference({
targetModel: "foo", targetModel: "foo",
}), }),
); ).unwrapErrorOrThrow();
if (!isError(result)) { expect(result).toBeInstanceOf(InvalidReferenceFieldError);
throw "Expected an error";
}
expect(result).toBeInstanceOf(InvalidReferenceField);
}); });
it("should not return an error if the target model is in the schema", () => { it("should not return an error if the target model is in the schema", () => {
const result = validateReferenceField( validateReferenceField(
schema, schema,
Field.reference({ Field.reference({
targetModel: "User", targetModel: "User",
}), }),
); ).unwrapOrThrow();
if (isError(result)) {
throw result.reason;
}
}); });
it("should return an error if the target key is not in the target model", () => { it("should return an error if the target key is not in the target model", () => {
@ -55,13 +47,9 @@ describe("Validate Reference Field", () => {
targetModel: "User", targetModel: "User",
targetKey: "foo", targetKey: "foo",
}), }),
); ).unwrapErrorOrThrow();
if (!isError(result)) { expect(result).toBeInstanceOf(InvalidReferenceFieldError);
throw "Expected an error";
}
expect(result).toBeInstanceOf(InvalidReferenceField);
}); });
it("should return error if the target key is not unique", () => { it("should return error if the target key is not unique", () => {
@ -71,13 +59,9 @@ describe("Validate Reference Field", () => {
targetModel: "User", targetModel: "User",
targetKey: "otherNotUnique", targetKey: "otherNotUnique",
}), }),
); ).unwrapErrorOrThrow();
if (!isError(result)) { expect(result).toBeInstanceOf(InvalidReferenceFieldError);
throw "Expected an error";
}
expect(result).toBeInstanceOf(InvalidReferenceField);
}); });
it("should not return an error if the target key is in the target model and is unique", () => { it("should not return an error if the target key is in the target model and is unique", () => {

View File

@ -27,16 +27,20 @@ export function getTargetKey(field: ReferenceField): string {
export function validateReferenceField( export function validateReferenceField(
schema: ModelSchema, schema: ModelSchema,
field: ReferenceField, field: ReferenceField,
): Result<void, InvalidReferenceField> { ): Result<void, InvalidReferenceFieldError> {
if (!schema[field.targetModel]) { if (!schema[field.targetModel]) {
return new InvalidReferenceField( return Result.failWith(
`The target model '${field.targetModel}' is not in the schema.`, new InvalidReferenceFieldError(
`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 new InvalidReferenceField( return Result.failWith(
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`, new InvalidReferenceFieldError(
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
),
); );
} }
@ -44,13 +48,17 @@ export function validateReferenceField(
field.targetKey && field.targetKey &&
!schema[field.targetModel].fields[field.targetKey].isUnique !schema[field.targetModel].fields[field.targetKey].isUnique
) { ) {
return new InvalidReferenceField( return Result.failWith(
`The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`, 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) { constructor(readonly reason: string) {
super("InvalidReferenceField"); super("InvalidReferenceField");
} }

View File

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

View File

@ -1,7 +1,7 @@
import { Model } from "./model.js"; import { Collection } from "./model.js";
export type ModelSchema = Record<string, Model>; export type ModelSchema = Record<string, Collection>;
export type ModelSchemaFromModels<TModels extends Model> = { export type ModelSchemaFromModels<TModels extends Collection> = {
[K in TModels["name"]]: Extract<TModels, { name: K }>; [K in TModels["name"]]: Extract<TModels, { name: K }>;
}; };

View File

@ -48,7 +48,7 @@ export function defineCollection<
} as const; } as const;
} }
export type ModelToType<TModel extends Model> = { export type ModelToType<TModel extends Collection> = {
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>; [K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
}; };

View File

@ -16,9 +16,9 @@ export type SingleFilterOption<T = any> = {
export type MultiFilterOption<T = any> = SingleFilterOption<T>[]; export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type"); export const FILTER_OPTION_TYPE_SYMBOL = "_filter_type";
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value"); export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value";
export const FILTER_OPTION_OPERATOR_SYMBOL = 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
? { ? {

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { AsyncResult, Keyof } from "@fabric/core"; import { AsyncResult, Keyof } from "@fabric/core";
import { StoreQueryError } from "../../errors/query-error.js"; import { StoreQueryError } from "../../errors/query-error.js";
import { StorageDriver } from "../../storage/storage-driver.js"; import { StorageDriver } from "../../storage/storage-driver.js";
@ -41,19 +42,23 @@ export class QueryBuilder<T> implements StoreQuery<T> {
}); });
} }
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>;
return this.driver.select(this.schema[this.query.from], { select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
return this.driver.select(this.schema, {
...this.query, ...this.query,
keys, keys,
}); });
} }
selectOne(): AsyncResult<T, StoreQueryError>;
selectOne<K extends Keyof<T>>( selectOne<K extends Keyof<T>>(
keys?: K[], keys: K,
): AsyncResult<Pick<T, K>, StoreQueryError> { ): AsyncResult<Pick<T, K>, StoreQueryError>;
return this.driver.selectOne(this.schema[this.query.from], { selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
return this.driver.selectOne(this.schema, {
...this.query, ...this.query,
keys, keys,
}); });

View File

@ -1,4 +1,4 @@
import { isError } from "@fabric/core"; import { isError, Run } from "@fabric/core";
import { SQLiteStorageDriver } from "@fabric/store-sqlite"; import { SQLiteStorageDriver } from "@fabric/store-sqlite";
import { import {
afterEach, afterEach,
@ -58,9 +58,7 @@ describe("State Store", () => {
if (isError(insertResult)) throw insertResult; if (isError(insertResult)) throw insertResult;
const result = await store.from("users").select(); const result = (await store.from("users").select()).unwrapOrThrow();
if (isError(result)) throw result;
expectTypeOf(result).toEqualTypeOf< expectTypeOf(result).toEqualTypeOf<
{ {
@ -83,34 +81,39 @@ describe("State Store", () => {
it("should query with a where clause", async () => { it("should query with a where clause", async () => {
const newUUID = UUIDGeneratorMock.generate(); const newUUID = UUIDGeneratorMock.generate();
await store.insertInto("users", {
name: "test",
id: newUUID,
streamId: newUUID,
streamVersion: 1n,
});
await store.insertInto("users", { await Run.seqUNSAFE(
name: "anotherName", () =>
id: UUIDGeneratorMock.generate(), store.insertInto("users", {
streamId: UUIDGeneratorMock.generate(), name: "test",
streamVersion: 1n, id: newUUID,
}); streamId: newUUID,
await store.insertInto("users", { streamVersion: 1n,
name: "anotherName2", }),
id: UUIDGeneratorMock.generate(), () =>
streamId: UUIDGeneratorMock.generate(), store.insertInto("users", {
streamVersion: 1n, 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 const result = await Run.UNSAFE(() =>
.from("users") store
.where({ .from("users")
name: isLike("te*"), .where({
}) name: isLike("te%"),
.select(); })
.select(),
if (isError(result)) throw result; );
expectTypeOf(result).toEqualTypeOf< expectTypeOf(result).toEqualTypeOf<
{ {

View File

@ -1,4 +1,5 @@
import { AsyncResult } from "@fabric/core"; import { AsyncResult } from "@fabric/core";
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
import { StorageDriver } from "../storage/storage-driver.js"; import { StorageDriver } from "../storage/storage-driver.js";
import { ModelSchemaFromModels } from "./model-schema.js"; import { ModelSchemaFromModels } from "./model-schema.js";
@ -20,8 +21,8 @@ export class StateStore<TModel extends Model> {
}, {} as ModelSchemaFromModels<TModel>); }, {} as ModelSchemaFromModels<TModel>);
} }
async migrate(): AsyncResult<void, StoreQueryError> { migrate(): AsyncResult<void, StoreQueryError | CircularDependencyError> {
await this.driver.sync(this.schema); return this.driver.sync(this.schema);
} }
async insertInto<T extends keyof ModelSchemaFromModels<TModel>>( async insertInto<T extends keyof ModelSchemaFromModels<TModel>>(

View File

@ -20,7 +20,7 @@ export interface StorageDriver {
* Run a select query against the store. * Run a select query against the store.
*/ */
select( select(
model: Collection, model: ModelSchema,
query: QueryDefinition, query: QueryDefinition,
): AsyncResult<any[], StoreQueryError>; ): AsyncResult<any[], StoreQueryError>;
@ -28,7 +28,7 @@ export interface StorageDriver {
* Run a select query against the store. * Run a select query against the store.
*/ */
selectOne( selectOne(
model: Collection, model: ModelSchema,
query: QueryDefinition, query: QueryDefinition,
): AsyncResult<any, StoreQueryError>; ): AsyncResult<any, StoreQueryError>;

View File

@ -1,4 +1,5 @@
export * from "./base-64.js"; export * from "./base-64.js";
export * from "./decimal.js";
export * from "./email.js"; export * from "./email.js";
export * from "./entity.js"; export * from "./entity.js";
export * from "./semver.js"; export * from "./semver.js";

View File

@ -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<T>(json: string): T {
return JSON.parse(json, reviver);
}
}

View File

@ -15,7 +15,7 @@ describe("sortByDependencies", () => {
const result = sortByDependencies(array, { const result = sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}); }).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ id: 3, name: "C", dependencies: [] }, { id: 3, name: "C", dependencies: [] },
@ -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(),
).toBeInstanceOf(CircularDependencyError); ).toBeInstanceOf(CircularDependencyError);
}); });
@ -45,7 +45,7 @@ describe("sortByDependencies", () => {
const result = sortByDependencies(array, { const result = sortByDependencies(array, {
keyGetter: (element) => element.name, keyGetter: (element) => element.name,
depGetter: (element) => element.dependencies, depGetter: (element) => element.dependencies,
}); }).unwrapOrThrow();
expect(result).toEqual([]); expect(result).toEqual([]);
}); });

View File

@ -34,14 +34,15 @@ export function sortByDependencies<T>(
visited.add(key); visited.add(key);
sorted.push(key); sorted.push(key);
}; };
try { return Result.tryFrom(
graph.forEach((deps, key) => { () => {
visit(key, []); 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 sorted.map( );
(key) => array.find((element) => keyGetter(element) === key) as T, },
(e) => e as CircularDependencyError,
); );
} }

View File

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

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Variant, VariantTag } from "@fabric/core"; 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 = { type FieldSQLDefinitionMap = {
[K in FieldDefinition[VariantTag]]: ( [K in FieldDefinition[VariantTag]]: (
@ -19,7 +21,9 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
"TEXT", "TEXT",
f.isPrimaryKey ? "PRIMARY KEY" : "", f.isPrimaryKey ? "PRIMARY KEY" : "",
modifiersFromOpts(f), modifiersFromOpts(f),
].join(" "); ]
.filter((x) => x)
.join(" ");
}, },
IntegerField: (n, f): string => { IntegerField: (n, f): string => {
return [n, "INTEGER", modifiersFromOpts(f)].join(" "); return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
@ -29,8 +33,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
n, n,
"TEXT", "TEXT",
modifiersFromOpts(f), modifiersFromOpts(f),
",", `REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
`FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
].join(" "); ].join(" ");
}, },
FloatField: (n, f): string => { FloatField: (n, f): string => {
@ -39,6 +42,12 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
DecimalField: (n, f): string => { DecimalField: (n, f): string => {
return [n, "REAL", modifiersFromOpts(f)].join(" "); 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) { function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
@ -48,17 +57,17 @@ function modifiersFromOpts(field: FieldDefinition) {
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
return; return;
} }
return [ return [!field.isOptional ? "NOT NULL" : "", field.isUnique ? "UNIQUE" : ""]
!field.isOptional ? "NOT NULL" : "", .filter((x) => x)
field.isUnique ? "UNIQUE" : "", .join(" ");
].join(" ");
} }
export function modelToSql( export function modelToSql(
model: Model<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))
.filter((x) => x)
.join(", "); .join(", ");
return `CREATE TABLE ${model.name} (${fields})`; return `CREATE TABLE ${model.name} (${fields})`;

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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"; import { Collection, FieldDefinition, FieldToType } from "@fabric/domain";
export function transformRow(model: Collection) { export function transformRow(model: Collection) {
@ -36,4 +36,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
ReferenceField: (f, v) => v, ReferenceField: (f, v) => v,
FloatField: (f, v) => v, FloatField: (f, v) => v,
DecimalField: (f, v) => v, DecimalField: (f, v) => v,
TimestampField: (f, v) => new PosixDate(v),
EmbeddedField: (f, v: string) => JSON.parse(v),
}; };

View File

@ -5,28 +5,32 @@ import { SQLiteStorageDriver } from "./sqlite-driver.js";
describe("SQLite Store Driver", () => { describe("SQLite Store Driver", () => {
const schema = { const schema = {
demo: defineModel("demo", {
value: Field.float(),
owner: Field.reference({ targetModel: "users" }),
}),
users: defineModel("users", { users: defineModel("users", {
name: Field.string(), name: Field.string(),
}), }),
}; };
let store: SQLiteStorageDriver; let driver: SQLiteStorageDriver;
beforeEach(() => { beforeEach(() => {
store = new SQLiteStorageDriver(":memory:"); driver = new SQLiteStorageDriver(":memory:");
}); });
afterEach(async () => { afterEach(async () => {
const result = await store.close(); const result = await driver.close();
if (isError(result)) throw result; if (isError(result)) throw result;
}); });
it("should synchronize the store and insert a record", async () => { 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; if (isError(result)) throw result;
const insertResult = await store.insert(schema.users, { const insertResult = await driver.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
@ -35,7 +39,9 @@ describe("SQLite Store Driver", () => {
if (isError(insertResult)) throw insertResult; 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([ expect(records).toEqual([
{ id: "1", name: "test", streamId: "1", streamVersion: 1n }, { id: "1", name: "test", streamId: "1", streamVersion: 1n },
@ -43,19 +49,21 @@ describe("SQLite Store Driver", () => {
}); });
it("should be update a record", async () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, 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; 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([ expect(records).toEqual([
{ id: "1", name: "updated", streamId: "1", streamVersion: 1n }, { 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 () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, 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([]); expect(records).toEqual([]);
}); });
it("should be able to select records", async () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await store.insert(schema.users, { await driver.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const records = await store.select(schema.users, { from: "users" }); const records = (
await driver.select(schema, { from: "users" })
).unwrapOrThrow();
expect(records).toEqual([ expect(records).toEqual([
{ id: "1", name: "test", streamId: "1", streamVersion: 1n }, { 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 () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await store.insert(schema.users, { await driver.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const record = await store.selectOne(schema.users, { from: "users" }); const record = (
await driver.selectOne(schema, { from: "users" })
).unwrapOrThrow();
expect(record).toEqual({ expect(record).toEqual({
id: "1", id: "1",
@ -130,25 +144,27 @@ describe("SQLite Store Driver", () => {
}); });
it("should select a record with a where clause", async () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await store.insert(schema.users, { await driver.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = await store.select(schema.users, { const result = (
from: "users", await driver.select(schema, {
where: { name: isLike("te%") }, from: "users",
}); where: { name: isLike("te%") },
})
).unwrapOrThrow();
expect(result).toEqual([ 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 () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await store.insert(schema.users, { await driver.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = await store.select(schema.users, { const result = (
from: "users", await driver.select(schema, {
where: { streamVersion: 1n }, from: "users",
}); where: { streamVersion: 1n },
})
).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ {
@ -198,26 +216,28 @@ describe("SQLite Store Driver", () => {
}); });
it("should select with a limit and offset", async () => { 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", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await store.insert(schema.users, { await driver.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = await store.select(schema.users, { const result = (
from: "users", await driver.select(schema, {
limit: 1, from: "users",
offset: 1, limit: 1,
}); offset: 1,
})
).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ {

View File

@ -39,10 +39,6 @@ export class SQLiteStorageDriver implements StorageDriver {
constructor(private path: string) { constructor(private path: string) {
this.db = new Database(path); 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, model: Model,
record: Record<string, any>, record: Record<string, any>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
try { return AsyncResult.tryFrom(
const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`; async () => {
const stmt = await this.getOrCreatePreparedStatement(sql); const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`;
return await run(stmt, recordToSQLParams(model, record)); const stmt = await this.getOrCreatePreparedStatement(sql);
} catch (error: any) { return await run(stmt, recordToSQLParams(model, record));
return new StoreQueryError(error.message, { },
error, (error) =>
collectionName: model.name, new StoreQueryError(error.message, {
record, error,
}); collectionName: model.name,
} record,
}),
);
} }
/** /**
* Run a select query against the store. * Run a select query against the store.
*/ */
async select( async select(
collection: Collection, schema: ModelSchema,
query: QueryDefinition, query: QueryDefinition,
): AsyncResult<any[], StoreQueryError> { ): AsyncResult<any[], StoreQueryError> {
try { return AsyncResult.tryFrom(
const [stmt, params] = await this.getSelectStatement(collection, query); async () => {
return await getAll(stmt, params, transformRow(collection)); const [stmt, params] = await this.getSelectStatement(
} catch (error: any) { schema[query.from],
return new StoreQueryError(error.message, { query,
error, );
query, return await getAll(stmt, params, transformRow(schema[query.from]));
}); },
} (err) =>
new StoreQueryError(err.message, {
err,
query,
}),
);
} }
/** /**
* Run a select query against the store. * Run a select query against the store.
*/ */
async selectOne( async selectOne(
collection: Collection, schema: ModelSchema,
query: QueryDefinition, query: QueryDefinition,
): AsyncResult<any, StoreQueryError> { ): AsyncResult<any, StoreQueryError> {
try { return AsyncResult.tryFrom(
const [stmt, params] = await this.getSelectStatement(collection, query); async () => {
return await getOne(stmt, params, transformRow(collection)); const [stmt, params] = await this.getSelectStatement(
} catch (error: any) { schema[query.from],
return new StoreQueryError(error.message, { query,
error, );
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( async sync(
schema: ModelSchema, schema: ModelSchema,
): AsyncResult<void, StoreQueryError | CircularDependencyError> { ): AsyncResult<void, StoreQueryError | CircularDependencyError> {
try { return AsyncResult.tryFrom(
await dbRun(this.db, "BEGIN TRANSACTION;"); async () => {
for (const modelKey in schema) { // Enable Write-Ahead Logging, which is faster and more reliable.
const model = schema[modelKey]; await dbRun(this.db, "PRAGMA journal_mode = WAL;");
await dbRun(this.db, modelToSql(model));
} // Enable foreign key constraints.
await dbRun(this.db, "COMMIT;"); await dbRun(this.db, "PRAGMA foreign_keys = ON;");
} catch (error: any) {
await dbRun(this.db, "ROLLBACK;"); // Begin a transaction to create the schema.
return new StoreQueryError(error.message, { await dbRun(this.db, "BEGIN TRANSACTION;");
error, for (const modelKey in schema) {
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. * Drop the store. This is a destructive operation.
*/ */
async drop(): AsyncResult<void, StoreQueryError> { async drop(): AsyncResult<void, StoreQueryError> {
try { return AsyncResult.tryFrom(
if (this.path === ":memory:") { async () => {
return new StoreQueryError("Cannot drop in-memory database", {}); if (this.path === ":memory:") {
} else { throw "Cannot drop in-memory database";
await unlink(this.path); } else {
} await unlink(this.path);
} catch (error: any) { }
return new StoreQueryError(error.message, { },
error, (error) =>
}); new StoreQueryError(error.message, {
} error,
}),
);
} }
async close(): AsyncResult<void, UnexpectedError> { async close(): AsyncResult<void, UnexpectedError> {
try { return AsyncResult.from(async () => {
for (const stmt of this.cachedStatements.values()) { for (const stmt of this.cachedStatements.values()) {
await finalize(stmt); await finalize(stmt);
} }
await dbClose(this.db); await dbClose(this.db);
} catch (error: any) { });
return new UnexpectedError({ error });
}
} }
/** /**
@ -200,21 +216,23 @@ export class SQLiteStorageDriver implements StorageDriver {
id: string, id: string,
record: Record<string, any>, record: Record<string, any>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
try { return AsyncResult.tryFrom(
const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`; async () => {
const stmt = await this.getOrCreatePreparedStatement(sql); const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`;
const params = recordToSQLParams(model, { const stmt = await this.getOrCreatePreparedStatement(sql);
...record, const params = recordToSQLParams(model, {
id, ...record,
}); id,
return await run(stmt, params); });
} catch (error: any) { return await run(stmt, params);
return new StoreQueryError(error.message, { },
error, (error) =>
collectionName: model.name, new StoreQueryError(error.message, {
record, error,
}); collectionName: model.name,
} record,
}),
);
} }
/** /**
@ -222,16 +240,18 @@ export class SQLiteStorageDriver implements StorageDriver {
*/ */
async delete(model: Model, id: string): AsyncResult<void, StoreQueryError> { async delete(model: Model, id: string): AsyncResult<void, StoreQueryError> {
try { return AsyncResult.tryFrom(
const sql = `DELETE FROM ${model.name} WHERE id = :id`; async () => {
const stmt = await this.getOrCreatePreparedStatement(sql); const sql = `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`;
return await run(stmt, { ":id": id }); const stmt = await this.getOrCreatePreparedStatement(sql);
} catch (error: any) { return await run(stmt, { [keyToParam("id")]: id });
return new StoreQueryError(error.message, { },
error, (error) =>
collectionName: model.name, new StoreQueryError(error.message, {
id, error,
}); collectionName: model.name,
} id,
}),
);
} }
} }

View File

@ -1,13 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Database, Statement } from "sqlite3"; import { Database, Statement } from "sqlite3";
export function dbRun(db: Database, statement: string): Promise<void> { export function dbRun(db: Database, statement: string): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.run(statement, (err) => { db.all(statement, (err, result) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(); resolve(result);
} }
}); });
}); });

View File

@ -20,6 +20,8 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
ReferenceField: (f, v) => v, ReferenceField: (f, v) => v,
FloatField: (f, v) => v, FloatField: (f, v) => v,
DecimalField: (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) { export function fieldValueToSQL(field: FieldDefinition, value: any) {