Compare commits

..

No commits in common. "14ca23ef74d011fc1bfc31f2e637caeeec95eb8d" and "27dbd447411f95de2bf2be43e1e8f926ecbc48f5" have entirely different histories.

31 changed files with 143 additions and 968 deletions

View File

@ -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"
} }

View File

@ -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",

View File

@ -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";

View File

@ -1 +0,0 @@
export * from "./services/mocks.js";

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
} }

View File

@ -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 }>;
};

View File

@ -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]>;
}; };

View File

@ -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>[];

View File

@ -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,
}); });

View File

@ -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",
},
]);
});
});

View File

@ -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]>>;
}
} }

View File

@ -1 +0,0 @@
export * from "./uuid-generator.js";

View File

@ -1 +0,0 @@
export * from "./uuid-generator.mock.js";

View File

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

View File

@ -1,5 +0,0 @@
import { UUID } from "../types/uuid.js";
export interface UUIDGenerator {
generate(): UUID;
}

View File

@ -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>;
} }

View File

@ -1 +0,0 @@
export { Decimal } from "decimal.js";

View File

@ -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",

View File

@ -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,
});
});
});

View File

@ -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),
};
}

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 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>>,
) { ) {

View File

@ -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}`;
}

View File

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

View File

@ -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,
},
]);
});
}); });

View File

@ -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,
}); });
} }

View File

@ -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);
} }
}); });
}); });

View File

@ -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);
}

View File

@ -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"