Compare commits

..

95 Commits

Author SHA1 Message Date
4cc3324b46 [fabric/sqlite-store] Add QueryBuilder tests and implement assertNone method 2024-10-30 22:42:13 -03:00
ce1f0a04f3 [fabric/sqlite-store] Refactor event types to use DomainEvent 2024-10-30 22:41:47 -03:00
60c7bacfb5 [fabric/domain] Add assertNone method and AlreadyExistsError class to StoreQuery interfaces 2024-10-30 22:41:14 -03:00
ae61c03bb9 [fabric/domain] Format Query type definition for improved readability 2024-10-30 22:40:57 -03:00
e92de85fe8 [fabric/domain] Rename event types to DomainEvent 2024-10-30 22:40:31 -03:00
55f6d788db Remove unused Deno settings from VSCode configuration 2024-10-30 22:39:00 -03:00
b77ba6dc83 [fabric/core] Overload AsyncResult.ok method to support optional value parameter 2024-10-30 22:38:28 -03:00
623e67afeb [fabric/sqlite-store] Refactor to include changes to JSONUtils and Models 2024-10-24 00:01:44 -03:00
a053ca225b [fabric/domain] Improve optional field support and move parsing inside model definition 2024-10-23 23:56:56 -03:00
f30535055f [fabric/validations] Update string sanitization to return undefined for non-string values 2024-10-23 23:54:46 -03:00
6329044415 [fabric/domain] Simplify model field definitions 2024-10-23 23:52:12 -03:00
36b5286a09 [fabric/core] Add JSON utility functions for parsing and stringifying with core types 2024-10-23 23:50:16 -03:00
de49970c0c [fabric/core] Improve documentation for Optional type 2024-10-22 20:12:05 -03:00
f189f8994f [fabric/core] Add variantConstructor utility for tagged variants 2024-10-22 20:07:00 -03:00
dd95d58e3a [fabric/core] Prepare deno.json for publish 2024-10-21 13:04:05 -03:00
ab41ff028d [fabric/core] Add variantConstructor utility for tagged variants 2024-10-21 13:02:26 -03:00
65432d1c54 [fabric/core] Rename mapError to errorMap; add Result.assert utility function 2024-10-21 13:01:58 -03:00
523578e310 [fabric/sqlite-store] Refactor SQL key handling functions for consistency and clarity 2024-10-20 23:13:41 -03:00
9f9419c2b6 [fabric/domain] Simplify model definition 2024-10-20 23:13:06 -03:00
25870c8558 [fabric/domain] move validation files to models directory and update imports 2024-10-20 11:53:59 -03:00
3b0533b0a9 [fabric/domain] Add base model validations 2024-10-20 11:36:03 -03:00
87ce76e13f Fix imports and remove some unused files 2024-10-20 11:35:24 -03:00
8d1528de23 Add validation utilities as a separate package 2024-10-20 11:34:14 -03:00
96d22f09d1 Update VSCode settings for improved Deno integration 2024-10-20 11:33:33 -03:00
de8c249faf Rename deno.jsonc to deno.json and improve imports definitions to better define dependencies 2024-10-20 11:32:58 -03:00
2cd252511a [fabric/core] Move some types from domain to core 2024-10-20 11:31:22 -03:00
53a7b31bdc [fabric/core] Add ClockTime class 2024-10-20 11:29:51 -03:00
c38f74414b [fabric/sqlite-store] Refactor collection references to use new model definitions 2024-10-17 14:41:14 -03:00
3c3ce276c0 [fabric/domain] Refactor aggregate model implementation and related types 2024-10-17 14:36:49 -03:00
2ed9291c4d [fabric/core] Add utility functions for value checks and string sanitization 2024-10-17 14:32:47 -03:00
4574b9871b [templates/domain] Refactor UseCases to use Query type instead of UseCaseDefinition 2024-10-16 16:22:41 -03:00
d8e4028193 Update VSCode settings for Deno integration and formatting 2024-10-16 16:22:16 -03:00
67921efac7 [fabric/sqlite-store] Refactor event store and query builder to use the new AsyncResult 2024-10-16 16:22:02 -03:00
307a82d83c [fabric/domain] Rename StoreQuery 2024-10-16 16:21:03 -03:00
558ee2b9bc [fabric/domain] Refactor use-cases to commands and queries 2024-10-16 16:18:19 -03:00
d56c4bd469 [fabric/core] convert AsyncResult to a class for improved usability and update Run functions to use AsyncResult 2024-10-16 16:15:57 -03:00
4950730d9e Merge pull request 'chore: migrate monorepo to deno 2' (#3) from chore-migrate-to-deno2 into main
Reviewed-on: #3
2024-10-16 13:13:55 -03:00
dfee950913 Remove src directory in template modules 2024-10-16 12:41:19 -03:00
aadefd30f8 [fabric/testing] Remove src directory 2024-10-16 12:33:17 -03:00
0b05168544 Add deno task check to perform full project checks 2024-10-16 11:59:11 -03:00
6d2218f9f5 [fabric/sqlite-store] Remove src directory 2024-10-16 11:53:28 -03:00
72780b9803 [fabric/domain] Remove src directory 2024-10-16 11:53:03 -03:00
26cc090284 [fabric/core] Remove src directory for improved usability 2024-10-16 02:07:20 -03:00
7f4a0bd06b [templates/domain] Update import statements to use TypeScript file extensions 2024-10-16 02:03:51 -03:00
b2e8b33dae Remove Yarn-related entries from .gitattributes and .gitignore; update VSCode extensions for Deno support 2024-10-16 02:01:19 -03:00
4cc99aec91 Format files and configure workspace formatting in vscode 2024-10-16 01:53:38 -03:00
a79683d9d4 Migrate template modules to deno 2 2024-10-16 01:53:15 -03:00
f6de496e73 Migrate packages to deno 2 2024-10-16 01:33:51 -03:00
4503ff4576 WIP Add deno 2024-10-15 16:16:28 -03:00
bfb471b166 Merge pull request 'Feature: Basic Events, Models and Projections' (#2) from feat-base-projections into main
Reviewed-on: #2
2024-10-15 15:20:23 -03:00
8c6f043f86 [fabric/domain] Add Projection interface to handle model projection from events 2024-10-15 15:16:10 -03:00
c9a061419c [fabric/sqlite-store] Add null handling for value conversion and update tests for deletedAt field 2024-10-15 15:13:59 -03:00
6a0be50ef7 [fabric/domain] Simplify StoreQueryError constructor 2024-10-15 15:13:21 -03:00
758f8d933a [fabric/domain] Add optional deletedAt field to DefaultModelFields 2024-10-15 15:12:01 -03:00
76af85a496 [fabric/core] Add ensureValue utility function and export utils 2024-10-15 15:10:37 -03:00
0ac04a839f [fabric/core] Add export for Record type in core types 2024-10-15 15:10:08 -03:00
7a56c34941 [fabric/core] Refactor TaggedError and UnexpectedError to accept custom error messages 2024-10-15 14:58:22 -03:00
1886c52ece [fabric/core] Enhance VariantMatcher to support return type parameterization in match function 2024-10-15 07:59:52 -03:00
38e23ba095 Refactor event interface to use VariantTag for event types 2024-10-15 07:45:11 -03:00
4ea00f515b [fabric/sqlite-store] Implement SQLiteEventStore 2024-10-14 09:56:45 -03:00
a6a303f256 [fabric/domain] Refactor EventStore interface and remove EventStream 2024-10-14 09:55:39 -03:00
9a63ba22f1 [fabric/domain] Add 'utils' export 2024-10-14 09:50:43 -03:00
3afdb5d230 [fabric/domain] Remove validator dependency and simplify isInMemoryFile function 2024-10-14 09:50:01 -03:00
b71ecb5de1 [fabric/core] Add EmptyRecord type to define an empty object structure 2024-10-14 09:49:12 -03:00
4171107227 [fabric/core] Add Run.seq overload to handle 4 params 2024-10-14 09:48:43 -03:00
6b46677be9 [fabric/core] Refactor AsyncResult to accept MaybePromise in tryFrom and from functions 2024-10-14 09:47:18 -03:00
559a3f3c22 Update packages and vitest configurations 2024-10-12 21:33:05 -03:00
d62b588033 Rename store-sqlite -> sqlite-store to match package name 2024-10-12 17:08:54 -03:00
d443e9e395 Remove storage-driver interface; Add state-store as driver-specific implementation 2024-10-12 17:01:25 -03:00
010e3eecfc Standardize exports and add exports field for improved module resolution 2024-10-12 16:59:09 -03:00
69caa775d1 Rename store-sqlite package to sqlite-store and update imports accordingly 2024-10-12 15:47:48 -03:00
475ec309cb [fabric/store-sqlite] Refactor SQLite wrapper: replace old functions with SQLiteDatabase class for improved management and caching of statements 2024-10-12 15:41:12 -03:00
6919a819b6 [fabric/domain] Refactor event store and event stream interfaces 2024-10-12 15:39:51 -03:00
4fff9f91f5 [fabric/domain] Improve Result & AsyncResult types; Refactor model types; Add timestamp and embedded field support 2024-10-10 15:19:15 -03:00
f30d2c47c5 [fabric/domain] Add decimal type export 2024-10-09 09:29:52 -03:00
14ca23ef74 [fabric/domain] Setup service and mock exports 2024-10-08 15:40:07 -03:00
f07928b893 [fabric/domain] Add StateStore filtering 2024-10-08 15:33:59 -03:00
f0c77398e6 [fabric/domain] Add typed insertion to state-store 2024-10-05 11:10:04 -03:00
27dbd44741 [fabric/store-sqlite] Pass all tests for base sqlite-driver implementation 2024-10-04 11:57:06 -03:00
09f045daf6 [fabric/domain] Add reference-field utility to get target key 2024-10-04 11:55:44 -03:00
290544dc9a [fabric/core] rename isVariant to Variant.is 2024-10-04 11:54:35 -03:00
9092b032b3 [fabric/domain] improve structure 2024-10-04 11:10:29 -03:00
3f91e35790 [fabric/core] Add isVariant utility function 2024-10-04 11:07:57 -03:00
80c34e4649 @fabric/domain: extract domain utils from fabric/core and create a new package 2024-09-25 08:20:40 -03:00
59810a2118 fabric-core: Improve some types names 2024-09-21 21:10:57 -03:00
029bf431dd fabric-core: Improve model definitions 2024-09-21 02:27:08 -03:00
527fa87f4f store-sqlite: export sqlite-driver from the main index file 2024-09-21 02:20:32 -03:00
c5cf78510a Update git hooks to protect main branch and force good practices 2024-09-19 00:03:13 -03:00
3713812956 Update yarn version 2024-09-18 22:45:31 -03:00
b5045ed4c8 Add basic sqlite-driver 2024-09-17 17:23:57 -03:00
61a92033f3 Update dependencies 2024-09-17 17:22:17 -03:00
8aaf4c73e8 Add Domain Template 2024-09-15 21:59:54 -03:00
dca326d0c5 (WIP) Add state-store utils 2024-09-15 21:16:06 -03:00
c4483f073e [fabric-core] Add basic array-element utility types 2024-09-04 20:08:34 -03:00
0ffe2838c1 [fabric-core] Improve some JSDocs, improve some names, and re-organice some files 2024-09-04 20:02:39 -03:00
207 changed files with 4107 additions and 5019 deletions

3
.gitattributes vendored
View File

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

8
.gitignore vendored
View File

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

4
.hooks/pre-commit Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,13 +2,11 @@
"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",
"vitest.explorer"
"denoland.vscode-deno"
]
}

