Compare commits

...

5 Commits

27 changed files with 255 additions and 191 deletions

View File

@ -0,0 +1,41 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { TaggedVariant, Variant, VariantTag } from "./variant.js";
interface SuccessVariant extends TaggedVariant<"success"> {
[VariantTag]: "success";
data: string;
}
interface ErrorVariant extends TaggedVariant<"error"> {
[VariantTag]: "error";
message: string;
}
describe("Variant", () => {
describe("isVariant", () => {
const successVariant = {
[VariantTag]: "success",
data: "Operation successful",
} as SuccessVariant | ErrorVariant;
const errorVariant = {
[VariantTag]: "error",
message: "Operation failed",
} as SuccessVariant | ErrorVariant;
it("should return true for a matching tag and correctly infer it", () => {
if (Variant.is(successVariant, "success")) {
expectTypeOf(successVariant).toEqualTypeOf<SuccessVariant>();
}
if (Variant.is(errorVariant, "error")) {
expectTypeOf(errorVariant).toEqualTypeOf<ErrorVariant>();
}
});
it("should return false for a non-matching tag", () => {
expect(Variant.is(successVariant, "error")).toBe(false);
expect(Variant.is(errorVariant, "success")).toBe(false);
});
});
});

View File

@ -9,3 +9,15 @@ export type VariantFromTag<
TVariant extends TaggedVariant<string>, TVariant extends TaggedVariant<string>,
TTag extends TVariant[typeof VariantTag], TTag extends TVariant[typeof VariantTag],
> = Extract<TVariant, { [VariantTag]: TTag }>; > = Extract<TVariant, { [VariantTag]: TTag }>;
export namespace Variant {
export function is<
TVariant extends TaggedVariant<string>,
TTag extends TVariant[VariantTag],
>(
variant: TVariant,
tag: TTag,
): variant is Extract<TVariant, { [VariantTag]: TTag }> {
return variant[VariantTag] === tag;
}
}

View File

