[fabric/domain] Add typed insertion to state-store
This commit is contained in:
parent
27dbd44741
commit
f0c77398e6
@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 }
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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 }>;
|
||||||
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
44
packages/fabric/domain/src/models/state-store.spec.ts
Normal file
44
packages/fabric/domain/src/models/state-store.spec.ts
Normal 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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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>>,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -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]),
|
||||||
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
37
packages/fabric/store-sqlite/src/sql-to-value.ts
Normal file
37
packages/fabric/store-sqlite/src/sql-to-value.ts
Normal 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,
|
||||||
|
};
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
26
packages/fabric/store-sqlite/src/value-to-sql.ts
Normal file
26
packages/fabric/store-sqlite/src/value-to-sql.ts
Normal 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);
|
||||||
|
}
|
||||||
26
yarn.lock
26
yarn.lock
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user