22
.vscode/settings.json vendored
View File

@ -1,21 +1,23 @@
{
"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"
},
"search.exclude": {
"**/.yarn": true,
"**/node_modules": true,
"packages/frontend/{android,ios}": true
},
"typescript.preferences.importModuleSpecifierEnding": "js",
"cSpell.words": ["autodocs", "Syntropy"],
"typescript.preferences.autoImportFileExcludePatterns": ["**/chai/**"]
"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"
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -1,15 +0,0 @@
# Syntropy
Syntropy is a platform for organizing development teams and projects based on the `Fabric` framework.
## Features
- [ ] Manage users
- [ ] Manage teams
- [ ] Manage projects
- [ ] Manage user stories
- [ ] Manage tasks and iterations
- [ ] Create and initialize projects and repositories
- [ ] Manage repositories
- [ ] Manage CI/CD pipelines
- [ ] Manage deployments

30
deno.json Normal file
View 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
View 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@*"
]
}
}
}
}

View File

@ -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,
);

View File

@ -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"
}
}

View File

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

View 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">();
});
});

View 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;

View File

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

View File

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

View File

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

View 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>();
}
});
});

View 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;
}

View 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;
}
}

View 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);
}
}

View 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 };

View File

@ -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"
}
}

View File

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

View 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);
});
});

