Compare commits

..

1 Commits
main ... loom

Author SHA1 Message Date
7766235e54 [loom] Add basic domain models 2024-09-04 19:21:45 -03:00
222 changed files with 5205 additions and 4107 deletions

3
.gitattributes vendored
View File

@ -1 +1,4 @@
/.yarn/** linguist-vendored
/.yarn/releases/* binary
/.yarn/plugins/**/* binary
* text=auto eol=lf

8
.gitignore vendored
View File

@ -1,3 +1,11 @@
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
node_modules
.env
dist
coverage

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/hook.sh"
deno task check

2
.husky/pre-commit Normal file
View File

@ -0,0 +1,2 @@
# .husky/pre-commit
yarn lint-staged

4
.lintstagedrc.json Normal file
View File

@ -0,0 +1,4 @@
{
"*": ["yarn prettier -u --write"],
"*.ts": ["yarn eslint --fix"]
}

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
dist
coverage
.yarn/**/*
yarn.lock

3
.prettierrc Normal file
View File

@ -0,0 +1,3 @@
{
"tabWidth": 2
}

View File

@ -2,11 +2,13 @@
"recommendations": [
"bierner.github-markdown-preview",
"bradlc.vscode-tailwindcss",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"streetsidesoftware.code-spell-checker",
"streetsidesoftware.code-spell-checker-spanish",
"usernamehw.errorlens",
"bourhaouta.tailwindshades",
"austenc.tailwind-docs",
"denoland.vscode-deno"
"vitest.explorer"
]
}

22
.vscode/settings.json vendored
View File

@ -1,23 +1,21 @@
{
"cSpell.enabled": true,
"cSpell.language": "en,es",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"files.autoSave": "off",
"files.eol": "\n",
"javascript.preferences.importModuleSpecifierEnding": "js",
"editor.codeActionsOnSave": {
"source.fixAll": "always",
"source.organizeImports": "always"
},
"cSpell.words": ["Syntropy"],
"deno.enable": true,
"editor.defaultFormatter": "denoland.vscode-deno",
"deno.future": true,
"deno.codeLens.references": true,
"deno.testing.args": [
"--allow-all"
],
"notebook.defaultFormatter": "denoland.vscode-deno",
"[json]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
"search.exclude": {
"**/.yarn": true,
"**/node_modules": true,
"packages/frontend/{android,ios}": true
},
"typescript.preferences.importModuleSpecifierEnding": "js",
"cSpell.words": ["autodocs", "Syntropy"],
"typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"]
}

925
.yarn/releases/yarn-4.4.1.cjs vendored Normal file

File diff suppressed because one or more lines are too long

2
.yarnrc.yml Normal file
View File

@ -0,0 +1,2 @@
yarnPath: .yarn/releases/yarn-4.4.1.cjs
nodeLinker: node-modules

3
apps/loom/README.md Normal file
View File

@ -0,0 +1,3 @@
# Loom
Loom is the design tool for the `Fabric` framework. It is a web-based tool that allows you to create and manage and design every part of your systems

View File

@ -0,0 +1,22 @@
{
"name": "@loom/domain",
"type": "module",
"module": "dist/index.js",
"main": "dist/index.js",
"files": [
"dist"
],
"private": true,
"packageManager": "yarn@4.1.1",
"devDependencies": {
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"dependencies": {
"@ulthar/fabric-core": "workspace:^"
},
"scripts": {
"test": "vitest",
"build": "tsc -p tsconfig.build.json"
}
}

View File

@ -0,0 +1,9 @@
import { Entity, SemVer } from "@ulthar/fabric-core";
import { EnvironmentVariable } from "./environment.js";
export interface App extends Entity {
title: string;
version: SemVer;
description: string;
environment: EnvironmentVariable[];
}

View File

@ -0,0 +1,13 @@
import { App } from "../app.js";
export const WebFramework = {
EXPRESS: "EXPRESS",
REACT: "REACT",
VUE: "VUE",
SVELTE: "SVELTE",
} as const;
export type WebFramework = (typeof WebFramework)[keyof typeof WebFramework];
export interface SSRWebApp extends App {
framework: WebFramework;
}

View File

@ -0,0 +1,13 @@
export const DefaultEnvironmentType = {
DEVELOPMENT: "DEVELOPMENT",
DEMO: "DEMO",
STAGING: "STAGING",
PRODUCTION: "PRODUCTION",
};
export type DefaultEnvironmentType = keyof typeof DefaultEnvironmentType;
export const DefaultUserType = {
ADMIN: "ADMIN",
REGULAR: "REGULAR",
};
export type DefaultUserType = keyof typeof DefaultUserType;

View File

@ -0,0 +1,12 @@
import { ValueSchema } from "./schema.js";
export interface Environment {
name: string;
variables: EnvironmentVariable[];
}
export interface EnvironmentVariable extends ValueSchema {
name: string;
description: string;
isSecret: boolean;
}

View File

@ -0,0 +1,6 @@
import { Entity } from "@ulthar/fabric-core";
export interface Permission extends Entity {
name: string;
description: string;
}

View File

@ -0,0 +1,6 @@
export interface Project {
name: string;
description: string;
tags: string[];
environmentTypes: string[];
}

View File

@ -0,0 +1,36 @@
export interface ObjectSchema {
name: string;
description: string;
optional: boolean;
fields: (ArraySchema | ValueSchema | ObjectSchema)[];
}
export interface ArraySchema {
name: string;
description: string;
type: "ARRAY";
itemSchema: ObjectSchema | ValueSchema;
}
export interface ValueSchema {
name: string;
description: string;
optional: boolean;
type: ValueType;
}
export const ValueType = {
STRING: "STRING",
EMAIL: "EMAIL",
NUMBER: "NUMBER",
BOOLEAN: "BOOLEAN",
URL: "URL",
} as const;
export type ValueType = keyof typeof ValueType;
export const FieldType = {
ARRAY: "ARRAY",
OBJECT: "OBJECT",
...ValueType,
} as const;
export type FieldType = keyof typeof FieldType;

View File

@ -0,0 +1,17 @@
import { ObjectSchema } from "./schema.js";
export const UseCaseType = {
QUERY: "QUERY",
COMMAND: "COMMAND",
} as const;
export type UseCaseType = (typeof UseCaseType)[keyof typeof UseCaseType];
export interface UseCase {
name: string;
type: UseCaseType;
description: string;
requiredPermissions: string[];
optionalPermissions: string[];
payloadSchema: ObjectSchema;
responseSchema: ObjectSchema;
}

View File

@ -0,0 +1,9 @@
import { DefaultUserType } from "./defaults.js";
import { UseCase } from "./use-case.js";
export interface UserStory {
name: string;
description: string;
userTypes: DefaultUserType | string;
useCases: UseCase[];
}

View File

@ -0,0 +1,5 @@
import { Entity } from "@ulthar/fabric-core";
export interface UserType extends Entity {
name: string;
}

View File

@ -0,0 +1,21 @@
import { UnexpectedError, UseCaseDefinition } from "@ulthar/fabric-core";
export interface InitProjectRequestModel {
title: string;
description?: string;
}
export type InitProjectErrors = UnexpectedError;
export const initProjectUseCase = {
name: "initProject",
isAuthRequired: true,
useCase: (async () => {
return {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
} as const satisfies UseCaseDefinition<
void,
InitProjectRequestModel,
void,
InitProjectErrors
>;

View File

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"allowImportingTsExtensions": false,
"outDir": "dist"
},
"exclude": [
"src/**/*.spec.ts",
"dist",
"node_modules",
"coverage",
"vitest.config.ts"
]
}

View File

@ -0,0 +1,4 @@
{
"extends": "../../../tsconfig.json",
"exclude": ["dist", "node_modules"]
}

View File

@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
exclude: ["**/index.ts"],
},
passWithNoTests: true,
},
});

