@azure-net/node-kit 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +301 -2
- package/VALIDATION.md +866 -0
- package/dist/delivery/http/ResponseBuilder.d.ts +44 -0
- package/dist/delivery/http/ResponseBuilder.js +135 -0
- package/dist/delivery/http/index.d.ts +1 -0
- package/dist/delivery/http/index.js +1 -0
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/domain/Ensure.d.ts +2 -0
- package/dist/domain/Ensure.js +7 -0
- package/dist/domain/EnsureError.d.ts +4 -0
- package/dist/domain/EnsureError.js +7 -0
- package/dist/domain/index.d.ts +2 -0
- package/dist/domain/index.js +2 -0
- package/dist/infrastructure/background/BackgroundTask.d.ts +12 -0
- package/dist/infrastructure/background/BackgroundTask.js +41 -0
- package/dist/infrastructure/background/index.d.ts +1 -0
- package/dist/infrastructure/background/index.js +1 -0
- package/dist/infrastructure/environment/Env.d.ts +19 -0
- package/dist/infrastructure/environment/Env.js +33 -0
- package/dist/infrastructure/environment/index.d.ts +1 -0
- package/dist/infrastructure/environment/index.js +1 -0
- package/dist/infrastructure/index.d.ts +2 -0
- package/dist/infrastructure/index.js +2 -0
- package/dist/tools/DateHelper.d.ts +13 -0
- package/dist/tools/DateHelper.js +77 -0
- package/dist/tools/DurationHelper.d.ts +6 -0
- package/dist/tools/DurationHelper.js +31 -0
- package/dist/tools/EnumHelper.d.ts +26 -0
- package/dist/tools/EnumHelper.js +40 -0
- package/dist/tools/ErrorHelper.d.ts +3 -0
- package/dist/tools/ErrorHelper.js +31 -0
- package/dist/tools/NumberHelper.d.ts +19 -0
- package/dist/tools/NumberHelper.js +49 -0
- package/dist/tools/ObjectHelper.d.ts +28 -0
- package/dist/tools/ObjectHelper.js +90 -0
- package/dist/tools/PromiseHelper.d.ts +3 -0
- package/dist/tools/PromiseHelper.js +7 -0
- package/dist/tools/StringHelper.d.ts +75 -0
- package/dist/tools/StringHelper.js +127 -0
- package/dist/tools/UidHelper.d.ts +56 -0
- package/dist/tools/UidHelper.js +132 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +9 -0
- package/dist/validation/Engine.d.ts +6 -0
- package/dist/validation/Engine.js +406 -0
- package/dist/validation/Result.d.ts +31 -0
- package/dist/validation/Result.js +264 -0
- package/dist/validation/Symbols.d.ts +1 -0
- package/dist/validation/Symbols.js +1 -0
- package/dist/validation/Types.d.ts +105 -0
- package/dist/validation/Types.js +1 -0
- package/dist/validation/Validator.d.ts +26 -0
- package/dist/validation/Validator.js +95 -0
- package/dist/validation/index.d.ts +5 -0
- package/dist/validation/index.js +5 -0
- package/dist/validation/messages/En.d.ts +3 -0
- package/dist/validation/messages/En.js +118 -0
- package/dist/validation/messages/Ru.d.ts +3 -0
- package/dist/validation/messages/Ru.js +118 -0
- package/dist/validation/messages/index.d.ts +7 -0
- package/dist/validation/messages/index.js +7 -0
- package/dist/validation/rules/CustomRules.d.ts +8 -0
- package/dist/validation/rules/CustomRules.js +64 -0
- package/dist/validation/rules/Factory.d.ts +4 -0
- package/dist/validation/rules/Factory.js +21 -0
- package/dist/validation/rules/StringRules.d.ts +105 -0
- package/dist/validation/rules/StringRules.js +481 -0
- package/dist/validation/rules/StructureRules.d.ts +30 -0
- package/dist/validation/rules/StructureRules.js +145 -0
- package/dist/validation/rules/ValueRules.d.ts +82 -0
- package/dist/validation/rules/ValueRules.js +365 -0
- package/dist/validation/rules/index.d.ts +151 -0
- package/dist/validation/rules/index.js +19 -0
- package/package.json +77 -3
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
declare const responseStatuses: readonly [200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307, 308, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511];
|
|
2
|
+
/** Standard final HTTP statuses that permit a response body. */
|
|
3
|
+
export type ResponseStatus = (typeof responseStatuses)[number];
|
|
4
|
+
export interface ResponseOptions<TMeta = null, TErrors = null, TStatus extends ResponseStatus = ResponseStatus> {
|
|
5
|
+
status?: TStatus;
|
|
6
|
+
message?: string;
|
|
7
|
+
meta?: TMeta;
|
|
8
|
+
errors?: TErrors;
|
|
9
|
+
code?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export type ResponsePresetOptions<TMeta = null, TErrors = null> = Omit<ResponseOptions<TMeta, TErrors>, "status">;
|
|
12
|
+
export interface ResponseEnvelope<TData, TMeta = null, TErrors = null, TStatus extends ResponseStatus = ResponseStatus> {
|
|
13
|
+
readonly success: boolean;
|
|
14
|
+
readonly status: TStatus;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly data: TData | null;
|
|
17
|
+
readonly meta: TMeta | null;
|
|
18
|
+
readonly errors: TErrors | null;
|
|
19
|
+
readonly code: string | null;
|
|
20
|
+
}
|
|
21
|
+
export declare class ResponseBuilder<TData = null, TMeta = null, TErrors = null, TStatus extends ResponseStatus = ResponseStatus> implements ResponseEnvelope<TData, TMeta, TErrors, TStatus> {
|
|
22
|
+
readonly success: boolean;
|
|
23
|
+
readonly status: TStatus;
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly data: TData | null;
|
|
26
|
+
readonly meta: TMeta | null;
|
|
27
|
+
readonly errors: TErrors | null;
|
|
28
|
+
readonly code: string | null;
|
|
29
|
+
constructor(data?: TData, options?: ResponseOptions<TMeta, TErrors, TStatus>);
|
|
30
|
+
static Ok<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 200>;
|
|
31
|
+
static Created<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 201>;
|
|
32
|
+
static Accepted<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 202>;
|
|
33
|
+
static BadRequest<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 400>;
|
|
34
|
+
static Unauthorized<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 401>;
|
|
35
|
+
static Forbidden<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 403>;
|
|
36
|
+
static NotFound<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 404>;
|
|
37
|
+
static Conflict<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 409>;
|
|
38
|
+
static UnprocessableEntity<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 422>;
|
|
39
|
+
static TooManyRequests<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 429>;
|
|
40
|
+
static InternalServerError<TData = null, TMeta = null, TErrors = null>(data?: TData, options?: ResponsePresetOptions<TMeta, TErrors>): ResponseBuilder<TData, TMeta, TErrors, 500>;
|
|
41
|
+
toJSON(): ResponseEnvelope<TData, TMeta, TErrors, TStatus>;
|
|
42
|
+
toResponse(headers?: ConstructorParameters<typeof Headers>[0]): Response;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const responseStatuses = [
|
|
2
|
+
200, 201, 202, 203, 206, 207, 208, 226, 300, 301, 302, 303, 305, 306, 307,
|
|
3
|
+
308, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413,
|
|
4
|
+
414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
|
|
5
|
+
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
|
|
6
|
+
];
|
|
7
|
+
const allowedStatuses = new Set(responseStatuses);
|
|
8
|
+
export class ResponseBuilder {
|
|
9
|
+
success;
|
|
10
|
+
status;
|
|
11
|
+
message;
|
|
12
|
+
data;
|
|
13
|
+
meta;
|
|
14
|
+
errors;
|
|
15
|
+
code;
|
|
16
|
+
constructor(data, options = {}) {
|
|
17
|
+
const status = options.status ?? 200;
|
|
18
|
+
if (!allowedStatuses.has(status)) {
|
|
19
|
+
throw new RangeError("Response status must be a standard final HTTP status permitting a body (not 204, 205 or 304).");
|
|
20
|
+
}
|
|
21
|
+
if (status < 400 && (options.code != null || options.errors != null)) {
|
|
22
|
+
throw new TypeError("Response code and errors are only allowed for error statuses.");
|
|
23
|
+
}
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.success = status < 400;
|
|
26
|
+
this.message = options.message ?? (this.success ? "OK" : "Error");
|
|
27
|
+
this.data = data ?? null;
|
|
28
|
+
this.meta = options.meta ?? null;
|
|
29
|
+
this.errors = options.errors ?? null;
|
|
30
|
+
this.code = options.code ?? null;
|
|
31
|
+
Object.freeze(this);
|
|
32
|
+
}
|
|
33
|
+
static Ok(data, options = {}) {
|
|
34
|
+
return new ResponseBuilder(data, {
|
|
35
|
+
message: "OK",
|
|
36
|
+
code: null,
|
|
37
|
+
...options,
|
|
38
|
+
status: 200,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
static Created(data, options = {}) {
|
|
42
|
+
return new ResponseBuilder(data, {
|
|
43
|
+
message: "Created",
|
|
44
|
+
code: null,
|
|
45
|
+
...options,
|
|
46
|
+
status: 201,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
static Accepted(data, options = {}) {
|
|
50
|
+
return new ResponseBuilder(data, {
|
|
51
|
+
message: "Accepted",
|
|
52
|
+
code: null,
|
|
53
|
+
...options,
|
|
54
|
+
status: 202,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
static BadRequest(data, options = {}) {
|
|
58
|
+
return new ResponseBuilder(data, {
|
|
59
|
+
message: "Bad Request",
|
|
60
|
+
code: "BAD_REQUEST",
|
|
61
|
+
...options,
|
|
62
|
+
status: 400,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
static Unauthorized(data, options = {}) {
|
|
66
|
+
return new ResponseBuilder(data, {
|
|
67
|
+
message: "Unauthorized",
|
|
68
|
+
code: "UNAUTHORIZED",
|
|
69
|
+
...options,
|
|
70
|
+
status: 401,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
static Forbidden(data, options = {}) {
|
|
74
|
+
return new ResponseBuilder(data, {
|
|
75
|
+
message: "Forbidden",
|
|
76
|
+
code: "FORBIDDEN",
|
|
77
|
+
...options,
|
|
78
|
+
status: 403,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
static NotFound(data, options = {}) {
|
|
82
|
+
return new ResponseBuilder(data, {
|
|
83
|
+
message: "Not Found",
|
|
84
|
+
code: "NOT_FOUND",
|
|
85
|
+
...options,
|
|
86
|
+
status: 404,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
static Conflict(data, options = {}) {
|
|
90
|
+
return new ResponseBuilder(data, {
|
|
91
|
+
message: "Conflict",
|
|
92
|
+
code: "CONFLICT",
|
|
93
|
+
...options,
|
|
94
|
+
status: 409,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
static UnprocessableEntity(data, options = {}) {
|
|
98
|
+
return new ResponseBuilder(data, {
|
|
99
|
+
message: "Unprocessable Entity",
|
|
100
|
+
code: "UNPROCESSABLE_ENTITY",
|
|
101
|
+
...options,
|
|
102
|
+
status: 422,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
static TooManyRequests(data, options = {}) {
|
|
106
|
+
return new ResponseBuilder(data, {
|
|
107
|
+
message: "Too Many Requests",
|
|
108
|
+
code: "TOO_MANY_REQUESTS",
|
|
109
|
+
...options,
|
|
110
|
+
status: 429,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
static InternalServerError(data, options = {}) {
|
|
114
|
+
return new ResponseBuilder(data, {
|
|
115
|
+
message: "Internal Server Error",
|
|
116
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
117
|
+
...options,
|
|
118
|
+
status: 500,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
toJSON() {
|
|
122
|
+
return {
|
|
123
|
+
success: this.success,
|
|
124
|
+
status: this.status,
|
|
125
|
+
message: this.message,
|
|
126
|
+
data: this.data,
|
|
127
|
+
meta: this.meta,
|
|
128
|
+
errors: this.errors,
|
|
129
|
+
code: this.code,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
toResponse(headers) {
|
|
133
|
+
return Response.json(this.toJSON(), { status: this.status, headers });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ResponseBuilder.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ResponseBuilder.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./http/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./http/index.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EnsureError } from "./EnsureError.js";
|
|
2
|
+
/** Enforces an invariant; error factories run only when the condition fails. */
|
|
3
|
+
export function Ensure(condition, error) {
|
|
4
|
+
if (condition)
|
|
5
|
+
return;
|
|
6
|
+
throw typeof error === "string" ? new EnsureError(error) : error();
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface BackgroundTaskOptions {
|
|
2
|
+
onError: (error: unknown) => void | PromiseLike<void>;
|
|
3
|
+
}
|
|
4
|
+
/** Tracks in-process I/O tasks, not durable jobs or CPU worker threads. */
|
|
5
|
+
export declare class BackgroundTask {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(options: BackgroundTaskOptions);
|
|
8
|
+
get pendingCount(): number;
|
|
9
|
+
run(task: () => unknown | PromiseLike<unknown>): void;
|
|
10
|
+
/** Includes tasks added while draining. Stop accepting requests before shutdown. */
|
|
11
|
+
drain(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Tracks in-process I/O tasks, not durable jobs or CPU worker threads. */
|
|
2
|
+
export class BackgroundTask {
|
|
3
|
+
#pending = new Set();
|
|
4
|
+
#onError;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
if (typeof options?.onError !== "function") {
|
|
7
|
+
throw new TypeError("BackgroundTask requires an onError handler.");
|
|
8
|
+
}
|
|
9
|
+
this.#onError = options.onError;
|
|
10
|
+
}
|
|
11
|
+
get pendingCount() {
|
|
12
|
+
return this.#pending.size;
|
|
13
|
+
}
|
|
14
|
+
run(task) {
|
|
15
|
+
if (typeof task !== "function") {
|
|
16
|
+
throw new TypeError("BackgroundTask.run requires a task function.");
|
|
17
|
+
}
|
|
18
|
+
const pending = Promise.resolve()
|
|
19
|
+
.then(task)
|
|
20
|
+
.then(() => undefined)
|
|
21
|
+
.catch(async (error) => {
|
|
22
|
+
try {
|
|
23
|
+
await this.#onError(error);
|
|
24
|
+
}
|
|
25
|
+
catch (handlerError) {
|
|
26
|
+
// A failing error handler must not cause an unhandled rejection.
|
|
27
|
+
console.error("BackgroundTask error handler failed", handlerError, "Task error:", error);
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.finally(() => {
|
|
31
|
+
this.#pending.delete(pending);
|
|
32
|
+
});
|
|
33
|
+
this.#pending.add(pending);
|
|
34
|
+
}
|
|
35
|
+
/** Includes tasks added while draining. Stop accepting requests before shutdown. */
|
|
36
|
+
async drain() {
|
|
37
|
+
while (this.#pending.size > 0) {
|
|
38
|
+
await Promise.all(this.#pending);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./BackgroundTask.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./BackgroundTask.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type EnvValues = Readonly<Record<string, string | undefined>>;
|
|
2
|
+
export interface EnvGetOptions {
|
|
3
|
+
fallback?: string;
|
|
4
|
+
allowEmpty?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface EnvParseContext {
|
|
7
|
+
value: string;
|
|
8
|
+
invalid: (message: string) => never;
|
|
9
|
+
}
|
|
10
|
+
export interface EnvParseOptions<T> extends EnvGetOptions {
|
|
11
|
+
parse: (context: EnvParseContext) => T;
|
|
12
|
+
}
|
|
13
|
+
/** Reads the supplied object on each get; never reads process.env implicitly. */
|
|
14
|
+
export declare class Env {
|
|
15
|
+
private readonly values;
|
|
16
|
+
constructor(values: EnvValues);
|
|
17
|
+
get<T>(name: string, options: EnvParseOptions<T>): T;
|
|
18
|
+
get(name: string, options?: EnvGetOptions): string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { StringHelper } from "../../tools/StringHelper.js";
|
|
2
|
+
/** Reads the supplied object on each get; never reads process.env implicitly. */
|
|
3
|
+
export class Env {
|
|
4
|
+
values;
|
|
5
|
+
constructor(values) {
|
|
6
|
+
this.values = values;
|
|
7
|
+
if (values === null ||
|
|
8
|
+
typeof values !== "object" ||
|
|
9
|
+
Array.isArray(values)) {
|
|
10
|
+
throw new TypeError("Env requires an object of string values");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
get(name, options = {}) {
|
|
14
|
+
if (!StringHelper.isNotEmpty(name)) {
|
|
15
|
+
throw new TypeError("Environment variable name must be a nonblank string");
|
|
16
|
+
}
|
|
17
|
+
const invalid = (message) => {
|
|
18
|
+
throw new Error(`Environment variable ${JSON.stringify(name)} ${message}`);
|
|
19
|
+
};
|
|
20
|
+
const original = Object.hasOwn(this.values, name)
|
|
21
|
+
? this.values[name]
|
|
22
|
+
: undefined;
|
|
23
|
+
const value = original === undefined ? options.fallback : original;
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
return invalid("must be configured.");
|
|
26
|
+
if (typeof value !== "string")
|
|
27
|
+
return invalid("must be a string.");
|
|
28
|
+
if (!options.allowEmpty && !StringHelper.isNotEmpty(value)) {
|
|
29
|
+
return invalid("must not be empty or whitespace-only.");
|
|
30
|
+
}
|
|
31
|
+
return options.parse ? options.parse({ value, invalid }) : value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Env.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./Env.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Date, millisecond timestamp or strict ISO string. Zone-less strings use UTC. */
|
|
2
|
+
export type DateInput = Date | string | number;
|
|
3
|
+
export declare class DateHelper {
|
|
4
|
+
private static timestamp;
|
|
5
|
+
static compare(left: unknown, right: unknown): -1 | 0 | 1 | null;
|
|
6
|
+
static isBefore(date: unknown, comparison: unknown): boolean;
|
|
7
|
+
static isAfter(date: unknown, comparison: unknown): boolean;
|
|
8
|
+
static isSame(date: unknown, comparison: unknown): boolean;
|
|
9
|
+
/** Bounds can be supplied in either order. */
|
|
10
|
+
static isBetween(date: unknown, start: unknown, end: unknown, inclusive?: boolean): boolean;
|
|
11
|
+
static isPast(date: unknown, now?: DateInput): boolean;
|
|
12
|
+
static isFuture(date: unknown, now?: DateInput): boolean;
|
|
13
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:\d{2})?)?$/;
|
|
2
|
+
export class DateHelper {
|
|
3
|
+
static timestamp(input) {
|
|
4
|
+
if (input instanceof Date) {
|
|
5
|
+
const value = input.getTime();
|
|
6
|
+
return Number.isFinite(value) ? value : null;
|
|
7
|
+
}
|
|
8
|
+
if (typeof input === "number") {
|
|
9
|
+
return Number.isFinite(input) &&
|
|
10
|
+
Number.isInteger(input) &&
|
|
11
|
+
Math.abs(input) <= 8.64e15
|
|
12
|
+
? input
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
if (typeof input !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
const match = ISO_DATE.exec(input.trim());
|
|
18
|
+
if (!match)
|
|
19
|
+
return null;
|
|
20
|
+
const [, y, m, d, h = "0", min = "0", s = "0", fraction = "", zone = "Z"] = match;
|
|
21
|
+
const year = Number(y), month = Number(m), day = Number(d);
|
|
22
|
+
const hours = Number(h), minutes = Number(min), seconds = Number(s);
|
|
23
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
24
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
25
|
+
if (month < 1 ||
|
|
26
|
+
month > 12 ||
|
|
27
|
+
day < 1 ||
|
|
28
|
+
day > days[month - 1] ||
|
|
29
|
+
hours > 23 ||
|
|
30
|
+
minutes > 59 ||
|
|
31
|
+
seconds > 59)
|
|
32
|
+
return null;
|
|
33
|
+
let offset = 0;
|
|
34
|
+
if (zone !== "Z") {
|
|
35
|
+
const offsetHours = Number(zone.slice(1, 3)), offsetMinutes = Number(zone.slice(4, 6));
|
|
36
|
+
if (offsetHours > 23 || offsetMinutes > 59)
|
|
37
|
+
return null;
|
|
38
|
+
offset =
|
|
39
|
+
(offsetHours * 60 + offsetMinutes) *
|
|
40
|
+
60_000 *
|
|
41
|
+
(zone[0] === "+" ? 1 : -1);
|
|
42
|
+
}
|
|
43
|
+
const date = new Date(0);
|
|
44
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
45
|
+
date.setUTCHours(hours, minutes, seconds, Number(fraction.slice(0, 3).padEnd(3, "0")));
|
|
46
|
+
return date.getTime() - offset;
|
|
47
|
+
}
|
|
48
|
+
static compare(left, right) {
|
|
49
|
+
const a = this.timestamp(left), b = this.timestamp(right);
|
|
50
|
+
return a === null || b === null ? null : a === b ? 0 : a < b ? -1 : 1;
|
|
51
|
+
}
|
|
52
|
+
static isBefore(date, comparison) {
|
|
53
|
+
return this.compare(date, comparison) === -1;
|
|
54
|
+
}
|
|
55
|
+
static isAfter(date, comparison) {
|
|
56
|
+
return this.compare(date, comparison) === 1;
|
|
57
|
+
}
|
|
58
|
+
static isSame(date, comparison) {
|
|
59
|
+
return this.compare(date, comparison) === 0;
|
|
60
|
+
}
|
|
61
|
+
/** Bounds can be supplied in either order. */
|
|
62
|
+
static isBetween(date, start, end, inclusive = true) {
|
|
63
|
+
const value = this.timestamp(date), a = this.timestamp(start), b = this.timestamp(end);
|
|
64
|
+
if (value === null || a === null || b === null)
|
|
65
|
+
return false;
|
|
66
|
+
const min = Math.min(a, b), max = Math.max(a, b);
|
|
67
|
+
return inclusive
|
|
68
|
+
? value >= min && value <= max
|
|
69
|
+
: value > min && value < max;
|
|
70
|
+
}
|
|
71
|
+
static isPast(date, now = Date.now()) {
|
|
72
|
+
return this.isBefore(date, now);
|
|
73
|
+
}
|
|
74
|
+
static isFuture(date, now = Date.now()) {
|
|
75
|
+
return this.isAfter(date, now);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Non-negative, timer-safe durations with exact millisecond precision. */
|
|
2
|
+
export declare class DurationHelper {
|
|
3
|
+
/** Unit-less strings represent seconds. Invalid input returns null, including numbers. */
|
|
4
|
+
static toMilliseconds(value: unknown): number | null;
|
|
5
|
+
static toSeconds(value: unknown): number | null;
|
|
6
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const DURATION = /^(\d+)(?:\.(\d+))?(ms|s|m|h)?$/;
|
|
2
|
+
const MILLISECONDS = { ms: 1n, s: 1000n, m: 60000n, h: 3600000n };
|
|
3
|
+
const MAX_MILLISECONDS = 2147483647n;
|
|
4
|
+
/** Non-negative, timer-safe durations with exact millisecond precision. */
|
|
5
|
+
export class DurationHelper {
|
|
6
|
+
/** Unit-less strings represent seconds. Invalid input returns null, including numbers. */
|
|
7
|
+
static toMilliseconds(value) {
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
return null;
|
|
10
|
+
const match = DURATION.exec(value);
|
|
11
|
+
if (!match)
|
|
12
|
+
return null;
|
|
13
|
+
const whole = match[1].replace(/^0+/, "") || "0";
|
|
14
|
+
const fraction = (match[2] ?? "").replace(/0+$/, "");
|
|
15
|
+
// Reject impossible values before BigInt conversion. The largest unit has
|
|
16
|
+
// at most seven factors of 2 or 5, so longer nonzero fractions cannot yield integer ms.
|
|
17
|
+
if (whole.length > 10 || fraction.length > 7)
|
|
18
|
+
return null;
|
|
19
|
+
const unit = (match[3] ?? "s");
|
|
20
|
+
const denominator = 10n ** BigInt(fraction.length);
|
|
21
|
+
const numerator = BigInt(whole + fraction) * MILLISECONDS[unit];
|
|
22
|
+
if (numerator % denominator !== 0n)
|
|
23
|
+
return null;
|
|
24
|
+
const milliseconds = numerator / denominator;
|
|
25
|
+
return milliseconds <= MAX_MILLISECONDS ? Number(milliseconds) : null;
|
|
26
|
+
}
|
|
27
|
+
static toSeconds(value) {
|
|
28
|
+
const milliseconds = this.toMilliseconds(value);
|
|
29
|
+
return milliseconds === null ? null : milliseconds / 1000;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends (arg: infer I) => void ? I : never;
|
|
2
|
+
type LastOf<U> = UnionToIntersection<U extends unknown ? () => U : never> extends () => infer R ? R : never;
|
|
3
|
+
type UnionToTuple<U, Last = LastOf<U>> = [U] extends [never] ? [] : [...UnionToTuple<Exclude<U, Last>>, Last];
|
|
4
|
+
type EnumLength<E> = UnionToTuple<Extract<keyof E, string>> extends infer T extends unknown[] ? T["length"] : never;
|
|
5
|
+
type TupleOf<N extends number, T, R extends T[] = []> = R["length"] extends N ? R : TupleOf<N, T, [...R, T]>;
|
|
6
|
+
type ObjectEntry<T extends object, K = Extract<keyof T, string>> = {
|
|
7
|
+
key: K;
|
|
8
|
+
value: T[keyof T];
|
|
9
|
+
};
|
|
10
|
+
export type EnumLike = Record<string, string | number>;
|
|
11
|
+
export type EnumValue<E extends EnumLike> = E[Extract<keyof E, string>];
|
|
12
|
+
export declare class EnumHelper {
|
|
13
|
+
private static getEntries;
|
|
14
|
+
/** Checks enum values, not names or numeric reverse mappings. No coercion. */
|
|
15
|
+
static isInEnum<const E extends EnumLike>(enumeration: E, value: unknown): value is EnumValue<E>;
|
|
16
|
+
static getValue<const E extends EnumLike>(enumeration: E, value: unknown): EnumValue<E> | null;
|
|
17
|
+
static associateByIndex<const E extends Record<string, string | number>>(enumeration: E, ...values: TupleOf<EnumLength<E>, string>): Record<E[Extract<keyof E, string>], string>;
|
|
18
|
+
static associateByKey<const T extends Record<string, string | number>, const A extends {
|
|
19
|
+
[K in keyof T]: unknown;
|
|
20
|
+
}>(enumeration: T, associations: A): Record<T[keyof T], A[keyof T]>;
|
|
21
|
+
static toEntries<const T extends object>(object: T): ObjectEntry<T>[];
|
|
22
|
+
static toEntries<const T extends object, K>(object: T, options: {
|
|
23
|
+
mapKey: (key: Extract<keyof T, string>, value: T[keyof T]) => K;
|
|
24
|
+
}): ObjectEntry<T, K>[];
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export class EnumHelper {
|
|
2
|
+
static getEntries(enumeration) {
|
|
3
|
+
return Object.entries(enumeration).filter(([key, value]) => !(String(Number(key)) === key &&
|
|
4
|
+
typeof value === "string" &&
|
|
5
|
+
Object.hasOwn(enumeration, value) &&
|
|
6
|
+
enumeration[value] === Number(key)));
|
|
7
|
+
}
|
|
8
|
+
/** Checks enum values, not names or numeric reverse mappings. No coercion. */
|
|
9
|
+
static isInEnum(enumeration, value) {
|
|
10
|
+
return ((typeof value === "string" || typeof value === "number") &&
|
|
11
|
+
this.getEntries(enumeration).some(([, item]) => item === value));
|
|
12
|
+
}
|
|
13
|
+
static getValue(enumeration, value) {
|
|
14
|
+
return this.isInEnum(enumeration, value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
static associateByIndex(enumeration, ...values) {
|
|
17
|
+
const entries = this.getEntries(enumeration);
|
|
18
|
+
const associations = values;
|
|
19
|
+
if (entries.length !== associations.length) {
|
|
20
|
+
throw new RangeError("Enum values count mismatch");
|
|
21
|
+
}
|
|
22
|
+
return Object.fromEntries(entries.map(([, value], index) => [value, associations[index]]));
|
|
23
|
+
}
|
|
24
|
+
static associateByKey(enumeration, associations) {
|
|
25
|
+
return Object.fromEntries(this.getEntries(enumeration).map(([key, value]) => [
|
|
26
|
+
value,
|
|
27
|
+
associations[key],
|
|
28
|
+
]));
|
|
29
|
+
}
|
|
30
|
+
static toEntries(object, options) {
|
|
31
|
+
return this.getEntries(object).map(([key, value]) => {
|
|
32
|
+
const typedKey = key;
|
|
33
|
+
const typedValue = value;
|
|
34
|
+
return {
|
|
35
|
+
key: options?.mapKey ? options.mapKey(typedKey, typedValue) : typedKey,
|
|
36
|
+
value: typedValue,
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class ErrorHelper {
|
|
2
|
+
static toError(value, fallbackMessage = "Unknown error") {
|
|
3
|
+
if (value instanceof Error)
|
|
4
|
+
return value;
|
|
5
|
+
let message = fallbackMessage;
|
|
6
|
+
let name;
|
|
7
|
+
if (typeof value === "string" && value.length > 0) {
|
|
8
|
+
message = value;
|
|
9
|
+
}
|
|
10
|
+
else if (typeof value === "object" && value !== null) {
|
|
11
|
+
const errorLike = value;
|
|
12
|
+
if (typeof errorLike.message === "string" && errorLike.message.length > 0)
|
|
13
|
+
message = errorLike.message;
|
|
14
|
+
if (typeof errorLike.name === "string" && errorLike.name.length > 0)
|
|
15
|
+
name = errorLike.name;
|
|
16
|
+
}
|
|
17
|
+
else if (value !== null && value !== undefined) {
|
|
18
|
+
message = String(value);
|
|
19
|
+
}
|
|
20
|
+
const error = new Error(message);
|
|
21
|
+
if (name)
|
|
22
|
+
error.name = name;
|
|
23
|
+
Object.defineProperty(error, "cause", {
|
|
24
|
+
value,
|
|
25
|
+
configurable: true,
|
|
26
|
+
enumerable: false,
|
|
27
|
+
writable: true,
|
|
28
|
+
});
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface NumberValidationOptions {
|
|
2
|
+
min?: number;
|
|
3
|
+
max?: number;
|
|
4
|
+
integer?: boolean;
|
|
5
|
+
safeInteger?: boolean;
|
|
6
|
+
}
|
|
7
|
+
/** Predicates never coerce strings, booleans or null into numbers. */
|
|
8
|
+
export declare class NumberHelper {
|
|
9
|
+
static isFinite(value: unknown): value is number;
|
|
10
|
+
static isInteger(value: unknown): value is number;
|
|
11
|
+
static isSafeInteger(value: unknown): value is number;
|
|
12
|
+
static isPositive(value: unknown): value is number;
|
|
13
|
+
static isNegative(value: unknown): value is number;
|
|
14
|
+
static isNonNegative(value: unknown): value is number;
|
|
15
|
+
static isPositiveInteger(value: unknown): value is number;
|
|
16
|
+
static isNonNegativeInteger(value: unknown): value is number;
|
|
17
|
+
static isInRange(value: unknown, min: number, max: number, inclusive?: boolean): value is number;
|
|
18
|
+
static isValid(value: unknown, options?: NumberValidationOptions): value is number;
|
|
19
|
+
}
|