Compare commits
No commits in common. "ab41ff028d246c45aa2dd8114a185f8979edb0b9" and "3b0533b0a93efef50c21379192dc728bb6e6a5b0" have entirely different histories.
ab41ff028d
...
3b0533b0a9
@ -110,35 +110,21 @@ export class AsyncResult<
|
|||||||
/**
|
/**
|
||||||
* Map a function over the error of the result.
|
* Map a function over the error of the result.
|
||||||
*/
|
*/
|
||||||
errorMap<TMappedError extends TaggedError>(
|
mapError<TMappedError extends TaggedError>(
|
||||||
fn: (error: TError) => TMappedError,
|
fn: (error: TError) => TMappedError,
|
||||||
): AsyncResult<TValue, TMappedError> {
|
): AsyncResult<TValue, TMappedError> {
|
||||||
return new AsyncResult(
|
return new AsyncResult(
|
||||||
this.r.then((result) => result.errorMap(fn)),
|
this.r.then((result) => result.mapError(fn)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a function if the result is not an error.
|
* Taps a function if the result is a success.
|
||||||
* The function does not affect the result.
|
* This is useful for side effects that do not modify the result.
|
||||||
*/
|
*/
|
||||||
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
|
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
|
||||||
return new AsyncResult(
|
return new AsyncResult(
|
||||||
this.r.then((result) => result.tap(fn)),
|
this.r.then((result) => result.tap(fn)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert<TResultValue, TResultError extends TaggedError>(
|
|
||||||
fn: (value: TValue) => AsyncResult<TResultValue, TResultError>,
|
|
||||||
): AsyncResult<TValue, TError | TResultError> {
|
|
||||||
return new AsyncResult(
|
|
||||||
this.r.then((result) => {
|
|
||||||
if (result.isError()) {
|
|
||||||
return result as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (fn(result.unwrapOrThrow())).promise();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,7 +127,7 @@ export class Result<TValue, TError extends TaggedError = never> {
|
|||||||
/**
|
/**
|
||||||
* Map a function over the error of the result.
|
* Map a function over the error of the result.
|
||||||
*/
|
*/
|
||||||
errorMap<TMappedError extends TaggedError>(
|
mapError<TMappedError extends TaggedError>(
|
||||||
fn: (error: TError) => TMappedError,
|
fn: (error: TError) => TMappedError,
|
||||||
): Result<TValue, TMappedError> {
|
): Result<TValue, TMappedError> {
|
||||||
if (isError(this.value)) {
|
if (isError(this.value)) {
|
||||||
@ -143,19 +143,9 @@ export class Result<TValue, TError extends TaggedError = never> {
|
|||||||
*/
|
*/
|
||||||
tap(fn: (value: TValue) => void): Result<TValue, TError> {
|
tap(fn: (value: TValue) => void): Result<TValue, TError> {
|
||||||
if (!isError(this.value)) {
|
if (!isError(this.value)) {
|
||||||
try {
|
|
||||||
fn(this.value as TValue);
|
fn(this.value as TValue);
|
||||||
} catch {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert<TResultValue, TResultError extends TaggedError>(
|
|
||||||
fn: (value: TValue) => Result<TResultValue, TResultError>,
|
|
||||||
): Result<TValue, TError | TResultError> {
|
|
||||||
return this.flatMap((value) => fn(value).map(() => value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
import type { TaggedVariant, VariantTag } from "./variant.ts";
|
|
||||||
|
|
||||||
export function variantConstructor<
|
|
||||||
const T extends TaggedVariant<string>,
|
|
||||||
>(
|
|
||||||
tag: T[VariantTag],
|
|
||||||
) {
|
|
||||||
return <TOpts extends Omit<T, VariantTag>>(options: TOpts) => {
|
|
||||||
return {
|
|
||||||
_tag: tag,
|
|
||||||
...options,
|
|
||||||
} as const;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./constructor.ts";
|
|
||||||
export * from "./match.ts";
|
export * from "./match.ts";
|
||||||
export * from "./variant.ts";
|
export * from "./variant.ts";
|
||||||
|
|||||||
@ -6,3 +6,4 @@ export * from "./security/index.ts";
|
|||||||
export * from "./services/index.ts";
|
export * from "./services/index.ts";
|
||||||
export * from "./use-case/index.ts";
|
export * from "./use-case/index.ts";
|
||||||
export * from "./utils/index.ts";
|
export * from "./utils/index.ts";
|
||||||
|
export * from "./validations/index.ts";
|
||||||
|
|||||||
37
packages/fabric/domain/models/aggregate-model.ts
Normal file
37
packages/fabric/domain/models/aggregate-model.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { Keyof } from "../../core/index.ts";
|
||||||
|
import type { CustomModelFields } from "./custom-model-fields.ts";
|
||||||
|
import { DefaultEntityFields, type EntityModel } from "./entity-model.ts";
|
||||||
|
import { Field } from "./fields/index.ts";
|
||||||
|
|
||||||
|
export const DefaultAggregateFields = {
|
||||||
|
...DefaultEntityFields,
|
||||||
|
streamId: Field.uuid({ isIndexed: true }),
|
||||||
|
streamVersion: Field.integer({
|
||||||
|
isUnsigned: true,
|
||||||
|
hasArbitraryPrecision: true,
|
||||||
|
}),
|
||||||
|
deletedAt: Field.timestamp({ isOptional: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AggregateModel<
|
||||||
|
TName extends string = string,
|
||||||
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
|
> extends EntityModel<TName, TFields> {
|
||||||
|
fields: typeof DefaultAggregateFields & TFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineAggregateModel<
|
||||||
|
TName extends string,
|
||||||
|
TFields extends CustomModelFields,
|
||||||
|
>(name: TName, fields: TFields): AggregateModel<TName, TFields> {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
fields: { ...DefaultAggregateFields, ...fields },
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModelAddressableFields<TModel extends AggregateModel> = {
|
||||||
|
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends { isUnique: true }
|
||||||
|
? K
|
||||||
|
: never;
|
||||||
|
}[Keyof<TModel["fields"]>];
|
||||||
3
packages/fabric/domain/models/custom-model-fields.ts
Normal file
3
packages/fabric/domain/models/custom-model-fields.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { FieldDefinition } from "./fields/index.ts";
|
||||||
|
|
||||||
|
export type CustomModelFields = Record<string, FieldDefinition>;
|
||||||
14
packages/fabric/domain/models/entity-model.ts
Normal file
14
packages/fabric/domain/models/entity-model.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { CustomModelFields } from "./custom-model-fields.ts";
|
||||||
|
import { Field } from "./fields/index.ts";
|
||||||
|
import type { Model } from "./model.ts";
|
||||||
|
|
||||||
|
export const DefaultEntityFields = {
|
||||||
|
id: Field.uuid({ isPrimaryKey: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EntityModel<
|
||||||
|
TName extends string = string,
|
||||||
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
|
> extends Model<TName, TFields> {
|
||||||
|
fields: typeof DefaultEntityFields & TFields;
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { isError } from "@fabric/core";
|
import { isError } from "@fabric/core";
|
||||||
import { describe, expect, test } from "@fabric/testing";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { Model } from "../model.ts";
|
import { defineAggregateModel } from "../aggregate-model.ts";
|
||||||
import { Field } from "./index.ts";
|
import { Field } from "./index.ts";
|
||||||
import {
|
import {
|
||||||
InvalidReferenceFieldError,
|
InvalidReferenceFieldError,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
describe("Validate Reference Field", () => {
|
describe("Validate Reference Field", () => {
|
||||||
const schema = {
|
const schema = {
|
||||||
User: Model.aggregateFrom("User", {
|
User: defineAggregateModel("User", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
password: Field.string(),
|
password: Field.string(),
|
||||||
otherUnique: Field.integer({ isUnique: true }),
|
otherUnique: Field.integer({ isUnique: true }),
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
|
export * from "./aggregate-model.ts";
|
||||||
|
export * from "./custom-model-fields.ts";
|
||||||
export * from "./fields/index.ts";
|
export * from "./fields/index.ts";
|
||||||
export * from "./model-schema.ts";
|
export * from "./model-schema.ts";
|
||||||
export * from "./model.ts";
|
export * from "./model.ts";
|
||||||
export * from "./state-store.ts";
|
export * from "./state-store.ts";
|
||||||
export * from "./store-query/index.ts";
|
export * from "./store-query/index.ts";
|
||||||
export * from "./validations/index.ts";
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import type { UUID } from "@fabric/core";
|
import type { UUID } from "@fabric/core";
|
||||||
import { describe, expectTypeOf, test } from "@fabric/testing";
|
import { describe, expectTypeOf, test } from "@fabric/testing";
|
||||||
import type { PosixDate } from "../../core/index.ts";
|
import type { PosixDate } from "../../core/index.ts";
|
||||||
|
import { defineAggregateModel } from "./aggregate-model.ts";
|
||||||
import { Field } from "./fields/index.ts";
|
import { Field } from "./fields/index.ts";
|
||||||
import { Model, type ModelToType } from "./model.ts";
|
import type { ModelToType } from "./model.ts";
|
||||||
|
|
||||||
describe("CreateModel", () => {
|
describe("CreateModel", () => {
|
||||||
test("should create a model and it's interface type", () => {
|
test("should create a model and it's interface type", () => {
|
||||||
const User = Model.aggregateFrom("User", {
|
const User = defineAggregateModel("User", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
password: Field.string(),
|
password: Field.string(),
|
||||||
phone: Field.string({ isOptional: true }),
|
phone: Field.string({ isOptional: true }),
|
||||||
|
|||||||
@ -1,73 +1,29 @@
|
|||||||
import type { Keyof } from "@fabric/core";
|
import type { Keyof } from "@fabric/core";
|
||||||
|
import type { CustomModelFields } from "./custom-model-fields.ts";
|
||||||
import type { FieldToType } from "./fields/field-to-type.ts";
|
import type { FieldToType } from "./fields/field-to-type.ts";
|
||||||
import { Field, type FieldDefinition } from "./fields/index.ts";
|
|
||||||
|
|
||||||
/**
|
export interface Model<
|
||||||
* A model is a schema definition for some type of structured data.
|
|
||||||
*/
|
|
||||||
export class Model<
|
|
||||||
TName extends string = string,
|
TName extends string = string,
|
||||||
TFields extends ModelFields = ModelFields,
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
> {
|
> {
|
||||||
static from<TName extends string, TFields extends ModelFields>(
|
name: TName;
|
||||||
name: TName,
|
fields: TFields;
|
||||||
fields: TFields,
|
|
||||||
) {
|
|
||||||
return new Model(name, fields);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static aggregateFrom<TName extends string, TFields extends ModelFields>(
|
export function defineModel<
|
||||||
name: TName,
|
TName extends string,
|
||||||
fields: TFields,
|
TFields extends CustomModelFields,
|
||||||
): Model<TName, TFields & typeof DefaultAggregateFields> {
|
>(name: TName, fields: TFields): Model<TName, TFields> {
|
||||||
return new Model(name, { ...fields, ...DefaultAggregateFields });
|
return {
|
||||||
|
name,
|
||||||
|
fields,
|
||||||
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
static entityFrom<TName extends string, TFields extends ModelFields>(
|
|
||||||
name: TName,
|
|
||||||
fields: TFields,
|
|
||||||
): Model<TName, TFields & typeof DefaultEntityFields> {
|
|
||||||
return new Model(name, { ...fields, ...DefaultEntityFields });
|
|
||||||
}
|
|
||||||
private constructor(readonly name: TName, readonly fields: TFields) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EntityModel = Model<
|
|
||||||
string,
|
|
||||||
typeof DefaultEntityFields & ModelFields
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type AggregateModel = Model<
|
|
||||||
string,
|
|
||||||
typeof DefaultAggregateFields & ModelFields
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type ModelToType<TModel extends Model> = {
|
export type ModelToType<TModel extends Model> = {
|
||||||
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelFieldNames<TModel extends ModelFields> = Keyof<
|
export type ModelFieldNames<TModel extends CustomModelFields> = Keyof<
|
||||||
TModel["fields"]
|
TModel["fields"]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type ModelAddressableFields<TModel extends Model> = {
|
|
||||||
[K in Keyof<TModel["fields"]>]: TModel["fields"][K] extends { isUnique: true }
|
|
||||||
? K
|
|
||||||
: never;
|
|
||||||
}[Keyof<TModel["fields"]>];
|
|
||||||
|
|
||||||
type ModelFields = Record<string, FieldDefinition>;
|
|
||||||
|
|
||||||
const DefaultEntityFields = {
|
|
||||||
id: Field.uuid({ isPrimaryKey: true }),
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const DefaultAggregateFields = {
|
|
||||||
...DefaultEntityFields,
|
|
||||||
streamId: Field.uuid({ isIndexed: true }),
|
|
||||||
streamVersion: Field.integer({
|
|
||||||
isUnsigned: true,
|
|
||||||
hasArbitraryPrecision: true,
|
|
||||||
}),
|
|
||||||
deletedAt: Field.timestamp({ isOptional: true }),
|
|
||||||
} as const;
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import type { VariantTag } from "@fabric/core";
|
import type { VariantTag } from "@fabric/core";
|
||||||
import type { Event } from "../events/event.ts";
|
import type { Event } from "../events/event.ts";
|
||||||
import type { StoredEvent } from "../events/stored-event.ts";
|
import type { StoredEvent } from "../events/stored-event.ts";
|
||||||
import type { AggregateModel, ModelToType } from "../models/model.ts";
|
import type { AggregateModel } from "../models/aggregate-model.ts";
|
||||||
|
import type { ModelToType } from "../models/model.ts";
|
||||||
|
|
||||||
export interface Projection<
|
export interface Projection<
|
||||||
TModel extends AggregateModel,
|
TModel extends AggregateModel,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
type VariantFromTag,
|
type VariantFromTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
||||||
import type { FieldDefinition, FieldToType } from "../index.ts";
|
import type { FieldDefinition, FieldToType } from "../models/index.ts";
|
||||||
|
|
||||||
export type FieldParsers = {
|
export type FieldParsers = {
|
||||||
[K in FieldDefinition["_tag"]]: FieldParser<
|
[K in FieldDefinition["_tag"]]: FieldParser<
|
||||||
@ -1,7 +1,7 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
|
|
||||||
import { isRecordEmpty, Result, TaggedError } from "@fabric/core";
|
import { isRecordEmpty, Result, TaggedError } from "@fabric/core";
|
||||||
import type { FieldDefinition, Model, ModelToType } from "../index.ts";
|
import type { FieldDefinition, Model, ModelToType } from "../models/index.ts";
|
||||||
import { fieldParsers, type FieldParsingError } from "./field-parsers.ts";
|
import { fieldParsers, type FieldParsingError } from "./field-parsers.ts";
|
||||||
|
|
||||||
export function parseFromModel<
|
export function parseFromModel<
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
defineModel,
|
||||||
Field,
|
Field,
|
||||||
isGreaterOrEqualTo,
|
isGreaterOrEqualTo,
|
||||||
isGreaterThan,
|
isGreaterThan,
|
||||||
@ -7,13 +8,12 @@ import {
|
|||||||
isLessThan,
|
isLessThan,
|
||||||
isLike,
|
isLike,
|
||||||
isNotEqualTo,
|
isNotEqualTo,
|
||||||
Model,
|
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { describe, expect, test } from "@fabric/testing";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { filterToParams, filterToSQL } from "./filter-to-sql.ts";
|
import { filterToParams, filterToSQL } from "./filter-to-sql.ts";
|
||||||
|
|
||||||
describe("SQL where clause from filter options", () => {
|
describe("SQL where clause from filter options", () => {
|
||||||
const col = Model.from("users", {
|
const col = defineModel("users", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
age: Field.integer(),
|
age: Field.integer(),
|
||||||
status: Field.string(),
|
status: Field.string(),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import {
|
|||||||
MultiFilterOption,
|
MultiFilterOption,
|
||||||
SingleFilterOption,
|
SingleFilterOption,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { keyToParamKey } from "./record-utils.ts";
|
import { keyToParam } from "./record-utils.ts";
|
||||||
import { fieldValueToSQL } from "./value-to-sql.ts";
|
import { fieldValueToSQL } from "./value-to-sql.ts";
|
||||||
|
|
||||||
export function filterToSQL(filterOptions?: FilterOptions) {
|
export function filterToSQL(filterOptions?: FilterOptions) {
|
||||||
@ -57,7 +57,7 @@ function getWhereFromSingleOption(
|
|||||||
const WHERE_KEY_PREFIX = "where_";
|
const WHERE_KEY_PREFIX = "where_";
|
||||||
|
|
||||||
function getWhereParamKey(key: string, opts: { postfix?: string } = {}) {
|
function getWhereParamKey(key: string, opts: { postfix?: string } = {}) {
|
||||||
return keyToParamKey(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`);
|
return keyToParam(`${WHERE_KEY_PREFIX}${key}${opts.postfix ?? ""}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWhereFromKeyValue(
|
function getWhereFromKeyValue(
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { Field, Model } from "@fabric/domain";
|
import { defineModel, Field } from "@fabric/domain";
|
||||||
import { describe, expect, test } from "@fabric/testing";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { modelToSql } from "./model-to-sql.ts";
|
import { modelToSql } from "./model-to-sql.ts";
|
||||||
|
|
||||||
describe("ModelToSQL", () => {
|
describe("ModelToSQL", () => {
|
||||||
const model = Model.from("something", {
|
const model = defineModel("something", {
|
||||||
id: Field.uuid({ isPrimaryKey: true }),
|
id: Field.uuid({ isPrimaryKey: true }),
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
age: Field.integer(),
|
age: Field.integer(),
|
||||||
|
|||||||
@ -14,23 +14,23 @@ export function recordToSQLKeys(record: Record<string, any>) {
|
|||||||
/**
|
/**
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
* Unfold a record into a string of it's keys separated by commas.
|
||||||
*/
|
*/
|
||||||
export function recordToSQLParamKeys(record: Record<string, any>) {
|
export function recordToSQLKeyParams(record: Record<string, any>) {
|
||||||
return Object.keys(record)
|
return Object.keys(record)
|
||||||
.map((key) => keyToParamKey(key))
|
.map((key) => keyToParam(key))
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unfold a record into a string of it's keys separated by commas.
|
* Unfold a record into a string of it's keys separated by commas.
|
||||||
*/
|
*/
|
||||||
export function recordToSQLParamRecord(
|
export function recordToSQLParams(
|
||||||
model: Model,
|
model: Model,
|
||||||
record: Record<string, any>,
|
record: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
return Object.keys(record).reduce(
|
return Object.keys(record).reduce(
|
||||||
(acc, key) => ({
|
(acc, key) => ({
|
||||||
...acc,
|
...acc,
|
||||||
[keyToParamKey(key)]: fieldValueToSQL(model.fields[key]!, record[key]),
|
[keyToParam(key)]: fieldValueToSQL(model.fields[key]!, record[key]),
|
||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@ -38,10 +38,10 @@ export function recordToSQLParamRecord(
|
|||||||
|
|
||||||
export function recordToSQLSet(record: Record<string, any>) {
|
export function recordToSQLSet(record: Record<string, any>) {
|
||||||
return Object.keys(record)
|
return Object.keys(record)
|
||||||
.map((key) => `${key} = ${keyToParamKey(key)}`)
|
.map((key) => `${key} = ${keyToParam(key)}`)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function keyToParamKey(key: string) {
|
export function keyToParam(key: string) {
|
||||||
return `$${key}`;
|
return `$${key}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,9 +26,8 @@ export class SQLiteDatabase {
|
|||||||
this.run("BEGIN TRANSACTION");
|
this.run("BEGIN TRANSACTION");
|
||||||
await fn();
|
await fn();
|
||||||
this.run("COMMIT");
|
this.run("COMMIT");
|
||||||
} catch (e) {
|
} catch {
|
||||||
this.run("ROLLBACK");
|
this.run("ROLLBACK");
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { AsyncResult, Keyof, Optional } from "@fabric/core";
|
|||||||
import {
|
import {
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
Model,
|
Model,
|
||||||
type ModelSchema,
|
ModelSchema,
|
||||||
NotFoundError,
|
|
||||||
OrderByOptions,
|
OrderByOptions,
|
||||||
SelectableQuery,
|
SelectableQuery,
|
||||||
StoreLimitableQuery,
|
StoreLimitableQuery,
|
||||||
@ -13,6 +12,7 @@ import {
|
|||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
StoreSortableQuery,
|
StoreSortableQuery,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
|
import { NotFoundError } from "../../domain/models/store-query/store-query.ts";
|
||||||
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.ts";
|
import { filterToParams, filterToSQL } from "../sqlite/filter-to-sql.ts";
|
||||||
import { transformRow } from "../sqlite/sql-to-value.ts";
|
import { transformRow } from "../sqlite/sql-to-value.ts";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Run, UUID } from "@fabric/core";
|
import { PosixDate, Run, UUID } from "@fabric/core";
|
||||||
import { Field, isLike, Model } from "@fabric/domain";
|
import { defineAggregateModel, Field, isLike } from "@fabric/domain";
|
||||||
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
import { UUIDGeneratorMock } from "@fabric/domain/mocks";
|
||||||
import {
|
import {
|
||||||
afterEach,
|
afterEach,
|
||||||
@ -13,11 +13,11 @@ import { SQLiteStateStore } from "./state-store.ts";
|
|||||||
|
|
||||||
describe("State Store", () => {
|
describe("State Store", () => {
|
||||||
const models = [
|
const models = [
|
||||||
Model.entityFrom("demo", {
|
defineAggregateModel("demo", {
|
||||||
value: Field.float(),
|
value: Field.float(),
|
||||||
owner: Field.reference({ targetModel: "users" }),
|
owner: Field.reference({ targetModel: "users" }),
|
||||||
}),
|
}),
|
||||||
Model.entityFrom("users", {
|
defineAggregateModel("users", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@ -39,6 +39,9 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
name: "test",
|
name: "test",
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,6 +51,9 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
|
|
||||||
const result = await store.from("users").select().unwrapOrThrow();
|
const result = await store.from("users").select().unwrapOrThrow();
|
||||||
@ -55,14 +61,20 @@ describe("State Store", () => {
|
|||||||
expectTypeOf(result).toEqualTypeOf<
|
expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
{
|
||||||
id: UUID;
|
id: UUID;
|
||||||
|
streamId: UUID;
|
||||||
|
streamVersion: bigint;
|
||||||
name: string;
|
name: string;
|
||||||
|
deletedAt: PosixDate | null;
|
||||||
}[]
|
}[]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
name: "test",
|
name: "test",
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -75,16 +87,25 @@ describe("State Store", () => {
|
|||||||
store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "anotherName",
|
name: "anotherName",
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
|
streamId: UUIDGeneratorMock.generate(),
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
store.insertInto("users", {
|
store.insertInto("users", {
|
||||||
name: "anotherName2",
|
name: "anotherName2",
|
||||||
id: UUIDGeneratorMock.generate(),
|
id: UUIDGeneratorMock.generate(),
|
||||||
|
streamId: UUIDGeneratorMock.generate(),
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -98,14 +119,20 @@ describe("State Store", () => {
|
|||||||
expectTypeOf(result).toEqualTypeOf<
|
expectTypeOf(result).toEqualTypeOf<
|
||||||
{
|
{
|
||||||
id: UUID;
|
id: UUID;
|
||||||
|
streamId: UUID;
|
||||||
|
streamVersion: bigint;
|
||||||
name: string;
|
name: string;
|
||||||
|
deletedAt: PosixDate | null;
|
||||||
}[]
|
}[]
|
||||||
>();
|
>();
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
name: "test",
|
name: "test",
|
||||||
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
@ -116,6 +143,9 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
|
|
||||||
await store.update("users", newUUID, {
|
await store.update("users", newUUID, {
|
||||||
@ -127,7 +157,10 @@ describe("State Store", () => {
|
|||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
name: "updated",
|
name: "updated",
|
||||||
|
deletedAt: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -137,6 +170,9 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
name: "test",
|
name: "test",
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
|
|
||||||
await store.delete("users", newUUID).orThrow();
|
await store.delete("users", newUUID).orThrow();
|
||||||
@ -156,12 +192,18 @@ describe("State Store", () => {
|
|||||||
await store.insertInto("users", {
|
await store.insertInto("users", {
|
||||||
id: ownerUUID,
|
id: ownerUUID,
|
||||||
name: "test",
|
name: "test",
|
||||||
|
streamId: ownerUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
|
|
||||||
await store.insertInto("demo", {
|
await store.insertInto("demo", {
|
||||||
id: newUUID,
|
id: newUUID,
|
||||||
value: 1.0,
|
value: 1.0,
|
||||||
owner: ownerUUID,
|
owner: ownerUUID,
|
||||||
|
streamId: newUUID,
|
||||||
|
streamVersion: 1n,
|
||||||
|
deletedAt: null,
|
||||||
}).orThrow();
|
}).orThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { AsyncResult, UnexpectedError, UUID } from "@fabric/core";
|
import { AsyncResult, UnexpectedError, UUID } from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
Model,
|
type AggregateModel,
|
||||||
ModelSchemaFromModels,
|
ModelSchemaFromModels,
|
||||||
ModelToType,
|
ModelToType,
|
||||||
StoreQuery,
|
StoreQuery,
|
||||||
@ -9,16 +9,16 @@ import {
|
|||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { modelToSql } from "../sqlite/model-to-sql.ts";
|
import { modelToSql } from "../sqlite/model-to-sql.ts";
|
||||||
import {
|
import {
|
||||||
keyToParamKey,
|
keyToParam,
|
||||||
|
recordToSQLKeyParams,
|
||||||
recordToSQLKeys,
|
recordToSQLKeys,
|
||||||
recordToSQLParamKeys,
|
recordToSQLParams,
|
||||||
recordToSQLParamRecord,
|
|
||||||
recordToSQLSet,
|
recordToSQLSet,
|
||||||
} from "../sqlite/record-utils.ts";
|
} from "../sqlite/record-utils.ts";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
||||||
import { QueryBuilder } from "./query-builder.ts";
|
import { QueryBuilder } from "./query-builder.ts";
|
||||||
|
|
||||||
export class SQLiteStateStore<TModel extends Model>
|
export class SQLiteStateStore<TModel extends AggregateModel>
|
||||||
implements WritableStateStore<TModel> {
|
implements WritableStateStore<TModel> {
|
||||||
private schema: ModelSchemaFromModels<TModel>;
|
private schema: ModelSchemaFromModels<TModel>;
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
@ -47,8 +47,8 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
recordToSQLKeys(
|
recordToSQLKeys(
|
||||||
record,
|
record,
|
||||||
)
|
)
|
||||||
}) VALUES (${recordToSQLParamKeys(record)})`,
|
}) VALUES (${recordToSQLKeyParams(record)})`,
|
||||||
recordToSQLParamRecord(model, record),
|
recordToSQLParams(model, record),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(error) => new StoreQueryError(error.message),
|
(error) => new StoreQueryError(error.message),
|
||||||
@ -72,7 +72,7 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
|
|
||||||
return AsyncResult.tryFrom(
|
return AsyncResult.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
const params = recordToSQLParamRecord(model, {
|
const params = recordToSQLParams(model, {
|
||||||
...record,
|
...record,
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
@ -81,7 +81,7 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
recordToSQLSet(
|
recordToSQLSet(
|
||||||
record,
|
record,
|
||||||
)
|
)
|
||||||
} WHERE id = ${keyToParamKey("id")}`,
|
} WHERE id = ${keyToParam("id")}`,
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -98,10 +98,8 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
return AsyncResult.tryFrom(
|
return AsyncResult.tryFrom(
|
||||||
() => {
|
() => {
|
||||||
this.db.runPrepared(
|
this.db.runPrepared(
|
||||||
`DELETE FROM ${model.name} WHERE id = ${keyToParamKey("id")}`,
|
`DELETE FROM ${model.name} WHERE id = ${keyToParam("id")}`,
|
||||||
{
|
{ $id: id },
|
||||||
[keyToParamKey("id")]: id,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
(error) => new StoreQueryError(error.message),
|
(error) => new StoreQueryError(error.message),
|
||||||
@ -111,7 +109,7 @@ export class SQLiteStateStore<TModel extends Model>
|
|||||||
migrate(): AsyncResult<void, StoreQueryError> {
|
migrate(): AsyncResult<void, StoreQueryError> {
|
||||||
return AsyncResult.tryFrom(
|
return AsyncResult.tryFrom(
|
||||||
async () => {
|
async () => {
|
||||||
this.db.init();
|
await this.db.init();
|
||||||
await this.db.withTransaction(() => {
|
await this.db.withTransaction(() => {
|
||||||
for (const modelKey in this.schema) {
|
for (const modelKey in this.schema) {
|
||||||
const model =
|
const model =
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user