Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7766235e54 |
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1 +1,4 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
* text=auto eol=lf
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,3 +1,11 @@
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
node_modules
|
||||
.env
|
||||
dist
|
||||
coverage
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/hook.sh"
|
||||
|
||||
deno task check
|
||||
2
.husky/pre-commit
Normal file
2
.husky/pre-commit
Normal file
@ -0,0 +1,2 @@
|
||||
# .husky/pre-commit
|
||||
yarn lint-staged
|
||||
4
.lintstagedrc.json
Normal file
4
.lintstagedrc.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"*": ["yarn prettier -u --write"],
|
||||
"*.ts": ["yarn eslint --fix"]
|
||||
}
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
dist
|
||||
coverage
|
||||
.yarn/**/*
|
||||
yarn.lock
|
||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@ -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
22
.vscode/settings.json
vendored
@ -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
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
2
.yarnrc.yml
Normal file
@ -0,0 +1,2 @@
|
||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||
nodeLinker: node-modules
|
||||
3
apps/loom/README.md
Normal file
3
apps/loom/README.md
Normal 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
|
||||
22
apps/loom/domain/package.json
Normal file
22
apps/loom/domain/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
9
apps/loom/domain/src/entities/app.ts
Normal file
9
apps/loom/domain/src/entities/app.ts
Normal 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[];
|
||||
}
|
||||
13
apps/loom/domain/src/entities/apps/web-app.ts
Normal file
13
apps/loom/domain/src/entities/apps/web-app.ts
Normal 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;
|
||||
}
|
||||
13
apps/loom/domain/src/entities/defaults.ts
Normal file
13
apps/loom/domain/src/entities/defaults.ts
Normal 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;
|
||||
12
apps/loom/domain/src/entities/environment.ts
Normal file
12
apps/loom/domain/src/entities/environment.ts
Normal 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;
|
||||
}
|
||||
6
apps/loom/domain/src/entities/permission.ts
Normal file
6
apps/loom/domain/src/entities/permission.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Entity } from "@ulthar/fabric-core";
|
||||
|
||||
export interface Permission extends Entity {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
6
apps/loom/domain/src/entities/project.ts
Normal file
6
apps/loom/domain/src/entities/project.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Project {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
environmentTypes: string[];
|
||||
}
|
||||
36
apps/loom/domain/src/entities/schema.ts
Normal file
36
apps/loom/domain/src/entities/schema.ts
Normal 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;
|
||||
17
apps/loom/domain/src/entities/use-case.ts
Normal file
17
apps/loom/domain/src/entities/use-case.ts
Normal 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;
|
||||
}
|
||||
9
apps/loom/domain/src/entities/user-story.ts
Normal file
9
apps/loom/domain/src/entities/user-story.ts
Normal 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[];
|
||||
}
|
||||
5
apps/loom/domain/src/entities/user-type.ts
Normal file
5
apps/loom/domain/src/entities/user-type.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Entity } from "@ulthar/fabric-core";
|
||||
|
||||
export interface UserType extends Entity {
|
||||
name: string;
|
||||
}
|
||||
@ -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
|
||||
>;
|
||||
15
apps/loom/domain/tsconfig.build.json
Normal file
15
apps/loom/domain/tsconfig.build.json
Normal 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"
|
||||
]
|
||||
}
|
||||
4
apps/loom/domain/tsconfig.json
Normal file
4
apps/loom/domain/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
10
apps/loom/domain/vitest.config.ts
Normal file
10
apps/loom/domain/vitest.config.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
coverage: {
|
||||
exclude: ["**/index.ts"],
|
||||
},
|
||||
passWithNoTests: true,
|
||||
},
|
||||
});
|
||||
30
deno.json
30
deno.json
@ -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
157
deno.lock
@ -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
10
eslint.config.js
Normal 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
33
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
# @fabric/core
|
||||
# @ulthar/fabric-core
|
||||
|
||||
@ -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">();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -1 +0,0 @@
|
||||
export * from "./array-element.ts";
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "@fabric/core",
|
||||
"version": "0.1.0",
|
||||
"exports": {
|
||||
".": "./index.ts"
|
||||
},
|
||||
"imports": {
|
||||
"decimal": "jsr:@quentinadam/decimal@^0.1.6"
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
export * from "./is-error.ts";
|
||||
export * from "./tagged-error.ts";
|
||||
export * from "./unexpected-error.ts";
|
||||
@ -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>();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 };
|
||||
31
packages/fabric/core/package.json
Normal file
31
packages/fabric/core/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./is-record-empty.ts";
|
||||
export * from "./is-record.ts";
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./async-result.ts";
|
||||
export * from "./result.ts";
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./run.ts";
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
11
packages/fabric/core/src/domain/entity/entity.ts
Normal file
11
packages/fabric/core/src/domain/entity/entity.ts
Normal 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;
|
||||
}
|
||||
@ -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.
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
10
packages/fabric/core/src/domain/entity/files/index.ts
Normal file
10
packages/fabric/core/src/domain/entity/files/index.ts
Normal 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";
|
||||
@ -1,4 +1,4 @@
|
||||
import { TaggedError } from "@fabric/core";
|
||||
import { TaggedError } from "../../../error/tagged-error.js";
|
||||
|
||||
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
|
||||
constructor() {
|
||||
@ -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);
|
||||
|
||||
11
packages/fabric/core/src/domain/entity/files/is-mime-type.ts
Normal file
11
packages/fabric/core/src/domain/entity/files/is-mime-type.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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}`;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
2
packages/fabric/core/src/domain/entity/index.ts
Normal file
2
packages/fabric/core/src/domain/entity/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./entity.js";
|
||||
export * from "./files/index.js";
|
||||
7
packages/fabric/core/src/domain/events/event.ts
Normal file
7
packages/fabric/core/src/domain/events/event.ts
Normal 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;
|
||||
}
|
||||
4
packages/fabric/core/src/domain/index.ts
Normal file
4
packages/fabric/core/src/domain/index.ts
Normal 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";
|
||||
1
packages/fabric/core/src/domain/security/index.ts
Normal file
1
packages/fabric/core/src/domain/security/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./policy-map.js";
|
||||
4
packages/fabric/core/src/domain/security/policy-map.ts
Normal file
4
packages/fabric/core/src/domain/security/policy-map.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type PolicyMap<
|
||||
UserType extends string,
|
||||
PolicyType extends string,
|
||||
> = Record<UserType, PolicyType[]>;
|
||||
1
packages/fabric/core/src/domain/types/base-64.ts
Normal file
1
packages/fabric/core/src/domain/types/base-64.ts
Normal file
@ -0,0 +1 @@
|
||||
export type Base64String = string;
|
||||
3
packages/fabric/core/src/domain/types/index.ts
Normal file
3
packages/fabric/core/src/domain/types/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./email.js";
|
||||
export * from "./sem-ver.js";
|
||||
export * from "./uuid.js";
|
||||
2
packages/fabric/core/src/domain/use-case/index.ts
Normal file
2
packages/fabric/core/src/domain/use-case/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./use-case-definition.js";
|
||||
export * from "./use-case.js";
|
||||
@ -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>;
|
||||
};
|
||||
22
packages/fabric/core/src/domain/use-case/use-case.ts
Normal file
22
packages/fabric/core/src/domain/use-case/use-case.ts
Normal 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>;
|
||||
2
packages/fabric/core/src/error/index.ts
Normal file
2
packages/fabric/core/src/error/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./tagged-error.js";
|
||||
export * from "./unexpected-error.js";
|
||||
23
packages/fabric/core/src/error/is-error.spec.ts
Normal file
23
packages/fabric/core/src/error/is-error.spec.ts
Normal 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">>();
|
||||
}
|
||||
});
|
||||
});
|
||||
15
packages/fabric/core/src/error/is-error.ts
Normal file
15
packages/fabric/core/src/error/is-error.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
18
packages/fabric/core/src/error/tagged-error.ts
Normal file
18
packages/fabric/core/src/error/tagged-error.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
14
packages/fabric/core/src/error/unexpected-error.ts
Normal file
14
packages/fabric/core/src/error/unexpected-error.ts
Normal 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");
|
||||
}
|
||||
}
|
||||
6
packages/fabric/core/src/index.ts
Normal file
6
packages/fabric/core/src/index.ts
Normal 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";
|
||||
2
packages/fabric/core/src/record/index.ts
Normal file
2
packages/fabric/core/src/record/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./is-record-empty.js";
|
||||
export * from "./is-record.js";
|
||||
19
packages/fabric/core/src/record/is-record-empty.spec.ts
Normal file
19
packages/fabric/core/src/record/is-record-empty.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
11
packages/fabric/core/src/result/async-result.ts
Normal file
11
packages/fabric/core/src/result/async-result.ts
Normal 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>>;
|
||||
2
packages/fabric/core/src/result/index.ts
Normal file
2
packages/fabric/core/src/result/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./async-result.js";
|
||||
export * from "./result.js";
|
||||
9
packages/fabric/core/src/result/result.ts
Normal file
9
packages/fabric/core/src/result/result.ts
Normal 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;
|
||||
3
packages/fabric/core/src/time/index.ts
Normal file
3
packages/fabric/core/src/time/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./posix-date.js";
|
||||
export * from "./time-constants.js";
|
||||
export * from "./timeout.js";
|
||||
9
packages/fabric/core/src/time/posix-date.ts
Normal file
9
packages/fabric/core/src/time/posix-date.ts
Normal 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;
|
||||
}
|
||||
44
packages/fabric/core/src/time/timeout.spec.ts
Normal file
44
packages/fabric/core/src/time/timeout.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
14
packages/fabric/core/src/time/timeout.ts
Normal file
14
packages/fabric/core/src/time/timeout.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
@ -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;
|
||||
|
||||
/**
|
||||
2
packages/fabric/core/src/types/index.ts
Normal file
2
packages/fabric/core/src/types/index.ts
Normal 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
Loading…
Reference in New Issue
Block a user