View File

@ -1,30 +0,0 @@
{
"tasks": {
"check": "deno fmt && deno lint --fix && deno check **/*.ts && deno test -A",
"hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts"
},
"workspace": [
"packages/fabric/core",
"packages/fabric/domain",
"packages/fabric/sqlite-store",
"packages/fabric/testing",
"packages/fabric/validations",
"packages/templates/domain",
"packages/templates/lib",
"apps/syntropy/domain"
],
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"noImplicitOverride": true,
"noUncheckedIndexedAccess": true
},
"unstable": ["ffi"],
"lint": {
"rules": {
"tags": ["recommended"],
"exclude": ["no-namespace"]
}
}
}

157
deno.lock
View File

@ -1,157 +0,0 @@
{
"version": "4",
"specifiers": {
"jsr:@db/sqlite@*": "0.12.0",
"jsr:@db/sqlite@0.12": "0.12.0",
"jsr:@denosaurs/plug@1": "1.0.6",
"jsr:@quentinadam/assert@~0.1.7": "0.1.7",
"jsr:@quentinadam/decimal@~0.1.6": "0.1.6",
"jsr:@std/assert@0.217": "0.217.0",
"jsr:@std/assert@0.221": "0.221.0",
"jsr:@std/assert@^1.0.6": "1.0.6",
"jsr:@std/data-structures@^1.0.4": "1.0.4",
"jsr:@std/encoding@0.221": "0.221.0",
"jsr:@std/expect@*": "1.0.5",
"jsr:@std/expect@^1.0.5": "1.0.5",
"jsr:@std/fmt@0.221": "0.221.0",
"jsr:@std/fs@0.221": "0.221.0",
"jsr:@std/fs@^1.0.4": "1.0.4",
"jsr:@std/internal@^1.0.4": "1.0.4",
"jsr:@std/path@0.217": "0.217.0",
"jsr:@std/path@0.221": "0.221.0",
"jsr:@std/path@^1.0.6": "1.0.6",
"jsr:@std/testing@^1.0.3": "1.0.3",
"npm:expect-type@*": "1.1.0",
"npm:expect-type@^1.1.0": "1.1.0"
},
"jsr": {
"@db/sqlite@0.12.0": {
"integrity": "dd1ef7f621ad50fc1e073a1c3609c4470bd51edc0994139c5bf9851de7a6d85f",
"dependencies": [
"jsr:@denosaurs/plug",
"jsr:@std/path@0.217"
]
},
"@denosaurs/plug@1.0.6": {
"integrity": "6cf5b9daba7799837b9ffbe89f3450510f588fafef8115ddab1ff0be9cb7c1a7",
"dependencies": [
"jsr:@std/encoding",
"jsr:@std/fmt",
"jsr:@std/fs@0.221",
"jsr:@std/path@0.221"
]
},
"@quentinadam/assert@0.1.7": {
"integrity": "0246fb7fd3aa7b286535db40feefdafc588272d3f287b4eb995c96263ab6dfca"
},
"@quentinadam/decimal@0.1.6": {
"integrity": "fd3e2684614355db8a6ef94a839cbe115f040e8dcc843a17a0c5afa23e3db408",
"dependencies": [
"jsr:@quentinadam/assert"
]
},
"@std/assert@0.217.0": {
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
},
"@std/assert@0.221.0": {
"integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a"
},
"@std/assert@1.0.6": {
"integrity": "1904c05806a25d94fe791d6d883b685c9e2dcd60e4f9fc30f4fc5cf010c72207",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/data-structures@1.0.4": {
"integrity": "fa0e20c11eb9ba673417450915c750a0001405a784e2a4e0c3725031681684a0"
},
"@std/encoding@0.221.0": {
"integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45"
},
"@std/expect@1.0.5": {
"integrity": "8c7ac797e2ffe57becc6399c0f2fd06230cb9ef124d45229c6e592c563824af1",
"dependencies": [
"jsr:@std/assert@^1.0.6",
"jsr:@std/internal"
]
},
"@std/fmt@0.221.0": {
"integrity": "379fed69bdd9731110f26b9085aeb740606b20428ce6af31ef6bd45ef8efa62a"
},
"@std/fs@0.221.0": {
"integrity": "028044450299de8ed5a716ade4e6d524399f035513b85913794f4e81f07da286",
"dependencies": [
"jsr:@std/assert@0.221",
"jsr:@std/path@0.221"
]
},
"@std/fs@1.0.4": {
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
"dependencies": [
"jsr:@std/path@^1.0.6"
]
},
"@std/internal@1.0.4": {
"integrity": "62e8e4911527e5e4f307741a795c0b0a9e6958d0b3790716ae71ce085f755422"
},
"@std/path@0.217.0": {
"integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11",
"dependencies": [
"jsr:@std/assert@0.217"
]
},
"@std/path@0.221.0": {
"integrity": "0a36f6b17314ef653a3a1649740cc8db51b25a133ecfe838f20b79a56ebe0095",
"dependencies": [
"jsr:@std/assert@0.221"
]
},
"@std/path@1.0.6": {
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
},
"@std/testing@1.0.3": {
"integrity": "f98c2bee53860a5916727d7e7d3abe920dd6f9edace022e2d059f00d05c2cf42",
"dependencies": [
"jsr:@std/assert@^1.0.6",
"jsr:@std/data-structures",
"jsr:@std/fs@^1.0.4",
"jsr:@std/internal",
"jsr:@std/path@^1.0.6"
]
}
},
"npm": {
"expect-type@1.1.0": {
"integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA=="
}
},
"workspace": {
"members": {
"packages/fabric/domain": {
"dependencies": [
"jsr:@fabric/core@*",
"jsr:@fabric/validations@*",
"jsr:@quentinadam/decimal@~0.1.6"
]
},
"packages/fabric/sqlite-store": {
"dependencies": [
"jsr:@db/sqlite@0.12",
"jsr:@fabric/domain@*"
]
},
"packages/fabric/testing": {
"dependencies": [
"jsr:@std/expect@^1.0.5",
"jsr:@std/testing@^1.0.3",
"npm:expect-type@^1.1.0"
]
},
"packages/fabric/validations": {
"dependencies": [
"jsr:@fabric/core@*"
]
}
}
}
}

