Compare commits
No commits in common. "b5045ed4c81659f367931e321fdf706a262de61a" and "8aaf4c73e88de830f14b3ee70605df1199f0181e" have entirely different histories.
b5045ed4c8
...
8aaf4c73e8
14
package.json
14
package.json
@ -9,18 +9,18 @@
|
|||||||
"apps/**/*"
|
"apps/**/*"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.10.0",
|
"@eslint/js": "^9.9.1",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/eslint__js": "^8.42.3",
|
"@types/eslint__js": "^8.42.3",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^9.10.0",
|
"eslint": "^9.9.1",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.5",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^15.2.10",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"tsx": "^4.19.1",
|
"tsx": "^4.19.0",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.5.4",
|
||||||
"typescript-eslint": "^8.6.0",
|
"typescript-eslint": "^8.4.0",
|
||||||
"zx": "^8.1.7"
|
"zx": "^8.1.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint . --fix --report-unused-disable-directives",
|
"lint": "eslint . --fix --report-unused-disable-directives",
|
||||||
|
|||||||
@ -16,9 +16,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/validator": "^13.12.2",
|
"@types/validator": "^13.12.1",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.5.4",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { TaggedVariant } from "../../variant/variant.js";
|
import { TaggedVariant } from "../../variant/variant.js";
|
||||||
import { UUID } from "../types/uuid.js";
|
import { UUID } from "../types/uuid.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event is a tagged variant with a payload and a timestamp.
|
* An event is a tagged variant with a payload and a timestamp.
|
||||||
*/
|
*/
|
||||||
export interface Event<TTag extends string = string, TPayload = any>
|
export interface Event<TTag extends string, TPayload>
|
||||||
extends TaggedVariant<TTag> {
|
extends TaggedVariant<TTag> {
|
||||||
streamId: UUID;
|
streamId: UUID;
|
||||||
payload: TPayload;
|
payload: TPayload;
|
||||||
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./entity/index.js";
|
export * from "./entity/index.js";
|
||||||
export * from "./models/index.js";
|
|
||||||
export * from "./security/index.js";
|
export * from "./security/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./use-case/index.js";
|
export * from "./use-case/index.js";
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { createStringField, StringField } from "./string-field.js";
|
import { createStringField, StringField } from "./string-field.js";
|
||||||
import { createUUIDField, UUIDField } from "./uuid-field.js";
|
import { createUUIDField } from "./uuid-field.js";
|
||||||
export * from "./base-field.js";
|
|
||||||
|
|
||||||
export type FieldDefinition = StringField | UUIDField;
|
export type FieldDefinition = StringField;
|
||||||
|
|
||||||
export namespace Field {
|
export namespace Field {
|
||||||
export const string = createStringField;
|
export const string = createStringField;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ export interface StringField
|
|||||||
StringFieldOptions {}
|
StringFieldOptions {}
|
||||||
|
|
||||||
export function createStringField<T extends StringFieldOptions>(
|
export function createStringField<T extends StringFieldOptions>(
|
||||||
opts: T = {} as T,
|
opts: T,
|
||||||
): StringField & T {
|
): StringField & T {
|
||||||
return {
|
return {
|
||||||
[VariantTag]: "StringField",
|
[VariantTag]: "StringField",
|
||||||
|
|||||||
@ -5,11 +5,11 @@ export interface UUIDOptions extends BaseField {
|
|||||||
isPrimaryKey?: boolean;
|
isPrimaryKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UUIDField extends TaggedVariant<"UUIDField">, UUIDOptions {}
|
export interface UUIDField extends TaggedVariant<"UUID_FIELD">, UUIDOptions {}
|
||||||
|
|
||||||
export function createUUIDField(opts: UUIDOptions): UUIDField {
|
export function createUUIDField(opts: UUIDOptions): UUIDField {
|
||||||
return {
|
return {
|
||||||
[VariantTag]: "UUIDField",
|
[VariantTag]: "UUID_FIELD",
|
||||||
...opts,
|
...opts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./create-model.js";
|
|
||||||
export * from "./fields/index.js";
|
|
||||||
export * from "./model-to-type.js";
|
|
||||||
export * from "./relations/index.js";
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./is-error.js";
|
|
||||||
export * from "./tagged-error.js";
|
export * from "./tagged-error.js";
|
||||||
export * from "./unexpected-error.js";
|
export * from "./unexpected-error.js";
|
||||||
|
|||||||
@ -8,12 +8,7 @@ import { TaggedError } from "./tagged-error.js";
|
|||||||
* we must be prepared to handle.
|
* we must be prepared to handle.
|
||||||
*/
|
*/
|
||||||
export class UnexpectedError extends TaggedError<"UnexpectedError"> {
|
export class UnexpectedError extends TaggedError<"UnexpectedError"> {
|
||||||
constructor(readonly context: Record<string, unknown> = {}) {
|
constructor() {
|
||||||
super("UnexpectedError");
|
super("UnexpectedError");
|
||||||
this.message = "An unexpected error occurred";
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `UnexpectedError: ${this.message}\n${JSON.stringify(this.context, null, 2)}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ export * from "./domain/index.js";
|
|||||||
export * from "./error/index.js";
|
export * from "./error/index.js";
|
||||||
export * from "./record/index.js";
|
export * from "./record/index.js";
|
||||||
export * from "./result/index.js";
|
export * from "./result/index.js";
|
||||||
export * from "./storage/index.js";
|
|
||||||
export * from "./time/index.js";
|
export * from "./time/index.js";
|
||||||
export * from "./types/index.js";
|
export * from "./types/index.js";
|
||||||
export * from "./variant/index.js";
|
export * from "./variant/index.js";
|
||||||
|
|||||||
54
packages/fabric/core/src/storage/driver.ts
Normal file
54
packages/fabric/core/src/storage/driver.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import { ModelDefinition } from "../domain/models/create-model.js";
|
||||||
|
import { TaggedError } from "../error/tagged-error.js";
|
||||||
|
import { AsyncResult } from "../result/async-result.js";
|
||||||
|
import { QueryDefinition } from "./query/query.js";
|
||||||
|
import { CircularDependencyError } from "./utils/sort-by-dependencies.js";
|
||||||
|
|
||||||
|
export interface StorageDriver {
|
||||||
|
/**
|
||||||
|
* Insert data into the store
|
||||||
|
*/
|
||||||
|
insert(collectionName: string, record: any): AsyncResult<void, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a select query against the store.
|
||||||
|
*/
|
||||||
|
select(query: QueryDefinition): AsyncResult<any[], QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a select query against the store.
|
||||||
|
*/
|
||||||
|
selectOne(query: QueryDefinition): AsyncResult<any, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sincronice the store with the schema.
|
||||||
|
*/
|
||||||
|
sync(
|
||||||
|
schema: ModelDefinition[],
|
||||||
|
): AsyncResult<void, QueryError | CircularDependencyError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the store. This is a destructive operation.
|
||||||
|
*/
|
||||||
|
drop(): AsyncResult<void, QueryError>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a record in the store.
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
collectionName: string,
|
||||||
|
id: string,
|
||||||
|
record: Record<string, any>,
|
||||||
|
): AsyncResult<void, QueryError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryError extends TaggedError<"QueryError"> {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
public context: any,
|
||||||
|
) {
|
||||||
|
super("QueryError");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { TaggedError } from "../../error/tagged-error.js";
|
|
||||||
|
|
||||||
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> {
|
|
||||||
context: { key: string; dep: string };
|
|
||||||
constructor(key: string, dep: string) {
|
|
||||||
super("CircularDependencyError");
|
|
||||||
this.context = { key, dep };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./circular-dependency-error.js";
|
|
||||||
export * from "./query-error.js";
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { TaggedError } from "../../error/tagged-error.js";
|
|
||||||
|
|
||||||
export class StoreQueryError extends TaggedError<"StoreQueryError"> {
|
|
||||||
constructor(
|
|
||||||
public message: string,
|
|
||||||
public context: any,
|
|
||||||
) {
|
|
||||||
super("StoreQueryError");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
import { Event } from "../domain/events/event.js";
|
|
||||||
import { UUID } from "../domain/index.js";
|
|
||||||
import { AsyncResult } from "../result/async-result.js";
|
|
||||||
import { PosixDate } from "../time/posix-date.js";
|
|
||||||
import { MaybePromise } from "../types/maybe-promise.js";
|
|
||||||
import { StoreQueryError } from "./errors/query-error.js";
|
|
||||||
|
|
||||||
export interface EventStore<TEvent extends Event = Event> {
|
|
||||||
getStream<TEventStreamEvent extends TEvent>(
|
|
||||||
streamId: UUID,
|
|
||||||
): AsyncResult<EventStream<TEventStreamEvent>, StoreQueryError>;
|
|
||||||
|
|
||||||
appendToStream<TEvent extends Event>(
|
|
||||||
streamId: UUID,
|
|
||||||
events: TEvent,
|
|
||||||
): AsyncResult<void, StoreQueryError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventStream<TEvent extends Event = Event> {
|
|
||||||
getCurrentVersion(): bigint;
|
|
||||||
|
|
||||||
append(events: TEvent): AsyncResult<StoredEvent<TEvent>, StoreQueryError>;
|
|
||||||
|
|
||||||
subscribe(callback: (event: StoredEvent<TEvent>) => MaybePromise<void>): void;
|
|
||||||
|
|
||||||
getEvents(
|
|
||||||
opts?: EventFilterOptions,
|
|
||||||
): AsyncResult<StoredEvent<TEvent>[], StoreQueryError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventFilterOptions {
|
|
||||||
fromDate?: PosixDate;
|
|
||||||
toDate?: PosixDate;
|
|
||||||
fromVersion?: number;
|
|
||||||
toVersion?: number;
|
|
||||||
limit?: number;
|
|
||||||
offset?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StoredEvent<TEvent extends Event = Event> = TEvent & {
|
|
||||||
version: bigint;
|
|
||||||
timestamp: number;
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./errors/index.js";
|
|
||||||
export * from "./event-store.js";
|
|
||||||
export * from "./query/index.js";
|
|
||||||
export * from "./state-store.js";
|
|
||||||
export * from "./storage-driver.js";
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./filter-options.js";
|
|
||||||
export * from "./order-by-options.js";
|
|
||||||
export * from "./query-builder.js";
|
|
||||||
export * from "./query.js";
|
|
||||||
@ -6,8 +6,7 @@ import {
|
|||||||
import { ModelToType } from "../../domain/models/model-to-type.js";
|
import { ModelToType } from "../../domain/models/model-to-type.js";
|
||||||
import { AsyncResult } from "../../result/async-result.js";
|
import { AsyncResult } from "../../result/async-result.js";
|
||||||
import { Keyof } from "../../types/index.js";
|
import { Keyof } from "../../types/index.js";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { QueryError, StorageDriver } from "../driver.js";
|
||||||
import { StorageDriver } from "../storage-driver.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 {
|
||||||
@ -57,7 +56,7 @@ export class QueryBuilder<
|
|||||||
|
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(
|
||||||
keys?: K[],
|
keys?: K[],
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError> {
|
): AsyncResult<Pick<T, K>[], QueryError> {
|
||||||
return this.driver.select({
|
return this.driver.select({
|
||||||
...this.query,
|
...this.query,
|
||||||
keys,
|
keys,
|
||||||
@ -66,7 +65,7 @@ export class QueryBuilder<
|
|||||||
|
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(
|
||||||
keys?: K[],
|
keys?: K[],
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError> {
|
): AsyncResult<Pick<T, K>, QueryError> {
|
||||||
return this.driver.selectOne({
|
return this.driver.selectOne({
|
||||||
...this.query,
|
...this.query,
|
||||||
keys,
|
keys,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { AsyncResult } from "../../result/async-result.js";
|
import { AsyncResult } from "../../result/async-result.js";
|
||||||
import { Keyof } from "../../types/keyof.js";
|
import { Keyof } from "../../types/keyof.js";
|
||||||
import { StoreQueryError } from "../errors/query-error.js";
|
import { QueryError } from "../driver.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";
|
||||||
|
|
||||||
@ -10,56 +10,40 @@ export interface StoreQuery<T> {
|
|||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): AsyncResult<T[], QueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreSortableQuery<T> {
|
export interface StoreSortableQuery<T> {
|
||||||
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T>;
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): AsyncResult<T[], QueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StoreLimitableQuery<T> {
|
export interface StoreLimitableQuery<T> {
|
||||||
limit(limit: number, offset?: number): SelectableQuery<T>;
|
limit(limit: number, offset?: number): SelectableQuery<T>;
|
||||||
|
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): AsyncResult<T[], QueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectableQuery<T> {
|
export interface SelectableQuery<T> {
|
||||||
select(): AsyncResult<T[], StoreQueryError>;
|
select(): AsyncResult<T[], QueryError>;
|
||||||
select<K extends Keyof<T>>(
|
select<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>[], QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>[], StoreQueryError>;
|
|
||||||
|
|
||||||
selectOne(): AsyncResult<T, StoreQueryError>;
|
selectOne(): AsyncResult<T, QueryError>;
|
||||||
selectOne<K extends Keyof<T>>(
|
selectOne<K extends Keyof<T>>(keys: K[]): AsyncResult<Pick<T, K>, QueryError>;
|
||||||
keys: K[],
|
|
||||||
): AsyncResult<Pick<T, K>, StoreQueryError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueryDefinition<K extends string = string> {
|
export interface QueryDefinition<K extends string = string> {
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
import { ModelDefinition } from "../domain/models/create-model.js";
|
|
||||||
import { UnexpectedError } from "../error/unexpected-error.js";
|
|
||||||
import { AsyncResult } from "../result/async-result.js";
|
|
||||||
import { CircularDependencyError } from "./errors/circular-dependency-error.js";
|
|
||||||
import { StoreQueryError } from "./errors/query-error.js";
|
|
||||||
import { QueryDefinition } from "./query/query.js";
|
|
||||||
|
|
||||||
export interface StorageDriver {
|
|
||||||
/**
|
|
||||||
* Insert data into the store
|
|
||||||
*/
|
|
||||||
insert(
|
|
||||||
collectionName: string,
|
|
||||||
record: Record<string, any>,
|
|
||||||
): AsyncResult<void, StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a select query against the store.
|
|
||||||
*/
|
|
||||||
select(query: QueryDefinition): AsyncResult<any[], StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a select query against the store.
|
|
||||||
*/
|
|
||||||
selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sincronice the store with the schema.
|
|
||||||
*/
|
|
||||||
sync(
|
|
||||||
schema: ModelDefinition[],
|
|
||||||
): AsyncResult<void, StoreQueryError | CircularDependencyError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the store. This is a destructive operation.
|
|
||||||
*/
|
|
||||||
drop(): AsyncResult<void, StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the store.
|
|
||||||
*/
|
|
||||||
close(): AsyncResult<void, UnexpectedError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a record in the store.
|
|
||||||
*/
|
|
||||||
update(
|
|
||||||
collectionName: string,
|
|
||||||
id: string,
|
|
||||||
record: Record<string, any>,
|
|
||||||
): AsyncResult<void, StoreQueryError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a record from the store.
|
|
||||||
*/
|
|
||||||
delete(
|
|
||||||
collectionName: string,
|
|
||||||
id: string,
|
|
||||||
): AsyncResult<void, StoreQueryError>;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
import {
|
||||||
import { sortByDependencies } from "./sort-by-dependencies.js";
|
CircularDependencyError,
|
||||||
|
sortByDependencies,
|
||||||
|
} from "./sort-by-dependencies.js";
|
||||||
|
|
||||||
describe("sortByDependencies", () => {
|
describe("sortByDependencies", () => {
|
||||||
it("should sort an array of objects by their dependencies", () => {
|
it("should sort an array of objects by their dependencies", () => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
import { TaggedError } from "../../error/tagged-error.js";
|
||||||
import { Result } from "../../result/result.js";
|
import { Result } from "../../result/result.js";
|
||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.js";
|
|
||||||
|
|
||||||
export function sortByDependencies<T>(
|
export function sortByDependencies<T>(
|
||||||
array: T[],
|
array: T[],
|
||||||
@ -45,3 +45,11 @@ export function sortByDependencies<T>(
|
|||||||
(key) => array.find((element) => keyGetter(element) === key) as T,
|
(key) => array.find((element) => keyGetter(element) === key) as T,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CircularDependencyError extends TaggedError<"CircularDependencyError"> {
|
||||||
|
context: { key: string; dep: string };
|
||||||
|
constructor(key: string, dep: string) {
|
||||||
|
super("CircularDependencyError");
|
||||||
|
this.context = { key, dep };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
export * from "./enum.js";
|
export * from "./enum.js";
|
||||||
export * from "./fn.js";
|
export * from "./fn.js";
|
||||||
export * from "./keyof.js";
|
export * from "./keyof.js";
|
||||||
export * from "./maybe-promise.js";
|
|
||||||
export * from "./optional.js";
|
export * from "./optional.js";
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export type MaybePromise<T> = T | Promise<T>;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# store-sqlite
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@ulthar/store-sqlite",
|
|
||||||
"type": "module",
|
|
||||||
"module": "dist/index.js",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"private": true,
|
|
||||||
"packageManager": "yarn@4.1.1",
|
|
||||||
"devDependencies": {
|
|
||||||
"typescript": "^5.6.2",
|
|
||||||
"vitest": "^2.1.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@ulthar/fabric-core": "workspace:^",
|
|
||||||
"sqlite3": "^5.1.7"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "vitest",
|
|
||||||
"build": "tsc -p tsconfig.build.json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import {
|
|
||||||
BaseField,
|
|
||||||
FieldDefinition,
|
|
||||||
ModelDefinition,
|
|
||||||
VariantTag,
|
|
||||||
} from "@ulthar/fabric-core";
|
|
||||||
|
|
||||||
type FieldMap = {
|
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
|
||||||
field: Extract<FieldDefinition, { [VariantTag]: K }>,
|
|
||||||
) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const FieldMap: FieldMap = {
|
|
||||||
StringField: (f) => {
|
|
||||||
return "TEXT" + modifiersFromOpts(f);
|
|
||||||
},
|
|
||||||
UUIDField: (f) => {
|
|
||||||
return [
|
|
||||||
"TEXT",
|
|
||||||
f.isPrimaryKey ? "PRIMARY KEY" : "",
|
|
||||||
modifiersFromOpts(f),
|
|
||||||
].join(" ");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function modifiersFromOpts(options: BaseField) {
|
|
||||||
return [
|
|
||||||
!options.isOptional ? "NOT NULL" : "",
|
|
||||||
options.isUnique ? "UNIQUE" : "",
|
|
||||||
].join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
function fieldDefinitionToSQL(field: FieldDefinition) {
|
|
||||||
return FieldMap[field[VariantTag]](field as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function modelToSql(
|
|
||||||
model: ModelDefinition<string, Record<string, FieldDefinition>>,
|
|
||||||
) {
|
|
||||||
return Object.entries(model.fields)
|
|
||||||
.map(([name, type]) => `${name} ${fieldDefinitionToSQL(type)}`)
|
|
||||||
.join(", ");
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
|
||||||
*/
|
|
||||||
export function recordToKeys(record: Record<string, any>, prefix = "") {
|
|
||||||
return Object.keys(record)
|
|
||||||
.map((key) => `${prefix}${key}`)
|
|
||||||
.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
|
||||||
*/
|
|
||||||
export function recordToParams(record: Record<string, any>) {
|
|
||||||
return Object.keys(record).reduce(
|
|
||||||
(acc, key) => ({ ...acc, [`:${key}`]: record[key] }),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function recordToSQLSet(record: Record<string, any>) {
|
|
||||||
return Object.keys(record)
|
|
||||||
.map((key) => `${key} = :${key}`)
|
|
||||||
.join(", ");
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
import { createModel, Field, isError } from "@ulthar/fabric-core";
|
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
||||||
import { SQLiteStorageDriver } from "./sqlite-driver.js";
|
|
||||||
|
|
||||||
describe("SQLite Store Driver", () => {
|
|
||||||
const model = createModel({
|
|
||||||
name: "test",
|
|
||||||
fields: {
|
|
||||||
id: Field.uuid({}),
|
|
||||||
name: Field.string(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let store: SQLiteStorageDriver;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = new SQLiteStorageDriver(":memory:");
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
const result = await store.close();
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be able to synchronize the store and insert a record", async () => {
|
|
||||||
const result = await store.sync([model]);
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
|
||||||
|
|
||||||
const records = await store.select({ from: "test" });
|
|
||||||
|
|
||||||
expect(records).toEqual([{ id: "1", name: "test" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be able to update a record", async () => {
|
|
||||||
const result = await store.sync([model]);
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
|
||||||
|
|
||||||
await store.update("test", "1", { name: "updated" });
|
|
||||||
|
|
||||||
const records = await store.select({ from: "test" });
|
|
||||||
|
|
||||||
expect(records).toEqual([{ id: "1", name: "updated" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be able to delete a record", async () => {
|
|
||||||
const result = await store.sync([model]);
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
|
||||||
|
|
||||||
await store.delete("test", "1");
|
|
||||||
|
|
||||||
const records = await store.select({ from: "test" });
|
|
||||||
|
|
||||||
expect(records).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be able to select records", async () => {
|
|
||||||
const result = await store.sync([model]);
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
|
||||||
await store.insert("test", { id: "2", name: "test" });
|
|
||||||
|
|
||||||
const records = await store.select({ from: "test" });
|
|
||||||
|
|
||||||
expect(records).toEqual([
|
|
||||||
{ id: "1", name: "test" },
|
|
||||||
{ id: "2", name: "test" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should be able to select one record", async () => {
|
|
||||||
const result = await store.sync([model]);
|
|
||||||
|
|
||||||
if (isError(result)) throw result;
|
|
||||||
|
|
||||||
await store.insert("test", { id: "1", name: "test" });
|
|
||||||
await store.insert("test", { id: "2", name: "test" });
|
|
||||||
|
|
||||||
const record = await store.selectOne({ from: "test" });
|
|
||||||
|
|
||||||
expect(record).toEqual({ id: "1", name: "test" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,207 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import {
|
|
||||||
AsyncResult,
|
|
||||||
CircularDependencyError,
|
|
||||||
ModelDefinition,
|
|
||||||
QueryDefinition,
|
|
||||||
StorageDriver,
|
|
||||||
StoreQueryError,
|
|
||||||
UnexpectedError,
|
|
||||||
} from "@ulthar/fabric-core";
|
|
||||||
import { unlink } from "fs/promises";
|
|
||||||
|
|
||||||
import { Database, Statement } from "sqlite3";
|
|
||||||
import { modelToSql } from "./model-to-sql.js";
|
|
||||||
import {
|
|
||||||
recordToKeys,
|
|
||||||
recordToParams,
|
|
||||||
recordToSQLSet,
|
|
||||||
} from "./record-utils.js";
|
|
||||||
import {
|
|
||||||
dbClose,
|
|
||||||
dbRun,
|
|
||||||
finalize,
|
|
||||||
getAll,
|
|
||||||
getOne,
|
|
||||||
prepare,
|
|
||||||
run,
|
|
||||||
} from "./sqlite-wrapper.js";
|
|
||||||
|
|
||||||
export class SQLiteStorageDriver implements StorageDriver {
|
|
||||||
private db: Database;
|
|
||||||
|
|
||||||
private cachedStatements = new Map<string, Statement>();
|
|
||||||
|
|
||||||
constructor(private path: string) {
|
|
||||||
this.db = new Database(path);
|
|
||||||
|
|
||||||
// Enable Write-Ahead Logging, which is faster and more reliable.
|
|
||||||
this.db.run("PRAGMA journal_mode= WAL;");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a statement from the cache or prepare a new one.
|
|
||||||
*/
|
|
||||||
private async getOrCreatePreparedStatement(sql: string): Promise<Statement> {
|
|
||||||
if (this.cachedStatements.has(sql)) {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- We know it's there.
|
|
||||||
return this.cachedStatements.get(sql)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stmt = await prepare(this.db, sql);
|
|
||||||
this.cachedStatements.set(sql, stmt);
|
|
||||||
|
|
||||||
return stmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert data into the store
|
|
||||||
*/
|
|
||||||
async insert(
|
|
||||||
collectionName: string,
|
|
||||||
record: Record<string, any>,
|
|
||||||
): AsyncResult<void, StoreQueryError> {
|
|
||||||
try {
|
|
||||||
const sql = `INSERT INTO ${collectionName} (${recordToKeys(record)}) VALUES (${recordToKeys(record, ":")})`;
|
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
|
||||||
return await run(stmt, recordToParams(record));
|
|
||||||
} catch (error: any) {
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
collectionName,
|
|
||||||
record,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a select query against the store.
|
|
||||||
*/
|
|
||||||
async select(query: QueryDefinition): AsyncResult<any[], StoreQueryError> {
|
|
||||||
try {
|
|
||||||
const sql = `SELECT * FROM ${query.from}`;
|
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
|
||||||
return await getAll(stmt);
|
|
||||||
} catch (error: any) {
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run a select query against the store.
|
|
||||||
*/
|
|
||||||
async selectOne(query: QueryDefinition): AsyncResult<any, StoreQueryError> {
|
|
||||||
try {
|
|
||||||
const sql = `SELECT * FROM ${query.from}`;
|
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
|
||||||
|
|
||||||
return await getOne(stmt);
|
|
||||||
} catch (error: any) {
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sincronice the store with the schema.
|
|
||||||
*/
|
|
||||||
async sync(
|
|
||||||
schema: ModelDefinition[],
|
|
||||||
): AsyncResult<void, StoreQueryError | CircularDependencyError> {
|
|
||||||
try {
|
|
||||||
await dbRun(this.db, "BEGIN TRANSACTION;");
|
|
||||||
for (const model of schema) {
|
|
||||||
const query = `CREATE TABLE ${model.name} (${modelToSql(model)});`;
|
|
||||||
await dbRun(this.db, query);
|
|
||||||
}
|
|
||||||
await dbRun(this.db, "COMMIT;");
|
|
||||||
} catch (error: any) {
|
|
||||||
await dbRun(this.db, "ROLLBACK;");
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the store. This is a destructive operation.
|
|
||||||
*/
|
|
||||||
async drop(): AsyncResult<void, StoreQueryError> {
|
|
||||||
try {
|
|
||||||
if (this.path === ":memory:") {
|
|
||||||
return new StoreQueryError("Cannot drop in-memory database", {});
|
|
||||||
} else {
|
|
||||||
await unlink(this.path);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async close(): AsyncResult<void, UnexpectedError> {
|
|
||||||
try {
|
|
||||||
for (const stmt of this.cachedStatements.values()) {
|
|
||||||
await finalize(stmt);
|
|
||||||
}
|
|
||||||
await dbClose(this.db);
|
|
||||||
} catch (error: any) {
|
|
||||||
return new UnexpectedError({ error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a record in the store.
|
|
||||||
*/
|
|
||||||
async update(
|
|
||||||
collectionName: string,
|
|
||||||
id: string,
|
|
||||||
record: Record<string, any>,
|
|
||||||
): AsyncResult<void, StoreQueryError> {
|
|
||||||
try {
|
|
||||||
const sql = `UPDATE ${collectionName} SET ${recordToSQLSet(record)} WHERE id = :id`;
|
|
||||||
const stmt = await this.getOrCreatePreparedStatement(sql);
|
|
||||||
return await run(
|
|
||||||
stmt,
|
|
||||||
recordToParams({
|
|
||||||
...record,
|
|
||||||
id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error: any) {
|
|
||||||
return new StoreQueryError(error.message, {
|
|
||||||
error,
|
|
||||||
collectionName,
|
|
||||||
record,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a record from the store.
|
|
||||||
*/
|
|
||||||
|
|
||||||
async delete(
|
|
||||||
collectionName: string,
|
|
||||||
id: string,
|
|
||||||
): AsyncResult<void, StoreQueryError> {
|
|
||||||
try {
|
|
||||||
const sql = `DELETE FROM ${collectionName} 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,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { Database, Statement } from "sqlite3";
|
|
||||||
|
|
||||||
export function dbRun(db: Database, statement: string): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.run(statement, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dbClose(db: Database): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
db.close((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepare(db: Database, statement: string): Promise<Statement> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const stmt = db.prepare(statement, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(stmt);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function run(
|
|
||||||
stmt: Statement,
|
|
||||||
params: Record<string, any>,
|
|
||||||
): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stmt.run(params, (err: Error | null) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAll(stmt: Statement): Promise<Record<string, any>[]> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stmt.all((err: Error | null, rows: Record<string, any>[]) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(rows);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOne(stmt: Statement): Promise<Record<string, any>> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stmt.get((err: Error | null, row: Record<string, any>) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve(row);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function finalize(stmt: Statement): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
stmt.finalize((err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"coverage",
|
|
||||||
"vitest.config.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"exclude": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
coverage: {
|
|
||||||
exclude: ["**/index.ts"],
|
|
||||||
},
|
|
||||||
passWithNoTests: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -9,8 +9,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.5.4",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ulthar/fabric-core": "workspace:^"
|
"@ulthar/fabric-core": "workspace:^"
|
||||||
|
|||||||
@ -9,8 +9,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.1.1",
|
"packageManager": "yarn@4.1.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.5.4",
|
||||||
"vitest": "^2.1.1"
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ulthar/fabric-core": "workspace:^"
|
"@ulthar/fabric-core": "workspace:^"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user