View File

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

View 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();
}),
);
}
}

View File

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

View 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();
});
});
});

View 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));
}
}

View File

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

View 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);
});
});
});

View 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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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}`;
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -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";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>;
};

View File

@ -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>;

View File

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

View File

@ -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">>();
}
});
});

View File

@ -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"
);
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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";

View File

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

View File

@ -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);
});
});

View File

@ -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>>;

View File

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

View File

@ -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;

View File

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

View File

@ -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;
}

View File

@ -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);
});
});

View File

@ -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);
});
}

View File

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

View File

@ -1,2 +0,0 @@
export * from "./match.js";
export * from "./variant.js";

View File

@ -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");
});
});

View File

@ -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 }>;

View 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);
}
}

View File

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

View 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;
}

View File

@ -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"
]
}

View File

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

View File

@ -0,0 +1 @@
export type EnumToType<T extends Record<string, string>> = T[keyof T];

View File

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

View File

@ -0,0 +1,9 @@
export * from "./email.ts";
export * from "./enum.ts";
export * from "./fn.ts";
export * from "./keyof.ts";
export * from "./maybe-promise.ts";
export * from "./optional.ts";
export * from "./record.ts";
export * from "./semver.ts";
export * from "./uuid.ts";

View File

@ -0,0 +1,4 @@
/**
* Only string keys are allowed in the keyof type
*/
export type Keyof<T> = Extract<keyof T, string>;

View File

@ -0,0 +1 @@
export type MaybePromise<T> = T | Promise<T>;

View File

@ -8,6 +8,6 @@ export type Nothing = null;
export const Nothing = null;
/**
* Un Optional es un tipo que puede ser un valor o no ser nada.
* An `Optional` type is a type that represents a value that may or may not be present.
*/
export type Optional<T> = T | Nothing;

View File

@ -0,0 +1 @@
export type EmptyRecord = Record<string, never>;

View File

@ -0,0 +1,8 @@
import { UnexpectedError } from "../error/unexpected-error.ts";
export function ensure<T>(value?: T): T {
if (!value) {
throw new UnexpectedError("Value is nullish.");
}
return value;
}

View File

@ -0,0 +1,2 @@
export * from "./ensure.ts";
export * from "./json-utils.ts";

View File

@ -0,0 +1,43 @@
import { isRecord, PosixDate } from "@fabric/core";
import Decimal from "decimal";
export namespace JSONUtils {
export function reviver(key: string, value: unknown) {
if (isRecord(value)) {
if (value._type === "bigint" && typeof value.value == "string") {
return BigInt(value.value);
}
if (value._type === "decimal" && typeof value.value == "string") {
return Decimal.from(value.value);
}
if (PosixDate.isPosixDateJSON(value)) {
return PosixDate.fromJson(value);
}
}
return value;
}
export function parse<T>(json: string): T {
return JSON.parse(json, reviver);
}
export function stringify<T>(value: T): string {
return JSON.stringify(value, (key, value) => {
if (typeof value === "bigint") {
return {
_type: "bigint",
value: value.toString(),
};
}
if (value instanceof Decimal) {
return {
_type: "decimal",
value: value.toString(),
};
}
return value;
});
}
}

View File

@ -0,0 +1,14 @@
import type { TaggedVariant, VariantTag } from "./variant.ts";
export function variantConstructor<
const T extends TaggedVariant<string>,
>(
tag: T[VariantTag],
) {
return <TOpts extends Omit<T, VariantTag>>(options: TOpts = {} as TOpts) => {
return {
_tag: tag,
...options,
} as const;
};
}

View File

@ -0,0 +1,3 @@
export * from "./constructor.ts";
export * from "./match.ts";
export * from "./variant.ts";

View File

@ -0,0 +1,35 @@
import { expect } from "jsr:@std/expect";
import { match } from "./match.ts";
import { type TaggedVariant, VariantTag } from "./variant.ts";
interface V1 extends TaggedVariant<"V1"> {
a: number;
}
interface V2 extends TaggedVariant<"V2"> {
b: string;
}
type Variant = V1 | V2;
const v = { [VariantTag]: "V1", a: 42 } as Variant;
Deno.test("match().case() calls the correct function", () => {
const result = match(v).case({
V1: (v) => v.a,
V2: (v) => v.b,
});
expect(result).toBe(42);
});
Deno.test(
"match().case() throws an error for non-exhaustive pattern matching",
() => {
expect(() =>
// @ts-expect-error Testing non-exhaustive pattern matching
match(v).case({
V2: (v) => v.b,
})
).toThrow("Non-exhaustive pattern match");
},
);

View File

@ -1,17 +1,25 @@
import { Fn } from "../types/fn.js";
import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js";
import type { Fn } from "../types/fn.ts";
import {
type TaggedVariant,
type VariantFromTag,
VariantTag,
} from "./variant.ts";
export type VariantMatcher<TVariant extends TaggedVariant<string>> = {
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>>;
export type VariantMatcher<TVariant extends TaggedVariant<string>, T> = {
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>, T>;
};
export function match<const TVariant extends TaggedVariant<string>>(
v: TVariant,
) {
return {
case<const TMatcher extends VariantMatcher<TVariant>>(
cases: TMatcher,
): ReturnType<TMatcher[TVariant[VariantTag]]> {
case<
const TReturnType,
const TMatcher extends VariantMatcher<
TVariant,
TReturnType
> = VariantMatcher<TVariant, TReturnType>,
>(cases: TMatcher): TReturnType {
if (!(v[VariantTag] in cases)) {
throw new Error("Non-exhaustive pattern match");
}

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