[fabric/domain] Improve Result & AsyncResult types; Refactor model types; Add timestamp and embedded field support
This commit is contained in:
parent
f30d2c47c5
commit
4fff9f91f5
@ -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>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
57
packages/fabric/core/src/result/result.spec.ts
Normal file
57
packages/fabric/core/src/result/result.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1
packages/fabric/core/src/run/index.ts
Normal file
1
packages/fabric/core/src/run/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./run.js";
|
||||||
28
packages/fabric/core/src/run/run.spec.ts
Normal file
28
packages/fabric/core/src/run/run.spec.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
75
packages/fabric/core/src/run/run.ts
Normal file
75
packages/fabric/core/src/run/run.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
20
packages/fabric/domain/src/models/fields/embedded.ts
Normal file
20
packages/fabric/domain/src/models/fields/embedded.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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", () => {
|
||||||
|
|||||||
@ -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(
|
||||||
|
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 new InvalidReferenceField(
|
return Result.failWith(
|
||||||
|
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}'.`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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(
|
||||||
|
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 InvalidReferenceField extends TaggedError<"InvalidReferenceField"> {
|
export class InvalidReferenceFieldError extends TaggedError<"InvalidReferenceField"> {
|
||||||
constructor(readonly reason: string) {
|
constructor(readonly reason: string) {
|
||||||
super("InvalidReferenceField");
|
super("InvalidReferenceField");
|
||||||
}
|
}
|
||||||
|
|||||||
18
packages/fabric/domain/src/models/fields/timestamp.ts
Normal file
18
packages/fabric/domain/src/models/fields/timestamp.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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 }>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
? {
|
? {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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", {
|
|
||||||
|
await Run.seqUNSAFE(
|
||||||
|
() =>
|
||||||
|
store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
streamId: newUUID,
|
streamId: newUUID,
|
||||||
streamVersion: 1n,
|
streamVersion: 1n,
|
||||||
});
|
}),
|
||||||
|
() =>
|
||||||
await store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "anotherName",
|
name: "anotherName",
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
streamId: UUIDGeneratorMock.generate(),
|
streamId: UUIDGeneratorMock.generate(),
|
||||||
streamVersion: 1n,
|
streamVersion: 1n,
|
||||||
});
|
}),
|
||||||
await store.insertInto("users", {
|
() =>
|
||||||
|
store.insertInto("users", {
|
||||||
name: "anotherName2",
|
name: "anotherName2",
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
streamId: UUIDGeneratorMock.generate(),
|
streamId: UUIDGeneratorMock.generate(),
|
||||||
streamVersion: 1n,
|
streamVersion: 1n,
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result = await store
|
const result = await Run.UNSAFE(() =>
|
||||||
|
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<
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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>>(
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|
||||||
|
|||||||
14
packages/fabric/domain/src/utils/json-utils.ts
Normal file
14
packages/fabric/domain/src/utils/json-utils.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) => {
|
graph.forEach((deps, key) => {
|
||||||
visit(key, []);
|
visit(key, []);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
|
||||||
return e as CircularDependencyError;
|
|
||||||
}
|
|
||||||
return sorted.map(
|
return sorted.map(
|
||||||
(key) => array.find((element) => keyGetter(element) === key) as T,
|
(key) => array.find((element) => keyGetter(element) === key) as T,
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
(e) => e as CircularDependencyError,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
22
packages/fabric/store-sqlite/src/model-to-sql.spec.ts
Normal file
22
packages/fabric/store-sqlite/src/model-to-sql.spec.ts
Normal 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))`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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})`;
|
||||||
|
|||||||
@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
await driver.select(schema, {
|
||||||
from: "users",
|
from: "users",
|
||||||
where: { name: isLike("te%") },
|
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 = (
|
||||||
|
await driver.select(schema, {
|
||||||
from: "users",
|
from: "users",
|
||||||
where: { streamVersion: 1n },
|
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 = (
|
||||||
|
await driver.select(schema, {
|
||||||
from: "users",
|
from: "users",
|
||||||
limit: 1,
|
limit: 1,
|
||||||
offset: 1,
|
offset: 1,
|
||||||
});
|
})
|
||||||
|
).unwrapOrThrow();
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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(
|
||||||
|
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) =>
|
||||||
|
new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName: model.name,
|
||||||
record,
|
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, {
|
|
||||||
error,
|
|
||||||
query,
|
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, {
|
|
||||||
error,
|
|
||||||
query,
|
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(
|
||||||
|
async () => {
|
||||||
|
// Enable Write-Ahead Logging, which is faster and more reliable.
|
||||||
|
await dbRun(this.db, "PRAGMA journal_mode = WAL;");
|
||||||
|
|
||||||
|
// Enable foreign key constraints.
|
||||||
|
await dbRun(this.db, "PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
|
// Begin a transaction to create the schema.
|
||||||
await dbRun(this.db, "BEGIN TRANSACTION;");
|
await dbRun(this.db, "BEGIN TRANSACTION;");
|
||||||
for (const modelKey in schema) {
|
for (const modelKey in schema) {
|
||||||
const model = schema[modelKey];
|
const model = schema[modelKey];
|
||||||
await dbRun(this.db, modelToSql(model));
|
await dbRun(this.db, modelToSql(model));
|
||||||
}
|
}
|
||||||
await dbRun(this.db, "COMMIT;");
|
await dbRun(this.db, "COMMIT;");
|
||||||
} catch (error: any) {
|
},
|
||||||
await dbRun(this.db, "ROLLBACK;");
|
(error) =>
|
||||||
return new StoreQueryError(error.message, {
|
new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
schema,
|
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(
|
||||||
|
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) =>
|
||||||
|
new StoreQueryError(error.message, {
|
||||||
error,
|
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,7 +216,8 @@ 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(
|
||||||
|
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, {
|
||||||
@ -208,13 +225,14 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
return await run(stmt, params);
|
return await run(stmt, params);
|
||||||
} catch (error: any) {
|
},
|
||||||
return new StoreQueryError(error.message, {
|
(error) =>
|
||||||
|
new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName: model.name,
|
||||||
record,
|
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 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) =>
|
||||||
|
new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName: model.name,
|
||||||
id,
|
id,
|
||||||
});
|
}),
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user