10
eslint.config.js Normal file
View File

@ -0,0 +1,10 @@
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strict,
...tseslint.configs.stylistic,
);

33
package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "ulthar-framework",
"packageManager": "yarn@4.4.1",
"version": "1.0.0",
"private": true,
"type": "module",
"workspaces": [
"packages/**/*",
"apps/**/*"
],
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/eslint": "^9.6.1",
"@types/eslint__js": "^8.42.3",
"cross-env": "^7.0.3",
"eslint": "^9.9.1",
"husky": "^9.1.5",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"tsx": "^4.19.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.4.0",
"zx": "^8.1.5"
},
"scripts": {
"lint": "eslint . --fix --report-unused-disable-directives",
"format": "prettier --write .",
"test": "yarn workspaces foreach -vvpA run test --run --clearScreen false",
"build": "yarn workspaces foreach -vvpA --topological-dev run build",
"add-package": "tsx ./scripts/add-package.ts",
"postinstall": "husky"
}
}

View File

@ -1 +1 @@
# @fabric/core
# @ulthar/fabric-core

View File

@ -1,10 +0,0 @@
import { describe, expectTypeOf, test } from "@fabric/testing";
import type { ArrayElement } from "./array-element.ts";
describe("ArrayElement", () => {
test("Given an array, it should return the element type of the array", () => {
type result = ArrayElement<["a", "b", "c"]>;
expectTypeOf<result>().toEqualTypeOf<"a" | "b" | "c">();
});
});

View File

@ -1,17 +0,0 @@
/**
* Get the element type of an array.
*/
export type ArrayElement<T extends readonly unknown[]> = T extends
readonly (infer U)[] ? U : never;
/**
* Get the first element type of a tuple.
*/
export type TupleFirstElement<T extends readonly unknown[]> = T extends
readonly [infer U, ...unknown[]] ? U : never;
/**
* Get the LAST element type of a tuple.
*/
export type TupleLastElement<T extends readonly unknown[]> = T extends
readonly [...unknown[], infer U] ? U : never;

View File

@ -1 +0,0 @@
export * from "./array-element.ts";

View File

@ -1,10 +0,0 @@
{
"name": "@fabric/core",
"version": "0.1.0",
"exports": {
".": "./index.ts"
},
"imports": {
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
}
}

View File

