Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cc3324b46 | |||
| ce1f0a04f3 | |||
| 60c7bacfb5 | |||
| ae61c03bb9 | |||
| e92de85fe8 | |||
| 55f6d788db | |||
| b77ba6dc83 | |||
| 623e67afeb | |||
| a053ca225b | |||
| f30535055f | |||
| 6329044415 | |||
| 36b5286a09 | |||
| de49970c0c | |||
| f189f8994f | |||
| dd95d58e3a | |||
| ab41ff028d | |||
| 65432d1c54 | |||
| 523578e310 | |||
| 9f9419c2b6 | |||
| 25870c8558 | |||
| 3b0533b0a9 | |||
| 87ce76e13f | |||
| 8d1528de23 | |||
| 96d22f09d1 | |||
| de8c249faf | |||
| 2cd252511a | |||
| 53a7b31bdc | |||
| c38f74414b | |||
| 3c3ce276c0 | |||
| 2ed9291c4d | |||
| 4574b9871b | |||
| d8e4028193 | |||
| 67921efac7 | |||
| 307a82d83c | |||
| 558ee2b9bc | |||
| d56c4bd469 | |||
| 4950730d9e | |||
| dfee950913 | |||
| aadefd30f8 | |||
| 0b05168544 | |||
| 6d2218f9f5 | |||
| 72780b9803 | |||
| 26cc090284 | |||
| 7f4a0bd06b | |||
| b2e8b33dae | |||
| 4cc99aec91 | |||
| a79683d9d4 | |||
| f6de496e73 | |||
| 4503ff4576 | |||
| bfb471b166 | |||
| 8c6f043f86 | |||
| c9a061419c | |||
| 6a0be50ef7 | |||
| 758f8d933a | |||
| 76af85a496 | |||
| 0ac04a839f | |||
| 7a56c34941 | |||
| 1886c52ece | |||
| 38e23ba095 | |||
| 4ea00f515b | |||
| a6a303f256 | |||
| 9a63ba22f1 | |||
| 3afdb5d230 | |||
| b71ecb5de1 | |||
| 4171107227 | |||
| 6b46677be9 | |||
| 559a3f3c22 | |||
| d62b588033 | |||
| d443e9e395 | |||
| 010e3eecfc | |||
| 69caa775d1 | |||
| 475ec309cb | |||
| 6919a819b6 | |||
| 4fff9f91f5 | |||
| f30d2c47c5 | |||
| 14ca23ef74 | |||
| f07928b893 | |||
| f0c77398e6 | |||
| 27dbd44741 | |||
| 09f045daf6 | |||
| 290544dc9a | |||
| 9092b032b3 | |||
| 3f91e35790 | |||
| 80c34e4649 | |||
| 59810a2118 | |||
| 029bf431dd | |||
| 527fa87f4f | |||
| c5cf78510a | |||
| 3713812956 | |||
| b5045ed4c8 | |||
| 61a92033f3 | |||
| 8aaf4c73e8 | |||
| dca326d0c5 | |||
| c4483f073e | |||
| 0ffe2838c1 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1,4 +1 @@
|
|||||||
/.yarn/** linguist-vendored
|
|
||||||
/.yarn/releases/* binary
|
|
||||||
/.yarn/plugins/**/* binary
|
|
||||||
* text=auto eol=lf
|
* text=auto eol=lf
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,11 +1,3 @@
|
|||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.env
|
.env
|
||||||
dist
|
dist
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
4
.hooks/pre-commit
Normal file
4
.hooks/pre-commit
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/hook.sh"
|
||||||
|
|
||||||
|
deno task check
|
||||||
@ -1,2 +0,0 @@
|
|||||||
# .husky/pre-commit
|
|
||||||
yarn lint-staged
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"*": ["yarn prettier -u --write"],
|
|
||||||
"*.ts": ["yarn eslint --fix"]
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
dist
|
|
||||||
coverage
|
|
||||||
.yarn/**/*
|
|
||||||
yarn.lock
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2
|
|
||||||
}
|
|
||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@ -2,13 +2,11 @@
|
|||||||
"recommendations": [
|
"recommendations": [
|
||||||
"bierner.github-markdown-preview",
|
"bierner.github-markdown-preview",
|
||||||
"bradlc.vscode-tailwindcss",
|
"bradlc.vscode-tailwindcss",
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
"streetsidesoftware.code-spell-checker-spanish",
|
"streetsidesoftware.code-spell-checker-spanish",
|
||||||
"usernamehw.errorlens",
|
"usernamehw.errorlens",
|
||||||
"bourhaouta.tailwindshades",
|
"bourhaouta.tailwindshades",
|
||||||
"austenc.tailwind-docs",
|
"austenc.tailwind-docs",
|
||||||
"vitest.explorer"
|
"denoland.vscode-deno"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
22
.vscode/settings.json
vendored
22
.vscode/settings.json
vendored
@ -1,21 +1,23 @@
|
|||||||
{
|
{
|
||||||
"cSpell.enabled": true,
|
"cSpell.enabled": true,
|
||||||
"cSpell.language": "en,es",
|
"cSpell.language": "en,es",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"files.autoSave": "off",
|
"files.autoSave": "off",
|
||||||
"files.eol": "\n",
|
"files.eol": "\n",
|
||||||
"javascript.preferences.importModuleSpecifierEnding": "js",
|
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "always",
|
"source.fixAll": "always",
|
||||||
"source.organizeImports": "always"
|
"source.organizeImports": "always"
|
||||||
},
|
},
|
||||||
"search.exclude": {
|
"cSpell.words": ["Syntropy"],
|
||||||
"**/.yarn": true,
|
"deno.enable": true,
|
||||||
"**/node_modules": true,
|
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||||
"packages/frontend/{android,ios}": true
|
"deno.future": true,
|
||||||
},
|
"deno.codeLens.references": true,
|
||||||
"typescript.preferences.importModuleSpecifierEnding": "js",
|
"deno.testing.args": [
|
||||||
"cSpell.words": ["autodocs", "Syntropy"],
|
"--allow-all"
|
||||||
"typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"]
|
],
|
||||||
|
"notebook.defaultFormatter": "denoland.vscode-deno",
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
925
.yarn/releases/yarn-4.4.1.cjs
vendored
925
.yarn/releases/yarn-4.4.1.cjs
vendored
File diff suppressed because one or more lines are too long
@ -1,2 +0,0 @@
|
|||||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
|
||||||
nodeLinker: node-modules
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
# 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
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
import { ValueSchema } from "./schema.js";
|
|
||||||
|
|
||||||
export interface Environment {
|
|
||||||
name: string;
|
|
||||||
variables: EnvironmentVariable[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EnvironmentVariable extends ValueSchema {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
isSecret: boolean;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { Entity } from "@ulthar/fabric-core";
|
|
||||||
|
|
||||||
export interface Permission extends Entity {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
export interface Project {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
tags: string[];
|
|
||||||
environmentTypes: string[];
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { DefaultUserType } from "./defaults.js";
|
|
||||||
import { UseCase } from "./use-case.js";
|
|
||||||
|
|
||||||
export interface UserStory {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
userTypes: DefaultUserType | string;
|
|
||||||
useCases: UseCase[];
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
import { Entity } from "@ulthar/fabric-core";
|
|
||||||
|
|
||||||
export interface UserType extends Entity {
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
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
|
|
||||||
>;
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"coverage",
|
|
||||||
"vitest.config.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"exclude": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
coverage: {
|
|
||||||
exclude: ["**/index.ts"],
|
|
||||||
},
|
|
||||||
passWithNoTests: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
30
deno.json
Normal file
30
deno.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"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
Normal file
157
deno.lock
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"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@*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +0,0 @@
|
|||||||
// @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
33
package.json
@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +1 @@
|
|||||||
# @ulthar/fabric-core
|
# @fabric/core
|
||||||
|
|||||||
10
packages/fabric/core/array/array-element.test.ts
Normal file
10
packages/fabric/core/array/array-element.test.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
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">();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/fabric/core/array/array-element.ts
Normal file
17
packages/fabric/core/array/array-element.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
1
packages/fabric/core/array/index.ts
Normal file
1
packages/fabric/core/array/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./array-element.ts";
|
||||||
10
packages/fabric/core/deno.json
Normal file
10
packages/fabric/core/deno.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "@fabric/core",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"exports": {
|
||||||
|
".": "./index.ts"
|
||||||
|
},
|
||||||
|
"imports": {
|
||||||
|
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/fabric/core/error/index.ts
Normal file
3
packages/fabric/core/error/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./is-error.ts";
|
||||||
|
export * from "./tagged-error.ts";
|
||||||
|
export * from "./unexpected-error.ts";
|
||||||
16
packages/fabric/core/error/is-error.test.ts
Normal file
16
packages/fabric/core/error/is-error.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
8
packages/fabric/core/error/is-error.ts
Normal file
8
packages/fabric/core/error/is-error.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
15
packages/fabric/core/error/tagged-error.ts
Normal file
15
packages/fabric/core/error/tagged-error.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/fabric/core/error/unexpected-error.ts
Normal file
14
packages/fabric/core/error/unexpected-error.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/fabric/core/index.ts
Normal file
11
packages/fabric/core/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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 };
|
||||||
@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
2
packages/fabric/core/record/index.ts
Normal file
2
packages/fabric/core/record/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./is-record-empty.ts";
|
||||||
|
export * from "./is-record.ts";
|
||||||
19
packages/fabric/core/record/is-record-empty.test.ts
Normal file
19
packages/fabric/core/record/is-record-empty.test.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,23 +1,23 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, test } from "@fabric/testing";
|
||||||
import { isRecord } from "./is-record.js";
|
import { isRecord } from "./is-record.ts";
|
||||||
|
|
||||||
describe("isRecord", () => {
|
describe("isRecord", () => {
|
||||||
it("should return true for an object", () => {
|
test("Given an empty object, it should return true", () => {
|
||||||
const obj = { name: "John", age: 30 };
|
const obj = { name: "John", age: 30 };
|
||||||
expect(isRecord(obj)).toBe(true);
|
expect(isRecord(obj)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for an array", () => {
|
test("Given an array, it should return false", () => {
|
||||||
const arr = [1, 2, 3];
|
const arr = [1, 2, 3];
|
||||||
expect(isRecord(arr)).toBe(false);
|
expect(isRecord(arr)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for null", () => {
|
test("Given a number, it should return false", () => {
|
||||||
const value = null;
|
const value = null;
|
||||||
expect(isRecord(value)).toBe(false);
|
expect(isRecord(value)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return false for a string", () => {
|
test("Given a string, it should return false", () => {
|
||||||
const value = "Hello";
|
const value = "Hello";
|
||||||
expect(isRecord(value)).toBe(false);
|
expect(isRecord(value)).toBe(false);
|
||||||
});
|
});
|
||||||
146
packages/fabric/core/result/async-result.ts
Normal file
146
packages/fabric/core/result/async-result.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// 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();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/fabric/core/result/index.ts
Normal file
2
packages/fabric/core/result/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./async-result.ts";
|
||||||
|
export * from "./result.ts";
|
||||||
57
packages/fabric/core/result/result.test.ts
Normal file
57
packages/fabric/core/result/result.test.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
161
packages/fabric/core/result/result.ts
Normal file
161
packages/fabric/core/result/result.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/fabric/core/run/index.ts
Normal file
1
packages/fabric/core/run/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./run.ts";
|
||||||
28
packages/fabric/core/run/run.test.ts
Normal file
28
packages/fabric/core/run/run.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
87
packages/fabric/core/run/run.ts
Normal file
87
packages/fabric/core/run/run.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { DomainFile } from "./domain-file.js";
|
|
||||||
import { ImageMimeType } from "./mime-type.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an image file.
|
|
||||||
*/
|
|
||||||
export interface ImageFile extends DomainFile {
|
|
||||||
mimeType: ImageMimeType;
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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";
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
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}`;
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./entity.js";
|
|
||||||
export * from "./files/index.js";
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { TaggedVariant } from "../../variant/variant.js";
|
|
||||||
|
|
||||||
export interface Event<TTag extends string, TPayload>
|
|
||||||
extends TaggedVariant<TTag> {
|
|
||||||
payload: TPayload;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./entity/index.js";
|
|
||||||
export * from "./security/index.js";
|
|
||||||
export * from "./types/index.js";
|
|
||||||
export * from "./use-case/index.js";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./policy-map.js";
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export type PolicyMap<
|
|
||||||
UserType extends string,
|
|
||||||
PolicyType extends string,
|
|
||||||
> = Record<UserType, PolicyType[]>;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export type Base64String = string;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./email.js";
|
|
||||||
export * from "./sem-ver.js";
|
|
||||||
export * from "./uuid.js";
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./use-case-definition.js";
|
|
||||||
export * from "./use-case.js";
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
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>;
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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>;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./tagged-error.js";
|
|
||||||
export * from "./unexpected-error.js";
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
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">>();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
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";
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./is-record-empty.js";
|
|
||||||
export * from "./is-record.js";
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
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>>;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./async-result.js";
|
|
||||||
export * from "./result.js";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./posix-date.js";
|
|
||||||
export * from "./time-constants.js";
|
|
||||||
export * from "./timeout.js";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { TaggedVariant } from "../variant/variant.js";
|
|
||||||
|
|
||||||
export class PosixDate {
|
|
||||||
constructor(public readonly timestamp: number) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimeZone extends TaggedVariant<"TimeZone"> {
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./fn.js";
|
|
||||||
export * from "./optional.js";
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./match.js";
|
|
||||||
export * from "./variant.js";
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
import { match } from "./match.js";
|
|
||||||
import { TaggedVariant, VariantTag } from "./variant.js";
|
|
||||||
|
|
||||||
interface V1 extends TaggedVariant<"V1"> {
|
|
||||||
a: number;
|
|
||||||
}
|
|
||||||
interface V2 extends TaggedVariant<"V2"> {
|
|
||||||
b: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Variant = V1 | V2;
|
|
||||||
|
|
||||||
describe("Pattern matching", () => {
|
|
||||||
it("Should match a pattern", () => {
|
|
||||||
const v = { [VariantTag]: "V1", a: 42 } as Variant;
|
|
||||||
|
|
||||||
const result = match(v).case({
|
|
||||||
V1: (v) => v.a,
|
|
||||||
V2: (v) => v.b,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBe(42);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Should alert that a pattern is not exhaustive", () => {
|
|
||||||
const v = { [VariantTag]: "V1", a: 42 } as Variant;
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
// @ts-expect-error Testing non-exhaustive pattern matching
|
|
||||||
match(v).case({
|
|
||||||
V2: (v) => v.b,
|
|
||||||
}),
|
|
||||||
).toThrowError("Non-exhaustive pattern match");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
export const VariantTag = "_tag";
|
|
||||||
export type VariantTag = typeof VariantTag;
|
|
||||||
|
|
||||||
export interface TaggedVariant<TTag extends string> {
|
|
||||||
readonly [VariantTag]: TTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type VariantFromTag<
|
|
||||||
TVariant extends TaggedVariant<string>,
|
|
||||||
TTag extends TVariant[typeof VariantTag],
|
|
||||||
> = Extract<TVariant, { [VariantTag]: TTag }>;
|
|
||||||
23
packages/fabric/core/time/clock-time.ts
Normal file
23
packages/fabric/core/time/clock-time.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/fabric/core/time/index.ts
Normal file
3
packages/fabric/core/time/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./clock-time.ts";
|
||||||
|
export * from "./posix-date.ts";
|
||||||
|
export * from "./time-constants.ts";
|
||||||
39
packages/fabric/core/time/posix-date.ts
Normal file
39
packages/fabric/core/time/posix-date.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { isRecord } from "../record/is-record.ts";
|
||||||
|
import type { TaggedVariant } from "../variant/variant.ts";
|
||||||
|
|
||||||
|
export class PosixDate {
|
||||||
|
constructor(public readonly timestamp: number = Date.now()) {}
|
||||||
|
|
||||||
|
public toJSON(): PosixDateJSON {
|
||||||
|
return {
|
||||||
|
type: "posix-date",
|
||||||
|
timestamp: this.timestamp,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromJson(json: PosixDateJSON): PosixDate {
|
||||||
|
return new PosixDate(json.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static isPosixDateJSON(value: unknown): value is PosixDateJSON {
|
||||||
|
if (
|
||||||
|
isRecord(value) &&
|
||||||
|
"type" in value &&
|
||||||
|
"timestamp" in value &&
|
||||||
|
value["type"] === "posix-date" &&
|
||||||
|
typeof value["timestamp"] === "number"
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeZone extends TaggedVariant<"TimeZone"> {
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PosixDateJSON {
|
||||||
|
type: "posix-date";
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"dist",
|
|
||||||
"node_modules",
|
|
||||||
"coverage",
|
|
||||||
"vitest.config.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../../tsconfig.json",
|
|
||||||
"exclude": ["dist", "node_modules"]
|
|
||||||
}
|
|
||||||
1
packages/fabric/core/types/enum.ts
Normal file
1
packages/fabric/core/types/enum.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type EnumToType<T extends Record<string, string>> = T[keyof T];
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user