@afora/core 0.0.9 → 0.0.12
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/README.md +62 -189
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/record/abstract-record.d.ts +2 -1
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { blueprintPluginOf } from "../core/blueprint/blueprint-plugin.js";
|
|
2
|
+
import { Email, Ip, Phone, Url } from "./properties/contact.js";
|
|
3
|
+
import { enumOf } from "./properties/enum.js";
|
|
4
|
+
import { SchemaProperty } from "./properties/headless.js";
|
|
5
|
+
import { Id, Slug } from "./properties/identity.js";
|
|
6
|
+
import { JsonProperty, jsonOf } from "./properties/json.js";
|
|
7
|
+
import { Password } from "./properties/password.js";
|
|
8
|
+
import {
|
|
9
|
+
BooleanProperty,
|
|
10
|
+
Integer,
|
|
11
|
+
NumberProperty,
|
|
12
|
+
StringProperty,
|
|
13
|
+
Text
|
|
14
|
+
} from "./properties/primitives.js";
|
|
15
|
+
import {
|
|
16
|
+
CreatedAt,
|
|
17
|
+
DateProperty,
|
|
18
|
+
DateTime,
|
|
19
|
+
DeletedAt,
|
|
20
|
+
UpdatedAt
|
|
21
|
+
} from "./properties/time.js";
|
|
22
|
+
export const Std = blueprintPluginOf("std", {
|
|
23
|
+
id: Id,
|
|
24
|
+
slug: Slug,
|
|
25
|
+
email: Email,
|
|
26
|
+
phone: Phone,
|
|
27
|
+
url: Url,
|
|
28
|
+
ip: Ip,
|
|
29
|
+
password: Password,
|
|
30
|
+
string: StringProperty,
|
|
31
|
+
text: Text,
|
|
32
|
+
number: NumberProperty,
|
|
33
|
+
integer: Integer,
|
|
34
|
+
boolean: BooleanProperty,
|
|
35
|
+
date: DateProperty,
|
|
36
|
+
dateTime: DateTime,
|
|
37
|
+
createdAt: CreatedAt,
|
|
38
|
+
updatedAt: UpdatedAt,
|
|
39
|
+
deletedAt: DeletedAt,
|
|
40
|
+
schema: SchemaProperty,
|
|
41
|
+
json: JsonProperty
|
|
42
|
+
}, { enumOf, jsonOf });
|
package/dist/std/blueprint.d.ts
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
* Builds a blueprint from the standard properties, e.g.
|
|
3
3
|
* `blueprintOf((ctx) => ({ name: ctx.string }))`, and from `sources`, plugins
|
|
4
4
|
* or blueprints, given before the definition, which can replace any `Std` key.
|
|
5
|
+
*
|
|
6
|
+
* @throws {@link ReservedBlueprintKey} when a source declares a key the context reserves (`optional`, `list`, `listOf`, `optionalOf`, `propertyOf`, `variantOf`).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { blueprintOf } from "@afora/core";
|
|
11
|
+
*
|
|
12
|
+
* const Signup = blueprintOf((ctx) => ({
|
|
13
|
+
* name: ctx.text,
|
|
14
|
+
* email: ctx.email, // trimmed and lowercased
|
|
15
|
+
* tags: ctx.listOf(ctx.string, { maxItems: 5 }),
|
|
16
|
+
* nickname: ctx.optional.string,
|
|
17
|
+
* }));
|
|
18
|
+
*
|
|
19
|
+
* const parsed = Signup.safeParse({
|
|
20
|
+
* name: "Ana",
|
|
21
|
+
* email: " ANA@example.com ",
|
|
22
|
+
* tags: [],
|
|
23
|
+
* });
|
|
24
|
+
* if (!parsed.issues) parsed.value.email; // "ana@example.com"
|
|
25
|
+
* ```
|
|
5
26
|
*/
|
|
6
27
|
export declare const blueprintOf: import("../index.js").BlueprintBuilder<readonly [import("../index.js").IBlueprintPlugin<{
|
|
7
28
|
id: typeof import("../index.js").Id;
|
package/dist/std/entity.d.ts
CHANGED
|
@@ -10,7 +10,26 @@ import { CreatedAt, DeletedAt, UpdatedAt } from "./properties/time.js";
|
|
|
10
10
|
* As a record, the class can be a field of another blueprint, e.g.
|
|
11
11
|
* `{ author: User }`, with `optionalOf` and `listOf`. `options.plugins` picks
|
|
12
12
|
* from the standard plugins, as `(ctx) => [ctx.publishable]`, or takes any
|
|
13
|
-
* plugin made by
|
|
13
|
+
* plugin made by {@link recordPluginOf}.
|
|
14
|
+
*
|
|
15
|
+
* @throws {@link ReservedEntityKey} when the blueprint declares an identity key or a member of an entity.
|
|
16
|
+
* @throws {@link ReservedRecordKey} when a key of the blueprint is a member of the record.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { blueprintOf, entityOf } from "@afora/core";
|
|
21
|
+
*
|
|
22
|
+
* class User extends entityOf(
|
|
23
|
+
* blueprintOf((ctx) => ({ name: ctx.string })),
|
|
24
|
+
* { change: ["name"] },
|
|
25
|
+
* ) {}
|
|
26
|
+
*
|
|
27
|
+
* const user = new User({ name: "Jane" });
|
|
28
|
+
* user.id; // a UUID v7
|
|
29
|
+
* user.updatedAt; // undefined until the first change
|
|
30
|
+
* user.change.name("Ann").updatedAt; // now
|
|
31
|
+
* user.destroy().isDeleted; // true
|
|
32
|
+
* ```
|
|
14
33
|
*/
|
|
15
34
|
export declare const entityOf: import("../index.js").EntityBuilder<import("../core/property/types/property-options.interface.js").StoredFields<{
|
|
16
35
|
id: typeof Id;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { blueprintBuilderOf } from "../core/blueprint/blueprint.js";
|
|
2
|
+
import { entityBuilderOf } from "../core/entity/entity.js";
|
|
3
|
+
import { vouching } from "../core/entity/trust.js";
|
|
4
|
+
import { Id } from "./properties/identity.js";
|
|
5
|
+
import { CreatedAt, DeletedAt, ExpiresAt, UpdatedAt } from "./properties/time.js";
|
|
6
|
+
import { StdRecordPlugins } from "./record-plugins.js";
|
|
7
|
+
import { generateId, makesDefaultIds } from "./runtime.js";
|
|
8
|
+
const Identity = blueprintBuilderOf()(() => ({
|
|
9
|
+
id: Id,
|
|
10
|
+
createdAt: CreatedAt,
|
|
11
|
+
updatedAt: UpdatedAt,
|
|
12
|
+
deletedAt: DeletedAt
|
|
13
|
+
}));
|
|
14
|
+
let lastMillisecond = Number.NaN;
|
|
15
|
+
let lastTimestamp = "";
|
|
16
|
+
function now() {
|
|
17
|
+
const millisecond = Date.now();
|
|
18
|
+
if (millisecond !== lastMillisecond) {
|
|
19
|
+
lastTimestamp = new Date(millisecond).toISOString();
|
|
20
|
+
lastMillisecond = millisecond;
|
|
21
|
+
}
|
|
22
|
+
return lastTimestamp;
|
|
23
|
+
}
|
|
24
|
+
export const entityOf = entityBuilderOf(vouching({
|
|
25
|
+
identity: Identity,
|
|
26
|
+
generateId,
|
|
27
|
+
now,
|
|
28
|
+
expiresAt: ExpiresAt,
|
|
29
|
+
plugins: [StdRecordPlugins]
|
|
30
|
+
}, { id: makesDefaultIds, createdAt: () => true }));
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
/** An email address of up to 254 characters, trimmed and lowercased. */
|
|
2
3
|
export declare const Email: import("../../index.js").PropertyClass<Type.TString, string>;
|
|
3
4
|
/** A phone number in E.164; spaces, dots, dashes and parentheses are dropped. */
|
|
4
5
|
export declare const Phone: import("../../index.js").PropertyClass<Type.TString, string>;
|
|
6
|
+
/** A URL of up to 2048 characters, trimmed. */
|
|
5
7
|
export declare const Url: import("../../index.js").PropertyClass<Type.TString, string>;
|
|
8
|
+
/** An IPv4 or IPv6 address, trimmed. */
|
|
6
9
|
export declare const Ip: import("../../index.js").PropertyClass<Type.TUnion<[Type.TString, Type.TString]>, string>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
3
|
+
export const Email = propertyOf({
|
|
4
|
+
name: "email",
|
|
5
|
+
schema: Type.String({ format: "email", maxLength: 254 }),
|
|
6
|
+
default: "demo@example.com",
|
|
7
|
+
preHandle: (value) => value.trim().toLowerCase()
|
|
8
|
+
});
|
|
9
|
+
export const Phone = propertyOf({
|
|
10
|
+
name: "phone",
|
|
11
|
+
schema: Type.String({ pattern: "^\\+[1-9]\\d{1,14}$" }),
|
|
12
|
+
default: "+5511999999999",
|
|
13
|
+
preHandle: (value) => value.replace(/[\s().-]/g, "")
|
|
14
|
+
});
|
|
15
|
+
export const Url = propertyOf({
|
|
16
|
+
name: "url",
|
|
17
|
+
schema: Type.String({ format: "url", maxLength: 2048 }),
|
|
18
|
+
default: "https://example.com",
|
|
19
|
+
preHandle: (value) => value.trim()
|
|
20
|
+
});
|
|
21
|
+
export const Ip = propertyOf({
|
|
22
|
+
name: "ip",
|
|
23
|
+
schema: Type.Union([
|
|
24
|
+
Type.String({ format: "ipv4" }),
|
|
25
|
+
Type.String({ format: "ipv6" })
|
|
26
|
+
]),
|
|
27
|
+
default: "127.0.0.1",
|
|
28
|
+
preHandle: (value) => value.trim()
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { recordPropertyLineage } from "../../core/property/lineage.js";
|
|
3
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
4
|
+
export function enumOf(values, {
|
|
5
|
+
name = "enum",
|
|
6
|
+
optional,
|
|
7
|
+
default: _default = values[0]
|
|
8
|
+
} = {}) {
|
|
9
|
+
const Enum = propertyOf({
|
|
10
|
+
name,
|
|
11
|
+
schema: Type.Enum(values),
|
|
12
|
+
optional,
|
|
13
|
+
default: _default
|
|
14
|
+
});
|
|
15
|
+
recordPropertyLineage(Enum, {
|
|
16
|
+
kind: "factory",
|
|
17
|
+
factory: "enumOf",
|
|
18
|
+
args: Object.freeze({
|
|
19
|
+
values: Object.freeze([...values]),
|
|
20
|
+
...name === "enum" ? {} : { name },
|
|
21
|
+
...optional ? { optional: true } : {},
|
|
22
|
+
..._default === values[0] ? {} : { default: _default }
|
|
23
|
+
})
|
|
24
|
+
});
|
|
25
|
+
return Enum;
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../../core/exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../../core/exception/exception.js";
|
|
3
|
+
import { markExported } from "../../core/exception/exported.js";
|
|
4
|
+
export const InvalidJsonData = exceptionOf({
|
|
5
|
+
layer: ExceptionLayers.Property,
|
|
6
|
+
name: "InvalidJsonData",
|
|
7
|
+
message: ({ issues }, source) => `Invalid JSON data for "${source}": ${issues.map((issue) => `${issue.path.length === 0 ? "/" : `/${issue.path.join("/")}`}: ${issue.message}`).join("; ")}`
|
|
8
|
+
});
|
|
9
|
+
export const InvalidJsonSchema = exceptionOf({
|
|
10
|
+
layer: ExceptionLayers.Schema,
|
|
11
|
+
name: "InvalidJsonSchema",
|
|
12
|
+
message: (_, source) => `The schema of "${source}" is not a JSON Schema.`
|
|
13
|
+
});
|
|
14
|
+
markExported(InvalidJsonData, InvalidJsonSchema);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Meta } from "typebox/schema";
|
|
3
|
+
import { InvalidPropertyValue } from "../../core/property/exceptions.js";
|
|
4
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
5
|
+
import { schemaOf } from "../../core/schema/schema.js";
|
|
6
|
+
const JSON_SCHEMA = Type.Unsafe({
|
|
7
|
+
type: "object",
|
|
8
|
+
allOf: [Meta["https://json-schema.org/draft/2020-12/schema"]]
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export class SchemaProperty extends propertyOf({
|
|
12
|
+
name: "schema",
|
|
13
|
+
schema: JSON_SCHEMA,
|
|
14
|
+
default: Type.String()
|
|
15
|
+
}) {
|
|
16
|
+
compiled;
|
|
17
|
+
static deserialize(json) {
|
|
18
|
+
let value;
|
|
19
|
+
try {
|
|
20
|
+
value = JSON.parse(json);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
throw new InvalidPropertyValue({
|
|
23
|
+
source: "schema",
|
|
24
|
+
details: { errors: [error.message] }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return new SchemaProperty(value);
|
|
28
|
+
}
|
|
29
|
+
serialize() {
|
|
30
|
+
return JSON.stringify(this.value);
|
|
31
|
+
}
|
|
32
|
+
match(data) {
|
|
33
|
+
return this.toSchema().match(data);
|
|
34
|
+
}
|
|
35
|
+
errors(data) {
|
|
36
|
+
return this.toSchema().errors(data);
|
|
37
|
+
}
|
|
38
|
+
toSchema() {
|
|
39
|
+
if (!this.compiled) {
|
|
40
|
+
const Schema = schemaOf("schema", this.value);
|
|
41
|
+
this.compiled = new Schema;
|
|
42
|
+
}
|
|
43
|
+
return this.compiled;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import slugify from "slugify";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
4
|
+
import { generateId } from "../runtime.js";
|
|
5
|
+
const UUID_V7 = "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";
|
|
6
|
+
|
|
7
|
+
export class Id extends propertyOf({
|
|
8
|
+
name: "id",
|
|
9
|
+
schema: Type.String({ pattern: UUID_V7 }),
|
|
10
|
+
default: "01a173d2-051c-77bb-8166-a371e999573e",
|
|
11
|
+
preHandle: (value) => value.toLowerCase()
|
|
12
|
+
}) {
|
|
13
|
+
static generate() {
|
|
14
|
+
return new Id(generateId());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Slug extends propertyOf({
|
|
19
|
+
name: "slug",
|
|
20
|
+
schema: Type.String({
|
|
21
|
+
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
22
|
+
maxLength: 255
|
|
23
|
+
}),
|
|
24
|
+
default: "demo-slug",
|
|
25
|
+
unique: true,
|
|
26
|
+
preHandle: (value) => slugify(value, { lower: true, strict: true, trim: true })
|
|
27
|
+
}) {
|
|
28
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Meta } from "typebox/schema";
|
|
3
|
+
import Value from "typebox/value";
|
|
4
|
+
import { recordPropertyLineage } from "../../core/property/lineage.js";
|
|
5
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
6
|
+
import { InvalidJsonData, InvalidJsonSchema } from "./exceptions.js";
|
|
7
|
+
function notJson(value, path = [], ancestors = []) {
|
|
8
|
+
if (value === null)
|
|
9
|
+
return;
|
|
10
|
+
switch (typeof value) {
|
|
11
|
+
case "string":
|
|
12
|
+
case "boolean":
|
|
13
|
+
return;
|
|
14
|
+
case "number":
|
|
15
|
+
return Number.isFinite(value) ? undefined : { path, message: `is not JSON (${value})` };
|
|
16
|
+
case "object":
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
return { path, message: `is not JSON (${typeof value})` };
|
|
20
|
+
}
|
|
21
|
+
if (ancestors.includes(value))
|
|
22
|
+
return { path, message: "is circular" };
|
|
23
|
+
const inner = [...ancestors, value];
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
for (let index = 0;index < value.length; index++) {
|
|
26
|
+
const found = notJson(value[index], [...path, index], inner);
|
|
27
|
+
if (found)
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const prototype = Object.getPrototypeOf(value);
|
|
33
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
34
|
+
return {
|
|
35
|
+
path,
|
|
36
|
+
message: `is not JSON (an instance of ${prototype?.constructor?.name ?? "a class"})`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
for (const [key, item] of Object.entries(value)) {
|
|
40
|
+
const found = notJson(item, [...path, key], inner);
|
|
41
|
+
if (found)
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const JSON_SCHEMA = Type.Unsafe({
|
|
47
|
+
type: "object",
|
|
48
|
+
allOf: [Meta["https://json-schema.org/draft/2020-12/schema"]]
|
|
49
|
+
});
|
|
50
|
+
const checked = new WeakSet;
|
|
51
|
+
function assertJsonSchema(schema, source) {
|
|
52
|
+
if (typeof schema === "object" && schema !== null && checked.has(schema)) {
|
|
53
|
+
return schema;
|
|
54
|
+
}
|
|
55
|
+
if (!Value.Check(JSON_SCHEMA, schema)) {
|
|
56
|
+
throw new InvalidJsonSchema({ source });
|
|
57
|
+
}
|
|
58
|
+
checked.add(schema);
|
|
59
|
+
return schema;
|
|
60
|
+
}
|
|
61
|
+
function pathOf(pointer) {
|
|
62
|
+
if (pointer === "")
|
|
63
|
+
return [];
|
|
64
|
+
return pointer.slice(1).split("/").map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~")).map((segment) => /^(?:0|[1-9][0-9]*)$/.test(segment) ? +segment : segment);
|
|
65
|
+
}
|
|
66
|
+
function issuesOfErrors(errors, source) {
|
|
67
|
+
const issues = [];
|
|
68
|
+
const add = (path, code, message) => {
|
|
69
|
+
issues.push({ path, code, message, source });
|
|
70
|
+
};
|
|
71
|
+
for (const error of errors) {
|
|
72
|
+
const path = pathOf(error.instancePath);
|
|
73
|
+
const keys = (name) => Array.isArray(error.params[name]) ? error.params[name] : [];
|
|
74
|
+
if (error.keyword === "required") {
|
|
75
|
+
for (const key of keys("requiredProperties")) {
|
|
76
|
+
add([...path, key], "required", "is required");
|
|
77
|
+
}
|
|
78
|
+
} else if (error.keyword === "additionalProperties") {
|
|
79
|
+
for (const key of keys("additionalProperties")) {
|
|
80
|
+
add([...path, key], "undeclared_key", "is not declared");
|
|
81
|
+
}
|
|
82
|
+
} else if (!(error.keyword === "boolean" && error.schemaPath.endsWith("/additionalProperties"))) {
|
|
83
|
+
add(path, error.keyword === "type" ? "invalid_type" : "invalid_value", error.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return issues;
|
|
87
|
+
}
|
|
88
|
+
export function jsonSchemaIssues(schema, value, { source = "json" } = {}) {
|
|
89
|
+
const checkedSchema = assertJsonSchema(schema, source);
|
|
90
|
+
const found = notJson(value);
|
|
91
|
+
if (found) {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
path: found.path,
|
|
95
|
+
code: "invalid_value",
|
|
96
|
+
message: found.message,
|
|
97
|
+
source
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
if (Value.Check(checkedSchema, value))
|
|
102
|
+
return [];
|
|
103
|
+
return issuesOfErrors(Value.Errors(checkedSchema, value), source);
|
|
104
|
+
}
|
|
105
|
+
export function checkJsonSchema(schema, value, options = {}) {
|
|
106
|
+
const issues = jsonSchemaIssues(schema, value, options);
|
|
107
|
+
if (issues.length > 0) {
|
|
108
|
+
throw new InvalidJsonData({
|
|
109
|
+
source: options.source ?? "json",
|
|
110
|
+
details: { issues }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function demoOf(schema) {
|
|
116
|
+
try {
|
|
117
|
+
return Value.Create(schema);
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function assertJson(value) {
|
|
123
|
+
const found = notJson(value);
|
|
124
|
+
if (found) {
|
|
125
|
+
throw new Error(`${found.path.length === 0 ? "" : `/${found.path.join("/")}: `}${found.message}`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
export const JsonProperty = propertyOf({
|
|
130
|
+
name: "json",
|
|
131
|
+
schema: Type.Unsafe({}),
|
|
132
|
+
default: {},
|
|
133
|
+
preHandle: assertJson
|
|
134
|
+
});
|
|
135
|
+
export function jsonOf({
|
|
136
|
+
schema,
|
|
137
|
+
name = "json",
|
|
138
|
+
optional,
|
|
139
|
+
default: _default
|
|
140
|
+
}) {
|
|
141
|
+
assertJsonSchema(schema, name);
|
|
142
|
+
const made = _default ?? demoOf(schema);
|
|
143
|
+
const Json = propertyOf({
|
|
144
|
+
name,
|
|
145
|
+
schema,
|
|
146
|
+
optional,
|
|
147
|
+
...made !== undefined && Value.Check(schema, made) ? { default: made } : {},
|
|
148
|
+
preHandle: assertJson
|
|
149
|
+
});
|
|
150
|
+
recordPropertyLineage(Json, {
|
|
151
|
+
kind: "factory",
|
|
152
|
+
factory: "jsonOf",
|
|
153
|
+
args: Object.freeze({
|
|
154
|
+
schema,
|
|
155
|
+
...name === "json" ? {} : { name },
|
|
156
|
+
...optional ? { optional: true } : {},
|
|
157
|
+
..._default === undefined ? {} : { default: _default }
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
return Json;
|
|
161
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { InvalidPropertyValue } from "../../core/property/exceptions.js";
|
|
3
|
+
import { PropertyInput } from "../../core/property/input.js";
|
|
4
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
5
|
+
import { currentHasher, verifySyncWithCurrent } from "../runtime.js";
|
|
6
|
+
const PASSWORD_MIN_LENGTH = 7;
|
|
7
|
+
export const PASSWORD_MAX_LENGTH = 128;
|
|
8
|
+
const PASSWORD_SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|\\;:'\",.<>/?`~";
|
|
9
|
+
const ARGON2_HASH = "^\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[A-Za-z0-9+/]+\\$[A-Za-z0-9+/]+$";
|
|
10
|
+
const DEMO_HASH = "$argon2id$v=19$m=65536,t=2,p=1$YO5NNmxyK9+adO95GxWsAaUX4TULmT3IGhTYfpshXl0$/C89OCh7bhDY2cR2UkgvOV3dPlkghNLhv/DJsdopwMo";
|
|
11
|
+
export function isPasswordHash(value) {
|
|
12
|
+
return new RegExp(ARGON2_HASH).test(value);
|
|
13
|
+
}
|
|
14
|
+
export function passwordIssues(value) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (value.length < PASSWORD_MIN_LENGTH) {
|
|
17
|
+
issues.push(`must have at least ${PASSWORD_MIN_LENGTH} characters`);
|
|
18
|
+
}
|
|
19
|
+
if (value.length > PASSWORD_MAX_LENGTH) {
|
|
20
|
+
issues.push(`must have at most ${PASSWORD_MAX_LENGTH} characters`);
|
|
21
|
+
}
|
|
22
|
+
if (!/[a-z]/.test(value))
|
|
23
|
+
issues.push("must contain a lowercase letter");
|
|
24
|
+
if (!/[A-Z]/.test(value))
|
|
25
|
+
issues.push("must contain an uppercase letter");
|
|
26
|
+
if (!/\d/.test(value))
|
|
27
|
+
issues.push("must contain a number");
|
|
28
|
+
if (![...value].some((char) => PASSWORD_SPECIAL_CHARACTERS.includes(char))) {
|
|
29
|
+
issues.push(`must contain a special character (${PASSWORD_SPECIAL_CHARACTERS})`);
|
|
30
|
+
}
|
|
31
|
+
return issues;
|
|
32
|
+
}
|
|
33
|
+
const escaped = PASSWORD_SPECIAL_CHARACTERS.replace(/[\\\]^-]/g, "\\$&");
|
|
34
|
+
|
|
35
|
+
export class PlainPassword extends propertyOf({
|
|
36
|
+
name: "plainPassword",
|
|
37
|
+
schema: Type.String({
|
|
38
|
+
minLength: PASSWORD_MIN_LENGTH,
|
|
39
|
+
maxLength: PASSWORD_MAX_LENGTH,
|
|
40
|
+
pattern: `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[${escaped}])`
|
|
41
|
+
}),
|
|
42
|
+
default: "Demo@123",
|
|
43
|
+
sensitive: true
|
|
44
|
+
}) {
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class Password extends propertyOf({
|
|
48
|
+
name: "password",
|
|
49
|
+
schema: Type.String({ pattern: ARGON2_HASH }),
|
|
50
|
+
default: DEMO_HASH,
|
|
51
|
+
sensitive: true
|
|
52
|
+
}) {
|
|
53
|
+
static [PropertyInput] = {
|
|
54
|
+
accepts: PlainPassword,
|
|
55
|
+
convert: async (plain) => (await Password.hash(plain)).value
|
|
56
|
+
};
|
|
57
|
+
static async hash(plain) {
|
|
58
|
+
const issues = passwordIssues(plain);
|
|
59
|
+
if (issues.length > 0) {
|
|
60
|
+
throw new InvalidPropertyValue({
|
|
61
|
+
source: "password",
|
|
62
|
+
details: { errors: issues }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return new Password(await currentHasher().hash(plain));
|
|
66
|
+
}
|
|
67
|
+
verify(input) {
|
|
68
|
+
const hash = this.value;
|
|
69
|
+
if (hash === undefined)
|
|
70
|
+
return Promise.resolve(false);
|
|
71
|
+
return currentHasher().verify(input, hash);
|
|
72
|
+
}
|
|
73
|
+
verifySync(input) {
|
|
74
|
+
const hash = this.value;
|
|
75
|
+
if (hash === undefined)
|
|
76
|
+
return false;
|
|
77
|
+
return verifySyncWithCurrent(input, hash);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
/** Any string. */
|
|
2
3
|
export declare const StringProperty: import("../../index.js").PropertyClass<Type.TString, string>;
|
|
3
4
|
/** A string that is not blank once trimmed. */
|
|
4
5
|
export declare const Text: import("../../index.js").PropertyClass<Type.TString, string>;
|
|
6
|
+
/** Any number. */
|
|
5
7
|
export declare const NumberProperty: import("../../index.js").PropertyClass<Type.TNumber, number>;
|
|
8
|
+
/** An integer. Narrow it with `Integer.with({ min, max })`. */
|
|
6
9
|
export declare const Integer: import("../../index.js").PropertyClass<Type.TInteger, number>;
|
|
10
|
+
/** A boolean. */
|
|
7
11
|
export declare const BooleanProperty: import("../../index.js").PropertyClass<Type.TBoolean, boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
3
|
+
export const StringProperty = propertyOf({
|
|
4
|
+
name: "string",
|
|
5
|
+
schema: Type.String(),
|
|
6
|
+
default: "demo"
|
|
7
|
+
});
|
|
8
|
+
export const Text = propertyOf({
|
|
9
|
+
name: "text",
|
|
10
|
+
schema: Type.String({ minLength: 1 }),
|
|
11
|
+
default: "Demo",
|
|
12
|
+
preHandle: (value) => value.trim()
|
|
13
|
+
});
|
|
14
|
+
export const NumberProperty = propertyOf({
|
|
15
|
+
name: "number",
|
|
16
|
+
schema: Type.Number(),
|
|
17
|
+
default: 0
|
|
18
|
+
});
|
|
19
|
+
export const Integer = propertyOf({
|
|
20
|
+
name: "integer",
|
|
21
|
+
schema: Type.Integer(),
|
|
22
|
+
default: 0
|
|
23
|
+
});
|
|
24
|
+
export const BooleanProperty = propertyOf({
|
|
25
|
+
name: "boolean",
|
|
26
|
+
schema: Type.Boolean(),
|
|
27
|
+
default: false
|
|
28
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { propertyOf } from "../../core/property/property.js";
|
|
3
|
+
const DEMO_DATE_TIME = "2026-01-01T00:00:00.000Z";
|
|
4
|
+
function dateTimeOf({
|
|
5
|
+
name,
|
|
6
|
+
optional,
|
|
7
|
+
default: _default
|
|
8
|
+
}) {
|
|
9
|
+
return class extends propertyOf({
|
|
10
|
+
name,
|
|
11
|
+
schema: Type.String({ format: "date-time" }),
|
|
12
|
+
optional,
|
|
13
|
+
default: _default
|
|
14
|
+
}) {
|
|
15
|
+
static now() {
|
|
16
|
+
return new this(new Date().toISOString());
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DateTime extends dateTimeOf({
|
|
22
|
+
name: "dateTime",
|
|
23
|
+
default: DEMO_DATE_TIME
|
|
24
|
+
}) {
|
|
25
|
+
}
|
|
26
|
+
export const DateProperty = propertyOf({
|
|
27
|
+
name: "date",
|
|
28
|
+
schema: Type.String({ format: "date" }),
|
|
29
|
+
default: "2026-01-01"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export class CreatedAt extends dateTimeOf({
|
|
33
|
+
name: "createdAt",
|
|
34
|
+
default: DEMO_DATE_TIME
|
|
35
|
+
}) {
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class UpdatedAt extends dateTimeOf({
|
|
39
|
+
name: "updatedAt",
|
|
40
|
+
optional: true,
|
|
41
|
+
default: DEMO_DATE_TIME
|
|
42
|
+
}) {
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class DeletedAt extends dateTimeOf({
|
|
46
|
+
name: "deletedAt",
|
|
47
|
+
optional: true
|
|
48
|
+
}) {
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ExpiresAt extends dateTimeOf({
|
|
52
|
+
name: "expiresAt",
|
|
53
|
+
default: DEMO_DATE_TIME
|
|
54
|
+
}) {
|
|
55
|
+
}
|