@ -1,3 +0,0 @@
export * from "./is-error.ts";
export * from "./tagged-error.ts";
export * from "./unexpected-error.ts";

View File

@ -1,16 +0,0 @@
import { describe, expect, expectTypeOf, test } from "@fabric/testing";
import { isError } from "./is-error.ts";
import { UnexpectedError } from "./unexpected-error.ts";
describe("is-error", () => {
test("Given a value that is an error, it should return true", () => {
const error = new UnexpectedError();
expect(isError(error)).toBe(true);
//After a check, typescript should be able to infer the type
if (isError(error)) {
expectTypeOf(error).toEqualTypeOf<UnexpectedError>();
}
});
});

View File

@ -1,8 +0,0 @@
import { TaggedError } from "./tagged-error.ts";
/**
* Indicates if a value is an error.
*/
export function isError(err: unknown): err is TaggedError<string> {
return err instanceof TaggedError;
}

View File

@ -1,15 +0,0 @@
import { type TaggedVariant, VariantTag } from "../variant/index.ts";
/**
* A TaggedError is a tagged variant with an error message.
*/
export abstract class TaggedError<Tag extends string = string> extends Error
implements TaggedVariant<Tag> {
readonly [VariantTag]: Tag;
constructor(tag: Tag, message?: string) {
super(message);
this[VariantTag] = tag;
this.name = tag;
}
}

View File

@ -1,14 +0,0 @@
import { TaggedError } from "./tagged-error.ts";
/**
* `UnexpectedError` represents any type of unexpected error.
*
* This error is used to represent errors that should not occur in
* the application logic, but that could always happen and
* we must be prepared to handle.
*/
export class UnexpectedError extends TaggedError<"UnexpectedError"> {
constructor(message?: string) {
super("UnexpectedError", message);
}
}

View File

@ -1,11 +0,0 @@
import Decimal from "decimal";
export * from "./array/index.ts";
export * from "./error/index.ts";
export * from "./record/index.ts";
export * from "./result/index.ts";
export * from "./run/index.ts";
export * from "./time/index.ts";
export * from "./types/index.ts";
export * from "./utils/index.ts";
export * from "./variant/index.ts";
export { Decimal };

View File

@ -0,0 +1,31 @@
{
"name": "@ulthar/fabric-core",
"type": "module",
"module": "dist/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./domain": "./dist/domain.js",
"./validation": "./dist/validation.js",
"./validation/fields": "./dist/validation/fields/index.js"
},
"files": [
"dist"
],
"private": true,
"packageManager": "yarn@4.1.1",
"devDependencies": {
"@types/validator": "^13.12.1",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"scripts": {
"test": "vitest",
"build": "tsc -p tsconfig.build.json"
},
"sideEffects": false,
"dependencies": {
"validator": "^13.12.0"
}
}

View File

@ -1,2 +0,0 @@
export * from "./is-record-empty.ts";
export * from "./is-record.ts";

View File

@ -1,19 +0,0 @@
import { describe, expect, test } from "@fabric/testing";
import { isRecordEmpty } from "./is-record-empty.ts";
describe("Record - Is Record Empty", () => {
test("Given an empty record, it should return true", () => {
const result = isRecordEmpty({});
expect(result).toBe(true);
});
test("Given a record with a single key, it should return false", () => {
const result = isRecordEmpty({ key: "value" });
expect(result).toBe(false);
});
test("Given a record with multiple keys, it should return false", () => {
const result = isRecordEmpty({ key1: "value1", key2: "value2" });
expect(result).toBe(false);
});
});

View File

@ -1,146 +0,0 @@
// deno-lint-ignore-file no-namespace no-explicit-any no-async-promise-executor
import { UnexpectedError } from "@fabric/core";
import type { TaggedError } from "../error/tagged-error.ts";
import type { MaybePromise } from "../types/maybe-promise.ts";
import { Result } from "./result.ts";
/**
* An AsyncResult represents the result of an asynchronous operation that can
* resolve to a value of type `TValue` or an error of type `TError`.
*/
export class AsyncResult<
TValue = any,
TError extends TaggedError = never,
> {
static tryFrom<T, TError extends TaggedError>(
fn: () => MaybePromise<T>,
errorMapper: (error: any) => TError,
): AsyncResult<T, TError> {
return new AsyncResult(
new Promise<Result<T, TError>>(async (resolve) => {
try {
const value = await fn();
resolve(Result.ok(value));
} catch (error) {
resolve(Result.failWith(errorMapper(error)));
}
}),
);
}
static from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
return AsyncResult.tryFrom(
fn,
(error) => new UnexpectedError(error) as never,
);
}
static ok(): AsyncResult<void, never>;
static ok<T>(value: T): AsyncResult<T, never>;
static ok(value?: any) {
return new AsyncResult(Promise.resolve(Result.ok(value)));
}
static succeedWith = AsyncResult.ok;
static failWith<TError extends TaggedError>(
error: TError,
): AsyncResult<never, TError> {
return new AsyncResult(Promise.resolve(Result.failWith(error)));
}
private constructor(private r: Promise<Result<TValue, TError>>) {
}
promise(): Promise<Result<TValue, TError>> {
return this.r;
}
async unwrapOrThrow(): Promise<TValue> {
return (await this.r).unwrapOrThrow();
}
async orThrow(): Promise<void> {
return (await this.r).orThrow();
}
async unwrapErrorOrThrow(): Promise<TError> {
return (await this.r).unwrapErrorOrThrow();
}
/**
* Map a function over the value of the result.
*/
map<TMappedValue>(
fn: (value: TValue) => TMappedValue,
): AsyncResult<TMappedValue, TError> {
return new AsyncResult(
this.r.then((result) => result.map(fn)),
);
}
/**
* Maps a function over the value of the result and flattens the result.
*/
flatMap<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => AsyncResult<TMappedValue, TMappedError>,
): AsyncResult<TMappedValue, TError | TMappedError> {
return new AsyncResult(
this.r.then((result) => {
if (result.isError()) {
return result as any;
}
return (fn(result.unwrapOrThrow())).promise();
}),
);
}
/**
* Try to map a function over the value of the result.
* If the function throws an error, the result will be a failure.
*/
tryMap<TMappedValue>(
fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError,
): AsyncResult<TMappedValue, TError> {
return new AsyncResult(
this.r.then((result) => result.tryMap(fn, errMapper)),
);
}
/**
* Map a function over the error of the result.
*/
errorMap<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError,
): AsyncResult<TValue, TMappedError> {
return new AsyncResult(
this.r.then((result) => result.errorMap(fn)),
);
}
/**
* Execute a function if the result is not an error.
* The function does not affect the result.
*/
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
return new AsyncResult(
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();
}),
);
}
}

