diff --git a/packages/fabric/domain/package.json b/packages/fabric/domain/package.json index 913751b..a9cd059 100644 --- a/packages/fabric/domain/package.json +++ b/packages/fabric/domain/package.json @@ -9,6 +9,7 @@ "private": true, "packageManager": "yarn@4.1.1", "devDependencies": { + "@fabric/store-sqlite": "workspace:^", "typescript": "^5.6.2", "vitest": "^2.1.1" }, diff --git a/packages/fabric/domain/src/models/fields/field-to-type.ts b/packages/fabric/domain/src/models/fields/field-to-type.ts index bbe974e..16d1bff 100644 --- a/packages/fabric/domain/src/models/fields/field-to-type.ts +++ b/packages/fabric/domain/src/models/fields/field-to-type.ts @@ -1,5 +1,6 @@ import { UUID } from "../../types/uuid.js"; import { IntegerField } from "./integer.js"; +import { ReferenceField } from "./reference-field.js"; import { StringField } from "./string-field.js"; import { UUIDField } from "./uuid-field.js"; @@ -13,8 +14,12 @@ export type FieldToType = TField extends StringField : TField extends IntegerField ? TField["hasArbitraryPrecision"] extends true ? ToOptional - : ToOptional - : never; + : TField["hasArbitraryPrecision"] extends false + ? ToOptional + : ToOptional + : TField extends ReferenceField + ? ToOptional + : never; type ToOptional = TField extends { isOptional: true } ? TType | null diff --git a/packages/fabric/domain/src/models/fields/index.ts b/packages/fabric/domain/src/models/fields/index.ts index f70e4e4..b29c267 100644 --- a/packages/fabric/domain/src/models/fields/index.ts +++ b/packages/fabric/domain/src/models/fields/index.ts @@ -3,6 +3,7 @@ import { createReferenceField, ReferenceField } from "./reference-field.js"; import { createStringField, StringField } from "./string-field.js"; import { createUUIDField, UUIDField } from "./uuid-field.js"; export * from "./base-field.js"; +export * from "./field-to-type.js"; export * from "./reference-field.js"; export type FieldDefinition = diff --git a/packages/fabric/domain/src/models/model-schema.ts b/packages/fabric/domain/src/models/model-schema.ts index 260cc66..8150980 100644 --- a/packages/fabric/domain/src/models/model-schema.ts +++ b/packages/fabric/domain/src/models/model-schema.ts @@ -1,3 +1,7 @@ import { Model } from "./model.js"; export type ModelSchema = Record; + +export type ModelSchemaFromModels = { + [K in TModels["name"]]: Extract; +}; diff --git a/packages/fabric/domain/src/models/query/query-builder.ts b/packages/fabric/domain/src/models/query/query-builder.ts index d3f5a95..40304e6 100644 --- a/packages/fabric/domain/src/models/query/query-builder.ts +++ b/packages/fabric/domain/src/models/query/query-builder.ts @@ -1,6 +1,7 @@ import { AsyncResult, Keyof } from "@fabric/core"; import { StoreQueryError } from "../../errors/query-error.js"; import { StorageDriver } from "../../storage/storage-driver.js"; +import { ModelSchema } from "../model-schema.js"; import { FilterOptions } from "./filter-options.js"; import { OrderByOptions } from "./order-by-options.js"; import { @@ -14,25 +15,26 @@ import { export class QueryBuilder implements StoreQuery { constructor( private driver: StorageDriver, + private schema: ModelSchema, private query: QueryDefinition, ) {} where(where: FilterOptions): StoreSortableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, where, }); } orderBy(opts: OrderByOptions): StoreLimitableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, orderBy: opts, }); } limit(limit: number, offset?: number | undefined): SelectableQuery { - return new QueryBuilder(this.driver, { + return new QueryBuilder(this.driver, this.schema, { ...this.query, limit, offset, @@ -42,7 +44,7 @@ export class QueryBuilder implements StoreQuery { select>( keys?: K[], ): AsyncResult[], StoreQueryError> { - return this.driver.select({ + return this.driver.select(this.schema[this.query.from], { ...this.query, keys, }); @@ -51,7 +53,7 @@ export class QueryBuilder implements StoreQuery { selectOne>( keys?: K[], ): AsyncResult, StoreQueryError> { - return this.driver.selectOne({ + return this.driver.selectOne(this.schema[this.query.from], { ...this.query, keys, }); diff --git a/packages/fabric/domain/src/models/state-store.spec.ts b/packages/fabric/domain/src/models/state-store.spec.ts new file mode 100644 index 0000000..1f63dd1 --- /dev/null +++ b/packages/fabric/domain/src/models/state-store.spec.ts @@ -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; + }); +}); diff --git a/packages/fabric/domain/src/models/state-store.ts b/packages/fabric/domain/src/models/state-store.ts index d72f93b..1dcfa25 100644 --- a/packages/fabric/domain/src/models/state-store.ts +++ b/packages/fabric/domain/src/models/state-store.ts @@ -1,5 +1,31 @@ +import { AsyncResult } from "@fabric/core"; +import { StoreQueryError } from "../errors/query-error.js"; import { StorageDriver } from "../storage/storage-driver.js"; +import { ModelSchemaFromModels } from "./model-schema.js"; +import { Model, ModelToType } from "./model.js"; -export class StateStore { - constructor(private driver: StorageDriver) {} +export class StateStore { + private schema: ModelSchemaFromModels; + constructor( + private driver: StorageDriver, + models: TModel[], + ) { + this.schema = models.reduce((acc, model: TModel) => { + return { + ...acc, + [model.name]: model, + }; + }, {} as ModelSchemaFromModels); + } + + async migrate(): AsyncResult { + await this.driver.sync(this.schema); + } + + async insertInto>( + collection: T, + record: ModelToType[T]>, + ): AsyncResult { + return this.driver.insert(this.schema[collection], record); + } } diff --git a/packages/fabric/domain/src/storage/storage-driver.ts b/packages/fabric/domain/src/storage/storage-driver.ts index 0ba690b..730463d 100644 --- a/packages/fabric/domain/src/storage/storage-driver.ts +++ b/packages/fabric/domain/src/storage/storage-driver.ts @@ -4,6 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core"; import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { StoreQueryError } from "../errors/query-error.js"; import { ModelSchema } from "../models/model-schema.js"; +import { Model } from "../models/model.js"; import { QueryDefinition } from "../models/query/query.js"; export interface StorageDriver { @@ -11,19 +12,25 @@ export interface StorageDriver { * Insert data into the store */ insert( - collectionName: string, + model: Model, record: Record, ): AsyncResult; /** * Run a select query against the store. */ - select(query: QueryDefinition): AsyncResult; + select( + model: Model, + query: QueryDefinition, + ): AsyncResult; /** * Run a select query against the store. */ - selectOne(query: QueryDefinition): AsyncResult; + selectOne( + model: Model, + query: QueryDefinition, + ): AsyncResult; /** * Sincronice the store with the schema. @@ -46,7 +53,7 @@ export interface StorageDriver { * Update a record in the store. */ update( - collectionName: string, + model: Model, id: string, record: Record, ): AsyncResult; @@ -54,8 +61,5 @@ export interface StorageDriver { /** * Delete a record from the store. */ - delete( - collectionName: string, - id: string, - ): AsyncResult; + delete(model: Model, id: string): AsyncResult; } diff --git a/packages/fabric/domain/src/types/uuid.ts b/packages/fabric/domain/src/types/uuid.ts index 07dd212..7c495b0 100644 --- a/packages/fabric/domain/src/types/uuid.ts +++ b/packages/fabric/domain/src/types/uuid.ts @@ -1 +1,5 @@ export type UUID = `${string}-${string}-${string}-${string}-${string}`; + +export function generateUUID(): UUID { + return crypto.randomUUID() as UUID; +} diff --git a/packages/fabric/store-sqlite/package.json b/packages/fabric/store-sqlite/package.json index b89b03f..813d1f6 100644 --- a/packages/fabric/store-sqlite/package.json +++ b/packages/fabric/store-sqlite/package.json @@ -1,5 +1,5 @@ { - "name": "@ulthar/store-sqlite", + "name": "@fabric/store-sqlite", "type": "module", "module": "dist/index.js", "main": "dist/index.js", diff --git a/packages/fabric/store-sqlite/src/model-to-sql.ts b/packages/fabric/store-sqlite/src/model-to-sql.ts index 9032b8d..270ed76 100644 --- a/packages/fabric/store-sqlite/src/model-to-sql.ts +++ b/packages/fabric/store-sqlite/src/model-to-sql.ts @@ -2,14 +2,14 @@ import { Variant, VariantTag } from "@fabric/core"; import { FieldDefinition, getTargetKey, Model } from "@fabric/domain"; -type FieldMap = { +type FieldSQLDefinitionMap = { [K in FieldDefinition[VariantTag]]: ( name: string, field: Extract, ) => string; }; -const FieldMap: FieldMap = { +const FieldSQLDefinitionMap: FieldSQLDefinitionMap = { StringField: (n, f) => { return [n, "TEXT", modifiersFromOpts(f)].join(" "); }, @@ -34,6 +34,9 @@ const FieldMap: FieldMap = { ].join(" "); }, }; +function fieldDefinitionToSQL(name: string, field: FieldDefinition) { + return FieldSQLDefinitionMap[field[VariantTag]](name, field as any); +} function modifiersFromOpts(field: FieldDefinition) { if (Variant.is(field, "UUIDField") && field.isPrimaryKey) { @@ -45,10 +48,6 @@ function modifiersFromOpts(field: FieldDefinition) { ].join(" "); } -function fieldDefinitionToSQL(name: string, field: FieldDefinition) { - return FieldMap[field[VariantTag]](name, field as any); -} - export function modelToSql( model: Model>, ) { diff --git a/packages/fabric/store-sqlite/src/record-utils.ts b/packages/fabric/store-sqlite/src/record-utils.ts index 66bc001..7d26eeb 100644 --- a/packages/fabric/store-sqlite/src/record-utils.ts +++ b/packages/fabric/store-sqlite/src/record-utils.ts @@ -1,5 +1,8 @@ /* 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. */ @@ -12,9 +15,12 @@ export function recordToKeys(record: Record, prefix = "") { /** * Unfold a record into a string of it's keys separated by commas. */ -export function recordToParams(record: Record) { +export function recordToParams(model: Model, record: Record) { return Object.keys(record).reduce( - (acc, key) => ({ ...acc, [`:${key}`]: record[key] }), + (acc, key) => ({ + ...acc, + [`:${key}`]: fieldValueToSQL(model.fields[key], record[key]), + }), {}, ); } diff --git a/packages/fabric/store-sqlite/src/sql-to-value.ts b/packages/fabric/store-sqlite/src/sql-to-value.ts new file mode 100644 index 0000000..584999d --- /dev/null +++ b/packages/fabric/store-sqlite/src/sql-to-value.ts @@ -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) => { + const result: Record = {}; + 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, + value: any, + ) => FieldToType>; +}; +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, +}; diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts index 8dc3f8c..36e4ee6 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.spec.ts @@ -26,17 +26,19 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + const insertResult = await store.insert(schema.users, { id: "1", name: "test", 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([ - { 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; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "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([ - { 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; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "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([]); }); @@ -85,24 +87,24 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.insert("users", { + await store.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1, }); - const records = await store.select({ from: "users" }); + const records = await store.select(schema.users, { from: "users" }); expect(records).toEqual([ - { id: "1", name: "test", streamId: "1", streamVersion: 1 }, - { id: "2", name: "test", streamId: "2", streamVersion: 1 }, + { id: "1", name: "test", streamId: "1", streamVersion: 1n }, + { id: "2", name: "test", streamId: "2", streamVersion: 1n }, ]); }); @@ -111,26 +113,26 @@ describe("SQLite Store Driver", () => { if (isError(result)) throw result; - await store.insert("users", { + await store.insert(schema.users, { id: "1", name: "test", streamId: "1", streamVersion: 1, }); - await store.insert("users", { + await store.insert(schema.users, { id: "2", name: "test", streamId: "2", streamVersion: 1, }); - const record = await store.selectOne({ from: "users" }); + const record = await store.selectOne(schema.users, { from: "users" }); expect(record).toEqual({ id: "1", name: "test", streamId: "1", - streamVersion: 1, + streamVersion: 1n, }); }); }); diff --git a/packages/fabric/store-sqlite/src/sqlite-driver.ts b/packages/fabric/store-sqlite/src/sqlite-driver.ts index 24fa8a0..30c95d0 100644 --- a/packages/fabric/store-sqlite/src/sqlite-driver.ts +++ b/packages/fabric/store-sqlite/src/sqlite-driver.ts @@ -4,6 +4,7 @@ import { unlink } from "fs/promises"; import { CircularDependencyError, + Model, ModelSchema, QueryDefinition, StorageDriver, @@ -16,6 +17,7 @@ import { recordToParams, recordToSQLSet, } from "./record-utils.js"; +import { transformRow } from "./sql-to-value.js"; import { dbClose, dbRun, @@ -35,7 +37,7 @@ export class SQLiteStorageDriver implements StorageDriver { this.db = new Database(path); // Enable Write-Ahead Logging, which is faster and more reliable. - this.db.run("PRAGMA journal_mode= WAL;"); + this.db.run("PRAGMA journal_mode = WAL;"); this.db.run("PRAGMA foreign_keys = ON;"); } @@ -58,17 +60,17 @@ export class SQLiteStorageDriver implements StorageDriver { * Insert data into the store */ async insert( - collectionName: string, + model: Model, record: Record, ): AsyncResult { 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); - return await run(stmt, recordToParams(record)); + return await run(stmt, recordToParams(model, record)); } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, record, }); } @@ -77,11 +79,14 @@ export class SQLiteStorageDriver implements StorageDriver { /** * Run a select query against the store. */ - async select(query: QueryDefinition): AsyncResult { + async select( + model: Model, + query: QueryDefinition, + ): AsyncResult { try { const sql = `SELECT * FROM ${query.from}`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await getAll(stmt); + return await getAll(stmt, transformRow(model)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -93,12 +98,15 @@ export class SQLiteStorageDriver implements StorageDriver { /** * Run a select query against the store. */ - async selectOne(query: QueryDefinition): AsyncResult { + async selectOne( + model: Model, + query: QueryDefinition, + ): AsyncResult { try { const sql = `SELECT * FROM ${query.from}`; const stmt = await this.getOrCreatePreparedStatement(sql); - return await getOne(stmt); + return await getOne(stmt, transformRow(model)); } catch (error: any) { return new StoreQueryError(error.message, { error, @@ -161,16 +169,16 @@ export class SQLiteStorageDriver implements StorageDriver { * Update a record in the store. */ async update( - collectionName: string, + model: Model, id: string, record: Record, ): AsyncResult { 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); return await run( stmt, - recordToParams({ + recordToParams(model, { ...record, id, }), @@ -178,7 +186,7 @@ export class SQLiteStorageDriver implements StorageDriver { } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, record, }); } @@ -188,18 +196,15 @@ export class SQLiteStorageDriver implements StorageDriver { * Delete a record from the store. */ - async delete( - collectionName: string, - id: string, - ): AsyncResult { + async delete(model: Model, id: string): AsyncResult { try { - const sql = `DELETE FROM ${collectionName} WHERE id = :id`; + const sql = `DELETE FROM ${model.name} WHERE id = :id`; const stmt = await this.getOrCreatePreparedStatement(sql); return await run(stmt, { ":id": id }); } catch (error: any) { return new StoreQueryError(error.message, { error, - collectionName, + collectionName: model.name, id, }); } diff --git a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts index ecc12a8..f98fc83 100644 --- a/packages/fabric/store-sqlite/src/sqlite-wrapper.ts +++ b/packages/fabric/store-sqlite/src/sqlite-wrapper.ts @@ -52,25 +52,31 @@ export function run( }); } -export function getAll(stmt: Statement): Promise[]> { +export function getAll( + stmt: Statement, + transformer: (row: any) => any, +): Promise[]> { return new Promise((resolve, reject) => { stmt.all((err: Error | null, rows: Record[]) => { if (err) { reject(err); } else { - resolve(rows); + resolve(rows.map(transformer)); } }); }); } -export function getOne(stmt: Statement): Promise> { +export function getOne( + stmt: Statement, + transformer: (row: any) => any, +): Promise> { return new Promise((resolve, reject) => { stmt.get((err: Error | null, row: Record) => { if (err) { reject(err); } else { - resolve(row); + resolve(transformer(row)); } }); }); diff --git a/packages/fabric/store-sqlite/src/value-to-sql.ts b/packages/fabric/store-sqlite/src/value-to-sql.ts new file mode 100644 index 0000000..d39bb13 --- /dev/null +++ b/packages/fabric/store-sqlite/src/value-to-sql.ts @@ -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, + value: FieldToType>, + ) => 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); +} diff --git a/yarn.lock b/yarn.lock index b4d4b7d..dc7d075 100644 --- a/yarn.lock +++ b/yarn.lock @@ -419,6 +419,19 @@ __metadata: resolution: "@fabric/domain@workspace:packages/fabric/domain" dependencies: "@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" vitest: "npm:^2.1.1" languageName: unknown @@ -855,23 +868,12 @@ __metadata: languageName: unknown 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": version: 0.0.0-use.local resolution: "@ulthar/template-domain@workspace:packages/templates/domain" dependencies: "@fabric/core": "workspace:^" + "@fabric/domain": "workspace:^" typescript: "npm:^5.6.2" vitest: "npm:^2.1.1" languageName: unknown