Feature: Basic Events, Models and Projections #2

Merged
piarrot merged 37 commits from feat-base-projections into main 2024-10-15 15:20:25 -03:00
3 changed files with 109 additions and 51 deletions
Showing only changes of commit 27dbd44741 - Show all commits

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