Compare commits
No commits in common. "14ca23ef74d011fc1bfc31f2e637caeeec95eb8d" and "27dbd447411f95de2bf2be43e1e8f926ecbc48f5" have entirely different histories.
14ca23ef74
...
27dbd44741
@ -26,7 +26,7 @@
|
|||||||
"lint": "eslint . --fix --report-unused-disable-directives",
|
"lint": "eslint . --fix --report-unused-disable-directives",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test": "yarn workspaces foreach -vvpA run test --run --clearScreen false",
|
"test": "yarn workspaces foreach -vvpA run test --run --clearScreen false",
|
||||||
"build": "yarn workspaces foreach -vvpA --topological run build",
|
"build": "yarn workspaces foreach -vvpA --topological-dev run build",
|
||||||
"add-package": "tsx ./scripts/add-package.ts",
|
"add-package": "tsx ./scripts/add-package.ts",
|
||||||
"postinstall": "husky"
|
"postinstall": "husky"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,13 +9,11 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@fabric/store-sqlite": "workspace:^",
|
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fabric/core": "workspace:^",
|
"@fabric/core": "workspace:^"
|
||||||
"decimal.js": "^10.4.3"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
|||||||
@ -3,7 +3,6 @@ export * from "./events/index.js";
|
|||||||
export * from "./files/index.js";
|
export * from "./files/index.js";
|
||||||
export * from "./models/index.js";
|
export * from "./models/index.js";
|
||||||
export * from "./security/index.js";
|
export * from "./security/index.js";
|
||||||
export * from "./services/index.js";
|
|
||||||
export * from "./storage/index.js";
|
export * from "./storage/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./use-case/index.js";
|
export * from "./use-case/index.js";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./services/mocks.js";
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { TaggedVariant, VariantTag } from "@fabric/core";
|
|
||||||
import { BaseField } from "./base-field.js";
|
|
||||||
|
|
||||||
export interface DecimalFieldOptions extends BaseField {
|
|
||||||
isUnsigned?: boolean;
|
|
||||||
precision?: number;
|
|
||||||
scale?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DecimalField
|
|
||||||
extends TaggedVariant<"DecimalField">,
|
|
||||||
DecimalFieldOptions {}
|
|
||||||
|
|
||||||
export function createDecimalField<T extends DecimalFieldOptions>(
|
|
||||||
opts: T = {} as T,
|
|
||||||
): DecimalField & T {
|
|
||||||
return {
|
|
||||||
[VariantTag]: "DecimalField",
|
|
||||||
...opts,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
@ -1,32 +1,21 @@
|
|||||||
import { Decimal } from "decimal.js";
|
|
||||||
import { UUID } from "../../types/uuid.js";
|
import { UUID } from "../../types/uuid.js";
|
||||||
import { DecimalField } from "./decimal.js";
|
|
||||||
import { FloatField } from "./float.js";
|
|
||||||
import { IntegerField } from "./integer.js";
|
import { IntegerField } from "./integer.js";
|
||||||
import { ReferenceField } from "./reference-field.js";
|
|
||||||
import { StringField } from "./string-field.js";
|
import { StringField } from "./string-field.js";
|
||||||
import { UUIDField } from "./uuid-field.js";
|
import { UUIDField } from "./uuid-field.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a field definition to its corresponding TypeScript type.
|
* Converts a field definition to its corresponding TypeScript type.
|
||||||
*/
|
*/
|
||||||
//prettier-ignore
|
export type FieldToType<TField> = TField extends StringField
|
||||||
export type FieldToType<TField> =
|
? ToOptional<TField, string>
|
||||||
TField extends StringField ? MaybeOptional<TField, string>
|
: TField extends UUIDField
|
||||||
: TField extends UUIDField ? MaybeOptional<TField, UUID>
|
? ToOptional<TField, UUID>
|
||||||
: TField extends IntegerField ? IntegerFieldToType<TField>
|
: TField extends IntegerField
|
||||||
: TField extends ReferenceField ? MaybeOptional<TField, UUID>
|
? TField["hasArbitraryPrecision"] extends true
|
||||||
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
|
? ToOptional<TField, bigint>
|
||||||
: TField extends FloatField ? MaybeOptional<TField, number>
|
: ToOptional<TField, number>
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
//prettier-ignore
|
type ToOptional<TField, TType> = TField extends { isOptional: true }
|
||||||
type IntegerFieldToType<TField extends IntegerField> = TField["hasArbitraryPrecision"] extends true
|
|
||||||
? MaybeOptional<TField, bigint>
|
|
||||||
: TField["hasArbitraryPrecision"] extends false
|
|
||||||
? MaybeOptional<TField, number>
|
|
||||||
: MaybeOptional<TField, number | bigint>;
|
|
||||||
|
|
||||||
type MaybeOptional<TField, TType> = TField extends { isOptional: true }
|
|
||||||
? TType | null
|
? TType | null
|
||||||
: TType;
|
: TType;
|
||||||
|
|||||||
@ -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 FloatFieldOptions extends BaseField {}
|
|
||||||
|
|
||||||
export interface FloatField
|
|
||||||
extends TaggedVariant<"FloatField">,
|
|
||||||
FloatFieldOptions {}
|
|
||||||
|
|
||||||
export function createFloatField<T extends FloatFieldOptions>(
|
|
||||||
opts: T = {} as T,
|
|
||||||
): FloatField & T {
|
|
||||||
return {
|
|
||||||
[VariantTag]: "FloatField",
|
|
||||||
...opts,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
@ -1,19 +1,14 @@
|
|||||||
import { createDecimalField, DecimalField } from "./decimal.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 { 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 "./reference-field.js";
|
export * from "./reference-field.js";
|
||||||
|
|
||||||
export type FieldDefinition =
|
export type FieldDefinition =
|
||||||
| StringField
|
| StringField
|
||||||
| UUIDField
|
| UUIDField
|
||||||
| IntegerField
|
| IntegerField
|
||||||
| FloatField
|
|
||||||
| DecimalField
|
|
||||||
| ReferenceField;
|
| ReferenceField;
|
||||||
|
|
||||||
export namespace Field {
|
export namespace Field {
|
||||||
@ -21,6 +16,4 @@ export namespace Field {
|
|||||||
export const uuid = createUUIDField;
|
export const uuid = createUUIDField;
|
||||||
export const integer = createIntegerField;
|
export const integer = createIntegerField;
|
||||||
export const reference = createReferenceField;
|
export const reference = createReferenceField;
|
||||||
export const decimal = createDecimalField;
|
|
||||||
export const float = createFloatField;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,3 @@
|
|||||||
import { Model } from "./model.js";
|
import { Model } from "./model.js";
|
||||||
|
|
||||||
export type ModelSchema = Record<string, Model>;
|
export type ModelSchema = Record<string, Model>;
|
||||||
|
|
||||||
export type ModelSchemaFromModels<TModels extends Model> = {
|
|
||||||
[K in TModels["name"]]: Extract<TModels, { name: K }>;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -4,14 +4,6 @@ import { Field, FieldDefinition } from "./fields/index.js";
|
|||||||
|
|
||||||
export type CustomModelFields = Record<string, FieldDefinition>;
|
export type CustomModelFields = Record<string, FieldDefinition>;
|
||||||
|
|
||||||
export interface Collection<
|
|
||||||
TName extends string = string,
|
|
||||||
TFields extends CustomModelFields = CustomModelFields,
|
|
||||||
> {
|
|
||||||
name: TName;
|
|
||||||
fields: TFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DefaultModelFields = {
|
export const DefaultModelFields = {
|
||||||
id: Field.uuid({ isPrimaryKey: true }),
|
id: Field.uuid({ isPrimaryKey: true }),
|
||||||
streamId: Field.uuid({ isIndexed: true }),
|
streamId: Field.uuid({ isIndexed: true }),
|
||||||
@ -20,11 +12,11 @@ export const DefaultModelFields = {
|
|||||||
hasArbitraryPrecision: true,
|
hasArbitraryPrecision: true,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Model<
|
export interface Model<
|
||||||
TName extends string = string,
|
TName extends string = string,
|
||||||
TFields extends CustomModelFields = CustomModelFields,
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
> extends Collection<TName, TFields> {
|
> {
|
||||||
|
name: TName;
|
||||||
fields: typeof DefaultModelFields & TFields;
|
fields: typeof DefaultModelFields & TFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,16 +30,6 @@ export function defineModel<
|
|||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defineCollection<
|
|
||||||
TName extends string,
|
|
||||||
TFields extends CustomModelFields,
|
|
||||||
>(name: TName, fields: TFields): Collection<TName, TFields> {
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
fields,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ModelToType<TModel extends Model> = {
|
export type ModelToType<TModel extends Model> = {
|
||||||
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,14 +4,12 @@ export type FilterOptions<T = any> =
|
|||||||
| SingleFilterOption<T>
|
| SingleFilterOption<T>
|
||||||
| MultiFilterOption<T>;
|
| MultiFilterOption<T>;
|
||||||
|
|
||||||
export type FilterValue<T = any, K extends keyof T = keyof T> =
|
export type SingleFilterOption<T = any> = {
|
||||||
|
[K in keyof T]?:
|
||||||
| T[K]
|
| T[K]
|
||||||
| LikeFilterOption<T[K]>
|
| LikeFilterOption<T[K]>
|
||||||
| ComparisonFilterOption<T[K]>
|
| ComparisonFilterOption<T[K]>
|
||||||
| InFilterOption<T[K]>;
|
| InFilterOption<T[K]>;
|
||||||
|
|
||||||
export type SingleFilterOption<T = any> = {
|
|
||||||
[K in keyof T]?: FilterValue<T, K>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
|
export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
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";
|
||||||
import { ModelSchema } from "../model-schema.js";
|
|
||||||
import { FilterOptions } from "./filter-options.js";
|
import { FilterOptions } from "./filter-options.js";
|
||||||
import { OrderByOptions } from "./order-by-options.js";
|
import { OrderByOptions } from "./order-by-options.js";
|
||||||
import {
|
import {
|
||||||
@ -15,26 +14,25 @@ import {
|
|||||||
export class QueryBuilder<T> implements StoreQuery<T> {
|
export class QueryBuilder<T> implements StoreQuery<T> {
|
||||||
constructor(
|
constructor(
|
||||||
private driver: StorageDriver,
|
private driver: StorageDriver,
|
||||||
private schema: ModelSchema,
|
|
||||||
private query: QueryDefinition,
|
private query: QueryDefinition,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
where(where: FilterOptions<T>): StoreSortableQuery<T> {
|
||||||
return new QueryBuilder(this.driver, this.schema, {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
where,
|
where,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
|
||||||
return new QueryBuilder(this.driver, this.schema, {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
orderBy: opts,
|
orderBy: opts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
|
limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
|
||||||
return new QueryBuilder(this.driver, this.schema, {
|
return new QueryBuilder(this.driver, {
|
||||||
...this.query,
|
...this.query,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@ -44,7 +42,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
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], {
|
return this.driver.select({
|
||||||
...this.query,
|
...this.query,
|
||||||
keys,
|
keys,
|
||||||
});
|
});
|
||||||
@ -53,7 +51,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
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], {
|
return this.driver.selectOne({
|
||||||
...this.query,
|
...this.query,
|
||||||
keys,
|
keys,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
import { isError } from "@fabric/core";
|
|
||||||
import { SQLiteStorageDriver } from "@fabric/store-sqlite";
|
|
||||||
import {
|
|
||||||
afterEach,
|
|
||||||
beforeEach,
|
|
||||||
describe,
|
|
||||||
expect,
|
|
||||||
expectTypeOf,
|
|
||||||
it,
|
|
||||||
} from "vitest";
|
|
||||||
import { UUIDGeneratorMock } from "../services/uuid-generator.mock.js";
|
|
||||||
import { UUID } from "../types/uuid.js";
|
|
||||||
import { Field } from "./fields/index.js";
|
|
||||||
import { defineModel } from "./model.js";
|
|
||||||
import { isLike } from "./query/filter-options.js";
|
|
||||||
import { StateStore } from "./state-store.js";
|
|
||||||
|
|
||||||
describe("State Store", () => {
|
|
||||||
const models = [
|
|
||||||
defineModel("users", {
|
|
||||||
name: Field.string(),
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
let driver: SQLiteStorageDriver;
|
|
||||||
let store: StateStore<(typeof models)[number]>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
driver = new SQLiteStorageDriver(":memory:");
|
|
||||||
store = new StateStore(driver, models);
|
|
||||||
const migrationResult = await store.migrate();
|
|
||||||
if (isError(migrationResult)) throw migrationResult;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
await driver.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should insert a record", async () => {
|
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
|
||||||
const insertResult = await store.insertInto("users", {
|
|
||||||
name: "test",
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
if (isError(insertResult)) throw insertResult;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should query with a basic select", async () => {
|
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
|
||||||
const insertResult = await store.insertInto("users", {
|
|
||||||
name: "test",
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isError(insertResult)) throw insertResult;
|
|
||||||
|
|
||||||
const result = await store.from("users").select();
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
|
||||||
{
|
|
||||||
id: UUID;
|
|
||||||
streamId: UUID;
|
|
||||||
streamVersion: bigint;
|
|
||||||
name: string;
|
|
||||||
}[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
streamVersion: 1n,
|
|
||||||
name: "test",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should query with a where clause", async () => {
|
|
||||||
const newUUID = UUIDGeneratorMock.generate();
|
|
||||||
await store.insertInto("users", {
|
|
||||||
name: "test",
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
await store.insertInto("users", {
|
|
||||||
name: "anotherName",
|
|
||||||
id: UUIDGeneratorMock.generate(),
|
|
||||||
streamId: UUIDGeneratorMock.generate(),
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
await store.insertInto("users", {
|
|
||||||
name: "anotherName2",
|
|
||||||
id: UUIDGeneratorMock.generate(),
|
|
||||||
streamId: UUIDGeneratorMock.generate(),
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await store
|
|
||||||
.from("users")
|
|
||||||
.where({
|
|
||||||
name: isLike("te*"),
|
|
||||||
})
|
|
||||||
.select();
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
expectTypeOf(result).toEqualTypeOf<
|
|
||||||
{
|
|
||||||
id: UUID;
|
|
||||||
streamId: UUID;
|
|
||||||
streamVersion: bigint;
|
|
||||||
name: string;
|
|
||||||
}[]
|
|
||||||
>();
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: newUUID,
|
|
||||||
streamId: newUUID,
|
|
||||||
streamVersion: 1n,
|
|
||||||
name: "test",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,41 +1,5 @@
|
|||||||
import { AsyncResult } from "@fabric/core";
|
|
||||||
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 { Model, ModelToType } from "./model.js";
|
|
||||||
import { QueryBuilder } from "./query/query-builder.js";
|
|
||||||
import { StoreQuery } from "./query/query.js";
|
|
||||||
|
|
||||||
export class StateStore<TModel extends Model> {
|
export class StateStore {
|
||||||
private schema: ModelSchemaFromModels<TModel>;
|
constructor(private driver: StorageDriver) {}
|
||||||
constructor(
|
|
||||||
private driver: StorageDriver,
|
|
||||||
models: TModel[],
|
|
||||||
) {
|
|
||||||
this.schema = models.reduce((acc, model: TModel) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[model.name]: model,
|
|
||||||
};
|
|
||||||
}, {} as ModelSchemaFromModels<TModel>);
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrate(): AsyncResult<void, StoreQueryError> {
|
|
||||||
await this.driver.sync(this.schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
async insertInto<T extends keyof ModelSchemaFromModels<TModel>>(
|
|
||||||
collection: T,
|
|
||||||
record: ModelToType<ModelSchemaFromModels<TModel>[T]>,
|
|
||||||
): AsyncResult<void, StoreQueryError> {
|
|
||||||
return this.driver.insert(this.schema[collection], record);
|
|
||||||
}
|
|
||||||
|
|
||||||
from<T extends keyof ModelSchemaFromModels<TModel>>(
|
|
||||||
collection: T,
|
|
||||||
): StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>> {
|
|
||||||
return new QueryBuilder(this.driver, this.schema, {
|
|
||||||
from: collection,
|
|
||||||
}) as StoreQuery<ModelToType<ModelSchemaFromModels<TModel>[T]>>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export * from "./uuid-generator.js";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./uuid-generator.mock.js";
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { UUID } from "../types/uuid.js";
|
|
||||||
import { UUIDGenerator } from "./uuid-generator.js";
|
|
||||||
|
|
||||||
export const UUIDGeneratorMock: UUIDGenerator = {
|
|
||||||
generate(): UUID {
|
|
||||||
return crypto.randomUUID() as UUID;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { UUID } from "../types/uuid.js";
|
|
||||||
|
|
||||||
export interface UUIDGenerator {
|
|
||||||
generate(): UUID;
|
|
||||||
}
|
|
||||||
@ -4,7 +4,6 @@ import { AsyncResult, UnexpectedError } from "@fabric/core";
|
|||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { StoreQueryError } from "../errors/query-error.js";
|
||||||
import { ModelSchema } from "../models/model-schema.js";
|
import { ModelSchema } from "../models/model-schema.js";
|
||||||
import { Collection } from "../models/model.js";
|
|
||||||
import { QueryDefinition } from "../models/query/query.js";
|
import { QueryDefinition } from "../models/query/query.js";
|
||||||
|
|
||||||
export interface StorageDriver {
|
export interface StorageDriver {
|
||||||
@ -12,25 +11,19 @@ export interface StorageDriver {
|
|||||||
* Insert data into the store
|
* Insert data into the store
|
||||||
*/
|
*/
|
||||||
insert(
|
insert(
|
||||||
model: Collection,
|
collectionName: string,
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
): AsyncResult<void, StoreQueryError>;
|
): AsyncResult<void, StoreQueryError>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a select query against the store.
|
* Run a select query against the store.
|
||||||
*/
|
*/
|
||||||
select(
|
select(query: QueryDefinition): AsyncResult<any[], StoreQueryError>;
|
||||||
model: Collection,
|
|
||||||
query: QueryDefinition,
|
|
||||||
): AsyncResult<any[], StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run a select query against the store.
|
* Run a select query against the store.
|
||||||
*/
|
*/
|
||||||
selectOne(
|
selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError>;
|
||||||
model: Collection,
|
|
||||||
query: QueryDefinition,
|
|
||||||
): AsyncResult<any, StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sincronice the store with the schema.
|
* Sincronice the store with the schema.
|
||||||
@ -53,7 +46,7 @@ export interface StorageDriver {
|
|||||||
* Update a record in the store.
|
* Update a record in the store.
|
||||||
*/
|
*/
|
||||||
update(
|
update(
|
||||||
model: Collection,
|
collectionName: string,
|
||||||
id: string,
|
id: string,
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
): AsyncResult<void, StoreQueryError>;
|
): AsyncResult<void, StoreQueryError>;
|
||||||
@ -61,5 +54,8 @@ export interface StorageDriver {
|
|||||||
/**
|
/**
|
||||||
* Delete a record from the store.
|
* Delete a record from the store.
|
||||||
*/
|
*/
|
||||||
delete(model: Collection, id: string): AsyncResult<void, StoreQueryError>;
|
delete(
|
||||||
|
collectionName: string,
|
||||||
|
id: string,
|
||||||
|
): AsyncResult<void, StoreQueryError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export { Decimal } from "decimal.js";
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@fabric/store-sqlite",
|
"name": "@ulthar/store-sqlite",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@ -1,150 +0,0 @@
|
|||||||
import {
|
|
||||||
defineCollection,
|
|
||||||
Field,
|
|
||||||
isGreaterOrEqualTo,
|
|
||||||
isGreaterThan,
|
|
||||||
isIn,
|
|
||||||
isLessOrEqualTo,
|
|
||||||
isLessThan,
|
|
||||||
isLike,
|
|
||||||
isNotEqualTo,
|
|
||||||
} from "@fabric/domain";
|
|
||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { filterToParams, filterToSQL } from "./filter-to-sql.js";
|
|
||||||
|
|
||||||
describe("SQL where clause from filter options", () => {
|
|
||||||
const col = defineCollection("users", {
|
|
||||||
name: Field.string(),
|
|
||||||
age: Field.integer(),
|
|
||||||
status: Field.string(),
|
|
||||||
salary: Field.decimal(),
|
|
||||||
rating: Field.float(),
|
|
||||||
quantity: Field.integer({
|
|
||||||
isUnsigned: true,
|
|
||||||
}),
|
|
||||||
price: Field.decimal(),
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with IN option", () => {
|
|
||||||
const opts = {
|
|
||||||
name: isIn(["John", "Jane"]),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
|
|
||||||
expect(result).toEqual("WHERE name IN ($where_name_0,$where_name_1)");
|
|
||||||
expect(params).toEqual({ $where_name_0: "John", $where_name_1: "Jane" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with LIKE option", () => {
|
|
||||||
const opts = {
|
|
||||||
name: isLike("%John%"),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE name LIKE $where_name");
|
|
||||||
expect(params).toEqual({ $where_name: "%John%" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with EQUALS option", () => {
|
|
||||||
const opts = {
|
|
||||||
age: 25,
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE age = $where_age");
|
|
||||||
expect(params).toEqual({ $where_age: 25 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with NOT EQUALS option", () => {
|
|
||||||
const opts = {
|
|
||||||
status: isNotEqualTo("inactive"),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE status <> $where_status");
|
|
||||||
expect(params).toEqual({ $where_status: "inactive" });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with GREATER THAN option", () => {
|
|
||||||
const opts = {
|
|
||||||
salary: isGreaterThan(50000),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE salary > $where_salary");
|
|
||||||
expect(params).toEqual({ $where_salary: 50000 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with LESS THAN option", () => {
|
|
||||||
const opts = {
|
|
||||||
rating: isLessThan(4.5),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE rating < $where_rating");
|
|
||||||
expect(params).toEqual({ $where_rating: 4.5 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with GREATER THAN OR EQUALS option", () => {
|
|
||||||
const opts = {
|
|
||||||
quantity: isGreaterOrEqualTo(10),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE quantity >= $where_quantity");
|
|
||||||
expect(params).toEqual({ $where_quantity: 10 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with LESS THAN OR EQUALS option", () => {
|
|
||||||
const opts = {
|
|
||||||
price: isLessOrEqualTo(100),
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE price <= $where_price");
|
|
||||||
expect(params).toEqual({ $where_price: 100 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with IS NULL option", () => {
|
|
||||||
const opts = {
|
|
||||||
price: undefined,
|
|
||||||
};
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual("WHERE price IS NULL");
|
|
||||||
expect(params).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should create a where clause from options with OR combination", () => {
|
|
||||||
const opts = [
|
|
||||||
{
|
|
||||||
name: isIn(["John", "Jane"]),
|
|
||||||
age: isGreaterThan(30),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: isNotEqualTo("inactive"),
|
|
||||||
salary: isGreaterThan(50000),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
rating: isLessThan(4.5),
|
|
||||||
quantity: isGreaterOrEqualTo(10),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const result = filterToSQL(opts);
|
|
||||||
const params = filterToParams(col, opts);
|
|
||||||
expect(result).toEqual(
|
|
||||||
"WHERE (name IN ($where_name_0_0,$where_name_0_1) AND age > $where_age_0) OR (status <> $where_status_1 AND salary > $where_salary_1) OR (rating < $where_rating_2 AND quantity >= $where_quantity_2)",
|
|
||||||
);
|
|
||||||
expect(params).toEqual({
|
|
||||||
$where_name_0_0: "John",
|
|
||||||
$where_name_0_1: "Jane",
|
|
||||||
$where_age_0: 30,
|
|
||||||
$where_status_1: "inactive",
|
|
||||||
$where_salary_1: 50000,
|
|
||||||
$where_rating_2: 4.5,
|
|
||||||
$where_quantity_2: 10,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,166 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import {
|
|
||||||
Collection,
|
|
||||||
FieldDefinition,
|
|
||||||
FILTER_OPTION_OPERATOR_SYMBOL,
|
|
||||||
FILTER_OPTION_TYPE_SYMBOL,
|
|
||||||
FILTER_OPTION_VALUE_SYMBOL,
|
|
||||||
FilterOptions,
|
|
||||||
FilterValue,
|
|
||||||
MultiFilterOption,
|
|
||||||
SingleFilterOption,
|
|
||||||
} from "@fabric/domain";
|
|
||||||
import { keyToParam } from "./record-utils.js";
|
|
||||||
import { fieldValueToSQL } from "./value-to-sql.js";
|
|
||||||
|
|
||||||
export function filterToSQL(filterOptions?: FilterOptions) {
|
|
||||||
if (!filterOptions) return "";
|
|
||||||
|
|
||||||
if (Array.isArray(filterOptions))
|
|
||||||
return `WHERE ${getWhereFromMultiOption(filterOptions)}`;
|
|
||||||
|
|
||||||
return `WHERE ${getWhereFromSingleOption(filterOptions)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterToParams(
|
|
||||||
collection: Collection,
|
|
||||||
filterOptions?: FilterOptions,
|
|
||||||
) {
|
|
||||||
if (!filterOptions) return {};
|
|
||||||
|
|
||||||
if (Array.isArray(filterOptions))
|
|
||||||
return getParamsFromMultiFilterOption(collection, filterOptions);
|
|
||||||
|
|
||||||
return getParamsFromSingleFilterOption(collection, filterOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWhereFromMultiOption(filterOptions: MultiFilterOption) {
|
|
||||||
return filterOptions
|
|
||||||
.map(
|
|
||||||
(option, i) =>
|
|
||||||
`(${getWhereFromSingleOption(option, { postfix: `_${i}` })})`,
|
|
||||||
)
|
|
||||||
.join(" OR ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWhereFromSingleOption(
|
|
||||||
filterOptions: SingleFilterOption,
|
|
||||||
opts: { postfix?: string } = {},
|
|
||||||
) {
|
|
||||||
return Object.entries(filterOptions)
|
|
||||||
.map(([key, value]) => getWhereFromKeyValue(key, value, opts))
|
|
||||||
.join(" AND ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const WHERE_KEY_PREFIX = "where_";
|
|
||||||
|
|
||||||
function getWhereParamKey(key: string, opts: { postfix?: string } = {}) {
|
|
||||||
return keyToParam(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWhereFromKeyValue(
|
|
||||||
key: string,
|
|
||||||
value: FilterValue,
|
|
||||||
opts: { postfix?: string } = {},
|
|
||||||
) {
|
|
||||||
if (value == undefined) {
|
|
||||||
return `${key} IS NULL`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === "object") {
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "like") {
|
|
||||||
return `${key} LIKE ${getWhereParamKey(key, opts)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") {
|
|
||||||
return `${key} IN (${value[FILTER_OPTION_VALUE_SYMBOL].map(
|
|
||||||
(v: any, i: number) =>
|
|
||||||
`${getWhereParamKey(key, {
|
|
||||||
postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`,
|
|
||||||
})}`,
|
|
||||||
).join(",")})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") {
|
|
||||||
return `${key} ${value[FILTER_OPTION_OPERATOR_SYMBOL]} ${getWhereParamKey(
|
|
||||||
key,
|
|
||||||
opts,
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${key} = ${getWhereParamKey(key, opts)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParamsFromMultiFilterOption(
|
|
||||||
collection: Collection,
|
|
||||||
filterOptions: MultiFilterOption,
|
|
||||||
) {
|
|
||||||
return filterOptions.reduce(
|
|
||||||
(acc, filterOption, i) => ({
|
|
||||||
...acc,
|
|
||||||
...getParamsFromSingleFilterOption(collection, filterOption, {
|
|
||||||
postfix: `_${i}`,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParamsFromSingleFilterOption(
|
|
||||||
collection: Collection,
|
|
||||||
filterOptions: SingleFilterOption,
|
|
||||||
opts: { postfix?: string } = {},
|
|
||||||
) {
|
|
||||||
return Object.entries(filterOptions)
|
|
||||||
.filter(([, value]) => {
|
|
||||||
return value !== undefined;
|
|
||||||
})
|
|
||||||
.reduce(
|
|
||||||
(acc, [key, value]) => ({
|
|
||||||
...acc,
|
|
||||||
...getParamsForFilterKeyValue(collection.fields[key], key, value, opts),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParamValueFromOptionValue(field: FieldDefinition, value: any) {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "like") {
|
|
||||||
return value[FILTER_OPTION_VALUE_SYMBOL];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "comparison") {
|
|
||||||
return fieldValueToSQL(field, value[FILTER_OPTION_VALUE_SYMBOL]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fieldValueToSQL(field, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getParamsForFilterKeyValue(
|
|
||||||
field: FieldDefinition,
|
|
||||||
key: string,
|
|
||||||
value: FilterValue,
|
|
||||||
opts: { postfix?: string } = {},
|
|
||||||
) {
|
|
||||||
if (typeof value === "object") {
|
|
||||||
if (value[FILTER_OPTION_TYPE_SYMBOL] === "in") {
|
|
||||||
return value[FILTER_OPTION_VALUE_SYMBOL].reduce(
|
|
||||||
(acc: Record<string, any>, v: any, i: number) => {
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
[getWhereParamKey(key, {
|
|
||||||
postfix: opts.postfix ? `${opts.postfix}_${i}` : `_${i}`,
|
|
||||||
})]: value[FILTER_OPTION_VALUE_SYMBOL][i],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
[getWhereParamKey(key, opts)]: getParamValueFromOptionValue(field, value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -2,14 +2,14 @@
|
|||||||
import { Variant, VariantTag } from "@fabric/core";
|
import { Variant, VariantTag } from "@fabric/core";
|
||||||
import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
|
import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
|
||||||
|
|
||||||
type FieldSQLDefinitionMap = {
|
type FieldMap = {
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
[K in FieldDefinition[VariantTag]]: (
|
||||||
name: string,
|
name: string,
|
||||||
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
||||||
) => string;
|
) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
const FieldMap: FieldMap = {
|
||||||
StringField: (n, f) => {
|
StringField: (n, f) => {
|
||||||
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
@ -21,10 +21,10 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
|||||||
modifiersFromOpts(f),
|
modifiersFromOpts(f),
|
||||||
].join(" ");
|
].join(" ");
|
||||||
},
|
},
|
||||||
IntegerField: (n, f): string => {
|
IntegerField: function (n, f): string {
|
||||||
return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
|
return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
ReferenceField: (n, f): string => {
|
ReferenceField: function (n, f): string {
|
||||||
return [
|
return [
|
||||||
n,
|
n,
|
||||||
"TEXT",
|
"TEXT",
|
||||||
@ -33,16 +33,7 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
|||||||
`FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
|
`FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
|
||||||
].join(" ");
|
].join(" ");
|
||||||
},
|
},
|
||||||
FloatField: (n, f): string => {
|
|
||||||
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
|
||||||
},
|
|
||||||
DecimalField: (n, f): string => {
|
|
||||||
return [n, "REAL", modifiersFromOpts(f)].join(" ");
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
|
||||||
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
function modifiersFromOpts(field: FieldDefinition) {
|
function modifiersFromOpts(field: FieldDefinition) {
|
||||||
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
|
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
|
||||||
@ -54,6 +45,10 @@ function modifiersFromOpts(field: FieldDefinition) {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||||
|
return FieldMap[field[VariantTag]](name, field as any);
|
||||||
|
}
|
||||||
|
|
||||||
export function modelToSql(
|
export function modelToSql(
|
||||||
model: Model<string, Record<string, FieldDefinition>>,
|
model: Model<string, Record<string, FieldDefinition>>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,44 +1,26 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { Model } from "@fabric/domain";
|
|
||||||
import { fieldValueToSQL } from "./value-to-sql.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
* Unfold a record into a string of it's keys separated by commas.
|
||||||
*/
|
*/
|
||||||
export function recordToSQLKeys(record: Record<string, any>) {
|
export function recordToKeys(record: Record<string, any>, prefix = "") {
|
||||||
return Object.keys(record)
|
return Object.keys(record)
|
||||||
.map((key) => key)
|
.map((key) => `${prefix}${key}`)
|
||||||
.join(", ");
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
|
||||||
*/
|
|
||||||
export function recordToSQLKeyParams(record: Record<string, any>) {
|
|
||||||
return Object.keys(record)
|
|
||||||
.map((key) => keyToParam(key))
|
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
* Unfold a record into a string of it's keys separated by commas.
|
||||||
*/
|
*/
|
||||||
export function recordToSQLParams(model: Model, record: Record<string, any>) {
|
export function recordToParams(record: Record<string, any>) {
|
||||||
return Object.keys(record).reduce(
|
return Object.keys(record).reduce(
|
||||||
(acc, key) => ({
|
(acc, key) => ({ ...acc, [`:${key}`]: record[key] }),
|
||||||
...acc,
|
|
||||||
[keyToParam(key)]: fieldValueToSQL(model.fields[key], record[key]),
|
|
||||||
}),
|
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recordToSQLSet(record: Record<string, any>) {
|
export function recordToSQLSet(record: Record<string, any>) {
|
||||||
return Object.keys(record)
|
return Object.keys(record)
|
||||||
.map((key) => `${key} = ${keyToParam(key)}`)
|
.map((key) => `${key} = :${key}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function keyToParam(key: string) {
|
|
||||||
return `$${key}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { VariantTag } from "@fabric/core";
|
|
||||||
import { Collection, FieldDefinition, FieldToType } from "@fabric/domain";
|
|
||||||
|
|
||||||
export function transformRow(model: Collection) {
|
|
||||||
return (row: Record<string, any>) => {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
for (const key in row) {
|
|
||||||
const field = model.fields[key];
|
|
||||||
result[key] = valueFromSQL(field, row[key]);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function valueFromSQL(field: FieldDefinition, value: any): any {
|
|
||||||
const r = FieldSQLInsertMap[field[VariantTag]];
|
|
||||||
return r(field as any, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldSQLInsertMap = {
|
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
|
||||||
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
|
||||||
value: any,
|
|
||||||
) => FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>>;
|
|
||||||
};
|
|
||||||
const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|
||||||
StringField: (f, v) => v,
|
|
||||||
UUIDField: (f, v) => v,
|
|
||||||
IntegerField: (f, v) => {
|
|
||||||
if (f.hasArbitraryPrecision) {
|
|
||||||
return BigInt(v);
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
},
|
|
||||||
ReferenceField: (f, v) => v,
|
|
||||||
FloatField: (f, v) => v,
|
|
||||||
DecimalField: (f, v) => v,
|
|
||||||
};
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { isError } from "@fabric/core";
|
import { isError } from "@fabric/core";
|
||||||
import { defineModel, Field, isLike } from "@fabric/domain";
|
import { defineModel, Field } from "@fabric/domain";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||||
import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
||||||
|
|
||||||
describe("SQLite Store Driver", () => {
|
describe("SQLite Store Driver", () => {
|
||||||
@ -21,211 +21,116 @@ describe("SQLite Store Driver", () => {
|
|||||||
if (isError(result)) throw result;
|
if (isError(result)) throw result;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should synchronize the store and insert a record", async () => {
|
test("should be able to synchronize the store and insert a record", async () => {
|
||||||
const result = await store.sync(schema);
|
const result = await store.sync(schema);
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
const insertResult = await store.insert(schema.users, {
|
await store.insert("users", {
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isError(insertResult)) throw insertResult;
|
const records = await store.select({ from: "users" });
|
||||||
|
|
||||||
const records = await store.select(schema.users, { from: "users" });
|
|
||||||
|
|
||||||
expect(records).toEqual([
|
expect(records).toEqual([
|
||||||
{ id: "1", name: "test", streamId: "1", streamVersion: 1n },
|
{ id: "1", name: "test", streamId: "1", streamVersion: 1 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be update a record", async () => {
|
test("should be able to update a record", async () => {
|
||||||
await store.sync(schema);
|
const result = await store.sync(schema);
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
|
await store.insert("users", {
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const err = await store.update(schema.users, "1", { name: "updated" });
|
await store.update("users", "1", { name: "updated" });
|
||||||
if (isError(err)) throw err;
|
|
||||||
|
|
||||||
const records = await store.select(schema.users, { from: "users" });
|
const records = await store.select({ from: "users" });
|
||||||
|
|
||||||
expect(records).toEqual([
|
expect(records).toEqual([
|
||||||
{ id: "1", name: "updated", streamId: "1", streamVersion: 1n },
|
{ id: "1", name: "updated", streamId: "1", streamVersion: 1 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to delete a record", async () => {
|
test("should be able to delete a record", async () => {
|
||||||
await store.sync(schema);
|
const result = await store.sync(schema);
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
|
await store.insert("users", {
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
await store.delete(schema.users, "1");
|
await store.delete("users", "1");
|
||||||
|
|
||||||
const records = await store.select(schema.users, { from: "users" });
|
const records = await store.select({ from: "users" });
|
||||||
|
|
||||||
expect(records).toEqual([]);
|
expect(records).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to select records", async () => {
|
test("should be able to select records", async () => {
|
||||||
await store.sync(schema);
|
const result = await store.sync(schema);
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
|
await store.insert("users", {
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
await store.insert(schema.users, {
|
await store.insert("users", {
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "2",
|
streamId: "2",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const records = await store.select(schema.users, { from: "users" });
|
const records = await store.select({ from: "users" });
|
||||||
|
|
||||||
expect(records).toEqual([
|
expect(records).toEqual([
|
||||||
{ id: "1", name: "test", streamId: "1", streamVersion: 1n },
|
{ id: "1", name: "test", streamId: "1", streamVersion: 1 },
|
||||||
{ id: "2", name: "test", streamId: "2", streamVersion: 1n },
|
{ id: "2", name: "test", streamId: "2", streamVersion: 1 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to select one record", async () => {
|
test("should be able to select one record", async () => {
|
||||||
await store.sync(schema);
|
const result = await store.sync(schema);
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
if (isError(result)) throw result;
|
||||||
|
|
||||||
|
await store.insert("users", {
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
await store.insert(schema.users, {
|
await store.insert("users", {
|
||||||
id: "2",
|
id: "2",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "2",
|
streamId: "2",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const record = await store.selectOne(schema.users, { from: "users" });
|
const record = await store.selectOne({ from: "users" });
|
||||||
|
|
||||||
expect(record).toEqual({
|
expect(record).toEqual({
|
||||||
id: "1",
|
id: "1",
|
||||||
name: "test",
|
name: "test",
|
||||||
streamId: "1",
|
streamId: "1",
|
||||||
streamVersion: 1n,
|
streamVersion: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should select a record with a where clause", async () => {
|
|
||||||
await store.sync(schema);
|
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "1",
|
|
||||||
name: "test",
|
|
||||||
streamId: "1",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "2",
|
|
||||||
name: "jamón",
|
|
||||||
streamId: "2",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await store.select(schema.users, {
|
|
||||||
from: "users",
|
|
||||||
where: { name: isLike("te%") },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "test",
|
|
||||||
streamId: "1",
|
|
||||||
streamVersion: 1n,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should select a record with a where clause of a specific type", async () => {
|
|
||||||
await store.sync(schema);
|
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "1",
|
|
||||||
name: "test",
|
|
||||||
streamId: "1",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "2",
|
|
||||||
name: "jamón",
|
|
||||||
streamId: "2",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await store.select(schema.users, {
|
|
||||||
from: "users",
|
|
||||||
where: { streamVersion: 1n },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
name: "test",
|
|
||||||
streamId: "1",
|
|
||||||
streamVersion: 1n,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "jamón",
|
|
||||||
streamId: "2",
|
|
||||||
streamVersion: 1n,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should select with a limit and offset", async () => {
|
|
||||||
await store.sync(schema);
|
|
||||||
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "1",
|
|
||||||
name: "test",
|
|
||||||
streamId: "1",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
await store.insert(schema.users, {
|
|
||||||
id: "2",
|
|
||||||
name: "jamón",
|
|
||||||
streamId: "2",
|
|
||||||
streamVersion: 1n,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await store.select(schema.users, {
|
|
||||||
from: "users",
|
|
||||||
limit: 1,
|
|
||||||
offset: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
name: "jamón",
|
|
||||||
streamId: "2",
|
|
||||||
streamVersion: 1n,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,24 +4,18 @@ import { unlink } from "fs/promises";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CircularDependencyError,
|
CircularDependencyError,
|
||||||
Collection,
|
|
||||||
Model,
|
|
||||||
ModelSchema,
|
ModelSchema,
|
||||||
QueryDefinition,
|
QueryDefinition,
|
||||||
StorageDriver,
|
StorageDriver,
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { Database, Statement } from "sqlite3";
|
import { Database, Statement } from "sqlite3";
|
||||||
import { filterToParams, filterToSQL } from "./filter-to-sql.js";
|
|
||||||
import { modelToSql } from "./model-to-sql.js";
|
import { modelToSql } from "./model-to-sql.js";
|
||||||
import {
|
import {
|
||||||
keyToParam,
|
recordToKeys,
|
||||||
recordToSQLKeyParams,
|
recordToParams,
|
||||||
recordToSQLKeys,
|
|
||||||
recordToSQLParams,
|
|
||||||
recordToSQLSet,
|
recordToSQLSet,
|
||||||
} from "./record-utils.js";
|
} from "./record-utils.js";
|
||||||
import { transformRow } from "./sql-to-value.js";
|
|
||||||
import {
|
import {
|
||||||
dbClose,
|
dbClose,
|
||||||
dbRun,
|
dbRun,
|
||||||
@ -60,47 +54,21 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
return stmt;
|
return stmt;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSelectStatement(
|
|
||||||
collection: Collection,
|
|
||||||
query: QueryDefinition,
|
|
||||||
): Promise<[Statement, Record<string, any>]> {
|
|
||||||
const selectFields = query.keys ? query.keys.join(", ") : "*";
|
|
||||||
|
|
||||||
const queryFilter = filterToSQL(query.where);
|
|
||||||
const limit = query.limit ? `LIMIT ${query.limit}` : "";
|
|
||||||
const offset = query.offset ? `OFFSET ${query.offset}` : "";
|
|
||||||
|
|
||||||
const sql = [
|
|
||||||
`SELECT ${selectFields}`,
|
|
||||||
`FROM ${query.from}`,
|
|
||||||
queryFilter,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
return [
|
|
||||||
await this.getOrCreatePreparedStatement(sql),
|
|
||||||
{
|
|
||||||
...filterToParams(collection, query.where),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert data into the store
|
* Insert data into the store
|
||||||
*/
|
*/
|
||||||
async insert(
|
async insert(
|
||||||
model: Model,
|
collectionName: string,
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
): AsyncResult<void, StoreQueryError> {
|
): AsyncResult<void, StoreQueryError> {
|
||||||
try {
|
try {
|
||||||
const sql = `INSERT INTO ${model.name} (${recordToSQLKeys(record)}) VALUES (${recordToSQLKeyParams(record)})`;
|
const sql = `INSERT INTO ${collectionName} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`;
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||||
return await run(stmt, recordToSQLParams(model, record));
|
return await run(stmt, recordToParams(record));
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new StoreQueryError(error.message, {
|
return new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName,
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -109,13 +77,11 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
/**
|
/**
|
||||||
* Run a select query against the store.
|
* Run a select query against the store.
|
||||||
*/
|
*/
|
||||||
async select(
|
async select(query: QueryDefinition): AsyncResult<any[], StoreQueryError> {
|
||||||
collection: Collection,
|
|
||||||
query: QueryDefinition,
|
|
||||||
): AsyncResult<any[], StoreQueryError> {
|
|
||||||
try {
|
try {
|
||||||
const [stmt, params] = await this.getSelectStatement(collection, query);
|
const sql = `SELECT * FROM ${query.from}`;
|
||||||
return await getAll(stmt, params, transformRow(collection));
|
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||||
|
return await getAll(stmt);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new StoreQueryError(error.message, {
|
return new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
@ -127,13 +93,12 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
/**
|
/**
|
||||||
* Run a select query against the store.
|
* Run a select query against the store.
|
||||||
*/
|
*/
|
||||||
async selectOne(
|
async selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError> {
|
||||||
collection: Collection,
|
|
||||||
query: QueryDefinition,
|
|
||||||
): AsyncResult<any, StoreQueryError> {
|
|
||||||
try {
|
try {
|
||||||
const [stmt, params] = await this.getSelectStatement(collection, query);
|
const sql = `SELECT * FROM ${query.from}`;
|
||||||
return await getOne(stmt, params, transformRow(collection));
|
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||||
|
|
||||||
|
return await getOne(stmt);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new StoreQueryError(error.message, {
|
return new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
@ -196,22 +161,24 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
* Update a record in the store.
|
* Update a record in the store.
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(
|
||||||
model: Model,
|
collectionName: string,
|
||||||
id: string,
|
id: string,
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
): AsyncResult<void, StoreQueryError> {
|
): AsyncResult<void, StoreQueryError> {
|
||||||
try {
|
try {
|
||||||
const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = ${keyToParam("id")}`;
|
const sql = `UPDATE ${collectionName} SET ${recordToSQLSet(record)} WHERE id = :id`;
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||||
const params = recordToSQLParams(model, {
|
return await run(
|
||||||
|
stmt,
|
||||||
|
recordToParams({
|
||||||
...record,
|
...record,
|
||||||
id,
|
id,
|
||||||
});
|
}),
|
||||||
return await run(stmt, params);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new StoreQueryError(error.message, {
|
return new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName,
|
||||||
record,
|
record,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -221,15 +188,18 @@ export class SQLiteStorageDriver implements StorageDriver {
|
|||||||
* Delete a record from the store.
|
* Delete a record from the store.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async delete(model: Model, id: string): AsyncResult<void, StoreQueryError> {
|
async delete(
|
||||||
|
collectionName: string,
|
||||||
|
id: string,
|
||||||
|
): AsyncResult<void, StoreQueryError> {
|
||||||
try {
|
try {
|
||||||
const sql = `DELETE FROM ${model.name} WHERE id = :id`;
|
const sql = `DELETE FROM ${collectionName} WHERE id = :id`;
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
const stmt = await this.getOrCreatePreparedStatement(sql);
|
||||||
return await run(stmt, { ":id": id });
|
return await run(stmt, { ":id": id });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return new StoreQueryError(error.message, {
|
return new StoreQueryError(error.message, {
|
||||||
error,
|
error,
|
||||||
collectionName: model.name,
|
collectionName,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,33 +52,25 @@ export function run(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAll(
|
export function getAll(stmt: Statement): Promise<Record<string, any>[]> {
|
||||||
stmt: Statement,
|
|
||||||
params: Record<string, any>,
|
|
||||||
transformer: (row: any) => any,
|
|
||||||
): Promise<Record<string, any>[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stmt.all(params, (err: Error | null, rows: Record<string, any>[]) => {
|
stmt.all((err: Error | null, rows: Record<string, any>[]) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve(rows.map(transformer));
|
resolve(rows);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOne(
|
export function getOne(stmt: Statement): Promise<Record<string, any>> {
|
||||||
stmt: Statement,
|
|
||||||
params: Record<string, any>,
|
|
||||||
transformer: (row: any) => any,
|
|
||||||
): Promise<Record<string, any>> {
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
stmt.all(params, (err: Error | null, rows: Record<string, any>[]) => {
|
stmt.get((err: Error | null, row: Record<string, any>) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} else {
|
} else {
|
||||||
resolve(rows.map(transformer)[0]);
|
resolve(row);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { VariantTag } from "@fabric/core";
|
|
||||||
import { FieldDefinition, FieldToType } from "@fabric/domain";
|
|
||||||
|
|
||||||
type FieldSQLInsertMap = {
|
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
|
||||||
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
|
||||||
value: FieldToType<Extract<FieldDefinition, { [VariantTag]: K }>>,
|
|
||||||
) => any;
|
|
||||||
};
|
|
||||||
const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|
||||||
StringField: (f, v) => v,
|
|
||||||
UUIDField: (f, v) => v,
|
|
||||||
IntegerField: (f, v: number | bigint) => {
|
|
||||||
if (f.hasArbitraryPrecision) {
|
|
||||||
return String(v);
|
|
||||||
}
|
|
||||||
return v as number;
|
|
||||||
},
|
|
||||||
ReferenceField: (f, v) => v,
|
|
||||||
FloatField: (f, v) => v,
|
|
||||||
DecimalField: (f, v) => v,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
|
||||||
const r = FieldSQLInsertMap[field[VariantTag]] as any;
|
|
||||||
return r(field as any, value);
|
|
||||||
}
|
|
||||||
34
yarn.lock
34
yarn.lock
@ -419,20 +419,6 @@ __metadata:
|
|||||||
resolution: "@fabric/domain@workspace:packages/fabric/domain"
|
resolution: "@fabric/domain@workspace:packages/fabric/domain"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fabric/core": "workspace:^"
|
"@fabric/core": "workspace:^"
|
||||||
"@fabric/store-sqlite": "workspace:^"
|
|
||||||
decimal.js: "npm:^10.4.3"
|
|
||||||
typescript: "npm:^5.6.2"
|
|
||||||
vitest: "npm:^2.1.1"
|
|
||||||
languageName: unknown
|
|
||||||
linkType: soft
|
|
||||||
|
|
||||||
"@fabric/store-sqlite@workspace:^, @fabric/store-sqlite@workspace:packages/fabric/store-sqlite":
|
|
||||||
version: 0.0.0-use.local
|
|
||||||
resolution: "@fabric/store-sqlite@workspace:packages/fabric/store-sqlite"
|
|
||||||
dependencies:
|
|
||||||
"@fabric/core": "workspace:^"
|
|
||||||
"@fabric/domain": "workspace:^"
|
|
||||||
sqlite3: "npm:^5.1.7"
|
|
||||||
typescript: "npm:^5.6.2"
|
typescript: "npm:^5.6.2"
|
||||||
vitest: "npm:^2.1.1"
|
vitest: "npm:^2.1.1"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
@ -869,12 +855,23 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@ulthar/store-sqlite@workspace:packages/fabric/store-sqlite":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@ulthar/store-sqlite@workspace:packages/fabric/store-sqlite"
|
||||||
|
dependencies:
|
||||||
|
"@fabric/core": "workspace:^"
|
||||||
|
"@fabric/domain": "workspace:^"
|
||||||
|
sqlite3: "npm:^5.1.7"
|
||||||
|
typescript: "npm:^5.6.2"
|
||||||
|
vitest: "npm:^2.1.1"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@ulthar/template-domain@workspace:packages/templates/domain":
|
"@ulthar/template-domain@workspace:packages/templates/domain":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@ulthar/template-domain@workspace:packages/templates/domain"
|
resolution: "@ulthar/template-domain@workspace:packages/templates/domain"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@fabric/core": "workspace:^"
|
"@fabric/core": "workspace:^"
|
||||||
"@fabric/domain": "workspace:^"
|
|
||||||
typescript: "npm:^5.6.2"
|
typescript: "npm:^5.6.2"
|
||||||
vitest: "npm:^2.1.1"
|
vitest: "npm:^2.1.1"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
@ -1410,13 +1407,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"decimal.js@npm:^10.4.3":
|
|
||||||
version: 10.4.3
|
|
||||||
resolution: "decimal.js@npm:10.4.3"
|
|
||||||
checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"decompress-response@npm:^6.0.0":
|
"decompress-response@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "decompress-response@npm:6.0.0"
|
resolution: "decompress-response@npm:6.0.0"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user