Add fabric-core
This commit is contained in:
parent
37df98d09c
commit
b164c7d97f
1
packages/fabric/core/README.md
Normal file
1
packages/fabric/core/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# @ulthar/fabric-core
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
10
packages/fabric/core/src/domain/entity/files/base-file.ts
Normal file
10
packages/fabric/core/src/domain/entity/files/base-file.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { MimeType } from "./mime-type.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a file. Its the base type for all files.
|
||||||
|
*/
|
||||||
|
export interface BaseFile {
|
||||||
|
name: string;
|
||||||
|
sizeInBytes: number;
|
||||||
|
mimeType: MimeType;
|
||||||
|
}
|
||||||
11
packages/fabric/core/src/domain/entity/files/bytes.ts
Normal file
11
packages/fabric/core/src/domain/entity/files/bytes.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export function KiBs(value: number): number {
|
||||||
|
return value * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MiBs(value: number): number {
|
||||||
|
return KiBs(value * 1024);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GiBs(value: number): number {
|
||||||
|
return MiBs(value * 1024);
|
||||||
|
}
|
||||||
@ -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";
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
import { TaggedError } from "../../../error/tagged-error.js";
|
||||||
|
|
||||||
|
export class InvalidFileTypeError extends TaggedError<"InvalidFileTypeError"> {
|
||||||
|
constructor() {
|
||||||
|
super("InvalidFileTypeError");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, expectTypeOf, it } from "vitest";
|
||||||
|
import { isMimeType } from "./is-mime-type.js";
|
||||||
|
|
||||||
|
describe("isMimeType", () => {
|
||||||
|
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);
|
||||||
|
if (result) {
|
||||||
|
expectTypeOf(fileType).toEqualTypeOf<"image/.*">();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const anotherFileType = "file:image/jpeg" as string;
|
||||||
|
expect(isMimeType("image/jpeg", anotherFileType)).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}`;
|
||||||
|
}
|
||||||
13
packages/fabric/core/src/domain/entity/files/mime-type.ts
Normal file
13
packages/fabric/core/src/domain/entity/files/mime-type.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export type MimeType = `${string}/${string}`;
|
||||||
|
|
||||||
|
export const ImageMimeType = `image/.*` as `image/${string}`;
|
||||||
|
export type ImageMimeType = typeof ImageMimeType;
|
||||||
|
|
||||||
|
export const VideoMimeType = `video/.*` as `video/${string}`;
|
||||||
|
export type VideoMimeType = typeof VideoMimeType;
|
||||||
|
|
||||||
|
export const AudioMimeType = `audio/.*` as `audio/${string}`;
|
||||||
|
export type AudioMimeType = typeof AudioMimeType;
|
||||||
|
|
||||||
|
export const PdfMimeType = `application/pdf` as `application/pdf`;
|
||||||
|
export type PdfMimeType = typeof PdfMimeType;
|
||||||
@ -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;
|
||||||
1
packages/fabric/core/src/domain/types/email.ts
Normal file
1
packages/fabric/core/src/domain/types/email.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type Email = `${string}@${string}.${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";
|
||||||
4
packages/fabric/core/src/domain/types/sem-ver.ts
Normal file
4
packages/fabric/core/src/domain/types/sem-ver.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Semantic versioning type. [Major].[Minor].[Patch]. Example: 1.0.0
|
||||||
|
*/
|
||||||
|
export type SemVer = `${number}.${number}.${number}`;
|
||||||
1
packages/fabric/core/src/domain/types/uuid.ts
Normal file
1
packages/fabric/core/src/domain/types/uuid.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type UUID = `${string}-${string}-${string}-${string}-${string}`;
|
||||||
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
6
packages/fabric/core/src/record/is-record-empty.ts
Normal file
6
packages/fabric/core/src/record/is-record-empty.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Check if a record is empty (A record is empty when it doesn't contain any keys).
|
||||||
|
*/
|
||||||
|
export function isRecordEmpty(value: Record<string, unknown>): boolean {
|
||||||
|
return Object.keys(value).length === 0;
|
||||||
|
}
|
||||||
24
packages/fabric/core/src/record/is-record.spec.ts
Normal file
24
packages/fabric/core/src/record/is-record.spec.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isRecord } from "./is-record.js";
|
||||||
|
|
||||||
|
describe("isRecord", () => {
|
||||||
|
it("should return true for an object", () => {
|
||||||
|
const obj = { name: "John", age: 30 };
|
||||||
|
expect(isRecord(obj)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for an array", () => {
|
||||||
|
const arr = [1, 2, 3];
|
||||||
|
expect(isRecord(arr)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for null", () => {
|
||||||
|
const value = null;
|
||||||
|
expect(isRecord(value)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for a string", () => {
|
||||||
|
const value = "Hello";
|
||||||
|
expect(isRecord(value)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
6
packages/fabric/core/src/record/is-record.ts
Normal file
6
packages/fabric/core/src/record/is-record.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Checks if a value is a record (an object).
|
||||||
|
*/
|
||||||
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
22
packages/fabric/core/src/time/time-constants.ts
Normal file
22
packages/fabric/core/src/time/time-constants.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Returns the number of milliseconds.
|
||||||
|
*
|
||||||
|
* This is a no-op function that is used to
|
||||||
|
* make the code more explicit and readable.
|
||||||
|
*/
|
||||||
|
export const ms = (n: number) => n;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number of seconds to milliseconds.
|
||||||
|
*/
|
||||||
|
export const seconds = (n: number) => n * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number of minutes to milliseconds.
|
||||||
|
*/
|
||||||
|
export const minutes = (n: number) => n * 60 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a number of hours to milliseconds.
|
||||||
|
*/
|
||||||
|
export const hours = (n: number) => n * 60 * 60 * 1000;
|
||||||
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
16
packages/fabric/core/src/types/fn.ts
Normal file
16
packages/fabric/core/src/types/fn.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action is a function that takes an argument of type `T` and returns nothing.
|
||||||
|
*/
|
||||||
|
export type Action<T> = Fn<T, void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A `Singleton`
|
||||||
|
* is function that takes no arguments and returns a value of type `T`.
|
||||||
|
*/
|
||||||
|
export type Singleton<T> = Fn<void, T>;
|
||||||
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";
|
||||||
13
packages/fabric/core/src/types/optional.ts
Normal file
13
packages/fabric/core/src/types/optional.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* `Nothing` is a type that represents the absence of a value.
|
||||||
|
* In JavaScript, `undefined` represents that a value was not defined
|
||||||
|
* and `null` represents the absence of a value, but `Nothing` is a type that
|
||||||
|
* can be used to represent the absence of a value in a more explicit way.
|
||||||
|
*/
|
||||||
|
export type Nothing = null;
|
||||||
|
export const Nothing = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un Optional es un tipo que puede ser un valor o no ser nada.
|
||||||
|
*/
|
||||||
|
export type Optional<T> = T | Nothing;
|
||||||
2
packages/fabric/core/src/variant/index.ts
Normal file
2
packages/fabric/core/src/variant/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./match.js";
|
||||||
|
export * from "./variant.js";
|
||||||
36
packages/fabric/core/src/variant/match.spec.ts
Normal file
36
packages/fabric/core/src/variant/match.spec.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
24
packages/fabric/core/src/variant/match.ts
Normal file
24
packages/fabric/core/src/variant/match.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Fn } from "../types/fn.js";
|
||||||
|
import { TaggedVariant, VariantFromTag, VariantTag } from "./variant.js";
|
||||||
|
|
||||||
|
export type VariantMatcher<TVariant extends TaggedVariant<string>> = {
|
||||||
|
[K in TVariant[VariantTag]]: Fn<VariantFromTag<TVariant, K>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function match<const TVariant extends TaggedVariant<string>>(
|
||||||
|
v: TVariant,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
case<const TMatcher extends VariantMatcher<TVariant>>(
|
||||||
|
cases: TMatcher,
|
||||||
|
): ReturnType<TMatcher[TVariant[VariantTag]]> {
|
||||||
|
if (!(v[VariantTag] in cases)) {
|
||||||
|
throw new Error("Non-exhaustive pattern match");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cases[v[VariantTag] as TVariant[VariantTag]](
|
||||||
|
v as Extract<TVariant, { [VariantTag]: TVariant[VariantTag] }>,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
11
packages/fabric/core/src/variant/variant.ts
Normal file
11
packages/fabric/core/src/variant/variant.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
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 }>;
|
||||||
15
packages/fabric/core/tsconfig.build.json
Normal file
15
packages/fabric/core/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
packages/fabric/core/tsconfig.json
Normal file
4
packages/fabric/core/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.json",
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
10
packages/fabric/core/vitest.config.ts
Normal file
10
packages/fabric/core/vitest.config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
coverage: {
|
||||||
|
exclude: ["**/index.ts"],
|
||||||
|
},
|
||||||
|
passWithNoTests: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -12,6 +12,9 @@
|
|||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vitest": "^2.0.5"
|
"vitest": "^2.0.5"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ulthar/fabric-core": "workspace:^"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"build": "tsc -p tsconfig.build.json"
|
"build": "tsc -p tsconfig.build.json"
|
||||||
|
|||||||
26
yarn.lock
26
yarn.lock
@ -703,6 +703,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/validator@npm:^13.12.1":
|
||||||
|
version: 13.12.1
|
||||||
|
resolution: "@types/validator@npm:13.12.1"
|
||||||
|
checksum: 10c0/473b12e287f569e08741c24d4d91663e740ec6264032eeb311c21c8f00dfa274c6fe5af9190ffe1b4b527e95a1bb31c81598682d5dbd76e1604f898bc19adc2b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@npm:8.4.0":
|
"@typescript-eslint/eslint-plugin@npm:8.4.0":
|
||||||
version: 8.4.0
|
version: 8.4.0
|
||||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.4.0"
|
resolution: "@typescript-eslint/eslint-plugin@npm:8.4.0"
|
||||||
@ -819,10 +826,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ulthar/fabric-core@workspace:^, @ulthar/fabric-core@workspace:packages/fabric/core":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@ulthar/fabric-core@workspace:packages/fabric/core"
|
||||||
|
dependencies:
|
||||||
|
"@types/validator": "npm:^13.12.1"
|
||||||
|
typescript: "npm:^5.5.4"
|
||||||
|
validator: "npm:^13.12.0"
|
||||||
|
vitest: "npm:^2.0.5"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@ulthar/lib-template@workspace:packages/templates/lib":
|
"@ulthar/lib-template@workspace:packages/templates/lib":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@ulthar/lib-template@workspace:packages/templates/lib"
|
resolution: "@ulthar/lib-template@workspace:packages/templates/lib"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@ulthar/fabric-core": "workspace:^"
|
||||||
typescript: "npm:^5.5.4"
|
typescript: "npm:^5.5.4"
|
||||||
vitest: "npm:^2.0.5"
|
vitest: "npm:^2.0.5"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
@ -3124,6 +3143,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"validator@npm:^13.12.0":
|
||||||
|
version: 13.12.0
|
||||||
|
resolution: "validator@npm:13.12.0"
|
||||||
|
checksum: 10c0/21d48a7947c9e8498790550f56cd7971e0e3d724c73388226b109c1bac2728f4f88caddfc2f7ed4b076f9b0d004316263ac786a17e9c4edf075741200718cd32
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"vite-node@npm:2.0.5":
|
"vite-node@npm:2.0.5":
|
||||||
version: 2.0.5
|
version: 2.0.5
|
||||||
resolution: "vite-node@npm:2.0.5"
|
resolution: "vite-node@npm:2.0.5"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user