[fabric/domain] Add typed insertion to state-store

This commit is contained in:
Pablo Baleztena 2024-10-05 11:10:04 -03:00
parent 27dbd44741
commit f0c77398e6
18 changed files with 255 additions and 81 deletions

View File

@ -9,6 +9,7 @@
"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"
}, },

View File

@ -1,5 +1,6 @@
import { UUID } from "../../types/uuid.js"; import { UUID } from "../../types/uuid.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";
@ -13,7 +14,11 @@ export type FieldToType<TField> = TField extends StringField
: TField extends IntegerField : TField extends IntegerField
? TField["hasArbitraryPrecision"] extends true ? TField["hasArbitraryPrecision"] extends true
? ToOptional<TField, bigint> ? ToOptional<TField, bigint>
: ToOptional<TField, number> : TField["hasArbitraryPrecision"] extends false
? ToOptional<TField, number>
: ToOptional<TField, number | bigint>
: TField extends ReferenceField
? ToOptional<TField, UUID>
: never; : never;
type ToOptional<TField, TType> = TField extends { isOptional: true } type ToOptional<TField, TType> = TField extends { isOptional: true }

View File

@ -3,6 +3,7 @@ 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 =

View File

@ -1,3 +1,7 @@
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 }>;
};

View File

@ -1,6 +1,7 @@
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 {
@ -14,25 +15,26 @@ 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, { return new QueryBuilder(this.driver, this.schema, {
...this.query, ...this.query,
where, where,
}); });
} }
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> { orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
return new QueryBuilder(this.driver, { return new QueryBuilder(this.driver, this.schema, {
...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, { return new QueryBuilder(this.driver, this.schema, {
...this.query, ...this.query,
limit, limit,
offset, offset,
@ -42,7 +44,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({ return this.driver.select(this.schema[this.query.from], {
...this.query, ...this.query,
keys, keys,
}); });
@ -51,7 +53,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({ return this.driver.selectOne(this.schema[this.query.from], {
...this.query, ...this.query,
keys, keys,
}); });

View File

@ -0,0 +1,44 @@
import { isError } from "@fabric/core";
import { SQLiteStorageDriver } from "@fabric/store-sqlite";
import { describe, it } from "vitest";
import { generateUUID } from "../types/uuid.js";
import { Field } from "./fields/index.js";
import { defineModel } from "./model.js";
import { StateStore } from "./state-store.js";
describe("State Store", () => {
const driver = new SQLiteStorageDriver(":memory:");
const models = [
defineModel("users", {
name: Field.string(),
}),
];
it("should be able to create a new state store and migrate", async () => {
const store = new StateStore(driver, models);
const migrationResult = await store.migrate();
if (isError(migrationResult)) throw migrationResult;
});
it("should be able to insert a record", async () => {
const store = new StateStore(driver, models);
const migrationResult = await store.migrate();
if (isError(migrationResult)) throw migrationResult;
const newUUID = generateUUID();
const insertResult = await store.insertInto("users", {
name: "test",
id: newUUID,
streamId: newUUID,
streamVersion: 1n,
});
if (isError(insertResult)) throw insertResult;
});
});

View File

@ -1,5 +1,31 @@
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";
export class StateStore { export class StateStore<TModel extends Model> {
constructor(private driver: StorageDriver) {} private schema: ModelSchemaFromModels<TModel>;
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);
}
} }

View File

@ -4,6 +4,7 @@ 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 { Model } from "../models/model.js";
import { QueryDefinition } from "../models/query/query.js"; import { QueryDefinition } from "../models/query/query.js";
export interface StorageDriver { export interface StorageDriver {
@ -11,19 +12,25 @@ export interface StorageDriver {
* Insert data into the store * Insert data into the store
*/ */
insert( insert(
collectionName: string, model: Model,
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(query: QueryDefinition): AsyncResult<any[], StoreQueryError>; select(
model: Model,
query: QueryDefinition,
): AsyncResult<any[], StoreQueryError>;
/** /**
* Run a select query against the store. * Run a select query against the store.
*/ */
selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError>; selectOne(
model: Model,
query: QueryDefinition,
): AsyncResult<any, StoreQueryError>;
/** /**
* Sincronice the store with the schema. * Sincronice the store with the schema.
@ -46,7 +53,7 @@ export interface StorageDriver {
* Update a record in the store. * Update a record in the store.
*/ */
update( update(
collectionName: string, model: Model,
id: string, id: string,
record: Record<string, any>, record: Record<string, any>,
): AsyncResult<void, StoreQueryError>; ): AsyncResult<void, StoreQueryError>;
@ -54,8 +61,5 @@ export interface StorageDriver {
/** /**
* Delete a record from the store. * Delete a record from the store.
*/ */
delete( delete(model: Model, id: string): AsyncResult<void, StoreQueryError>;
collectionName: string,
id: string,
): AsyncResult<void, StoreQueryError>;
} }

View File

@ -1 +1,5 @@
export type UUID = `${string}-${string}-${string}-${string}-${string}`; export type UUID = `${string}-${string}-${string}-${string}-${string}`;
export function generateUUID(): UUID {
return crypto.randomUUID() as UUID;
}

View File

@ -1,5 +1,5 @@
{ {
"name": "@ulthar/store-sqlite", "name": "@fabric/store-sqlite",
"type": "module", "type": "module",
"module": "dist/index.js", "module": "dist/index.js",
"main": "dist/index.js", "main": "dist/index.js",

View File

@ -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 FieldMap = { type FieldSQLDefinitionMap = {
[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 FieldMap: FieldMap = { const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
StringField: (n, f) => { StringField: (n, f) => {
return [n, "TEXT", modifiersFromOpts(f)].join(" "); return [n, "TEXT", modifiersFromOpts(f)].join(" ");
}, },
@ -34,6 +34,9 @@ const FieldMap: FieldMap = {
].join(" "); ].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) {
@ -45,10 +48,6 @@ 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>>,
) { ) {

View File

@ -1,5 +1,8 @@
/* 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.
*/ */
@ -12,9 +15,12 @@ export function recordToKeys(record: Record<string, any>, prefix = "") {
/** /**
* 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 recordToParams(record: Record<string, any>) { export function recordToParams(model: Model, record: Record<string, any>) {
return Object.keys(record).reduce( return Object.keys(record).reduce(
(acc, key) => ({ ...acc, [`:${key}`]: record[key] }), (acc, key) => ({
...acc,
[`:${key}`]: fieldValueToSQL(model.fields[key], record[key]),
}),
{}, {},
); );
} }

View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VariantTag } from "@fabric/core";
import { FieldDefinition, FieldToType, Model } from "@fabric/domain";
export function transformRow(model: Model) {
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,
};

View File

@ -26,17 +26,19 @@ describe("SQLite Store Driver", () => {
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("users", { const insertResult = await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1n,
}); });
const records = await store.select({ from: "users" }); if (isError(insertResult)) throw insertResult;
const records = await store.select(schema.users, { from: "users" });
expect(records).toEqual([ expect(records).toEqual([
{ id: "1", name: "test", streamId: "1", streamVersion: 1 }, { id: "1", name: "test", streamId: "1", streamVersion: 1n },
]); ]);
}); });
@ -45,19 +47,19 @@ describe("SQLite Store Driver", () => {
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("users", { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1,
}); });
await store.update("users", "1", { name: "updated" }); await store.update(schema.users, "1", { name: "updated" });
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: "updated", streamId: "1", streamVersion: 1 }, { id: "1", name: "updated", streamId: "1", streamVersion: 1n },
]); ]);
}); });
@ -66,16 +68,16 @@ describe("SQLite Store Driver", () => {
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("users", { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1,
}); });
await store.delete("users", "1"); await store.delete(schema.users, "1");
const records = await store.select({ from: "users" }); const records = await store.select(schema.users, { from: "users" });
expect(records).toEqual([]); expect(records).toEqual([]);
}); });
@ -85,24 +87,24 @@ describe("SQLite Store Driver", () => {
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("users", { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1,
}); });
await store.insert("users", { await store.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1, streamVersion: 1,
}); });
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: 1 }, { id: "1", name: "test", streamId: "1", streamVersion: 1n },
{ id: "2", name: "test", streamId: "2", streamVersion: 1 }, { id: "2", name: "test", streamId: "2", streamVersion: 1n },
]); ]);
}); });
@ -111,26 +113,26 @@ describe("SQLite Store Driver", () => {
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("users", { await store.insert(schema.users, {
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1,
}); });
await store.insert("users", { await store.insert(schema.users, {
id: "2", id: "2",
name: "test", name: "test",
streamId: "2", streamId: "2",
streamVersion: 1, streamVersion: 1,
}); });
const record = await store.selectOne({ from: "users" }); const record = await store.selectOne(schema.users, { from: "users" });
expect(record).toEqual({ expect(record).toEqual({
id: "1", id: "1",
name: "test", name: "test",
streamId: "1", streamId: "1",
streamVersion: 1, streamVersion: 1n,
}); });
}); });
}); });

View File

@ -4,6 +4,7 @@ import { unlink } from "fs/promises";
import { import {
CircularDependencyError, CircularDependencyError,
Model,
ModelSchema, ModelSchema,
QueryDefinition, QueryDefinition,
StorageDriver, StorageDriver,
@ -16,6 +17,7 @@ import {
recordToParams, recordToParams,
recordToSQLSet, recordToSQLSet,
} from "./record-utils.js"; } from "./record-utils.js";
import { transformRow } from "./sql-to-value.js";
import { import {
dbClose, dbClose,
dbRun, dbRun,
@ -35,7 +37,7 @@ export class SQLiteStorageDriver implements StorageDriver {
this.db = new Database(path); this.db = new Database(path);
// Enable Write-Ahead Logging, which is faster and more reliable. // Enable Write-Ahead Logging, which is faster and more reliable.
this.db.run("PRAGMA journal_mode= WAL;"); this.db.run("PRAGMA journal_mode = WAL;");
this.db.run("PRAGMA foreign_keys = ON;"); this.db.run("PRAGMA foreign_keys = ON;");
} }
@ -58,17 +60,17 @@ export class SQLiteStorageDriver implements StorageDriver {
* Insert data into the store * Insert data into the store
*/ */
async insert( async insert(
collectionName: string, model: Model,
record: Record<string, any>, record: Record<string, any>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
try { try {
const sql = `INSERT INTO ${collectionName} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`; const sql = `INSERT INTO ${model.name} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`;
const stmt = await this.getOrCreatePreparedStatement(sql); const stmt = await this.getOrCreatePreparedStatement(sql);
return await run(stmt, recordToParams(record)); return await run(stmt, recordToParams(model, record));
} catch (error: any) { } catch (error: any) {
return new StoreQueryError(error.message, { return new StoreQueryError(error.message, {
error, error,
collectionName, collectionName: model.name,
record, record,
}); });
} }
@ -77,11 +79,14 @@ export class SQLiteStorageDriver implements StorageDriver {
/** /**
* Run a select query against the store. * Run a select query against the store.
*/ */
async select(query: QueryDefinition): AsyncResult<any[], StoreQueryError> { async select(
model: Model,
query: QueryDefinition,
): AsyncResult<any[], StoreQueryError> {
try { try {
const sql = `SELECT * FROM ${query.from}`; const sql = `SELECT * FROM ${query.from}`;
const stmt = await this.getOrCreatePreparedStatement(sql); const stmt = await this.getOrCreatePreparedStatement(sql);
return await getAll(stmt); return await getAll(stmt, transformRow(model));
} catch (error: any) { } catch (error: any) {
return new StoreQueryError(error.message, { return new StoreQueryError(error.message, {
error, error,
@ -93,12 +98,15 @@ export class SQLiteStorageDriver implements StorageDriver {
/** /**
* Run a select query against the store. * Run a select query against the store.
*/ */
async selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError> { async selectOne(
model: Model,
query: QueryDefinition,
): AsyncResult<any, StoreQueryError> {
try { try {
const sql = `SELECT * FROM ${query.from}`; const sql = `SELECT * FROM ${query.from}`;
const stmt = await this.getOrCreatePreparedStatement(sql); const stmt = await this.getOrCreatePreparedStatement(sql);
return await getOne(stmt); return await getOne(stmt, transformRow(model));
} catch (error: any) { } catch (error: any) {
return new StoreQueryError(error.message, { return new StoreQueryError(error.message, {
error, error,
@ -161,16 +169,16 @@ export class SQLiteStorageDriver implements StorageDriver {
* Update a record in the store. * Update a record in the store.
*/ */
async update( async update(
collectionName: string, model: Model,
id: string, id: string,
record: Record<string, any>, record: Record<string, any>,
): AsyncResult<void, StoreQueryError> { ): AsyncResult<void, StoreQueryError> {
try { try {
const sql = `UPDATE ${collectionName} SET ${recordToSQLSet(record)} WHERE id = :id`; const sql = `UPDATE ${model.name} SET ${recordToSQLSet(record)} WHERE id = :id`;
const stmt = await this.getOrCreatePreparedStatement(sql); const stmt = await this.getOrCreatePreparedStatement(sql);
return await run( return await run(
stmt, stmt,
recordToParams({ recordToParams(model, {
...record, ...record,
id, id,
}), }),
@ -178,7 +186,7 @@ export class SQLiteStorageDriver implements StorageDriver {
} catch (error: any) { } catch (error: any) {
return new StoreQueryError(error.message, { return new StoreQueryError(error.message, {
error, error,
collectionName, collectionName: model.name,
record, record,
}); });
} }
@ -188,18 +196,15 @@ export class SQLiteStorageDriver implements StorageDriver {
* Delete a record from the store. * Delete a record from the store.
*/ */
async delete( async delete(model: Model, id: string): AsyncResult<void, StoreQueryError> {
collectionName: string,
id: string,
): AsyncResult<void, StoreQueryError> {
try { try {
const sql = `DELETE FROM ${collectionName} WHERE id = :id`; const sql = `DELETE FROM ${model.name} 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, collectionName: model.name,
id, id,
}); });
} }

View File

@ -52,25 +52,31 @@ export function run(
}); });
} }
export function getAll(stmt: Statement): Promise<Record<string, any>[]> { export function getAll(
stmt: Statement,
transformer: (row: any) => any,
): Promise<Record<string, any>[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stmt.all((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); resolve(rows.map(transformer));
} }
}); });
}); });
} }
export function getOne(stmt: Statement): Promise<Record<string, any>> { export function getOne(
stmt: Statement,
transformer: (row: any) => any,
): Promise<Record<string, any>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stmt.get((err: Error | null, row: Record<string, any>) => { stmt.get((err: Error | null, row: Record<string, any>) => {
if (err) { if (err) {
reject(err); reject(err);
} else { } else {
resolve(row); resolve(transformer(row));
} }
}); });
}); });

View File

@ -0,0 +1,26 @@
/* 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,
};
export function fieldValueToSQL(field: FieldDefinition, value: any) {
const r = FieldSQLInsertMap[field[VariantTag]] as any;
return r(field as any, value);
}

View File

@ -419,6 +419,19 @@ __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:^"
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
@ -855,23 +868,12 @@ __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