View File

@ -1,2 +0,0 @@
export * from "./async-result.ts";
export * from "./result.ts";

View File

@ -1,57 +0,0 @@
import { describe, expect, expectTypeOf, fn, test } from "@fabric/testing";
import { UnexpectedError } from "../error/unexpected-error.ts";
import { Result } from "./result.ts";
describe("Result", () => {
describe("isOk", () => {
test("should return true if the result is ok", () => {
const result = Result.succeedWith(1) as Result<number, UnexpectedError>;
expect(result.isOk()).toBe(true);
if (result.isOk()) {
expect(result.value).toEqual(1);
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
}
});
});
describe("isError", () => {
test("should return true if the result is an error", () => {
const result = Result.failWith(new UnexpectedError()) as Result<
number,
UnexpectedError
>;
expect(result.isError()).toBe(true);
if (result.isError()) {
expect(result.value).toBeInstanceOf(UnexpectedError);
expectTypeOf(result).toEqualTypeOf<Result<never, UnexpectedError>>();
}
});
});
describe("Map", () => {
test("should return the result of the last function", () => {
const x = 0;
const result = Result.succeedWith(x + 1).map((x) => x * 2);
expect(result.unwrapOrThrow()).toEqual(2);
expectTypeOf(result).toEqualTypeOf<Result<number, never>>();
});
test("should not execute the function if the result is an error", () => {
const mock = fn() as () => number;
const result = Result.failWith(new UnexpectedError()).map(mock);
expect(result.isError()).toBe(true);
expect(mock).not.toHaveBeenCalled();
});
});
});

View File

@ -1,161 +0,0 @@
// deno-lint-ignore-file no-explicit-any
import { isError } from "../error/is-error.ts";
import type { TaggedError } from "../error/tagged-error.ts";
/**
* A Result represents the outcome of an operation
* that can be either a value of type `TValue` or an error `TError`.
*/
export class Result<TValue, TError extends TaggedError = never> {
static succeedWith<T>(value: T): Result<T, never> {
return new Result<T, never>(value);
}
static failWith<T extends TaggedError>(error: T): Result<never, T> {
return new Result<never, T>(error);
}
static ok(): Result<void, never>;
static ok<T>(value: T): Result<T, never>;
static ok(value?: any) {
return new Result(value ?? undefined);
}
static tryFrom<T, TError extends TaggedError>(
fn: () => T,
errorMapper: (error: any) => TError,
): Result<T, TError> {
try {
return Result.succeedWith(fn());
} catch (error) {
return Result.failWith(errorMapper(error));
}
}
private constructor(readonly value: TValue | TError) {}
/**
* Unwrap the value of the result.
* If the result is an error, it will throw the error.
*/
unwrapOrThrow(): TValue {
if (isError(this.value)) {
throw this.value;
}
return this.value as TValue;
}
/**
* Throw the error if the result is an error.
* otherwise, do nothing.
*/
orThrow(): void {
if (isError(this.value)) {
throw this.value;
}
}
unwrapErrorOrThrow(): TError {
if (!isError(this.value)) {
throw new Error("Result is not an error");
}
return this.value;
}
/**
* Check if the result is a success.
*/
isOk(): this is Result<TValue, never> {
return !isError(this.value);
}
/**
* Check if the result is an error.
*/
isError(): this is Result<never, TError> {
return isError(this.value);
}
/**
* Map a function over the value of the result.
*/
map<TMappedValue>(
fn: (value: TValue) => TMappedValue,
): Result<TMappedValue, TError> {
if (!isError(this.value)) {
return Result.succeedWith(fn(this.value as TValue));
}
return this as any;
}
/**
* Maps a function over the value of the result and flattens the result.
*/
flatMap<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => Result<TMappedValue, TMappedError>,
): Result<TMappedValue, TError | TMappedError> {
if (!isError(this.value)) {
return fn(this.value as TValue) as any;
}
return this as any;
}
/**
* Try to map a function over the value of the result.
* If the function throws an error, the result will be a failure.
*/
tryMap<TMappedValue>(
fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError,
): Result<TMappedValue, TError> {
if (!isError(this.value)) {
try {
return Result.succeedWith(fn(this.value as TValue));
} catch (error) {
return Result.failWith(errMapper(error));
}
}
return this as any;
}
/**
* Map a function over the error of the result.
*/
errorMap<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError,
): Result<TValue, TMappedError> {
if (isError(this.value)) {
return Result.failWith(fn(this.value as TError));
}
return this as unknown as Result<TValue, TMappedError>;
}
/**
* Taps a function if the result is a success.
* This is useful for side effects that do not modify the result.
*/
tap(fn: (value: TValue) => void): Result<TValue, TError> {
if (!isError(this.value)) {
try {
fn(this.value as TValue);
} catch {
// do nothing
}
}
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));
}
}

