Compare commits
No commits in common. "3b0533b0a93efef50c21379192dc728bb6e6a5b0" and "4574b9871b04ba278d6d50fa0c57017b09937565" have entirely different histories.
3b0533b0a9
...
4574b9871b
18
.vscode/settings.json
vendored
18
.vscode/settings.json
vendored
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"cSpell.enabled": true,
|
"cSpell.enabled": true,
|
||||||
"cSpell.language": "en,es",
|
"cSpell.language": "en,es",
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.autoSave": "off",
|
"files.autoSave": "off",
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
@ -8,18 +9,9 @@
|
|||||||
"source.fixAll": "always",
|
"source.fixAll": "always",
|
||||||
"source.organizeImports": "always"
|
"source.organizeImports": "always"
|
||||||
},
|
},
|
||||||
"cSpell.words": ["Syntropy"],
|
"cSpell.words": ["autodocs", "Syntropy"],
|
||||||
"deno.enable": true,
|
"deno.enable": true,
|
||||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
"deno.lint": true,
|
||||||
"deno.future": true,
|
"deno.unstable": [],
|
||||||
"deno.codeLens.implementations": true,
|
"deno.suggest.imports.autoDiscover": true
|
||||||
"deno.codeLens.references": true,
|
|
||||||
"typescript.referencesCodeLens.enabled": true,
|
|
||||||
"deno.testing.args": [
|
|
||||||
"--allow-all"
|
|
||||||
],
|
|
||||||
"notebook.defaultFormatter": "denoland.vscode-deno",
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "denoland.vscode-deno"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt && deno lint --fix && deno check **/*.ts && deno test -A",
|
"test": "deno test --allow-all --unstable-ffi",
|
||||||
|
"test:dev": "deno test --allow-all --unstable-ffi --watch",
|
||||||
|
"check": "deno fmt && deno lint --fix && deno check **/*.ts && deno task test",
|
||||||
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts"
|
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts"
|
||||||
},
|
},
|
||||||
"workspace": [
|
"workspace": [
|
||||||
@ -8,11 +10,16 @@
|
|||||||
"packages/fabric/domain",
|
"packages/fabric/domain",
|
||||||
"packages/fabric/sqlite-store",
|
"packages/fabric/sqlite-store",
|
||||||
"packages/fabric/testing",
|
"packages/fabric/testing",
|
||||||
"packages/fabric/validations",
|
|
||||||
"packages/templates/domain",
|
"packages/templates/domain",
|
||||||
"packages/templates/lib",
|
"packages/templates/lib"
|
||||||
"apps/syntropy/domain"
|
|
||||||
],
|
],
|
||||||
|
"imports": {
|
||||||
|
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
||||||
|
"@quentinadam/decimal": "jsr:@quentinadam/decimal@^0.1.6",
|
||||||
|
"@std/expect": "jsr:@std/expect@^1.0.5",
|
||||||
|
"@std/testing": "jsr:@std/testing@^1.0.3",
|
||||||
|
"expect-type": "npm:expect-type@^1.1.0"
|
||||||
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"exactOptionalPropertyTypes": true,
|
"exactOptionalPropertyTypes": true,
|
||||||
@ -20,8 +27,8 @@
|
|||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noUncheckedIndexedAccess": true
|
"noUncheckedIndexedAccess": true
|
||||||
},
|
},
|
||||||
"unstable": ["ffi"],
|
|
||||||
"lint": {
|
"lint": {
|
||||||
|
"include": ["src/"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"tags": ["recommended"],
|
"tags": ["recommended"],
|
||||||
"exclude": ["no-namespace"]
|
"exclude": ["no-namespace"]
|
||||||
23
deno.lock
23
deno.lock
@ -5,6 +5,7 @@
|
|||||||
"jsr:@db/sqlite@0.12": "0.12.0",
|
"jsr:@db/sqlite@0.12": "0.12.0",
|
||||||
"jsr:@denosaurs/plug@1": "1.0.6",
|
"jsr:@denosaurs/plug@1": "1.0.6",
|
||||||
"jsr:@quentinadam/assert@~0.1.7": "0.1.7",
|
"jsr:@quentinadam/assert@~0.1.7": "0.1.7",
|
||||||
|
"jsr:@quentinadam/decimal@*": "0.1.6",
|
||||||
"jsr:@quentinadam/decimal@~0.1.6": "0.1.6",
|
"jsr:@quentinadam/decimal@~0.1.6": "0.1.6",
|
||||||
"jsr:@std/assert@0.217": "0.217.0",
|
"jsr:@std/assert@0.217": "0.217.0",
|
||||||
"jsr:@std/assert@0.221": "0.221.0",
|
"jsr:@std/assert@0.221": "0.221.0",
|
||||||
@ -126,32 +127,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"members": {
|
|
||||||
"packages/fabric/domain": {
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@fabric/core@*",
|
|
||||||
"jsr:@fabric/validations@*",
|
|
||||||
"jsr:@quentinadam/decimal@~0.1.6"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"packages/fabric/sqlite-store": {
|
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@db/sqlite@0.12",
|
"jsr:@db/sqlite@0.12",
|
||||||
"jsr:@fabric/domain@*"
|
"jsr:@quentinadam/decimal@~0.1.6",
|
||||||
]
|
|
||||||
},
|
|
||||||
"packages/fabric/testing": {
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@std/expect@^1.0.5",
|
"jsr:@std/expect@^1.0.5",
|
||||||
"jsr:@std/testing@^1.0.3",
|
"jsr:@std/testing@^1.0.3",
|
||||||
"npm:expect-type@^1.1.0"
|
"npm:expect-type@^1.1.0"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"packages/fabric/validations": {
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@fabric/core@*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
/**
|
|
||||||
* Represents a time of day in hours, minutes, and seconds.
|
|
||||||
*/
|
|
||||||
export class ClockTime {
|
|
||||||
readonly hours: number;
|
|
||||||
readonly minutes: number;
|
|
||||||
readonly seconds: number;
|
|
||||||
|
|
||||||
constructor(hours?: number, minutes?: number, seconds?: number) {
|
|
||||||
this.hours = hours ?? 0;
|
|
||||||
this.minutes = minutes ?? 0;
|
|
||||||
this.seconds = seconds ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `${this.hours}:${this.minutes}:${this.seconds}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromString(time: string): ClockTime {
|
|
||||||
const [hours, minutes, seconds] = time.split(":").map(Number);
|
|
||||||
return new ClockTime(hours, minutes, seconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./clock-time.ts";
|
|
||||||
export * from "./posix-date.ts";
|
export * from "./posix-date.ts";
|
||||||
export * from "./time-constants.ts";
|
export * from "./time-constants.ts";
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
export * from "./email.ts";
|
|
||||||
export * from "./enum.ts";
|
export * from "./enum.ts";
|
||||||
export * from "./fn.ts";
|
export * from "./fn.ts";
|
||||||
export * from "./keyof.ts";
|
export * from "./keyof.ts";
|
||||||
export * from "./maybe-promise.ts";
|
export * from "./maybe-promise.ts";
|
||||||
export * from "./optional.ts";
|
export * from "./optional.ts";
|
||||||
export * from "./record.ts";
|
export * from "./record.ts";
|
||||||
export * from "./semver.ts";
|
|
||||||
export * from "./uuid.ts";
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { UnexpectedError } from "../error/unexpected-error.ts";
|
import { UnexpectedError } from "../error/unexpected-error.ts";
|
||||||
|
|
||||||
export function ensure<T>(value?: T): T {
|
export function ensureValue<T>(value?: T): T {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new UnexpectedError("Value is nullish.");
|
throw new UnexpectedError("Value is nullish.");
|
||||||
}
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export * from "./ensure.ts";
|
export * from "./ensure-value.ts";
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@fabric/domain",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.ts",
|
|
||||||
"./mocks": "./mocks.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@fabric/core": "jsr:@fabric/core",
|
|
||||||
"@fabric/validations": "jsr:@fabric/validations",
|
|
||||||
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
packages/fabric/domain/deno.jsonc
Normal file
7
packages/fabric/domain/deno.jsonc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "@fabric/domain",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts",
|
||||||
|
"./mocks": "./mocks.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,11 +2,11 @@ import type {
|
|||||||
AsyncResult,
|
AsyncResult,
|
||||||
MaybePromise,
|
MaybePromise,
|
||||||
PosixDate,
|
PosixDate,
|
||||||
UUID,
|
|
||||||
VariantFromTag,
|
VariantFromTag,
|
||||||
VariantTag,
|
VariantTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
import type { StoreQueryError } from "../errors/query-error.ts";
|
import type { StoreQueryError } from "../errors/query-error.ts";
|
||||||
|
import type { UUID } from "../types/uuid.ts";
|
||||||
import type { Event } from "./event.ts";
|
import type { Event } from "./event.ts";
|
||||||
import type { StoredEvent } from "./stored-event.ts";
|
import type { StoredEvent } from "./stored-event.ts";
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import type { VariantTag } from "@fabric/core";
|
import type { VariantTag } from "@fabric/core";
|
||||||
import type { UUID } from "../../core/types/uuid.ts";
|
import type { UUID } from "../types/uuid.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event is a tagged variant with a payload and a timestamp.
|
* An event is a tagged variant with a payload and a timestamp.
|
||||||
|
|||||||
9
packages/fabric/domain/files/image-file.ts
Normal file
9
packages/fabric/domain/files/image-file.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { ImageMimeType } from "./mime-type.ts";
|
||||||
|
import type { StoredFile } from "./stored-file.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an image file.
|
||||||
|
*/
|
||||||
|
export interface ImageFile extends StoredFile {
|
||||||
|
mimeType: ImageMimeType;
|
||||||
|
}
|
||||||
9
packages/fabric/domain/files/in-memory-file.ts
Normal file
9
packages/fabric/domain/files/in-memory-file.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Base64String } from "../types/base-64.ts";
|
||||||
|
import type { BaseFile } from "./base-file.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a file with its contents in memory.
|
||||||
|
*/
|
||||||
|
export interface InMemoryFile extends BaseFile {
|
||||||
|
data: Base64String;
|
||||||
|
}
|
||||||
@ -1,5 +1,10 @@
|
|||||||
export * from "./base-file.ts";
|
export * from "./base-file.ts";
|
||||||
export * from "./bytes.ts";
|
export * from "./bytes.ts";
|
||||||
|
export * from "./image-file.ts";
|
||||||
|
export * from "./in-memory-file.ts";
|
||||||
export * from "./invalid-file-type-error.ts";
|
export * from "./invalid-file-type-error.ts";
|
||||||
|
export * from "./is-in-memory-file.ts";
|
||||||
export * from "./is-mime-type.ts";
|
export * from "./is-mime-type.ts";
|
||||||
|
export * from "./media-file.ts";
|
||||||
export * from "./mime-type.ts";
|
export * from "./mime-type.ts";
|
||||||
|
export * from "./stored-file.ts";
|
||||||
|
|||||||
21
packages/fabric/domain/files/is-in-memory-file.ts
Normal file
21
packages/fabric/domain/files/is-in-memory-file.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { isRecord } from "@fabric/core";
|
||||||
|
import type { InMemoryFile } from "./in-memory-file.ts";
|
||||||
|
|
||||||
|
export function isInMemoryFile(value: unknown): value is InMemoryFile {
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
isRecord(value) &&
|
||||||
|
"data" in value &&
|
||||||
|
typeof value.data === "string" &&
|
||||||
|
"mimeType" in value &&
|
||||||
|
typeof value.mimeType === "string" &&
|
||||||
|
"name" in value &&
|
||||||
|
typeof value.name === "string" &&
|
||||||
|
"sizeInBytes" in value &&
|
||||||
|
typeof value.sizeInBytes === "number" &&
|
||||||
|
value.sizeInBytes >= 1
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
packages/fabric/domain/files/media-file.ts
Normal file
8
packages/fabric/domain/files/media-file.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { StoredFile } from "./stored-file.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a media file, either an image, a video or an audio file.
|
||||||
|
*/
|
||||||
|
export interface MediaFile extends StoredFile {
|
||||||
|
mimeType: `image/${string}` | `video/${string}` | `audio/${string}`;
|
||||||
|
}
|
||||||
9
packages/fabric/domain/files/stored-file.ts
Normal file
9
packages/fabric/domain/files/stored-file.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Entity } from "../types/entity.ts";
|
||||||
|
import type { BaseFile } from "./base-file.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a file as managed by the domain.
|
||||||
|
*/
|
||||||
|
export interface StoredFile extends BaseFile, Entity {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
@ -4,6 +4,6 @@ export * from "./files/index.ts";
|
|||||||
export * from "./models/index.ts";
|
export * from "./models/index.ts";
|
||||||
export * from "./security/index.ts";
|
export * from "./security/index.ts";
|
||||||
export * from "./services/index.ts";
|
export * from "./services/index.ts";
|
||||||
|
export * from "./types/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";
|
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
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"]>];
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import type { FieldDefinition } from "./fields/index.ts";
|
|
||||||
|
|
||||||
export type CustomModelFields = Record<string, FieldDefinition>;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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,16 +0,0 @@
|
|||||||
import { type TaggedVariant, VariantTag } from "@fabric/core";
|
|
||||||
import type { BaseField } from "./base-field.ts";
|
|
||||||
|
|
||||||
export interface BooleanFieldOptions extends BaseField {}
|
|
||||||
|
|
||||||
export interface BooleanField
|
|
||||||
extends TaggedVariant<"BooleanField">, BooleanFieldOptions {}
|
|
||||||
|
|
||||||
export function createBooleanField<T extends BooleanFieldOptions>(
|
|
||||||
opts: T = {} as T,
|
|
||||||
): BooleanField & T {
|
|
||||||
return {
|
|
||||||
[VariantTag]: "BooleanField",
|
|
||||||
...opts,
|
|
||||||
} as const;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import type { PosixDate } from "@fabric/core";
|
import type { PosixDate } from "@fabric/core";
|
||||||
import type Decimal from "decimal";
|
import type Decimal from "jsr:@quentinadam/decimal";
|
||||||
import type { UUID } from "../../../core/types/uuid.ts";
|
import type { UUID } from "../../types/uuid.ts";
|
||||||
import type { BooleanField } from "./boolean-field.ts";
|
|
||||||
import type { DecimalField } from "./decimal.ts";
|
import type { DecimalField } from "./decimal.ts";
|
||||||
import type { EmbeddedField } from "./embedded.ts";
|
import type { EmbeddedField } from "./embedded.ts";
|
||||||
import type { FloatField } from "./float.ts";
|
import type { FloatField } from "./float.ts";
|
||||||
@ -23,7 +22,6 @@ export type FieldToType<TField> = TField extends StringField
|
|||||||
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
|
: TField extends DecimalField ? MaybeOptional<TField, Decimal>
|
||||||
: TField extends FloatField ? MaybeOptional<TField, number>
|
: TField extends FloatField ? MaybeOptional<TField, number>
|
||||||
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
|
: TField extends TimestampField ? MaybeOptional<TField, PosixDate>
|
||||||
: TField extends BooleanField ? MaybeOptional<TField, boolean>
|
|
||||||
: TField extends EmbeddedField<infer TSubModel>
|
: TField extends EmbeddedField<infer TSubModel>
|
||||||
? MaybeOptional<TField, TSubModel>
|
? MaybeOptional<TField, TSubModel>
|
||||||
: never;
|
: never;
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { type BooleanField, createBooleanField } from "./boolean-field.ts";
|
|
||||||
import { createDecimalField, type DecimalField } from "./decimal.ts";
|
import { createDecimalField, type DecimalField } from "./decimal.ts";
|
||||||
import { createEmbeddedField, type EmbeddedField } from "./embedded.ts";
|
import { createEmbeddedField, type EmbeddedField } from "./embedded.ts";
|
||||||
import { createFloatField, type FloatField } from "./float.ts";
|
import { createFloatField, type FloatField } from "./float.ts";
|
||||||
@ -13,7 +12,6 @@ import { createUUIDField, type UUIDField } from "./uuid-field.ts";
|
|||||||
export * from "./base-field.ts";
|
export * from "./base-field.ts";
|
||||||
export * from "./field-to-type.ts";
|
export * from "./field-to-type.ts";
|
||||||
export * from "./reference-field.ts";
|
export * from "./reference-field.ts";
|
||||||
export * from "./uuid-field.ts";
|
|
||||||
|
|
||||||
export type FieldDefinition =
|
export type FieldDefinition =
|
||||||
| StringField
|
| StringField
|
||||||
@ -23,8 +21,7 @@ export type FieldDefinition =
|
|||||||
| DecimalField
|
| DecimalField
|
||||||
| ReferenceField
|
| ReferenceField
|
||||||
| TimestampField
|
| TimestampField
|
||||||
| EmbeddedField
|
| EmbeddedField;
|
||||||
| BooleanField;
|
|
||||||
|
|
||||||
export namespace Field {
|
export namespace Field {
|
||||||
export const string = createStringField;
|
export const string = createStringField;
|
||||||
@ -35,5 +32,4 @@ export namespace Field {
|
|||||||
export const float = createFloatField;
|
export const float = createFloatField;
|
||||||
export const timestamp = createTimestampField;
|
export const timestamp = createTimestampField;
|
||||||
export const embedded = createEmbeddedField;
|
export const embedded = createEmbeddedField;
|
||||||
export const boolean = createBooleanField;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { defineAggregateModel } from "../aggregate-model.ts";
|
import { defineModel } from "../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: defineAggregateModel("User", {
|
User: defineModel("User", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
password: Field.string(),
|
password: Field.string(),
|
||||||
otherUnique: Field.integer({ isUnique: true }),
|
otherUnique: Field.integer({ isUnique: true }),
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
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";
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import type { UUID } from "@fabric/core";
|
import type { PosixDate } 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 { UUID } from "../types/uuid.ts";
|
||||||
import { defineAggregateModel } from "./aggregate-model.ts";
|
|
||||||
import { Field } from "./fields/index.ts";
|
import { Field } from "./fields/index.ts";
|
||||||
import type { ModelToType } from "./model.ts";
|
import { defineModel, 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 = defineAggregateModel("User", {
|
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 }),
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
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<
|
export type CustomModelFields = Record<string, FieldDefinition>;
|
||||||
|
|
||||||
|
export interface Collection<
|
||||||
TName extends string = string,
|
TName extends string = string,
|
||||||
TFields extends CustomModelFields = CustomModelFields,
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
> {
|
> {
|
||||||
@ -10,20 +12,53 @@ export interface Model<
|
|||||||
fields: TFields;
|
fields: TFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const DefaultModelFields = {
|
||||||
|
id: Field.uuid({ isPrimaryKey: true }),
|
||||||
|
streamId: Field.uuid({ isIndexed: true }),
|
||||||
|
streamVersion: Field.integer({
|
||||||
|
isUnsigned: true,
|
||||||
|
hasArbitraryPrecision: true,
|
||||||
|
}),
|
||||||
|
deletedAt: Field.timestamp({ isOptional: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Model<
|
||||||
|
TName extends string = string,
|
||||||
|
TFields extends CustomModelFields = CustomModelFields,
|
||||||
|
> extends Collection<TName, TFields> {
|
||||||
|
fields: typeof DefaultModelFields & TFields;
|
||||||
|
}
|
||||||
|
|
||||||
export function defineModel<
|
export function defineModel<
|
||||||
TName extends string,
|
TName extends string,
|
||||||
TFields extends CustomModelFields,
|
TFields extends CustomModelFields,
|
||||||
>(name: TName, fields: TFields): Model<TName, TFields> {
|
>(name: TName, fields: TFields): Model<TName, TFields> {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
fields: { ...DefaultModelFields, ...fields },
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineCollection<
|
||||||
|
TName extends string,
|
||||||
|
TFields extends CustomModelFields,
|
||||||
|
>(name: TName, fields: TFields): Collection<TName, TFields> {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
fields,
|
fields,
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ModelToType<TModel extends Model> = {
|
export type ModelToType<TModel extends Collection> = {
|
||||||
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
[K in Keyof<TModel["fields"]>]: FieldToType<TModel["fields"][K]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModelFieldNames<TModel extends CustomModelFields> = 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"]>];
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
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 } from "../models/aggregate-model.ts";
|
import type { Model, ModelToType } from "../models/model.ts";
|
||||||
import type { ModelToType } from "../models/model.ts";
|
|
||||||
|
|
||||||
export interface Projection<
|
export interface Projection<TModel extends Model, TEvents extends Event> {
|
||||||
TModel extends AggregateModel,
|
|
||||||
TEvents extends Event,
|
|
||||||
> {
|
|
||||||
model: TModel;
|
model: TModel;
|
||||||
events: TEvents[VariantTag][];
|
events: TEvents[VariantTag][];
|
||||||
projection: (
|
projection: (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UUID } from "../../core/types/uuid.ts";
|
import type { UUID } from "../types/uuid.ts";
|
||||||
import type { UUIDGenerator } from "./uuid-generator.ts";
|
import type { UUIDGenerator } from "./uuid-generator.ts";
|
||||||
|
|
||||||
export const UUIDGeneratorMock: UUIDGenerator = {
|
export const UUIDGeneratorMock: UUIDGenerator = {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { UUID } from "../../core/types/uuid.ts";
|
import type { UUID } from "../types/uuid.ts";
|
||||||
|
|
||||||
export interface UUIDGenerator {
|
export interface UUIDGenerator {
|
||||||
generate(): UUID;
|
generate(): UUID;
|
||||||
|
|||||||
1
packages/fabric/domain/types/base-64.ts
Normal file
1
packages/fabric/domain/types/base-64.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Base64String = string;
|
||||||
11
packages/fabric/domain/types/entity.ts
Normal file
11
packages/fabric/domain/types/entity.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { UUID } from "./uuid.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entity is a domain object that is defined by its identity.
|
||||||
|
*
|
||||||
|
* Entities have a unique identity (`id`), which distinguishes
|
||||||
|
* them from other entities.
|
||||||
|
*/
|
||||||
|
export interface Entity {
|
||||||
|
id: UUID;
|
||||||
|
}
|
||||||
5
packages/fabric/domain/types/index.ts
Normal file
5
packages/fabric/domain/types/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./base-64.ts";
|
||||||
|
export * from "./email.ts";
|
||||||
|
export * from "./entity.ts";
|
||||||
|
export * from "./semver.ts";
|
||||||
|
export * from "./uuid.ts";
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, test } from "@fabric/testing";
|
import { expect } from "@std/expect";
|
||||||
|
import { describe, it } from "@std/testing/bdd";
|
||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.ts";
|
import { CircularDependencyError } from "../errors/circular-dependency-error.ts";
|
||||||
import { sortByDependencies } from "./sort-by-dependencies.ts";
|
import { sortByDependencies } from "./sort-by-dependencies.ts";
|
||||||
|
|
||||||
describe("sortByDependencies", () => {
|
describe("sortByDependencies", () => {
|
||||||
test("should sort an array of objects by their dependencies", () => {
|
it("should sort an array of objects by their dependencies", () => {
|
||||||
const array = [
|
const array = [
|
||||||
{ id: 1, name: "A", dependencies: ["C", "D"] },
|
{ id: 1, name: "A", dependencies: ["C", "D"] },
|
||||||
{ id: 2, name: "B", dependencies: ["A", "D"] },
|
{ id: 2, name: "B", dependencies: ["A", "D"] },
|
||||||
@ -24,7 +25,7 @@ describe("sortByDependencies", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw a CircularDependencyError when circular dependencies are detected", () => {
|
it("should throw a CircularDependencyError when circular dependencies are detected", () => {
|
||||||
const array = [
|
const array = [
|
||||||
{ id: 1, name: "A", dependencies: ["B"] },
|
{ id: 1, name: "A", dependencies: ["B"] },
|
||||||
{ id: 2, name: "B", dependencies: ["A"] },
|
{ id: 2, name: "B", dependencies: ["A"] },
|
||||||
@ -38,7 +39,7 @@ describe("sortByDependencies", () => {
|
|||||||
).toBeInstanceOf(CircularDependencyError);
|
).toBeInstanceOf(CircularDependencyError);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return an empty array when the input array is empty", () => {
|
it("should return an empty array when the input array is empty", () => {
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
const array: any[] = [];
|
const array: any[] = [];
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
import { Result } from "@fabric/core";
|
import { Result } from "@fabric/core";
|
||||||
import { CircularDependencyError } from "../errors/circular-dependency-error.ts";
|
import { CircularDependencyError } from "../errors/circular-dependency-error.ts";
|
||||||
|
|
||||||
/**
|
|
||||||
* Sorts an array of elements based on their dependencies.
|
|
||||||
*/
|
|
||||||
export function sortByDependencies<T>(
|
export function sortByDependencies<T>(
|
||||||
array: T[],
|
array: T[],
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,134 +0,0 @@
|
|||||||
import {
|
|
||||||
PosixDate,
|
|
||||||
Result,
|
|
||||||
TaggedError,
|
|
||||||
type VariantFromTag,
|
|
||||||
} from "@fabric/core";
|
|
||||||
import { isUUID, parseAndSanitizeString } from "@fabric/validations";
|
|
||||||
import type { FieldDefinition, FieldToType } from "../models/index.ts";
|
|
||||||
|
|
||||||
export type FieldParsers = {
|
|
||||||
[K in FieldDefinition["_tag"]]: FieldParser<
|
|
||||||
VariantFromTag<FieldDefinition, K>
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
export const fieldParsers: FieldParsers = {
|
|
||||||
StringField: (f, v) => {
|
|
||||||
return parseStringValue(f, v);
|
|
||||||
},
|
|
||||||
UUIDField: (f, v) => {
|
|
||||||
return parseStringValue(f, v)
|
|
||||||
.flatMap((parsedString) =>
|
|
||||||
isUUID(parsedString)
|
|
||||||
? Result.ok(parsedString)
|
|
||||||
: Result.failWith(new InvalidFieldTypeError())
|
|
||||||
);
|
|
||||||
},
|
|
||||||
ReferenceField: (f, v) => {
|
|
||||||
return parseStringValue(f, v)
|
|
||||||
.flatMap((parsedString) =>
|
|
||||||
isUUID(parsedString)
|
|
||||||
? Result.ok(parsedString)
|
|
||||||
: Result.failWith(new InvalidFieldTypeError())
|
|
||||||
);
|
|
||||||
},
|
|
||||||
TimestampField: (f, v) => {
|
|
||||||
return parseOptionality(f, v).flatMap((parsedValue) => {
|
|
||||||
if (parsedValue === undefined) return Result.ok(undefined);
|
|
||||||
|
|
||||||
if (!(v instanceof PosixDate)) {
|
|
||||||
return Result.failWith(new InvalidFieldTypeError());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(v);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
BooleanField: (f, v) => {
|
|
||||||
if (!f.isOptional && v === undefined) {
|
|
||||||
return Result.failWith(new MissingRequiredFieldError());
|
|
||||||
}
|
|
||||||
if (v === undefined) {
|
|
||||||
return Result.ok(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof v === "boolean") {
|
|
||||||
return Result.ok(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.failWith(new InvalidFieldTypeError());
|
|
||||||
},
|
|
||||||
IntegerField: function (): Result<
|
|
||||||
number | bigint | undefined,
|
|
||||||
FieldParsingError
|
|
||||||
> {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
FloatField: function () {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
DecimalField: function () {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
EmbeddedField: function () {
|
|
||||||
throw new Error("Function not implemented.");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function that takes a field definition and a value and returns a result
|
|
||||||
*/
|
|
||||||
export type FieldParser<TField extends FieldDefinition> = (
|
|
||||||
field: TField,
|
|
||||||
value: unknown,
|
|
||||||
) => Result<FieldToType<TField> | undefined, FieldParsingError>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field parsing errors
|
|
||||||
*/
|
|
||||||
export type FieldParsingError =
|
|
||||||
| InvalidFieldTypeError
|
|
||||||
| MissingRequiredFieldError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error that occurs when a field is invalid
|
|
||||||
*/
|
|
||||||
export class InvalidFieldTypeError extends TaggedError<"InvalidField"> {
|
|
||||||
constructor() {
|
|
||||||
super("InvalidField");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An error that occurs when a required field is missing
|
|
||||||
*/
|
|
||||||
export class MissingRequiredFieldError extends TaggedError<"RequiredField"> {
|
|
||||||
constructor() {
|
|
||||||
super("RequiredField");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses a string value including optionality
|
|
||||||
*/
|
|
||||||
function parseStringValue(
|
|
||||||
field: FieldDefinition,
|
|
||||||
value: unknown,
|
|
||||||
): Result<string | undefined, FieldParsingError> {
|
|
||||||
const parsedValue = parseAndSanitizeString(value);
|
|
||||||
return parseOptionality(field, parsedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the optionality of a field.
|
|
||||||
* In other words, if a field is required and the value is undefined, it will return a MissingRequiredFieldError.
|
|
||||||
* If the field is optional and the value is undefined, it will return the value as undefined.
|
|
||||||
*/
|
|
||||||
function parseOptionality<T>(
|
|
||||||
field: FieldDefinition,
|
|
||||||
value: T | undefined,
|
|
||||||
): Result<T | undefined, FieldParsingError> {
|
|
||||||
if (!field.isOptional && value === undefined) {
|
|
||||||
return Result.failWith(new MissingRequiredFieldError());
|
|
||||||
}
|
|
||||||
return Result.ok(value);
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./parse-from-model.ts";
|
|
||||||
@ -1,43 +0,0 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
|
||||||
|
|
||||||
import { isRecordEmpty, Result, TaggedError } from "@fabric/core";
|
|
||||||
import type { FieldDefinition, Model, ModelToType } from "../models/index.ts";
|
|
||||||
import { fieldParsers, type FieldParsingError } from "./field-parsers.ts";
|
|
||||||
|
|
||||||
export function parseFromModel<
|
|
||||||
TModel extends Model,
|
|
||||||
>(
|
|
||||||
model: TModel,
|
|
||||||
value: unknown,
|
|
||||||
): Result<ModelToType<TModel>, SchemaParsingError<TModel>> {
|
|
||||||
const parsingErrors = {} as Record<keyof TModel, FieldParsingError>;
|
|
||||||
const parsedValue = {} as ModelToType<TModel>;
|
|
||||||
|
|
||||||
for (const key in model) {
|
|
||||||
const field = model[key] as FieldDefinition;
|
|
||||||
const fieldResult = fieldParsers[field._tag](field as any, value);
|
|
||||||
|
|
||||||
if (fieldResult.isOk()) {
|
|
||||||
parsedValue[key as keyof ModelToType<TModel>] = fieldResult
|
|
||||||
.value();
|
|
||||||
} else {
|
|
||||||
parsingErrors[key] = fieldResult.unwrapErrorOrThrow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isRecordEmpty(parsingErrors)) {
|
|
||||||
return Result.failWith(new SchemaParsingError(parsingErrors, parsedValue));
|
|
||||||
} else {
|
|
||||||
return Result.succeedWith(parsedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SchemaParsingError<TModel extends Model>
|
|
||||||
extends TaggedError<"SchemaParsingFailed"> {
|
|
||||||
constructor(
|
|
||||||
public readonly errors: Record<keyof TModel, FieldParsingError>,
|
|
||||||
public readonly value?: Partial<ModelToType<TModel>>,
|
|
||||||
) {
|
|
||||||
super("SchemaParsingFailed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@fabric/sqlite-store",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
|
||||||
"@fabric/domain": "jsr:@fabric/domain"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
packages/fabric/sqlite-store/deno.jsonc
Normal file
6
packages/fabric/sqlite-store/deno.jsonc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@fabric/sqlite-store",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ import {
|
|||||||
MaybePromise,
|
MaybePromise,
|
||||||
PosixDate,
|
PosixDate,
|
||||||
Run,
|
Run,
|
||||||
UUID,
|
|
||||||
VariantTag,
|
VariantTag,
|
||||||
} from "@fabric/core";
|
} from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
@ -14,6 +13,7 @@ import {
|
|||||||
JSONUtils,
|
JSONUtils,
|
||||||
StoredEvent,
|
StoredEvent,
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
|
UUID,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
import { SQLiteDatabase } from "../sqlite/sqlite-database.ts";
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
defineModel,
|
defineCollection,
|
||||||
Field,
|
Field,
|
||||||
isGreaterOrEqualTo,
|
isGreaterOrEqualTo,
|
||||||
isGreaterThan,
|
isGreaterThan,
|
||||||
@ -13,7 +13,7 @@ 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 = defineModel("users", {
|
const col = defineCollection("users", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
age: Field.integer(),
|
age: Field.integer(),
|
||||||
status: Field.string(),
|
status: Field.string(),
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import {
|
import {
|
||||||
|
Collection,
|
||||||
FieldDefinition,
|
FieldDefinition,
|
||||||
FILTER_OPTION_OPERATOR_SYMBOL,
|
FILTER_OPTION_OPERATOR_SYMBOL,
|
||||||
FILTER_OPTION_TYPE_SYMBOL,
|
FILTER_OPTION_TYPE_SYMBOL,
|
||||||
FILTER_OPTION_VALUE_SYMBOL,
|
FILTER_OPTION_VALUE_SYMBOL,
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
FilterValue,
|
FilterValue,
|
||||||
Model,
|
|
||||||
MultiFilterOption,
|
MultiFilterOption,
|
||||||
SingleFilterOption,
|
SingleFilterOption,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
@ -24,7 +24,7 @@ export function filterToSQL(filterOptions?: FilterOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function filterToParams(
|
export function filterToParams(
|
||||||
collection: Model,
|
collection: Collection,
|
||||||
filterOptions?: FilterOptions,
|
filterOptions?: FilterOptions,
|
||||||
) {
|
) {
|
||||||
if (!filterOptions) return {};
|
if (!filterOptions) return {};
|
||||||
@ -100,7 +100,7 @@ function getWhereFromKeyValue(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getParamsFromMultiFilterOption(
|
function getParamsFromMultiFilterOption(
|
||||||
collection: Model,
|
collection: Collection,
|
||||||
filterOptions: MultiFilterOption,
|
filterOptions: MultiFilterOption,
|
||||||
) {
|
) {
|
||||||
return filterOptions.reduce(
|
return filterOptions.reduce(
|
||||||
@ -115,7 +115,7 @@ function getParamsFromMultiFilterOption(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getParamsFromSingleFilterOption(
|
function getParamsFromSingleFilterOption(
|
||||||
collection: Model,
|
collection: Collection,
|
||||||
filterOptions: SingleFilterOption,
|
filterOptions: SingleFilterOption,
|
||||||
opts: { postfix?: string } = {},
|
opts: { postfix?: string } = {},
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { defineModel, Field } from "@fabric/domain";
|
import { defineCollection, 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 = defineModel("something", {
|
const model = defineCollection("something", {
|
||||||
id: Field.uuid({ isPrimaryKey: true }),
|
id: Field.uuid({ isPrimaryKey: true }),
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
age: Field.integer(),
|
age: Field.integer(),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import { Variant, VariantTag } from "@fabric/core";
|
import { Variant, VariantTag } from "@fabric/core";
|
||||||
import { FieldDefinition, getTargetKey, Model } from "@fabric/domain";
|
import { Collection, FieldDefinition, getTargetKey } from "@fabric/domain";
|
||||||
|
|
||||||
type FieldSQLDefinitionMap = {
|
type FieldSQLDefinitionMap = {
|
||||||
[K in FieldDefinition[VariantTag]]: (
|
[K in FieldDefinition[VariantTag]]: (
|
||||||
@ -46,9 +46,6 @@ const FieldSQLDefinitionMap: FieldSQLDefinitionMap = {
|
|||||||
EmbeddedField: (n, f): string => {
|
EmbeddedField: (n, f): string => {
|
||||||
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
return [n, "TEXT", modifiersFromOpts(f)].join(" ");
|
||||||
},
|
},
|
||||||
BooleanField: (n, f): string => {
|
|
||||||
return [n, "BOOLEAN", modifiersFromOpts(f)].join(" ");
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
function fieldDefinitionToSQL(name: string, field: FieldDefinition) {
|
||||||
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
return FieldSQLDefinitionMap[field[VariantTag]](name, field as any);
|
||||||
@ -64,7 +61,7 @@ function modifiersFromOpts(field: FieldDefinition) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function modelToSql(
|
export function modelToSql(
|
||||||
model: Model<string, Record<string, FieldDefinition>>,
|
model: Collection<string, Record<string, FieldDefinition>>,
|
||||||
) {
|
) {
|
||||||
const fields = Object.entries(model.fields)
|
const fields = Object.entries(model.fields)
|
||||||
.map(([name, type]) => fieldDefinitionToSQL(name, type))
|
.map(([name, type]) => fieldDefinitionToSQL(name, type))
|
||||||
|
|||||||
@ -23,10 +23,7 @@ export function recordToSQLKeyParams(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 recordToSQLParams(
|
export function recordToSQLParams(model: Model, record: Record<string, any>) {
|
||||||
model: Model,
|
|
||||||
record: Record<string, any>,
|
|
||||||
) {
|
|
||||||
return Object.keys(record).reduce(
|
return Object.keys(record).reduce(
|
||||||
(acc, key) => ({
|
(acc, key) => ({
|
||||||
...acc,
|
...acc,
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import { PosixDate, VariantTag } from "@fabric/core";
|
import { PosixDate, VariantTag } from "@fabric/core";
|
||||||
import { FieldDefinition, FieldToType, Model } from "@fabric/domain";
|
import { Collection, FieldDefinition, FieldToType } from "@fabric/domain";
|
||||||
|
|
||||||
export function transformRow(model: Model) {
|
export function transformRow(model: Collection) {
|
||||||
return (row: Record<string, any>) => {
|
return (row: Record<string, any>) => {
|
||||||
const result: Record<string, any> = {};
|
const result: Record<string, any> = {};
|
||||||
for (const key in row) {
|
for (const key in row) {
|
||||||
@ -41,5 +41,4 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|||||||
DecimalField: (_, v) => v,
|
DecimalField: (_, v) => v,
|
||||||
TimestampField: (_, v) => new PosixDate(v),
|
TimestampField: (_, v) => new PosixDate(v),
|
||||||
EmbeddedField: (_, v: string) => JSON.parse(v),
|
EmbeddedField: (_, v: string) => JSON.parse(v),
|
||||||
BooleanField: (_, v) => v,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,6 @@ const FieldSQLInsertMap: FieldSQLInsertMap = {
|
|||||||
DecimalField: (_, v) => v,
|
DecimalField: (_, v) => v,
|
||||||
TimestampField: (_, v) => v.timestamp,
|
TimestampField: (_, v) => v.timestamp,
|
||||||
EmbeddedField: (_, v: string) => JSON.stringify(v),
|
EmbeddedField: (_, v: string) => JSON.stringify(v),
|
||||||
BooleanField: (_, v) => v,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
export function fieldValueToSQL(field: FieldDefinition, value: any) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
// deno-lint-ignore-file no-explicit-any
|
// deno-lint-ignore-file no-explicit-any
|
||||||
import { AsyncResult, Keyof, Optional } from "@fabric/core";
|
import { AsyncResult, Keyof, Optional } from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
|
Collection,
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
Model,
|
|
||||||
ModelSchema,
|
ModelSchema,
|
||||||
OrderByOptions,
|
OrderByOptions,
|
||||||
SelectableQuery,
|
SelectableQuery,
|
||||||
@ -128,7 +128,7 @@ export class QueryBuilder<T> implements StoreQuery<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectStatement(
|
export function getSelectStatement(
|
||||||
collection: Model,
|
collection: Collection,
|
||||||
query: StoreQueryDefinition,
|
query: StoreQueryDefinition,
|
||||||
): [string, Record<string, any>] {
|
): [string, Record<string, any>] {
|
||||||
const selectFields = query.keys ? query.keys.join(", ") : "*";
|
const selectFields = query.keys ? query.keys.join(", ") : "*";
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { PosixDate, Run, UUID } from "@fabric/core";
|
import { PosixDate, Run } from "@fabric/core";
|
||||||
import { defineAggregateModel, Field, isLike } from "@fabric/domain";
|
import { defineModel, Field, isLike, UUID } 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 = [
|
||||||
defineAggregateModel("demo", {
|
defineModel("demo", {
|
||||||
value: Field.float(),
|
value: Field.float(),
|
||||||
owner: Field.reference({ targetModel: "users" }),
|
owner: Field.reference({ targetModel: "users" }),
|
||||||
}),
|
}),
|
||||||
defineAggregateModel("users", {
|
defineModel("users", {
|
||||||
name: Field.string(),
|
name: Field.string(),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { AsyncResult, UnexpectedError, UUID } from "@fabric/core";
|
import { AsyncResult, UnexpectedError } from "@fabric/core";
|
||||||
import {
|
import {
|
||||||
type AggregateModel,
|
Model,
|
||||||
ModelSchemaFromModels,
|
ModelSchemaFromModels,
|
||||||
ModelToType,
|
ModelToType,
|
||||||
StoreQuery,
|
StoreQuery,
|
||||||
StoreQueryError,
|
StoreQueryError,
|
||||||
|
UUID,
|
||||||
WritableStateStore,
|
WritableStateStore,
|
||||||
} from "@fabric/domain";
|
} from "@fabric/domain";
|
||||||
import { modelToSql } from "../sqlite/model-to-sql.ts";
|
import { modelToSql } from "../sqlite/model-to-sql.ts";
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
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 AggregateModel>
|
export class SQLiteStateStore<TModel extends Model>
|
||||||
implements WritableStateStore<TModel> {
|
implements WritableStateStore<TModel> {
|
||||||
private schema: ModelSchemaFromModels<TModel>;
|
private schema: ModelSchemaFromModels<TModel>;
|
||||||
private db: SQLiteDatabase;
|
private db: SQLiteDatabase;
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@fabric/testing",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"expect-type": "npm:expect-type@^1.1.0",
|
|
||||||
"@std/expect": "jsr:@std/expect@^1.0.5",
|
|
||||||
"@std/testing": "jsr:@std/testing@^1.0.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
packages/fabric/testing/deno.jsonc
Normal file
6
packages/fabric/testing/deno.jsonc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@fabric/testing",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@fabric/validations",
|
|
||||||
"exports": {
|
|
||||||
".": "./index.ts"
|
|
||||||
},
|
|
||||||
"imports": {
|
|
||||||
"@fabric/core": "jsr:@fabric/core"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./nullish/index.ts";
|
|
||||||
export * from "./number/index.ts";
|
|
||||||
export * from "./string/index.ts";
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./is-null.ts";
|
|
||||||
export * from "./is-nullish.ts";
|
|
||||||
export * from "./is-undefined.ts";
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export function isNull(value: unknown): value is null {
|
|
||||||
return value === null;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { isNull } from "./is-null.ts";
|
|
||||||
import { isUndefined } from "./is-undefined.ts";
|
|
||||||
|
|
||||||
export function isNullish(value: unknown): value is null | undefined {
|
|
||||||
return isNull(value) || isUndefined(value);
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export function isUndefined(value: unknown): value is undefined {
|
|
||||||
return value === undefined;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./is-number.ts";
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { describe, expect, test } from "@fabric/testing";
|
|
||||||
import { isNumber } from "./is-number.ts";
|
|
||||||
|
|
||||||
describe("Is a number", () => {
|
|
||||||
test("Given a number it should return true", () => {
|
|
||||||
expect(isNumber(1)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a string it should return false", () => {
|
|
||||||
expect(isNumber("a")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given an empty string it should return false", () => {
|
|
||||||
expect(isNumber("")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a boolean it should return false", () => {
|
|
||||||
expect(isNumber(false)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given an object it should return false", () => {
|
|
||||||
expect(isNumber({})).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given an array it should return false", () => {
|
|
||||||
expect(isNumber([])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a null it should return false", () => {
|
|
||||||
expect(isNumber(null)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given an undefined it should return false", () => {
|
|
||||||
expect(isNumber(undefined)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
/**
|
|
||||||
* Checks if a value is a number even if it is a string that can be converted to a number
|
|
||||||
*/
|
|
||||||
export function isNumber(value: unknown): value is number {
|
|
||||||
if (typeof value === "number") {
|
|
||||||
return !isNaN(value);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./is-string.ts";
|
|
||||||
export * from "./is-uuid.ts";
|
|
||||||
export * from "./sanitize-string.ts";
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export function isString(value: unknown): value is string {
|
|
||||||
return typeof value === "string";
|
|
||||||
}
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import { describe, expect, test } from "@fabric/testing";
|
|
||||||
import { isUUID } from "./is-uuid.ts";
|
|
||||||
|
|
||||||
describe("isUUID", () => {
|
|
||||||
test("should return true for a valid UUID", () => {
|
|
||||||
const validUUID = "123e4567-e89b-12d3-a456-426614174000";
|
|
||||||
expect(isUUID(validUUID)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return true for a valid UUID with uppercase letters", () => {
|
|
||||||
const validUUID = "123E4567-E89B-12D3-A456-426614174000";
|
|
||||||
expect(isUUID(validUUID)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return true for a nil UUID", () => {
|
|
||||||
const nilUUID = "00000000-0000-0000-0000-000000000000";
|
|
||||||
expect(isUUID(nilUUID)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return true for a max UUID", () => {
|
|
||||||
const maxUUID = "ffffffff-ffff-ffff-ffff-ffffffffffff";
|
|
||||||
expect(isUUID(maxUUID)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for an invalid UUID", () => {
|
|
||||||
const invalidUUID = "123e4567-e89b-12d3-a456-42661417400";
|
|
||||||
expect(isUUID(invalidUUID)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for a string that is not a UUID", () => {
|
|
||||||
const notUUID = "not-a-uuid";
|
|
||||||
expect(isUUID(notUUID)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for a number", () => {
|
|
||||||
const number = 1234567890;
|
|
||||||
expect(isUUID(number)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for a boolean", () => {
|
|
||||||
const boolean = true;
|
|
||||||
expect(isUUID(boolean)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for null", () => {
|
|
||||||
const nullValue = null;
|
|
||||||
expect(isUUID(nullValue)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return false for undefined", () => {
|
|
||||||
const undefinedValue = undefined;
|
|
||||||
expect(isUUID(undefinedValue)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import type { UUID } from "@fabric/core";
|
|
||||||
import { isString } from "./is-string.ts";
|
|
||||||
|
|
||||||
// From https://github.com/uuidjs/uuid/blob/main/src/regex.ts
|
|
||||||
const uuidRegex =
|
|
||||||
/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
|
|
||||||
|
|
||||||
export function isUUID(value: unknown): value is UUID {
|
|
||||||
return isString(value) && uuidRegex.test(value);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { describe, expect, test } from "@fabric/testing";
|
|
||||||
import { parseAndSanitizeString } from "../string/sanitize-string.ts";
|
|
||||||
|
|
||||||
describe("Sanitize String", () => {
|
|
||||||
test("Given a string with low (control) characters it should sanitize it", () => {
|
|
||||||
const sanitized = parseAndSanitizeString("John\x00");
|
|
||||||
|
|
||||||
expect(sanitized).toBe("John");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a string with leading and trailing spaces it should trim them", () => {
|
|
||||||
const sanitized = parseAndSanitizeString(" John ");
|
|
||||||
|
|
||||||
expect(sanitized).toBe("John");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a number value it should convert it to a string", () => {
|
|
||||||
const sanitized = parseAndSanitizeString(123);
|
|
||||||
|
|
||||||
expect(sanitized).toBe("123");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a boolean value it should convert it to a string", () => {
|
|
||||||
const sanitized = parseAndSanitizeString(true);
|
|
||||||
|
|
||||||
expect(sanitized).toBe("true");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given a null value it should return null", () => {
|
|
||||||
const sanitized = parseAndSanitizeString(null);
|
|
||||||
|
|
||||||
expect(sanitized).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Given an undefined value it should return undefined", () => {
|
|
||||||
const sanitized = parseAndSanitizeString(undefined);
|
|
||||||
|
|
||||||
expect(sanitized).toBe(undefined);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { isNullish } from "../nullish/is-nullish.ts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses and sanitizes an unknown value into a string
|
|
||||||
* The string is trimmed and all low characters are removed
|
|
||||||
*/
|
|
||||||
export function parseAndSanitizeString(
|
|
||||||
value: unknown,
|
|
||||||
): string | undefined {
|
|
||||||
if (isNullish(value)) return undefined;
|
|
||||||
return stripLow((String(value)).trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// deno-lint-ignore no-control-regex
|
|
||||||
const lowCharsRegex = /[\x00-\x09\x0B\x0C\x0E-\x1F\x7F]/g;
|
|
||||||
|
|
||||||
const stripLow = (str: string) => str.replace(lowCharsRegex, "");
|
|
||||||
Loading…
Reference in New Issue
Block a user