@ -1,7 +1,7 @@
import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core"; import { AsyncResult, MaybePromise, PosixDate } from "@fabric/core";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
import { Event, StoredEvent } from "../events/event.js";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
import { Event, StoredEvent } from "./event.js";
export interface EventStore<TEvent extends Event = Event> { export interface EventStore<TEvent extends Event = Event> {
getStream<TEventStreamEvent extends TEvent>( getStream<TEventStreamEvent extends TEvent>(

View File

@ -1 +1,2 @@
export * from "./event-store.js";
export * from "./event.js"; export * from "./event.js";

View File

@ -2,7 +2,6 @@ export * from "./errors/index.js";
export * from "./events/index.js"; 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 "./query/index.js";
export * from "./security/index.js"; export * from "./security/index.js";
export * from "./storage/index.js"; export * from "./storage/index.js";
export * from "./types/index.js"; export * from "./types/index.js";

View File

@ -3,6 +3,7 @@ import { createReferenceField, ReferenceField } from "./reference-field.js";
import { createStringField, StringField } from "./string-field.js"; import { createStringField, StringField } from "./string-field.js";
import { createUUIDField, UUIDField } from "./uuid-field.js"; import { createUUIDField, UUIDField } from "./uuid-field.js";
export * from "./base-field.js"; export * from "./base-field.js";
export * from "./reference-field.js";
export type FieldDefinition = export type FieldDefinition =
| StringField | StringField

View File

@ -9,22 +9,22 @@ import {
describe("Validate Reference Field", () => { describe("Validate Reference Field", () => {
const schema = { const schema = {
user: defineModel({ User: defineModel("User", {
name: Field.string(), name: Field.string(),
password: Field.string(), password: Field.string(),
phone: Field.string({ isOptional: true }),
otherUnique: Field.integer({ isUnique: true }), otherUnique: Field.integer({ isUnique: true }),
otherNotUnique: Field.uuid(), otherNotUnique: Field.uuid(),
otherUser: Field.reference({
targetModel: "User",
}),
}), }),
}; };
it("should return an error when the target model is not in the schema", () => { it("should return an error when the target model is not in the schema", () => {
const result = validateReferenceField( const result = validateReferenceField(
schema, schema,
"post",
"authorId",
Field.reference({ Field.reference({
model: "foo", targetModel: "foo",
}), }),
); );
@ -33,33 +33,26 @@ describe("Validate Reference Field", () => {
} }
expect(result).toBeInstanceOf(InvalidReferenceField); expect(result).toBeInstanceOf(InvalidReferenceField);
expect(result.toString()).toBe(
"InvalidReferenceField: post.authorId. The target model 'foo' is not in the schema.",
);
}); });
it("should not return an error if the target model is in the schema", () => { it("should not return an error if the target model is in the schema", () => {
const result = validateReferenceField( const result = validateReferenceField(
schema, schema,
"post",
"authorId",
Field.reference({ Field.reference({
model: "user", targetModel: "User",
}), }),
); );
if (isError(result)) { if (isError(result)) {
throw result.toString(); throw result.reason;
} }
}); });
it("should return an error if the target key is not in the target model", () => { it("should return an error if the target key is not in the target model", () => {
const result = validateReferenceField( const result = validateReferenceField(
schema, schema,
"post",
"authorId",
Field.reference({ Field.reference({
model: "user", targetModel: "User",
targetKey: "foo", targetKey: "foo",
}), }),
); );
@ -69,34 +62,13 @@ describe("Validate Reference Field", () => {
} }
expect(result).toBeInstanceOf(InvalidReferenceField); expect(result).toBeInstanceOf(InvalidReferenceField);
expect(result.toString()).toBe(
"InvalidReferenceField: post.authorId. The target key 'foo' is not in the target model 'user'.",
);
});
it("should not return an error if the target key is in the target model", () => {
const result = validateReferenceField(
schema,
"post",
"authorId",
Field.reference({
model: "user",
targetKey: "otherUnique",
}),
);
if (isError(result)) {
throw result.toString();
}
}); });
it("should return error if the target key is not unique", () => { it("should return error if the target key is not unique", () => {
const result = validateReferenceField( const result = validateReferenceField(
schema, schema,
"post",
"authorId",
Field.reference({ Field.reference({
model: "user", targetModel: "User",
targetKey: "otherNotUnique", targetKey: "otherNotUnique",
}), }),
); );
@ -106,8 +78,19 @@ describe("Validate Reference Field", () => {
} }
expect(result).toBeInstanceOf(InvalidReferenceField); expect(result).toBeInstanceOf(InvalidReferenceField);
expect(result.toString()).toBe( });
"InvalidReferenceField: post.authorId. The target key 'user'.'otherNotUnique' is not unique.",
it("should not return an error if the target key is in the target model and is unique", () => {
const result = validateReferenceField(
schema,
Field.reference({
targetModel: "User",
targetKey: "otherUnique",
}),
); );
if (isError(result)) {
throw result.toString();
}
}); });
}); });

View File

@ -3,7 +3,7 @@ import { ModelSchema } from "../model-schema.js";
import { BaseField } from "./base-field.js"; import { BaseField } from "./base-field.js";
export interface ReferenceFieldOptions extends BaseField { export interface ReferenceFieldOptions extends BaseField {
model: string; targetModel: string;
targetKey?: string; targetKey?: string;
} }
@ -20,47 +20,38 @@ export function createReferenceField<T extends ReferenceFieldOptions>(
} as const; } as const;
} }
export function getTargetKey(field: ReferenceField): string {
return field.targetKey || "id";
}
export function validateReferenceField( export function validateReferenceField(
schema: ModelSchema, schema: ModelSchema,
modelName: string,
fieldName: string,
field: ReferenceField, field: ReferenceField,
): Result<void, InvalidReferenceField> { ): Result<void, InvalidReferenceField> {
if (!schema[field.model]) { if (!schema[field.targetModel]) {
return new InvalidReferenceField( return new InvalidReferenceField(
modelName, `The target model '${field.targetModel}' is not in the schema.`,
fieldName,
`The target model '${field.model}' is not in the schema.`,
); );
} }
if (field.targetKey && !schema[field.model][field.targetKey]) { if (field.targetKey && !schema[field.targetModel].fields[field.targetKey]) {
return new InvalidReferenceField( return new InvalidReferenceField(
modelName, `The target key '${field.targetKey}' is not in the target model '${field.targetModel}'.`,
fieldName,
`The target key '${field.targetKey}' is not in the target model '${field.model}'.`,
); );
} }
if (field.targetKey && !schema[field.model][field.targetKey].isUnique) { if (
field.targetKey &&
!schema[field.targetModel].fields[field.targetKey].isUnique
) {
return new InvalidReferenceField( return new InvalidReferenceField(
modelName, `The target key '${field.targetModel}'.'${field.targetKey}' is not unique.`,
fieldName,
`The target key '${field.model}'.'${field.targetKey}' is not unique.`,
); );
} }
} }
export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> { export class InvalidReferenceField extends TaggedError<"InvalidReferenceField"> {
constructor( constructor(readonly reason: string) {
readonly modelName: string,
readonly fieldName: string,
readonly reason: string,
) {
super("InvalidReferenceField"); super("InvalidReferenceField");
} }
toString() {
return `InvalidReferenceField: ${this.modelName}.${this.fieldName}. ${this.reason}`;
}
} }

View File

@ -1,4 +1,5 @@
export * from "./fields/index.js"; export * from "./fields/index.js";
export * from "./model-schema.js"; export * from "./model-schema.js";
export * from "./model.js"; export * from "./model.js";
export * from "./types/index.js"; export * from "./query/index.js";
export * from "./state-store.js";

View File

@ -1,12 +1,11 @@
import { describe, expectTypeOf, it } from "vitest"; import { describe, expectTypeOf, it } from "vitest";
import { UUID } from "../types/uuid.js"; import { UUID } from "../types/uuid.js";
import { Field } from "./fields/index.js"; import { Field } from "./fields/index.js";
import { defineModel } from "./model.js"; import { defineModel, ModelToType } from "./model.js";
import { ModelToType } from "./types/model-to-type.js";
describe("CreateModel", () => { describe("CreateModel", () => {
it("should create a model and it's interface type", () => { it("should create a model and it's interface type", () => {
const User = defineModel({ const User = defineModel("User", {
name: Field.string(), name: Field.string(),
password: Field.string(), password: Field.string(),
phone: Field.string({ isOptional: true }), phone: Field.string({ isOptional: true }),

View File

@ -1,3 +1,5 @@
import { Keyof } from "@fabric/core";
import { FieldToType } from "./fields/field-to-type.js";
import { Field, FieldDefinition } from "./fields/index.js"; import { Field, FieldDefinition } from "./fields/index.js";
export type CustomModelFields = Record<string, FieldDefinition>; export type CustomModelFields = Record<string, FieldDefinition>;
@ -10,14 +12,34 @@ export const DefaultModelFields = {
hasArbitraryPrecision: true, hasArbitraryPrecision: true,
}), }),
}; };
export type Model<TFields extends CustomModelFields = CustomModelFields> = export interface Model<
typeof DefaultModelFields & TFields; TName extends string = string,
TFields extends CustomModelFields = CustomModelFields,
> {
name: TName;
fields: typeof DefaultModelFields & TFields;
}
export function defineModel<TFields extends CustomModelFields>( export function defineModel<
fields: TFields, TName extends string,
): Model<TFields> { TFields extends CustomModelFields,
>(name: TName, fields: TFields): Model<TName, TFields> {
return { return {
...fields, name,
...DefaultModelFields, fields: { ...DefaultModelFields, ...fields },
} as const; } as const;
} }
export type ModelToType<TModel extends Model> = {
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
};
export type ModelFieldNames<TModel extends CustomModelFields> = Keyof<
TModel["fields"]
>;
export type ModelAddressableFields<TModel extends Model> = {
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends { isUnique: true }
? K
: never;
}[Keyof<TModel["fields"]>];

View File

@ -14,9 +14,9 @@ export type SingleFilterOption<T = any> = {
export type MultiFilterOption<T = any> = SingleFilterOption<T>[]; export type MultiFilterOption<T = any> = SingleFilterOption<T>[];
export const FILTER_OPTION_TYPE_SYMBOL = Symbol("$type"); export const FILTER_OPTION_TYPE_SYMBOL = Symbol("filter_type");
export const FILTER_OPTION_VALUE_SYMBOL = Symbol("$value"); export const FILTER_OPTION_VALUE_SYMBOL = Symbol("filter_value");
export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("$operator"); export const FILTER_OPTION_OPERATOR_SYMBOL = Symbol("filter_operator");
export type LikeFilterOption<T> = T extends string export type LikeFilterOption<T> = T extends string
? { ? {

View File

@ -1,9 +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 { ModelToType } from "../models/index.js"; import { StorageDriver } from "../../storage/storage-driver.js";
import { ModelSchema } from "../models/model-schema.js";
import { StorageDriver } from "../storage/storage-driver.js";
import { AggregateOptions } from "./aggregate-options.js";
import { FilterOptions } from "./filter-options.js"; import { FilterOptions } from "./filter-options.js";
import { OrderByOptions } from "./order-by-options.js"; import { OrderByOptions } from "./order-by-options.js";
import { import {
@ -14,44 +11,32 @@ import {
StoreSortableQuery, StoreSortableQuery,
} from "./query.js"; } from "./query.js";
export class QueryBuilder< export class QueryBuilder<T> implements StoreQuery<T> {
TModels extends ModelSchema,
TEntityName extends Keyof<TModels>,
T = ModelToType<TModels[TEntityName]>,
> implements StoreQuery<T>
{
constructor( constructor(
private driver: StorageDriver, private driver: StorageDriver,
private query: QueryDefinition<TEntityName>, private query: QueryDefinition,
) {} ) {}
aggregate<K extends AggregateOptions<T>>(): SelectableQuery<K> {
throw new Error("Method not implemented.");
}
where(where: FilterOptions<T>): StoreSortableQuery<T> { where(where: FilterOptions<T>): StoreSortableQuery<T> {
this.query = { return new QueryBuilder(this.driver, {
...this.query, ...this.query,
where, where,
}; });
return this;
} }
orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> { orderBy(opts: OrderByOptions<T>): StoreLimitableQuery<T> {
this.query = { return new QueryBuilder(this.driver, {
...this.query, ...this.query,
orderBy: opts, orderBy: opts,
}; });
return this;
} }
limit(limit: number, offset?: number | undefined): SelectableQuery<T> { limit(limit: number, offset?: number | undefined): SelectableQuery<T> {
this.query = { return new QueryBuilder(this.driver, {
...this.query, ...this.query,
limit, limit,
offset, offset,
}; });
return this;
} }
select<K extends Keyof<T>>( select<K extends Keyof<T>>(

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
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 { AggregateOptions } from "./aggregate-options.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,8 +9,6 @@ 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>;
aggregate<K extends AggregateOptions<T>>(opts: K): SelectableQuery<K>;
select(): AsyncResult<T[], StoreQueryError>; select(): AsyncResult<T[], StoreQueryError>;
select<K extends Keyof<T>>( select<K extends Keyof<T>>(
keys: K[], keys: K[],

View File

@ -0,0 +1,5 @@
import { StorageDriver } from "../storage/storage-driver.js";
export class StateStore {
constructor(private driver: StorageDriver) {}
}

View File

@ -1,2 +0,0 @@
export * from "./model-field-names.js";
export * from "./model-to-type.js";

View File

@ -1,3 +0,0 @@
import { CustomModelFields } from "../model.js";
export type ModelFieldNames<TModel extends CustomModelFields> = keyof TModel;

View File

@ -1,6 +0,0 @@
import { FieldToType } from "../fields/field-to-type.js";
import { Model } from "../model.js";
export type ModelToType<TModel extends Model> = {
[K in keyof TModel]: FieldToType<TModel[K]>;
};

View File

@ -1,3 +1 @@
export * from "./event-store.js";
export * from "./state-store.js";
export * from "./storage-driver.js"; export * from "./storage-driver.js";

View File

@ -1,18 +0,0 @@
import { Keyof } from "@fabric/core";
import { ModelToType } from "../models/index.js";
import { ModelSchema } from "../models/model-schema.js";
import { QueryBuilder } from "../query/query-builder.js";
import { StoreQuery } from "../query/query.js";
import { StorageDriver } from "./storage-driver.js";
export class StateStore<TModels extends ModelSchema> {
constructor(private driver: StorageDriver) {}
from<TEntityName extends Keyof<TModels>>(
entityName: TEntityName,
): StoreQuery<ModelToType<TModels[TEntityName]>> {
return new QueryBuilder(this.driver, {
from: entityName,
}) as StoreQuery<ModelToType<TModels[TEntityName]>>;
}
}

View File

@ -4,7 +4,7 @@ import { AsyncResult, UnexpectedError } from "@fabric/core";
import { CircularDependencyError } from "../errors/circular-dependency-error.js"; import { CircularDependencyError } from "../errors/circular-dependency-error.js";
import { StoreQueryError } from "../errors/query-error.js"; import { StoreQueryError } from "../errors/query-error.js";
import { ModelSchema } from "../models/model-schema.js"; import { ModelSchema } from "../models/model-schema.js";
import { QueryDefinition } from "../query/query.js"; import { QueryDefinition } from "../models/query/query.js";
export interface StorageDriver { export interface StorageDriver {
/** /**

View File

@ -1,47 +1,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { VariantTag } from "@fabric/core"; import { Variant, VariantTag } from "@fabric/core";
import { BaseField, FieldDefinition, Model } from "@fabric/domain"; import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
type FieldMap = { type FieldMap = {
[K in FieldDefinition[VariantTag]]: ( [K in FieldDefinition[VariantTag]]: (
name: string,
field: Extract<FieldDefinition, { [VariantTag]: K }>, field: Extract<FieldDefinition, { [VariantTag]: K }>,
) => string; ) => string;
}; };
const FieldMap: FieldMap = { const FieldMap: FieldMap = {
StringField: (f) => { StringField: (n, f) => {
return "TEXT" + modifiersFromOpts(f); return [n, "TEXT", modifiersFromOpts(f)].join(" ");
}, },
UUIDField: (f) => { UUIDField: (n, f) => {
return [ return [
n,
"TEXT", "TEXT",
f.isPrimaryKey ? "PRIMARY KEY" : "", f.isPrimaryKey ? "PRIMARY KEY" : "",
modifiersFromOpts(f), modifiersFromOpts(f),
].join(" "); ].join(" ");
}, },
IntegerField: function (): string { IntegerField: function (n, f): string {
throw new Error("Function not implemented."); return [n, "INTEGER", modifiersFromOpts(f)].join(" ");
}, },
ReferenceField: function (): string { ReferenceField: function (n, f): string {
throw new Error("Function not implemented."); return [
n,
"TEXT",
modifiersFromOpts(f),
",",
`FOREIGN KEY (${n}) REFERENCES ${f.targetModel}(${getTargetKey(f)})`,
].join(" ");
}, },
}; };
function modifiersFromOpts(options: BaseField) { function modifiersFromOpts(field: FieldDefinition) {
if (Variant.is(field, "UUIDField") && field.isPrimaryKey) {
return;
}
return [ return [
!options.isOptional ? "NOT NULL" : "", !field.isOptional ? "NOT NULL" : "",
options.isUnique ? "UNIQUE" : "", field.isUnique ? "UNIQUE" : "",
].join(" "); ].join(" ");
} }
function fieldDefinitionToSQL(field: FieldDefinition) { function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
return FieldMap[field[VariantTag]](field as any); 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>>,
) { ) {
return Object.entries(model.fields) const fields = Object.entries(model.fields)
.map(([name, type]) => `${name} ${fieldDefinitionToSQL(type)}`) .map(([name, type]) => fieldDefinitionToSQL(name, type))
.join(", "); .join(", ");
return `CREATE TABLE ${model.name} (${fields})`;
} }

View File

@ -1,15 +1,14 @@
import { createModel, Field, isError } from "@fabric/core"; import { isError } from "@fabric/core";
import { defineModel, Field } from "@fabric/domain";
import { afterEach, beforeEach, describe, expect, test } 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", () => {
const model = createModel({ const schema = {
name: "test", users: defineModel("users", {
fields: {
id: Field.uuid({}),
name: Field.string(), name: Field.string(),
}, }),
}); };
let store: SQLiteStorageDriver; let store: SQLiteStorageDriver;
@ -23,71 +22,115 @@ describe("SQLite Store Driver", () => {
}); });
test("should be able to 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([model]); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("test", { id: "1", name: "test" }); await store.insert("users", {
id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
const records = await store.select({ from: "test" }); const records = await store.select({ from: "users" });
expect(records).toEqual([{ id: "1", name: "test" }]); expect(records).toEqual([
{ id: "1", name: "test", streamId: "1", streamVersion: 1 },
]);
}); });
test("should be able to update a record", async () => { test("should be able to update a record", async () => {
const result = await store.sync([model]); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("test", { id: "1", name: "test" }); await store.insert("users", {
id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
await store.update("test", "1", { name: "updated" }); await store.update("users", "1", { name: "updated" });
const records = await store.select({ from: "test" }); const records = await store.select({ from: "users" });
expect(records).toEqual([{ id: "1", name: "updated" }]); expect(records).toEqual([
{ id: "1", name: "updated", streamId: "1", streamVersion: 1 },
]);
}); });
test("should be able to delete a record", async () => { test("should be able to delete a record", async () => {
const result = await store.sync([model]); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("test", { id: "1", name: "test" }); await store.insert("users", {
id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
await store.delete("test", "1"); await store.delete("users", "1");
const records = await store.select({ from: "test" }); const records = await store.select({ from: "users" });
expect(records).toEqual([]); expect(records).toEqual([]);
}); });
test("should be able to select records", async () => { test("should be able to select records", async () => {
const result = await store.sync([model]); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("test", { id: "1", name: "test" }); await store.insert("users", {
await store.insert("test", { id: "2", name: "test" }); id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
await store.insert("users", {
id: "2",
name: "test",
streamId: "2",
streamVersion: 1,
});
const records = await store.select({ from: "test" }); const records = await store.select({ from: "users" });
expect(records).toEqual([ expect(records).toEqual([
{ id: "1", name: "test" }, { id: "1", name: "test", streamId: "1", streamVersion: 1 },
{ id: "2", name: "test" }, { id: "2", name: "test", streamId: "2", streamVersion: 1 },
]); ]);
}); });
test("should be able to select one record", async () => { test("should be able to select one record", async () => {
const result = await store.sync([model]); const result = await store.sync(schema);
if (isError(result)) throw result; if (isError(result)) throw result;
await store.insert("test", { id: "1", name: "test" }); await store.insert("users", {
await store.insert("test", { id: "2", name: "test" }); id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
await store.insert("users", {
id: "2",
name: "test",
streamId: "2",
streamVersion: 1,
});
const record = await store.selectOne({ from: "test" }); const record = await store.selectOne({ from: "users" });
expect(record).toEqual({ id: "1", name: "test" }); expect(record).toEqual({
id: "1",
name: "test",
streamId: "1",
streamVersion: 1,
});
}); });
}); });

View File

@ -4,6 +4,7 @@ import { unlink } from "fs/promises";
import { import {
CircularDependencyError, CircularDependencyError,
ModelSchema,
QueryDefinition, QueryDefinition,
StorageDriver, StorageDriver,
StoreQueryError, StoreQueryError,
@ -35,6 +36,7 @@ export class SQLiteStorageDriver implements StorageDriver {
// Enable Write-Ahead Logging, which is faster and more reliable. // Enable Write-Ahead Logging, which is faster and more reliable.
this.db.run("PRAGMA journal_mode= WAL;"); this.db.run("PRAGMA journal_mode= WAL;");
this.db.run("PRAGMA foreign_keys = ON;");
} }
/** /**
@ -109,13 +111,13 @@ export class SQLiteStorageDriver implements StorageDriver {
* Sincronice the store with the schema. * Sincronice the store with the schema.
*/ */
async sync( async sync(
schema: ModelDefinition[], schema: ModelSchema,
): AsyncResult<void, StoreQueryError | CircularDependencyError> { ): AsyncResult<void, StoreQueryError | CircularDependencyError> {
try { try {
await dbRun(this.db, "BEGIN TRANSACTION;"); await dbRun(this.db, "BEGIN TRANSACTION;");
for (const model of schema) { for (const modelKey in schema) {
const query = `CREATE TABLE ${model.name} (${modelToSql(model)});`; const model = schema[modelKey];
await dbRun(this.db, query); await dbRun(this.db, modelToSql(model));
} }
await dbRun(this.db, "COMMIT;"); await dbRun(this.db, "COMMIT;");
} catch (error: any) { } catch (error: any) {