View File

@ -1 +0,0 @@
export * from "./run.ts";

View File

@ -1,28 +0,0 @@
import { AsyncResult } from "@fabric/core";
import { describe, expect, test } from "@fabric/testing";
import { UnexpectedError } from "../error/unexpected-error.ts";
import { Run } from "./run.ts";
describe("Run", () => {
describe("In Sequence", () => {
test("should pipe the results of multiple async functions", async () => {
const result = Run.seq(
() => AsyncResult.succeedWith(1),
(x) => AsyncResult.succeedWith(x + 1),
(x) => AsyncResult.succeedWith(x * 2),
);
expect(await result.unwrapOrThrow()).toEqual(4);
});
test("should return the first error if one of the functions fails", async () => {
const result = await Run.seq(
() => AsyncResult.succeedWith(1),
() => AsyncResult.failWith(new UnexpectedError()),
(x) => AsyncResult.succeedWith(x * 2),
).promise();
expect(result.isError()).toBe(true);
});
});
});

View File

@ -1,87 +0,0 @@
// deno-lint-ignore-file no-namespace no-explicit-any
import type { TaggedError } from "../error/tagged-error.ts";
import type { AsyncResult } from "../result/async-result.ts";
export namespace Run {
// prettier-ignore
export function seq<
T1,
TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): AsyncResult<T2, TE1 | TE2>;
// prettier-ignore
export function seq<
T1,
TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): AsyncResult<T3, TE1 | TE2 | TE3>;
// prettier-ignore
export function seq<
T1,
TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
T4,
TE4 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
fn4: (value: T3) => AsyncResult<T4, TE4>,
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
export function seq(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): AsyncResult<any, any> {
let result = fns[0]!();
for (let i = 1; i < fns.length; i++) {
result = result.flatMap((value) => fns[i]!(value));
}
return result;
}
// prettier-ignore
export function seqOrThrow<
T1,
TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
): Promise<T2>;
// prettier-ignore
export function seqOrThrow<
T1,
TE1 extends TaggedError,
T2,
TE2 extends TaggedError,
T3,
TE3 extends TaggedError,
>(
fn1: () => AsyncResult<T1, TE1>,
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): Promise<T2>;
export function seqOrThrow(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): Promise<any> {
const result = (seq as any)(...fns);
return result.unwrapOrThrow();
}
}

View File

@ -0,0 +1,11 @@
import { UUID } from "../types/uuid.js";
/**
* 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;
}

View File

@ -1,4 +1,4 @@
import type { MimeType } from "./mime-type.ts";
import { MimeType } from "./mime-type.js";
/**
* Represents a file. Its the base type for all files.

View File

@ -0,0 +1,9 @@
import { Entity } from "../entity.js";
import { BaseFile } from "./base-file.js";
/**
* Represents a file as managed by the domain.
*/
export interface DomainFile extends BaseFile, Entity {
url: string;
}

View File

@ -0,0 +1,9 @@
import { DomainFile } from "./domain-file.js";
import { ImageMimeType } from "./mime-type.js";
/**
* Represents an image file.
*/
export interface ImageFile extends DomainFile {
mimeType: ImageMimeType;
}

View File

@ -0,0 +1,10 @@
export * from "./base-file.js";
export * from "./bytes.js";
export * from "./domain-file.js";
export * from "./image-file.js";
export * from "./invalid-file-type-error.js";
export * from "./is-mime-type.js";
export * from "./is-uploaded-file.js";
export * from "./media-file.js";
export * from "./mime-type.js";
export * from "./uploaded-file.js";

View File

