Compare commits

..

No commits in common. "4fff9f91f5fcadb935242b051b04394d5e0ca4d8" and "14ca23ef74d011fc1bfc31f2e637caeeec95eb8d" have entirely different histories.

33 changed files with 253 additions and 746 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 = string> export class TaggedError<Tag extends string>
extends Error extends Error
implements TaggedVariant<Tag> implements TaggedVariant<Tag>
{ {

View File

@ -2,7 +2,6 @@ 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,32 +1,11 @@
/* 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";
/** /**
* An AsyncResult represents the result of an asynchronous operation that can * Un AsyncResult representa el resultado de una operación asíncrona que puede
* resolve to a value of type `TValue` or an error of type `TError`. * resolver en un valor de tipo `TValue` o en un error de tipo `TError`.
*/ */
export type AsyncResult< export type AsyncResult<
TValue = any, TValue,
TError extends TaggedError = never, TError extends TaggedError<string> = 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

@ -1,57 +0,0 @@
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,151 +1,11 @@
/* 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";
/** /**
* A Result represents the outcome of an operation * Un Result representa el resultado de una operación
* that can be either a value of type `TValue` or an error `TError`. * que puede ser un valor de tipo `TValue` o un error `TError`.
*/ */
export class Result<TValue, TError extends TaggedError = never> { export type Result<
static succeedWith<T>(value: T): Result<T, never> { TValue,
return new Result<T, never>(value); TError extends TaggedError<string> = UnexpectedError,
} > = 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

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

View File

@ -1,28 +0,0 @@
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

@ -1,75 +0,0 @@
/* 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,38 +1,9 @@
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 = Date.now()) {} constructor(public readonly timestamp: number) {}
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

@ -1,20 +0,0 @@
/* 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,13 +1,10 @@
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";
/** /**
@ -21,8 +18,6 @@ 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,10 +1,8 @@
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";
@ -16,9 +14,7 @@ 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;
@ -27,6 +23,4 @@ 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 {
InvalidReferenceFieldError, InvalidReferenceField,
validateReferenceField, validateReferenceField,
} from "./reference-field.js"; } from "./reference-field.js";
@ -26,18 +26,26 @@ describe("Validate Reference Field", () => {
Field.reference({ Field.reference({
targetModel: "foo", targetModel: "foo",
}), }),
).unwrapErrorOrThrow(); );
expect(result).toBeInstanceOf(InvalidReferenceFieldError); if (!isError(result)) {
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", () => {
validateReferenceField( const result = 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", () => {
@ -47,9 +55,13 @@ describe("Validate Reference Field", () => {
targetModel: "User", targetModel: "User",
targetKey: "foo", targetKey: "foo",
}), }),
).unwrapErrorOrThrow(); );
expect(result).toBeInstanceOf(InvalidReferenceFieldError); if (!isError(result)) {
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", () => {
@ -59,9 +71,13 @@ describe("Validate Reference Field", () => {
targetModel: "User", targetModel: "User",
targetKey: "otherNotUnique", targetKey: "otherNotUnique",
}), }),
).unwrapErrorOrThrow(); );
expect(result).toBeInstanceOf(InvalidReferenceFieldError); if (!isError(result)) {
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,20 +27,16 @@ export function getTargetKey(field: ReferenceField): string {
export function validateReferenceField( export function validateReferenceField(
schema: ModelSchema, schema: ModelSchema,
field: ReferenceField, field: ReferenceField,
): Result<void, InvalidReferenceFieldError> { ): Result<void, InvalidReferenceField> {
if (!schema[field.targetModel]) { if (!schema[field.targetModel]) {
return Result.failWith( return new InvalidReferenceField(
new InvalidReferenceFieldError( `The target model '${field.targetModel}' is not in the schema.`,
`The target model '${field.targetModel}' is not in the schema.`,
),
); );
} }
if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) { if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) {
return Result.failWith( return new InvalidReferenceField(
new InvalidReferenceFieldError( `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
),
); );
} }
@ -48,17 +44,13 @@ export function validateReferenceField(
field.targetKey && field.targetKey &&
!schema[field.targetModel].fields[field.targetKey].isUnique !schema[field.targetModel].fields[field.targetKey].isUnique
) { ) {
return Result.failWith( return new InvalidReferenceField(
new InvalidReferenceFieldError( `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
`The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
),
); );
} }
return Result.ok();
} }
export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> { export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> {
constructor(readonly reason: string) { constructor(readonly reason: string) {
super("InvalidReferenceField"); super("InvalidReferenceField");
} }

View File

@ -1,18 +0,0 @@
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 { Collection } from "./model.js"; import { Model } from "./model.js";
export type ModelSchema = Record<string, Collection>; export type ModelSchema = Record<string, Model>;
export type ModelSchemaFromModels<TModels extends Collection> = { export type ModelSchemaFromModels<TModels extends Model> = {
[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 Collection> = { export type ModelToType<TModel extends Model> = {
[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 = "_filter_type"; export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type");
export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value"; export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value");
export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator"; export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator");
export type LikeFilterOption<T> = T extends string export type LikeFilterOption<T> = T extends string
? { ? {

View File

@ -1,4 +1,3 @@
/* 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";
@ -42,23 +41,19 @@ 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> {
select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> { return this.driver.select(this.schema[this.query.from], {
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> {
selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> { return this.driver.selectOne(this.schema[this.query.from], {
return this.driver.selectOne(this.schema, {
...this.query, ...this.query,
keys, keys,
}); });

View File

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

View File

@ -1,5 +1,4 @@
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";
@ -21,8 +20,8 @@ export class StateStore<TModel extends Model> {
}, {} as ModelSchemaFromModels<TModel>); }, {} as ModelSchemaFromModels<TModel>);
} }
migrate(): AsyncResult<void, StoreQueryError | CircularDependencyError> { async migrate(): AsyncResult<void, StoreQueryError> {
return this.driver.sync(this.schema); await 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: ModelSchema, model: Collection,
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: ModelSchema, model: Collection,
query: QueryDefinition, query: QueryDefinition,
): AsyncResult<any, StoreQueryError>; ): AsyncResult<any, StoreQueryError>;

View File

@ -1,5 +1,4 @@
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

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

@ -1,22 +0,0 @@
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,8 +1,6 @@
/* 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 { Collection, FieldDefinition, getTargetKey } from "@fabric/domain"; import { FieldDefinition, getTargetKey, Model } 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]]: (
@ -21,9 +19,7 @@ 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(" ");
@ -33,7 +29,8 @@ 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 => {
@ -42,12 +39,6 @@ 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);
@ -57,17 +48,17 @@ function modifiersFromOpts(field: FieldDefinition) {
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
return; return;
} }
return [!field.isOptional ? "NOT NULL" : "", field.isUnique ? "UNIQUE" : ""] return [
.filter((x) => x) !field.isOptional ? "NOT NULL" : "",
.join(" "); field.isUnique ? "UNIQUE" : "",
].join(" ");
} }
export function modelToSql( export function modelToSql(
model: Collection<string, Record<string, FieldDefinition>>, model: Model<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 { PosixDate, VariantTag } from "@fabric/core"; import { 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,6 +36,4 @@ 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,32 +5,28 @@ 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 driver: SQLiteStorageDriver; let store: SQLiteStorageDriver;
beforeEach(() => { beforeEach(() => {
driver = new SQLiteStorageDriver(":memory:"); store = new SQLiteStorageDriver(":memory:");
}); });
afterEach(async () => { afterEach(async () => {
const result = await driver.close(); const result = await store.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 driver.sync(schema); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
const insertResult = await driver.insert(schema.users, { const insertResult = await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
@ -39,9 +35,7 @@ describe("SQLite Store Driver", () => {
if (isError(insertResult)) throw insertResult; if (isError(insertResult)) throw insertResult;
const records = ( const records = await store.select(schema.users, { from: "users" });
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 },
@ -49,21 +43,19 @@ describe("SQLite Store Driver", () => {
}); });
it("should be update a record", async () => { it("should be update a record", async () => {
await driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
const err = await driver.update(schema.users, "1", { name: "updated" }); const err = await store.update(schema.users, "1", { name: "updated" });
if (isError(err)) throw err; if (isError(err)) throw err;
const records = ( const records = await store.select(schema.users, { from: "users" });
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 },
@ -71,43 +63,39 @@ describe("SQLite Store Driver", () => {
}); });
it("should be able to delete a record", async () => { it("should be able to delete a record", async () => {
await driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.delete(schema.users, "1"); await store.delete(schema.users, "1");
const records = ( const records = await store.select(schema.users, { from: "users" });
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 driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const records = ( const records = await store.select(schema.users, { from: "users" });
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 },
@ -116,24 +104,22 @@ describe("SQLite Store Driver", () => {
}); });
it("should be able to select one record", async () => { it("should be able to select one record", async () => {
await driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const record = ( const record = await store.selectOne(schema.users, { from: "users" });
await driver.selectOne(schema, { from: "users" })
).unwrapOrThrow();
expect(record).toEqual({ expect(record).toEqual({
id: "1", id: "1",
@ -144,27 +130,25 @@ 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 driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = ( const result = await store.select(schema.users, {
await driver.select(schema, { from: "users",
from: "users", where: { name: isLike("te%") },
where: { name: isLike("te%") }, });
})
).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ {
@ -177,27 +161,25 @@ 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 driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = ( const result = await store.select(schema.users, {
await driver.select(schema, { from: "users",
from: "users", where: { streamVersion: 1n },
where: { streamVersion: 1n }, });
})
).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ {
@ -216,28 +198,26 @@ describe("SQLite Store Driver", () => {
}); });
it("should select with a limit and offset", async () => { it("should select with a limit and offset", async () => {
await driver.sync(schema); await store.sync(schema);
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1n, streamVersion: 1n,
}); });
await driver.insert(schema.users, { await store.insert(schema.users, {
id: "2", id: "2",
name: "jamón", name: "jamón",
streamId: "2", streamId: "2",
streamVersion: 1n, streamVersion: 1n,
}); });
const result = ( const result = await store.select(schema.users, {
await driver.select(schema, { from: "users",
from: "users", limit: 1,
limit: 1, offset: 1,
offset: 1, });
})
).unwrapOrThrow();
expect(result).toEqual([ expect(result).toEqual([
{ {

View File

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

View File

@ -20,8 +20,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) => v.timestamp,
EmbeddedField: (f, v: string) => JSON.stringify(v),
}; };
export function fieldValueToSQL(field: FieldDefinition, value: any) { export function fieldValueToSQL(field: FieldDefinition, value: any) {