Compare commits
No commits in common. "4fff9f91f5fcadb935242b051b04394d5e0ca4d8" and "14ca23ef74d011fc1bfc31f2e637caeeec95eb8d" have entirely different histories.
4fff9f91f5
...
14ca23ef74
@ -3,7 +3,7 @@ import { TaggedVariant, VariantTag } from "../variant/index.js";
|
||||
/**
|
||||
* 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
|
||||
implements TaggedVariant<Tag>
|
||||
{
|
||||
|
||||
@ -2,7 +2,6 @@ export * from "./array/index.js";
|
||||
export * from "./error/index.js";
|
||||
export * from "./record/index.js";
|
||||
export * from "./result/index.js";
|
||||
export * from "./run/index.js";
|
||||
export * from "./time/index.js";
|
||||
export * from "./types/index.js";
|
||||
export * from "./variant/index.js";
|
||||
|
||||
@ -1,32 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { TaggedError } from "../error/tagged-error.js";
|
||||
import { UnexpectedError } from "../error/unexpected-error.js";
|
||||
import { Result } from "./result.js";
|
||||
|
||||
/**
|
||||
* An AsyncResult represents the result of an asynchronous operation that can
|
||||
* resolve to a value of type `TValue` or an error of type `TError`.
|
||||
* Un AsyncResult representa el resultado de una operación asíncrona que puede
|
||||
* resolver en un valor de tipo `TValue` o en un error de tipo `TError`.
|
||||
*/
|
||||
export type AsyncResult<
|
||||
TValue = any,
|
||||
TError extends TaggedError = never,
|
||||
TValue,
|
||||
TError extends TaggedError<string> = never,
|
||||
> = 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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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 { UnexpectedError } from "../error/unexpected-error.js";
|
||||
|
||||
/**
|
||||
* A Result represents the outcome of an operation
|
||||
* that can be either a value of type `TValue` or an error `TError`.
|
||||
* Un Result representa el resultado de una operación
|
||||
* que puede ser un valor de tipo `TValue` o un error `TError`.
|
||||
*/
|
||||
export class Result<TValue, TError extends TaggedError = never> {
|
||||
static succeedWith<T>(value: T): Result<T, never> {
|
||||
return new Result<T, never>(value);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
export type Result<
|
||||
TValue,
|
||||
TError extends TaggedError<string> = UnexpectedError,
|
||||
> = TValue | TError;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./run.js";
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -1,38 +1,9 @@
|
||||
import { isRecord } from "../record/is-record.js";
|
||||
import { TaggedVariant } from "../variant/variant.js";
|
||||
|
||||
export class PosixDate {
|
||||
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;
|
||||
}
|
||||
constructor(public readonly timestamp: number) {}
|
||||
}
|
||||
|
||||
export interface TimeZone extends TaggedVariant<"TimeZone"> {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface PosixDateJSON {
|
||||
type: "posix-date";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1,13 +1,10 @@
|
||||
import { PosixDate } from "@fabric/core";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { UUID } from "../../types/uuid.js";
|
||||
import { DecimalField } from "./decimal.js";
|
||||
import { EmbeddedField } from "./embedded.js";
|
||||
import { FloatField } from "./float.js";
|
||||
import { IntegerField } from "./integer.js";
|
||||
import { ReferenceField } from "./reference-field.js";
|
||||
import { StringField } from "./string-field.js";
|
||||
import { TimestampField } from "./timestamp.js";
|
||||
import { UUIDField } from "./uuid-field.js";
|
||||
|
||||
/**
|
||||
@ -21,8 +18,6 @@ export type FieldToType<TField> =
|
||||
: TField extends ReferenceField ? MaybeOptional<TField, UUID>
|
||||
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
|
||||
: TField extends FloatField ? MaybeOptional<TField, number>
|
||||
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
|
||||
: TField extends EmbeddedField<infer TSubModel> ? MaybeOptional<TField, TSubModel>
|
||||
: never;
|
||||
|
||||
//prettier-ignore
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { createDecimalField, DecimalField } from "./decimal.js";
|
||||
import { createEmbeddedField, EmbeddedField } from "./embedded.js";
|
||||
import { createFloatField, FloatField } from "./float.js";
|
||||
import { createIntegerField, IntegerField } from "./integer.js";
|
||||
import { createReferenceField, ReferenceField } from "./reference-field.js";
|
||||
import { createStringField, StringField } from "./string-field.js";
|
||||
import { createTimestampField, TimestampField } from "./timestamp.js";
|
||||
import { createUUIDField, UUIDField } from "./uuid-field.js";
|
||||
export * from "./base-field.js";
|
||||
export * from "./field-to-type.js";
|
||||
@ -16,9 +14,7 @@ export type FieldDefinition =
|
||||
| IntegerField
|
||||
| FloatField
|
||||
| DecimalField
|
||||
| ReferenceField
|
||||
| TimestampField
|
||||
| EmbeddedField;
|
||||
| ReferenceField;
|
||||
|
||||
export namespace Field {
|
||||
export const string = createStringField;
|
||||
@ -27,6 +23,4 @@ export namespace Field {
|
||||
export const reference = createReferenceField;
|
||||
export const decimal = createDecimalField;
|
||||
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 { Field } from "./index.js";
|
||||
import {
|
||||
InvalidReferenceFieldError,
|
||||
InvalidReferenceField,
|
||||
validateReferenceField,
|
||||
} from "./reference-field.js";
|
||||
|
||||
@ -26,18 +26,26 @@ describe("Validate Reference Field", () => {
|
||||
Field.reference({
|
||||
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", () => {
|
||||
validateReferenceField(
|
||||
const result = validateReferenceField(
|
||||
schema,
|
||||
Field.reference({
|
||||
targetModel: "User",
|
||||
}),
|
||||
).unwrapOrThrow();
|
||||
);
|
||||
|
||||
if (isError(result)) {
|
||||
throw result.reason;
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
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", () => {
|
||||
@ -59,9 +71,13 @@ describe("Validate Reference Field", () => {
|
||||
targetModel: "User",
|
||||
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", () => {
|
||||
|
||||
@ -27,20 +27,16 @@ export function getTargetKey(field: ReferenceField): string {
|
||||
export function validateReferenceField(
|
||||
schema: ModelSchema,
|
||||
field: ReferenceField,
|
||||
): Result<void, InvalidReferenceFieldError> {
|
||||
): Result<void, InvalidReferenceField> {
|
||||
if (!schema[field.targetModel]) {
|
||||
return Result.failWith(
|
||||
new InvalidReferenceFieldError(
|
||||
return new InvalidReferenceField(
|
||||
`The target model '${field.targetModel}' is not in the schema.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) {
|
||||
return Result.failWith(
|
||||
new InvalidReferenceFieldError(
|
||||
return new InvalidReferenceField(
|
||||
`The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -48,17 +44,13 @@ export function validateReferenceField(
|
||||
field.targetKey &&
|
||||
!schema[field.targetModel].fields[field.targetKey].isUnique
|
||||
) {
|
||||
return Result.failWith(
|
||||
new InvalidReferenceFieldError(
|
||||
return new InvalidReferenceField(
|
||||
`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) {
|
||||
super("InvalidReferenceField");
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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 }>;
|
||||
};
|
||||
|
||||
@ -48,7 +48,7 @@ export function defineCollection<
|
||||
} as const;
|
||||
}
|
||||
|
||||
export type ModelToType<TModel extends Collection> = {
|
||||
export type ModelToType<TModel extends Model> = {
|
||||
[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 const FILTER_OPTION_TYPE_SYMBOL = "_filter_type";
|
||||
export const FILTER_OPTION_VALUE_SYMBOL = "_filter_value";
|
||||
export const FILTER_OPTION_OPERATOR_SYMBOL = "_filter_operator";
|
||||
export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type");
|
||||
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value");
|
||||
export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator");
|
||||
|
||||
export type LikeFilterOption<T> = T extends string
|
||||
? {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { AsyncResult, Keyof } from "@fabric/core";
|
||||
import { StoreQueryError } from "../../errors/query-error.js";
|
||||
import { StorageDriver } from "../../storage/storage-driver.js";
|
||||
@ -42,23 +41,19 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
||||
});
|
||||
}
|
||||
|
||||
select(): AsyncResult<T[], StoreQueryError>;
|
||||
select<K extends Keyof<T>>(
|
||||
keys: K[],
|
||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
||||
select<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
|
||||
return this.driver.select(this.schema, {
|
||||
keys?: K[],
|
||||
): AsyncResult<Pick<T, K>[], StoreQueryError> {
|
||||
return this.driver.select(this.schema[this.query.from], {
|
||||
...this.query,
|
||||
keys,
|
||||
});
|
||||
}
|
||||
|
||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
||||
selectOne<K extends Keyof<T>>(
|
||||
keys: K,
|
||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
||||
selectOne<K extends Keyof<T>>(keys?: K[]): AsyncResult<any, StoreQueryError> {
|
||||
return this.driver.selectOne(this.schema, {
|
||||
keys?: K[],
|
||||
): AsyncResult<Pick<T, K>, StoreQueryError> {
|
||||
return this.driver.selectOne(this.schema[this.query.from], {
|
||||
...this.query,
|
||||
keys,
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { isError, Run } from "@fabric/core";
|
||||
import { isError } from "@fabric/core";
|
||||
import { SQLiteStorageDriver } from "@fabric/store-sqlite";
|
||||
import {
|
||||
afterEach,
|
||||
@ -58,7 +58,9 @@ describe("State Store", () => {
|
||||
|
||||
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<
|
||||
{
|
||||
@ -81,39 +83,34 @@ describe("State Store", () => {
|
||||
|
||||
it("should query with a where clause", async () => {
|
||||
const newUUID = UUIDGeneratorMock.generate();
|
||||
|
||||
await Run.seqUNSAFE(
|
||||
() =>
|
||||
store.insertInto("users", {
|
||||
await store.insertInto("users", {
|
||||
name: "test",
|
||||
id: newUUID,
|
||||
streamId: newUUID,
|
||||
streamVersion: 1n,
|
||||
}),
|
||||
() =>
|
||||
store.insertInto("users", {
|
||||
});
|
||||
|
||||
await store.insertInto("users", {
|
||||
name: "anotherName",
|
||||
id: UUIDGeneratorMock.generate(),
|
||||
streamId: UUIDGeneratorMock.generate(),
|
||||
streamVersion: 1n,
|
||||
}),
|
||||
() =>
|
||||
store.insertInto("users", {
|
||||
});
|
||||
await store.insertInto("users", {
|
||||
name: "anotherName2",
|
||||
id: UUIDGeneratorMock.generate(),
|
||||
streamId: UUIDGeneratorMock.generate(),
|
||||
streamVersion: 1n,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const result = await Run.UNSAFE(() =>
|
||||
store
|
||||
const result = await store
|
||||
.from("users")
|
||||
.where({
|
||||
name: isLike("te%"),
|
||||
name: isLike("te*"),
|
||||
})
|
||||
.select(),
|
||||
);
|
||||
.select();
|
||||
|
||||
if (isError(result)) throw result;
|
||||
|
||||
expectTypeOf(result).toEqualTypeOf<
|
||||
{
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { AsyncResult } from "@fabric/core";
|
||||
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
||||
import { StoreQueryError } from "../errors/query-error.js";
|
||||
import { StorageDriver } from "../storage/storage-driver.js";
|
||||
import { ModelSchemaFromModels } from "./model-schema.js";
|
||||
@ -21,8 +20,8 @@ export class StateStore<TModel extends Model> {
|
||||
}, {} as ModelSchemaFromModels<TModel>);
|
||||
}
|
||||
|
||||
migrate(): AsyncResult<void, StoreQueryError | CircularDependencyError> {
|
||||
return this.driver.sync(this.schema);
|
||||
async migrate(): AsyncResult<void, StoreQueryError> {
|
||||
await this.driver.sync(this.schema);
|
||||
}
|
||||
|
||||
async insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
||||
|
||||
@ -20,7 +20,7 @@ export interface StorageDriver {
|
||||
* Run a select query against the store.
|
||||
*/
|
||||
select(
|
||||
model: ModelSchema,
|
||||
model: Collection,
|
||||
query: QueryDefinition,
|
||||
): AsyncResult<any[], StoreQueryError>;
|
||||
|
||||
@ -28,7 +28,7 @@ export interface StorageDriver {
|
||||
* Run a select query against the store.
|
||||
*/
|
||||
selectOne(
|
||||
model: ModelSchema,
|
||||
model: Collection,
|
||||
query: QueryDefinition,
|
||||
): AsyncResult<any, StoreQueryError>;
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
export * from "./base-64.js";
|
||||
export * from "./decimal.js";
|
||||
export * from "./email.js";
|
||||
export * from "./entity.js";
|
||||
export * from "./semver.js";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@ describe("sortByDependencies", () => {
|
||||
const result = sortByDependencies(array, {
|
||||
keyGetter: (element) => element.name,
|
||||
depGetter: (element) => element.dependencies,
|
||||
}).unwrapOrThrow();
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{ id: 3, name: "C", dependencies: [] },
|
||||
@ -35,7 +35,7 @@ describe("sortByDependencies", () => {
|
||||
sortByDependencies(array, {
|
||||
keyGetter: (element) => element.name,
|
||||
depGetter: (element) => element.dependencies,
|
||||
}).unwrapErrorOrThrow(),
|
||||
}),
|
||||
).toBeInstanceOf(CircularDependencyError);
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ describe("sortByDependencies", () => {
|
||||
const result = sortByDependencies(array, {
|
||||
keyGetter: (element) => element.name,
|
||||
depGetter: (element) => element.dependencies,
|
||||
}).unwrapOrThrow();
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
@ -34,15 +34,14 @@ export function sortByDependencies<T>(
|
||||
visited.add(key);
|
||||
sorted.push(key);
|
||||
};
|
||||
return Result.tryFrom(
|
||||
() => {
|
||||
try {
|
||||
graph.forEach((deps, key) => {
|
||||
visit(key, []);
|
||||
});
|
||||
} catch (e) {
|
||||
return e as CircularDependencyError;
|
||||
}
|
||||
return sorted.map(
|
||||
(key) => array.find((element) => keyGetter(element) === key) as T,
|
||||
);
|
||||
},
|
||||
(e) => e as CircularDependencyError,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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))`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -1,8 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Variant, VariantTag } from "@fabric/core";
|
||||
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";
|
||||
import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
|
||||
|
||||
type FieldSQLDefinitionMap = {
|
||||
[K in FieldDefinition[VariantTag]]: (
|
||||
@ -21,9 +19,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
||||
"TEXT",
|
||||
f.isPrimaryKey ? "PRIMARY KEY" : "",
|
||||
modifiersFromOpts(f),
|
||||
]
|
||||
.filter((x) => x)
|
||||
.join(" ");
|
||||
].join(" ");
|
||||
},
|
||||
IntegerField: (n, f): string => {
|
||||
return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
|
||||
@ -33,7 +29,8 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
||||
n,
|
||||
"TEXT",
|
||||
modifiersFromOpts(f),
|
||||
`REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
|
||||
",",
|
||||
`FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
|
||||
].join(" ");
|
||||
},
|
||||
FloatField: (n, f): string => {
|
||||
@ -42,12 +39,6 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
||||
DecimalField: (n, f): string => {
|
||||
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
TimestampField: (n, f: TimestampField): string => {
|
||||
return [n, "NUMERIC", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
EmbeddedField: (n, f: EmbeddedField): string => {
|
||||
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||
},
|
||||
};
|
||||
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
||||
@ -57,17 +48,17 @@ function modifiersFromOpts(field: FieldDefinition) {
|
||||
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
|
||||
return;
|
||||
}
|
||||
return [!field.isOptional ? "NOT NULL" : "", field.isUnique ? "UNIQUE" : ""]
|
||||
.filter((x) => x)
|
||||
.join(" ");
|
||||
return [
|
||||
!field.isOptional ? "NOT NULL" : "",
|
||||
field.isUnique ? "UNIQUE" : "",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
export function modelToSql(
|
||||
model: Collection<string, Record<string, FieldDefinition>>,
|
||||
model: Model<string, Record<string, FieldDefinition>>,
|
||||
) {
|
||||
const fields = Object.entries(model.fields)
|
||||
.map(([name, type]) => fieldDefinitionToSQL(name, type))
|
||||
.filter((x) => x)
|
||||
.join(", ");
|
||||
|
||||
return `CREATE TABLE ${model.name} (${fields})`;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/* 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";
|
||||
|
||||
export function transformRow(model: Collection) {
|
||||
@ -36,6 +36,4 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
||||
ReferenceField: (f, v) => v,
|
||||
FloatField: (f, v) => v,
|
||||
DecimalField: (f, v) => v,
|
||||
TimestampField: (f, v) => new PosixDate(v),
|
||||
EmbeddedField: (f, v: string) => JSON.parse(v),
|
||||
};
|
||||
|
||||
@ -5,32 +5,28 @@ import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
||||
|
||||
describe("SQLite Store Driver", () => {
|
||||
const schema = {
|
||||
demo: defineModel("demo", {
|
||||
value: Field.float(),
|
||||
owner: Field.reference({ targetModel: "users" }),
|
||||
}),
|
||||
users: defineModel("users", {
|
||||
name: Field.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
let driver: SQLiteStorageDriver;
|
||||
let store: SQLiteStorageDriver;
|
||||
|
||||
beforeEach(() => {
|
||||
driver = new SQLiteStorageDriver(":memory:");
|
||||
store = new SQLiteStorageDriver(":memory:");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
const result = await driver.close();
|
||||
const result = await store.close();
|
||||
if (isError(result)) throw result;
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
const insertResult = await driver.insert(schema.users, {
|
||||
const insertResult = await store.insert(schema.users, {
|
||||
id: "1",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
@ -39,9 +35,7 @@ describe("SQLite Store Driver", () => {
|
||||
|
||||
if (isError(insertResult)) throw insertResult;
|
||||
|
||||
const records = (
|
||||
await driver.select(schema, { from: "users" })
|
||||
).unwrapOrThrow();
|
||||
const records = await store.select(schema.users, { from: "users" });
|
||||
|
||||
expect(records).toEqual([
|
||||
{ id: "1", name: "test", streamId: "1", streamVersion: 1n },
|
||||
@ -49,21 +43,19 @@ describe("SQLite Store Driver", () => {
|
||||
});
|
||||
|
||||
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",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
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;
|
||||
|
||||
const records = (
|
||||
await driver.select(schema, { from: "users" })
|
||||
).unwrapOrThrow();
|
||||
const records = await store.select(schema.users, { from: "users" });
|
||||
|
||||
expect(records).toEqual([
|
||||
{ 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 () => {
|
||||
await driver.sync(schema);
|
||||
await store.sync(schema);
|
||||
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "1",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
await driver.delete(schema.users, "1");
|
||||
await store.delete(schema.users, "1");
|
||||
|
||||
const records = (
|
||||
await driver.select(schema, { from: "users" })
|
||||
).unwrapOrThrow();
|
||||
const records = await store.select(schema.users, { from: "users" });
|
||||
|
||||
expect(records).toEqual([]);
|
||||
});
|
||||
|
||||
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",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "2",
|
||||
name: "test",
|
||||
streamId: "2",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
const records = (
|
||||
await driver.select(schema, { from: "users" })
|
||||
).unwrapOrThrow();
|
||||
const records = await store.select(schema.users, { from: "users" });
|
||||
|
||||
expect(records).toEqual([
|
||||
{ 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 () => {
|
||||
await driver.sync(schema);
|
||||
await store.sync(schema);
|
||||
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "1",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "2",
|
||||
name: "test",
|
||||
streamId: "2",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
const record = (
|
||||
await driver.selectOne(schema, { from: "users" })
|
||||
).unwrapOrThrow();
|
||||
const record = await store.selectOne(schema.users, { from: "users" });
|
||||
|
||||
expect(record).toEqual({
|
||||
id: "1",
|
||||
@ -144,27 +130,25 @@ describe("SQLite Store Driver", () => {
|
||||
});
|
||||
|
||||
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",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "2",
|
||||
name: "jamón",
|
||||
streamId: "2",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
const result = (
|
||||
await driver.select(schema, {
|
||||
const result = await store.select(schema.users, {
|
||||
from: "users",
|
||||
where: { name: isLike("te%") },
|
||||
})
|
||||
).unwrapOrThrow();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await driver.sync(schema);
|
||||
await store.sync(schema);
|
||||
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "1",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "2",
|
||||
name: "jamón",
|
||||
streamId: "2",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
const result = (
|
||||
await driver.select(schema, {
|
||||
const result = await store.select(schema.users, {
|
||||
from: "users",
|
||||
where: { streamVersion: 1n },
|
||||
})
|
||||
).unwrapOrThrow();
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
@ -216,28 +198,26 @@ describe("SQLite Store Driver", () => {
|
||||
});
|
||||
|
||||
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",
|
||||
name: "test",
|
||||
streamId: "1",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
await driver.insert(schema.users, {
|
||||
await store.insert(schema.users, {
|
||||
id: "2",
|
||||
name: "jamón",
|
||||
streamId: "2",
|
||||
streamVersion: 1n,
|
||||
});
|
||||
|
||||
const result = (
|
||||
await driver.select(schema, {
|
||||
const result = await store.select(schema.users, {
|
||||
from: "users",
|
||||
limit: 1,
|
||||
offset: 1,
|
||||
})
|
||||
).unwrapOrThrow();
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
|
||||
@ -39,6 +39,10 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
|
||||
constructor(private path: string) {
|
||||
this.db = new Database(path);
|
||||
|
||||
// Enable Write-Ahead Logging, which is faster and more reliable.
|
||||
this.db.run("PRAGMA journal_mode = WAL;");
|
||||
this.db.run("PRAGMA foreign_keys = ON;");
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,65 +93,53 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
model: Model,
|
||||
record: Record<string, any>,
|
||||
): AsyncResult<void, StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
try {
|
||||
const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`;
|
||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||
return await run(stmt, recordToSQLParams(model, record));
|
||||
},
|
||||
(error) =>
|
||||
new StoreQueryError(error.message, {
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
collectionName: model.name,
|
||||
record,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a select query against the store.
|
||||
*/
|
||||
async select(
|
||||
schema: ModelSchema,
|
||||
collection: Collection,
|
||||
query: QueryDefinition,
|
||||
): AsyncResult<any[], StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
const [stmt, params] = await this.getSelectStatement(
|
||||
schema[query.from],
|
||||
try {
|
||||
const [stmt, params] = await this.getSelectStatement(collection, query);
|
||||
return await getAll(stmt, params, transformRow(collection));
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
query,
|
||||
);
|
||||
return await getAll(stmt, params, transformRow(schema[query.from]));
|
||||
},
|
||||
(err) =>
|
||||
new StoreQueryError(err.message, {
|
||||
err,
|
||||
query,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a select query against the store.
|
||||
*/
|
||||
async selectOne(
|
||||
schema: ModelSchema,
|
||||
collection: Collection,
|
||||
query: QueryDefinition,
|
||||
): AsyncResult<any, StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
const [stmt, params] = await this.getSelectStatement(
|
||||
schema[query.from],
|
||||
try {
|
||||
const [stmt, params] = await this.getSelectStatement(collection, query);
|
||||
return await getOne(stmt, params, transformRow(collection));
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
query,
|
||||
);
|
||||
return await getOne(stmt, params, transformRow(schema[query.from]));
|
||||
},
|
||||
(err) =>
|
||||
new StoreQueryError(err.message, {
|
||||
err,
|
||||
query,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,56 +148,48 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
async sync(
|
||||
schema: ModelSchema,
|
||||
): AsyncResult<void, StoreQueryError | CircularDependencyError> {
|
||||
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.
|
||||
try {
|
||||
await dbRun(this.db, "BEGIN TRANSACTION;");
|
||||
for (const modelKey in schema) {
|
||||
const model = schema[modelKey];
|
||||
await dbRun(this.db, modelToSql(model));
|
||||
}
|
||||
await dbRun(this.db, "COMMIT;");
|
||||
},
|
||||
(error) =>
|
||||
new StoreQueryError(error.message, {
|
||||
} catch (error: any) {
|
||||
await dbRun(this.db, "ROLLBACK;");
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
schema,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the store. This is a destructive operation.
|
||||
*/
|
||||
async drop(): AsyncResult<void, StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
try {
|
||||
if (this.path === ":memory:") {
|
||||
throw "Cannot drop in-memory database";
|
||||
return new StoreQueryError("Cannot drop in-memory database", {});
|
||||
} else {
|
||||
await unlink(this.path);
|
||||
}
|
||||
},
|
||||
(error) =>
|
||||
new StoreQueryError(error.message, {
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async close(): AsyncResult<void, UnexpectedError> {
|
||||
return AsyncResult.from(async () => {
|
||||
try {
|
||||
for (const stmt of this.cachedStatements.values()) {
|
||||
await finalize(stmt);
|
||||
}
|
||||
await dbClose(this.db);
|
||||
});
|
||||
} catch (error: any) {
|
||||
return new UnexpectedError({ error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -216,8 +200,7 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
id: string,
|
||||
record: Record<string, any>,
|
||||
): AsyncResult<void, StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
try {
|
||||
const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`;
|
||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||
const params = recordToSQLParams(model, {
|
||||
@ -225,14 +208,13 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
id,
|
||||
});
|
||||
return await run(stmt, params);
|
||||
},
|
||||
(error) =>
|
||||
new StoreQueryError(error.message, {
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
collectionName: model.name,
|
||||
record,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -240,18 +222,16 @@ export class SQLiteStorageDriver implements StorageDriver {
|
||||
*/
|
||||
|
||||
async delete(model: Model, id: string): AsyncResult<void, StoreQueryError> {
|
||||
return AsyncResult.tryFrom(
|
||||
async () => {
|
||||
const sql = `DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`;
|
||||
try {
|
||||
const sql = `DELETE FROM ${model.name} WHERE id = :id`;
|
||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||
return await run(stmt, { [keyToParam("id")]: id });
|
||||
},
|
||||
(error) =>
|
||||
new StoreQueryError(error.message, {
|
||||
return await run(stmt, { ":id": id });
|
||||
} catch (error: any) {
|
||||
return new StoreQueryError(error.message, {
|
||||
error,
|
||||
collectionName: model.name,
|
||||
id,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
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) => {
|
||||
db.all(statement, (err, result) => {
|
||||
db.run(statement, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -20,8 +20,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
||||
ReferenceField: (f, v) => v,
|
||||
FloatField: (f, v) => v,
|
||||
DecimalField: (f, v) => v,
|
||||
TimestampField: (f, v) => v.timestamp,
|
||||
EmbeddedField: (f, v: string) => JSON.stringify(v),
|
||||
};
|
||||
|
||||
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user