@ -1,4 +1,4 @@
import { TaggedError } from "@fabric/core";
import { TaggedError } from "../../../error/tagged-error.js";
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
constructor() {

View File

@ -1,8 +1,8 @@
import { describe, expect, expectTypeOf, test } from "@fabric/testing";
import { isMimeType } from "./is-mime-type.ts";
import { describe, expect, expectTypeOf, it } from "vitest";
import { isMimeType } from "./is-mime-type.js";
describe("isMimeType", () => {
test("should return true if the file type is the same as the mime type", () => {
it("should return true if the file type is the same as the mime type", () => {
const fileType = "image/png" as string;
const result = isMimeType("image/.*", fileType);
expect(result).toBe(true);
@ -11,7 +11,7 @@ describe("isMimeType", () => {
}
});
test("should return false if the file type is not the same as the mime type", () => {
it("should return false if the file type is not the same as the mime type", () => {
const fileType = "image/png" as string;
expect(isMimeType("image/jpeg", fileType)).toBe(false);

View File

@ -0,0 +1,11 @@
import { MimeType } from "./mime-type.js";
/**
* Checks if the actual file type is the same as the expected mime type.
*/
export function isMimeType<T extends MimeType>(
expectedMimeType: T,
actualFileType: string,
): actualFileType is T {
return actualFileType.match("^" + expectedMimeType + "$") !== null;
}

View File

@ -0,0 +1,26 @@
import validator from "validator";
import { isRecord } from "../../../record/is-record.js";
import { InMemoryFile } from "./uploaded-file.js";
const { isBase64, isMimeType } = validator;
export function isInMemoryFile(value: unknown): value is InMemoryFile {
try {
return (
isRecord(value) &&
"data" in value &&
typeof value.data === "string" &&
isBase64(value.data.split(",")[1]) &&
"mimeType" in value &&
typeof value.mimeType === "string" &&
isMimeType(value.mimeType) &&
"name" in value &&
typeof value.name === "string" &&
"sizeInBytes" in value &&
typeof value.sizeInBytes === "number" &&
value.sizeInBytes >= 1
);
} catch {
return false;
}
}

View File

@ -0,0 +1,8 @@
import { DomainFile } from "./domain-file.js";
/**
* Represents a media file, either an image, a video or an audio file.
*/
export interface MediaFile extends DomainFile {
mimeType: `image/${string}` | `video/${string}` | `audio/${string}`;
}

View File

@ -0,0 +1,9 @@
import { Base64String } from "../../types/base-64.js";
import { BaseFile } from "./base-file.js";
/**
* Represents a file with its contents in memory.
*/
export interface InMemoryFile extends BaseFile {
data: Base64String;
}

View File

@ -0,0 +1,2 @@
export * from "./entity.js";
export * from "./files/index.js";

View File

@ -0,0 +1,7 @@
import { TaggedVariant } from "../../variant/variant.js";
export interface Event<TTag extends string, TPayload>
extends TaggedVariant<TTag> {
payload: TPayload;
timestamp: number;
}

View File

@ -0,0 +1,4 @@
export * from "./entity/index.js";
export * from "./security/index.js";
export * from "./types/index.js";
export * from "./use-case/index.js";

View File

@ -0,0 +1 @@
export * from "./policy-map.js";

View File

@ -0,0 +1,4 @@
export type PolicyMap<
UserType extends string,
PolicyType extends string,
> = Record<UserType, PolicyType[]>;

View File

@ -0,0 +1 @@
export type Base64String = string;

View File

@ -0,0 +1,3 @@
export * from "./email.js";
export * from "./sem-ver.js";
export * from "./uuid.js";

View File

@ -0,0 +1,2 @@
export * from "./use-case-definition.js";
export * from "./use-case.js";

View File

@ -0,0 +1,51 @@
import { TaggedError } from "../../error/tagged-error.js";
import { UseCase } from "./use-case.js";
export type UseCaseDefinition<
TDependencies,
TPayload,
TOutput,
TErrors extends TaggedError<string>,
> = TPayload extends undefined
? {
/**
* The use case name.
*/
name: string;
/**
* Whether the use case requires authentication or not.
*/
isAuthRequired?: boolean;
/**
* The required permissions to execute the use case.
*/
requiredPermissions?: string[];
/**
* The use case function.
*/
useCase: UseCase<TDependencies, TPayload, TOutput, TErrors>;
}
: {
/**
* The use case name.
*/
name: string;
/**
* Whether the use case requires authentication or not.
*/
isAuthRequired?: boolean;
/**
* The required permissions to execute the use case.
*/
requiredPermissions?: string[];
/**
* The use case function.
*/
useCase: UseCase<TDependencies, TPayload, TOutput, TErrors>;
};

View File

@ -0,0 +1,22 @@
import { TaggedError } from "../../error/tagged-error.js";
import { AsyncResult } from "../../result/async-result.js";
/**
* A use case is a piece of domain logic that can be executed.
*
* It can be one of two types:
*
* - `Query`: A use case that only reads data.
* - `Command`: A use case that modifies data.
*/
export type UseCase<
TDependencies,
TPayload,
TOutput,
TErrors extends TaggedError<string>,
> = TPayload extends undefined
? (dependencies: TDependencies) => AsyncResult<TOutput, TErrors>
: (
dependencies: TDependencies,
payload: TPayload,
) => AsyncResult<TOutput, TErrors>;

View File

@ -0,0 +1,2 @@
export * from "./tagged-error.js";
export * from "./unexpected-error.js";

View File

@ -0,0 +1,23 @@
import { describe, expect, expectTypeOf, it } from "vitest";
import { Result } from "../result/result.js";
import { isError } from "./is-error.js";
import { TaggedError } from "./tagged-error.js";
describe("is-error", () => {
it("should determine if a value is an error", () => {
type DemoResult = Result<number, TaggedError<"DemoError">>;
//Ok should not be an error
const ok: DemoResult = 42;
expect(isError(ok)).toBe(false);
//Error should be an error
const error: DemoResult = new TaggedError("DemoError");
expect(isError(error)).toBe(true);
//After a check, typescript should be able to infer the type
if (isError(error)) {
expectTypeOf(error).toEqualTypeOf<TaggedError<"DemoError">>();
}
});
});

View File

@ -0,0 +1,15 @@
import { VariantTag } from "../variant/variant.js";
import { TaggedError } from "./tagged-error.js";
/**
* Indicates if a value is an error.
*
* In case it is an error, the type of the error is able to be inferred.
*/
export function isError(err: unknown): err is TaggedError<string> {
return (
err instanceof Error &&
VariantTag in err &&
typeof err[VariantTag] === "string"
);
}

View File

@ -0,0 +1,18 @@
import { TaggedVariant, VariantTag } from "../variant/index.js";
/**
* Un TaggedError es un error que tiene un tag que lo identifica, lo cual
* permite a los consumidores de la instancia de error identificar el tipo de
* error que ocurrió.
*/
export class TaggedError<Tag extends string>
extends Error
implements TaggedVariant<Tag>
{
readonly [VariantTag]: Tag;
constructor(tag: Tag) {
super();
this[VariantTag] = tag;
}
}

View File

@ -0,0 +1,14 @@
import { TaggedError } from "./tagged-error.js";
/**
* `UnexpectedError` representa cualquier tipo de error inesperado.
*
* Este error se utiliza para representar errores que no deberían ocurrir en
* la lógica de la aplicación, pero que siempre podrían suceder y
* debemos estar preparados para manejarlos.
*/
export class UnexpectedError extends TaggedError<"UnexpectedError"> {
constructor() {
super("UnexpectedError");
}
}

View File

@ -0,0 +1,6 @@
export * from "./domain/index.js";
export * from "./error/index.js";
export * from "./record/index.js";
export * from "./result/index.js";
export * from "./types/index.js";
export * from "./variant/index.js";

View File

@ -0,0 +1,2 @@
export * from "./is-record-empty.js";
export * from "./is-record.js";

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { isRecordEmpty } from "./is-record-empty.js";
describe("Record - Is Record Empty", () => {
it("should return true for an empty record", () => {
const result = isRecordEmpty({});
expect(result).toBe(true);
});
it("should return false for a non-empty record", () => {
const result = isRecordEmpty({ key: "value" });
expect(result).toBe(false);
});
it("should return false for a record with multiple keys", () => {
const result = isRecordEmpty({ key1: "value1", key2: "value2" });
expect(result).toBe(false);
});
});

View File

@ -1,23 +1,23 @@
import { describe, expect, test } from "@fabric/testing";
import { isRecord } from "./is-record.ts";
import { describe, expect, it } from "vitest";
import { isRecord } from "./is-record.js";
describe("isRecord", () => {
test("Given an empty object, it should return true", () => {
it("should return true for an object", () => {
const obj = { name: "John", age: 30 };
expect(isRecord(obj)).toBe(true);
});
test("Given an array, it should return false", () => {
it("should return false for an array", () => {
const arr = [1, 2, 3];
expect(isRecord(arr)).toBe(false);
});
test("Given a number, it should return false", () => {
it("should return false for null", () => {
const value = null;
expect(isRecord(value)).toBe(false);
});
test("Given a string, it should return false", () => {
it("should return false for a string", () => {
const value = "Hello";
expect(isRecord(value)).toBe(false);
});

View File

@ -0,0 +1,11 @@
import { TaggedError } from "../error/tagged-error.js";
import { Result } from "./result.js";
/**
* Un AsyncResult representa el resultado de una operación asíncrona que puede
* resolver en un valor de tipo `TValue` o en un error de tipo `TError`.
*/
export type AsyncResult<
TValue,
TError extends TaggedError<string> = never,
> = Promise<Result<TValue, TError>>;

View File

@ -0,0 +1,2 @@
export * from "./async-result.js";
export * from "./result.js";

View File

@ -0,0 +1,9 @@
import { TaggedError } from "../error/tagged-error.js";
/**
* Un Result representa el resultado de una operación
* que puede ser un valor de tipo `TValue` o un error `TError`.
*/
export type Result<TValue, TError extends TaggedError<string>> =
| TValue
| TError;

View File

@ -0,0 +1,3 @@
export * from "./posix-date.js";
export * from "./time-constants.js";
export * from "./timeout.js";

View File

@ -0,0 +1,9 @@
import { TaggedVariant } from "../variant/variant.js";
export class PosixDate {
constructor(public readonly timestamp: number) {}
}
export interface TimeZone extends TaggedVariant<"TimeZone"> {
timestamp: number;
}

View File

@ -0,0 +1,44 @@
import { describe, expect, test } from "vitest";
import { timeout } from "./timeout.js";
const HEAVY_TESTS =
process.env.HEAVY_TESTS === "true" || process.env.RUN_ALL_TESTS === "true";
describe("timeout", () => {
test.runIf(HEAVY_TESTS)(
"timeout never triggers *before* the input time",
async () => {
const count = 10000;
const maxTimeInMs = 1000;
const result = await Promise.all(
new Array(count).fill(0).map(async (e, i) => {
const start = Date.now();
const ms = i % maxTimeInMs;
await timeout(ms);
const end = Date.now();
return end - start;
}),
);
expect(
result
.map((t, i) => {
return [t, i % maxTimeInMs]; //Actual time and expected time
})
.filter((e) => {
return e[0] < e[1]; //Actual time is less than the expected time
}),
).toEqual([]);
},
);
test("using ms we can define a timeout in milliseconds", async () => {
const start = Date.now();
await timeout(100);
const end = Date.now();
const time = end - start;
expect(time).toBeGreaterThanOrEqual(100);
});
});

View File

@ -0,0 +1,14 @@
export function timeout(ms: number) {
return new Promise<void>((resolve) => {
const start = Date.now();
setTimeout(() => {
const end = Date.now();
const remaining = ms - (end - start);
if (remaining > 0) {
timeout(remaining).then(resolve);
} else {
resolve();
}
}, ms);
});
}

View File

@ -1,7 +1,7 @@
// deno-lint-ignore-file no-explicit-any
/**
* A function that takes an argument of type `T` and returns a value of type `R`.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Fn<T = any, R = any> = (arg: T) => R;
/**

View File

@ -0,0 +1,2 @@
export * from "./fn.js";
export * from "./optional.js";

Some files were not shown because too many files have changed in this diff Show More