@afora/core 0.0.9 → 0.0.11
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 +52 -30
- 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 +10 -3
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Kind } from "../symbols/kind.js";
|
|
2
|
+
import {
|
|
3
|
+
IncompatibleRecordPlugin,
|
|
4
|
+
InvalidRecordPlugin,
|
|
5
|
+
UnknownRecordPlugin
|
|
6
|
+
} from "./exceptions.js";
|
|
7
|
+
import { isRecordPlugin, isVersion, satisfies } from "./plugin.js";
|
|
8
|
+
const LISTS = ["keys", "sensitive", "unique", "change", "members"];
|
|
9
|
+
function isDescriptor(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null)
|
|
11
|
+
return false;
|
|
12
|
+
const descriptor = value;
|
|
13
|
+
const isList = (list) => Array.isArray(list) && list.every((item) => typeof item === "string");
|
|
14
|
+
return descriptor.kind === "record-plugin" && typeof descriptor.name === "string" && isVersion(descriptor.version) && LISTS.every((list) => isList(descriptor[list])) && Array.isArray(descriptor.requires) && descriptor.requires.every((required) => typeof required === "object" && required !== null && typeof required.name === "string" && isVersion(required.version));
|
|
15
|
+
}
|
|
16
|
+
function mismatchOf(plugin, descriptor) {
|
|
17
|
+
if (!satisfies(plugin.version, descriptor.version)) {
|
|
18
|
+
return `version ${plugin.version} does not satisfy ${descriptor.version}`;
|
|
19
|
+
}
|
|
20
|
+
const current = {
|
|
21
|
+
keys: plugin.keys,
|
|
22
|
+
sensitive: plugin.options.sensitive,
|
|
23
|
+
unique: plugin.options.unique,
|
|
24
|
+
change: plugin.options.change,
|
|
25
|
+
members: plugin.members
|
|
26
|
+
};
|
|
27
|
+
for (const list of LISTS) {
|
|
28
|
+
const missing = descriptor[list].find((item) => !current[list].includes(item));
|
|
29
|
+
if (missing !== undefined)
|
|
30
|
+
return `"${missing}" is not in its ${list}`;
|
|
31
|
+
}
|
|
32
|
+
for (const required of descriptor.requires) {
|
|
33
|
+
const found = plugin.requires.find(({ name }) => name === required.name);
|
|
34
|
+
if (!found)
|
|
35
|
+
return `it does not require "${required.name}"`;
|
|
36
|
+
if (!satisfies(found.version, required.version)) {
|
|
37
|
+
return `its "${required.name}" ${found.version} does not satisfy ${required.version}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
export function recordPluginCatalogOf(...plugins) {
|
|
43
|
+
const byName = {};
|
|
44
|
+
for (const plugin of plugins) {
|
|
45
|
+
if (!isRecordPlugin(plugin)) {
|
|
46
|
+
throw new InvalidRecordPlugin({
|
|
47
|
+
source: "catalog",
|
|
48
|
+
details: {
|
|
49
|
+
reason: "a catalog takes plugins made by `recordPluginOf`"
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (Object.hasOwn(byName, plugin.name)) {
|
|
54
|
+
throw new InvalidRecordPlugin({
|
|
55
|
+
source: plugin.name,
|
|
56
|
+
details: { reason: "the catalog has another plugin with its name" }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
byName[plugin.name] = plugin;
|
|
60
|
+
}
|
|
61
|
+
const find = (name) => {
|
|
62
|
+
const plugin = Object.hasOwn(byName, name) ? byName[name] : undefined;
|
|
63
|
+
if (!plugin) {
|
|
64
|
+
throw new UnknownRecordPlugin({ source: "catalog", details: { name } });
|
|
65
|
+
}
|
|
66
|
+
return plugin;
|
|
67
|
+
};
|
|
68
|
+
const resolve = (reference) => {
|
|
69
|
+
if (typeof reference === "string") {
|
|
70
|
+
const at = reference.lastIndexOf("@");
|
|
71
|
+
if (at <= 0)
|
|
72
|
+
return find(reference);
|
|
73
|
+
const plugin = find(reference.slice(0, at));
|
|
74
|
+
const version = reference.slice(at + 1);
|
|
75
|
+
if (!isVersion(version)) {
|
|
76
|
+
throw new IncompatibleRecordPlugin({
|
|
77
|
+
source: plugin.name,
|
|
78
|
+
details: { reason: `"${version}" is not a semantic version` }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!satisfies(plugin.version, version)) {
|
|
82
|
+
throw new IncompatibleRecordPlugin({
|
|
83
|
+
source: plugin.name,
|
|
84
|
+
details: {
|
|
85
|
+
reason: `version ${plugin.version} does not satisfy ${version}`
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return plugin;
|
|
90
|
+
}
|
|
91
|
+
if (!isDescriptor(reference)) {
|
|
92
|
+
const name = reference?.name;
|
|
93
|
+
throw new IncompatibleRecordPlugin({
|
|
94
|
+
source: typeof name === "string" ? name : "unknown",
|
|
95
|
+
details: { reason: "it is not a record plugin descriptor" }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const plugin = find(reference.name);
|
|
99
|
+
const reason = mismatchOf(plugin, reference);
|
|
100
|
+
if (reason !== undefined) {
|
|
101
|
+
throw new IncompatibleRecordPlugin({
|
|
102
|
+
source: plugin.name,
|
|
103
|
+
details: { reason }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return plugin;
|
|
107
|
+
};
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
[Kind]: "record-plugin-catalog",
|
|
110
|
+
plugins: Object.freeze(byName),
|
|
111
|
+
list: Object.freeze([...plugins]),
|
|
112
|
+
resolve,
|
|
113
|
+
resolveAll: (references) => Object.freeze(references.map(resolve))
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { isRecordClass } from "../blueprint/record-field.js";
|
|
2
|
+
import { Kind } from "../symbols/kind.js";
|
|
3
|
+
import { RESERVED_RECORD_KEYS } from "./abstract-record.js";
|
|
4
|
+
import { InvalidRecordPlugin, RecordPluginConflict } from "./exceptions.js";
|
|
5
|
+
export const RecordPlugins = Symbol("recordPlugins");
|
|
6
|
+
export const IdentifiedRecord = Symbol("identifiedRecord");
|
|
7
|
+
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
8
|
+
export function isVersion(version) {
|
|
9
|
+
return typeof version === "string" && SEMVER.test(version);
|
|
10
|
+
}
|
|
11
|
+
function versionParts(version) {
|
|
12
|
+
const [, major, minor, patch] = SEMVER.exec(version);
|
|
13
|
+
return [Number(major), Number(minor), Number(patch)];
|
|
14
|
+
}
|
|
15
|
+
export function satisfies(available, requested) {
|
|
16
|
+
const [major, minor, patch] = versionParts(available);
|
|
17
|
+
const [wantedMajor, wantedMinor, wantedPatch] = versionParts(requested);
|
|
18
|
+
if (major !== wantedMajor)
|
|
19
|
+
return false;
|
|
20
|
+
if (major === 0 && minor !== wantedMinor)
|
|
21
|
+
return false;
|
|
22
|
+
if (minor !== wantedMinor)
|
|
23
|
+
return minor > wantedMinor;
|
|
24
|
+
return patch >= wantedPatch;
|
|
25
|
+
}
|
|
26
|
+
const HOOKS = ["onSet", "onSetAll"];
|
|
27
|
+
const internals = new WeakMap;
|
|
28
|
+
export function pluginsOf(value) {
|
|
29
|
+
const Class = typeof value === "function" ? value : typeof value === "object" && value !== null ? value.constructor : undefined;
|
|
30
|
+
return Class?.[RecordPlugins] ?? [];
|
|
31
|
+
}
|
|
32
|
+
export function isRecordPlugin(value) {
|
|
33
|
+
return typeof value === "object" && value !== null && value[Kind] === "record-plugin";
|
|
34
|
+
}
|
|
35
|
+
export function hasPlugin(value, plugin) {
|
|
36
|
+
const plugins = pluginsOf(value);
|
|
37
|
+
if (typeof plugin === "string") {
|
|
38
|
+
return plugins.some((applied) => applied.name === plugin);
|
|
39
|
+
}
|
|
40
|
+
if (plugins.includes(plugin))
|
|
41
|
+
return true;
|
|
42
|
+
return typeof value === "function" ? value === plugin.record || value.prototype instanceof plugin.record : value instanceof plugin.record;
|
|
43
|
+
}
|
|
44
|
+
function membersOf(Record, name) {
|
|
45
|
+
const descriptors = {};
|
|
46
|
+
const hooks = {};
|
|
47
|
+
for (let prototype = Record.prototype;prototype !== null && !Object.hasOwn(prototype.constructor, "blueprint"); prototype = Object.getPrototypeOf(prototype)) {
|
|
48
|
+
for (const member of Object.getOwnPropertyNames(prototype)) {
|
|
49
|
+
if (member === "constructor")
|
|
50
|
+
continue;
|
|
51
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, member);
|
|
52
|
+
if (HOOKS.includes(member)) {
|
|
53
|
+
hooks[member] ??= descriptor.value;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (RESERVED_RECORD_KEYS.includes(member)) {
|
|
57
|
+
throw new InvalidRecordPlugin({
|
|
58
|
+
source: name,
|
|
59
|
+
details: {
|
|
60
|
+
reason: `"${member}" is a member of every record and cannot be replaced`
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
descriptors[member] ??= descriptor;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { descriptors: Object.freeze(descriptors), hooks };
|
|
68
|
+
}
|
|
69
|
+
export function recordPluginOf(name, Record, { version = "1.0.0", description } = {}) {
|
|
70
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
71
|
+
throw new InvalidRecordPlugin({
|
|
72
|
+
source: String(name),
|
|
73
|
+
details: { reason: "its name must be a non-empty string" }
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (!isRecordClass(Record)) {
|
|
77
|
+
throw new InvalidRecordPlugin({
|
|
78
|
+
source: name,
|
|
79
|
+
details: { reason: "it must be made from a class of `recordOf`" }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (Record[IdentifiedRecord]) {
|
|
83
|
+
throw new InvalidRecordPlugin({
|
|
84
|
+
source: name,
|
|
85
|
+
details: {
|
|
86
|
+
reason: "it must be made from a record, not an entity: plugins have no identity"
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!isVersion(version)) {
|
|
91
|
+
throw new InvalidRecordPlugin({
|
|
92
|
+
source: name,
|
|
93
|
+
details: { reason: `"${version}" is not a semantic version` }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const requires = pluginsOf(Record);
|
|
97
|
+
const inherited = new Set(requires.flatMap((required) => required.keys));
|
|
98
|
+
const declaration = Record;
|
|
99
|
+
const keys = Object.freeze(Object.keys(declaration.blueprint.properties).filter((key) => !inherited.has(key)));
|
|
100
|
+
const own = membersOf(Record, name);
|
|
101
|
+
const plugin = Object.freeze({
|
|
102
|
+
[Kind]: "record-plugin",
|
|
103
|
+
name,
|
|
104
|
+
version,
|
|
105
|
+
...description === undefined ? {} : { description },
|
|
106
|
+
record: Record,
|
|
107
|
+
keys,
|
|
108
|
+
members: Object.freeze(Object.keys(own.descriptors)),
|
|
109
|
+
options: declaration.options,
|
|
110
|
+
requires,
|
|
111
|
+
is: (value) => hasPlugin(value, plugin),
|
|
112
|
+
toJSON: () => descriptorOf(plugin)
|
|
113
|
+
});
|
|
114
|
+
internals.set(plugin, own);
|
|
115
|
+
return plugin;
|
|
116
|
+
}
|
|
117
|
+
function descriptorOf(plugin) {
|
|
118
|
+
return Object.freeze({
|
|
119
|
+
kind: "record-plugin",
|
|
120
|
+
name: plugin.name,
|
|
121
|
+
version: plugin.version,
|
|
122
|
+
...plugin.description === undefined ? {} : { description: plugin.description },
|
|
123
|
+
keys: plugin.keys,
|
|
124
|
+
sensitive: plugin.options.sensitive,
|
|
125
|
+
unique: plugin.options.unique,
|
|
126
|
+
change: plugin.options.change,
|
|
127
|
+
members: plugin.members,
|
|
128
|
+
requires: Object.freeze(plugin.requires.map(({ name, version }) => Object.freeze({ name, version })))
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function applyPlugins(blueprint, plugins, name, reserved) {
|
|
132
|
+
const flat = [];
|
|
133
|
+
for (const plugin of plugins) {
|
|
134
|
+
if (!isRecordPlugin(plugin)) {
|
|
135
|
+
throw new InvalidRecordPlugin({
|
|
136
|
+
source: name,
|
|
137
|
+
details: {
|
|
138
|
+
reason: "`options.plugins` takes plugins made by `recordPluginOf`"
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (const applied of [...plugin.requires, plugin]) {
|
|
143
|
+
if (flat.includes(applied))
|
|
144
|
+
continue;
|
|
145
|
+
if (flat.some((other) => other.name === applied.name)) {
|
|
146
|
+
throw new RecordPluginConflict({
|
|
147
|
+
source: name,
|
|
148
|
+
details: {
|
|
149
|
+
plugin: applied.name,
|
|
150
|
+
key: applied.name,
|
|
151
|
+
owner: "another plugin"
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
flat.push(applied);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const owners = new Map(Object.keys(blueprint.properties).map((key) => [key, `record "${name}"`]));
|
|
159
|
+
for (const member of reserved)
|
|
160
|
+
owners.set(member, `record "${name}"`);
|
|
161
|
+
const claim = (plugin, key) => {
|
|
162
|
+
const owner = owners.get(key);
|
|
163
|
+
if (owner !== undefined) {
|
|
164
|
+
throw new RecordPluginConflict({
|
|
165
|
+
source: name,
|
|
166
|
+
details: { plugin: plugin.name, key, owner }
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
owners.set(key, `plugin "${plugin.name}"`);
|
|
170
|
+
};
|
|
171
|
+
const descriptors = {};
|
|
172
|
+
const hooks = [];
|
|
173
|
+
for (const plugin of flat) {
|
|
174
|
+
for (const key of plugin.keys)
|
|
175
|
+
claim(plugin, key);
|
|
176
|
+
const own = internals.get(plugin);
|
|
177
|
+
for (const member of plugin.members) {
|
|
178
|
+
claim(plugin, member);
|
|
179
|
+
descriptors[member] = own.descriptors[member];
|
|
180
|
+
}
|
|
181
|
+
if (own.hooks.onSet || own.hooks.onSetAll) {
|
|
182
|
+
hooks.push(Object.freeze({
|
|
183
|
+
plugin,
|
|
184
|
+
keys: Object.freeze(Object.keys(plugin.record.blueprint.properties)),
|
|
185
|
+
...own.hooks
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
blueprint: flat.length === 0 ? blueprint : blueprint.join(...flat.map((plugin) => plugin.record.blueprint)),
|
|
191
|
+
plugins: Object.freeze(flat),
|
|
192
|
+
sensitive: flat.flatMap((plugin) => plugin.options.sensitive),
|
|
193
|
+
unique: flat.flatMap((plugin) => plugin.options.unique),
|
|
194
|
+
change: flat.flatMap((plugin) => plugin.options.change),
|
|
195
|
+
descriptors,
|
|
196
|
+
hooks: Object.freeze(hooks)
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Kind } from "../symbols/kind.js";
|
|
2
|
+
import {
|
|
3
|
+
AbstractRecord,
|
|
4
|
+
exposeRecordKeys,
|
|
5
|
+
RESERVED_RECORD_KEYS,
|
|
6
|
+
RecordPluginHooks
|
|
7
|
+
} from "./abstract-record.js";
|
|
8
|
+
import {
|
|
9
|
+
InvalidRecordDeclaration,
|
|
10
|
+
RecordPluginConflict,
|
|
11
|
+
ReservedRecordKey,
|
|
12
|
+
UndeclaredRecordProperty
|
|
13
|
+
} from "./exceptions.js";
|
|
14
|
+
import { isMarked } from "./marks.js";
|
|
15
|
+
import { declareRecordName, isRecordName } from "./name.js";
|
|
16
|
+
import { applyPlugins, RecordPlugins } from "./plugin.js";
|
|
17
|
+
export function recordBuilderOf(...catalogs) {
|
|
18
|
+
const context = pluginContextOf(catalogs);
|
|
19
|
+
return (blueprint, options = {}) => declareRecord(AbstractRecord, blueprint, options, "record", [], context);
|
|
20
|
+
}
|
|
21
|
+
export function pluginContextOf(catalogs) {
|
|
22
|
+
return Object.freeze(Object.assign({}, ...catalogs.map((catalog) => catalog.plugins)));
|
|
23
|
+
}
|
|
24
|
+
export const recordOf = recordBuilderOf();
|
|
25
|
+
export function declareRecord(Base, declared, { plugins = [], name: declaredName, ...declaredOptions }, fallback, reserved = [], context = {}) {
|
|
26
|
+
if (declaredName !== undefined && !isRecordName(declaredName)) {
|
|
27
|
+
throw new InvalidRecordDeclaration({
|
|
28
|
+
source: fallback,
|
|
29
|
+
details: `"name" must be an identifier, got ${JSON.stringify(declaredName)}.`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const name = declaredName ?? fallback;
|
|
33
|
+
const applied = applyPlugins(declared, typeof plugins === "function" ? plugins(context) : plugins, name, reserved);
|
|
34
|
+
const blueprint = applied.blueprint;
|
|
35
|
+
const sensitive = [
|
|
36
|
+
...declaredOptions.sensitive ?? [],
|
|
37
|
+
...applied.sensitive
|
|
38
|
+
];
|
|
39
|
+
const unique = [...declaredOptions.unique ?? [], ...applied.unique];
|
|
40
|
+
const declaredChange = declaredOptions.change ?? [];
|
|
41
|
+
const keys = Object.keys(blueprint.properties);
|
|
42
|
+
const change = keys.filter((key) => declaredChange.includes(key) || applied.change.includes(key));
|
|
43
|
+
for (const key of [...sensitive, ...unique, ...declaredChange]) {
|
|
44
|
+
if (!keys.includes(key)) {
|
|
45
|
+
throw new UndeclaredRecordProperty({
|
|
46
|
+
source: name,
|
|
47
|
+
details: { key }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const methods = change.length > 0 ? [CHANGE_MEMBER] : [];
|
|
52
|
+
for (const member of Object.keys(applied.descriptors)) {
|
|
53
|
+
if (methods.includes(member)) {
|
|
54
|
+
const plugin = applied.plugins.find((plugin) => plugin.members.includes(member));
|
|
55
|
+
throw new RecordPluginConflict({
|
|
56
|
+
source: name,
|
|
57
|
+
details: {
|
|
58
|
+
plugin: plugin.name,
|
|
59
|
+
key: member,
|
|
60
|
+
owner: `record "${name}" (the \`change\` namespace)`
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
if (RESERVED_RECORD_KEYS.includes(key) || reserved.includes(key) || methods.includes(key)) {
|
|
67
|
+
throw new ReservedRecordKey({ source: name, details: { key } });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const options = Object.freeze({
|
|
71
|
+
sensitive: Object.freeze(keys.filter((key) => sensitive.includes(key) || isMarked(blueprint.properties[key], "sensitive", { lists: true }))),
|
|
72
|
+
unique: Object.freeze(keys.filter((key) => unique.includes(key) || isMarked(blueprint.properties[key], "unique"))),
|
|
73
|
+
change: Object.freeze([...change])
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
class DeclaredRecord extends Base {
|
|
77
|
+
static [Kind] = "record";
|
|
78
|
+
static blueprint = blueprint;
|
|
79
|
+
static options = options;
|
|
80
|
+
static plugins = applied.plugins;
|
|
81
|
+
static [RecordPlugins] = applied.plugins;
|
|
82
|
+
static [RecordPluginHooks] = applied.hooks;
|
|
83
|
+
}
|
|
84
|
+
if (declaredName !== undefined) {
|
|
85
|
+
declareRecordName(DeclaredRecord, declaredName);
|
|
86
|
+
}
|
|
87
|
+
Object.defineProperty(DeclaredRecord, "recordName", {
|
|
88
|
+
get() {
|
|
89
|
+
return declaredName ?? this.name;
|
|
90
|
+
},
|
|
91
|
+
configurable: true
|
|
92
|
+
});
|
|
93
|
+
exposeRecordKeys(DeclaredRecord.prototype, keys);
|
|
94
|
+
for (const [member, descriptor] of Object.entries(applied.descriptors)) {
|
|
95
|
+
Object.defineProperty(DeclaredRecord.prototype, member, descriptor);
|
|
96
|
+
}
|
|
97
|
+
if (change.length > 0) {
|
|
98
|
+
Object.defineProperty(DeclaredRecord.prototype, CHANGE_MEMBER, {
|
|
99
|
+
get() {
|
|
100
|
+
let namespace = changeNamespaces.get(this);
|
|
101
|
+
if (namespace === undefined) {
|
|
102
|
+
namespace = Object.freeze(Object.fromEntries(change.map((key) => [
|
|
103
|
+
key,
|
|
104
|
+
(value) => this.set(key, value)
|
|
105
|
+
])));
|
|
106
|
+
changeNamespaces.set(this, namespace);
|
|
107
|
+
}
|
|
108
|
+
return namespace;
|
|
109
|
+
},
|
|
110
|
+
configurable: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
Object.defineProperty(DeclaredRecord, "name", { value: name });
|
|
114
|
+
return DeclaredRecord;
|
|
115
|
+
}
|
|
116
|
+
const CHANGE_MEMBER = "change";
|
|
117
|
+
const changeNamespaces = new WeakMap;
|
|
@@ -155,6 +155,11 @@ type JsonValue<Properties extends IBlueprintCollection, Sensitive, Result = 0 ex
|
|
|
155
155
|
* lists `Sensitive`: the plain value, no key left out, sensitive values masked.
|
|
156
156
|
*/
|
|
157
157
|
export type RecordJson<Properties extends IBlueprintCollection, Sensitive extends string = never> = JsonValue<Properties, Sensitive>;
|
|
158
|
+
/**
|
|
159
|
+
* What every record class made by {@link recordOf} carries: the declaration
|
|
160
|
+
* metadata of its blueprint and options, and `toJSON`, which gives the value
|
|
161
|
+
* with the sensitive keys masked.
|
|
162
|
+
*/
|
|
158
163
|
export interface IRecord<Properties extends IBlueprintCollection, Sensitive extends string = never> {
|
|
159
164
|
/** Declaration metadata, separate from domain keys. */
|
|
160
165
|
readonly [Meta]: {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { InvalidRepositoryCursor } from "./exceptions.js";
|
|
2
|
+
const VERSION = 1;
|
|
3
|
+
const signatureOf = (orders) => orders.map(({ key, direction = "asc" }) => `${key}:${direction}`);
|
|
4
|
+
function pack(value) {
|
|
5
|
+
if (value === undefined || value === null)
|
|
6
|
+
return null;
|
|
7
|
+
if (value instanceof Date)
|
|
8
|
+
return { d: value.toISOString() };
|
|
9
|
+
if (typeof value === "bigint")
|
|
10
|
+
return { b: value.toString() };
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function unpack(value) {
|
|
14
|
+
if (value === null)
|
|
15
|
+
return;
|
|
16
|
+
if (["string", "number", "boolean"].includes(typeof value))
|
|
17
|
+
return value;
|
|
18
|
+
if (typeof value === "object") {
|
|
19
|
+
const { d, b } = value;
|
|
20
|
+
const keys = Object.keys(value);
|
|
21
|
+
if (keys.length === 1 && typeof d === "string") {
|
|
22
|
+
const date = new Date(d);
|
|
23
|
+
if (!Number.isNaN(date.getTime()))
|
|
24
|
+
return date;
|
|
25
|
+
}
|
|
26
|
+
if (keys.length === 1 && typeof b === "string" && /^-?\d+$/.test(b))
|
|
27
|
+
return BigInt(b);
|
|
28
|
+
}
|
|
29
|
+
throw new SyntaxError("value");
|
|
30
|
+
}
|
|
31
|
+
function encodeText(text) {
|
|
32
|
+
let binary = "";
|
|
33
|
+
for (const byte of new TextEncoder().encode(text))
|
|
34
|
+
binary += String.fromCharCode(byte);
|
|
35
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
36
|
+
}
|
|
37
|
+
function decodeText(encoded) {
|
|
38
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded))
|
|
39
|
+
throw new SyntaxError("alphabet");
|
|
40
|
+
const binary = atob(encoded.replaceAll("-", "+").replaceAll("_", "/"));
|
|
41
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
|
|
42
|
+
}
|
|
43
|
+
export function encodeCursor(orders, row) {
|
|
44
|
+
const payload = {
|
|
45
|
+
v: VERSION,
|
|
46
|
+
k: signatureOf(orders),
|
|
47
|
+
a: orders.map(({ key }) => pack(row[key]))
|
|
48
|
+
};
|
|
49
|
+
return encodeText(JSON.stringify(payload));
|
|
50
|
+
}
|
|
51
|
+
export function decodeCursor(source, orders, cursor) {
|
|
52
|
+
const fail = (reason) => {
|
|
53
|
+
throw new InvalidRepositoryCursor({
|
|
54
|
+
source,
|
|
55
|
+
details: {
|
|
56
|
+
reason,
|
|
57
|
+
issues: [
|
|
58
|
+
{ code: "invalid_value", path: ["after"], message: reason, source }
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
if (typeof cursor !== "string" || cursor === "")
|
|
64
|
+
return fail("Expected a cursor given by findCursor.");
|
|
65
|
+
let payload;
|
|
66
|
+
let values;
|
|
67
|
+
try {
|
|
68
|
+
payload = JSON.parse(decodeText(cursor));
|
|
69
|
+
if (typeof payload !== "object" || payload === null)
|
|
70
|
+
throw new SyntaxError;
|
|
71
|
+
if (!Array.isArray(payload.k) || !Array.isArray(payload.a))
|
|
72
|
+
throw new SyntaxError;
|
|
73
|
+
values = payload.a.map(unpack);
|
|
74
|
+
} catch {
|
|
75
|
+
return fail("The cursor is not one given by findCursor.");
|
|
76
|
+
}
|
|
77
|
+
if (payload.v !== VERSION)
|
|
78
|
+
return fail("The cursor was made by another version.");
|
|
79
|
+
const expected = signatureOf(orders);
|
|
80
|
+
if (payload.k?.length !== expected.length || values.length !== expected.length || expected.some((key, index) => payload.k?.[index] !== key))
|
|
81
|
+
return fail("The cursor was made for another orderBy.");
|
|
82
|
+
return values;
|
|
83
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidRepositoryQuery = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.Repository,
|
|
5
|
+
name: "InvalidRepositoryQuery",
|
|
6
|
+
message: ({ reason }) => reason
|
|
7
|
+
});
|
|
8
|
+
export const InvalidRepositoryCursor = exceptionOf({
|
|
9
|
+
layer: ExceptionLayers.Repository,
|
|
10
|
+
name: "InvalidRepositoryCursor",
|
|
11
|
+
message: ({
|
|
12
|
+
reason
|
|
13
|
+
}) => reason
|
|
14
|
+
});
|
|
15
|
+
export const InvalidRepositoryDeclaration = exceptionOf({
|
|
16
|
+
layer: ExceptionLayers.Repository,
|
|
17
|
+
name: "InvalidRepositoryDeclaration",
|
|
18
|
+
message: ({ reason }) => reason
|
|
19
|
+
});
|
|
20
|
+
export const InvalidRepositoryEntity = exceptionOf({
|
|
21
|
+
layer: ExceptionLayers.Repository,
|
|
22
|
+
name: "InvalidRepositoryEntity",
|
|
23
|
+
message: (_details, source) => `"${source}" expected an entity of its own class.`
|
|
24
|
+
});
|
|
25
|
+
export const InvalidStoredSchemaVersion = exceptionOf({
|
|
26
|
+
layer: ExceptionLayers.Repository,
|
|
27
|
+
name: "InvalidStoredSchemaVersion",
|
|
28
|
+
message: ({
|
|
29
|
+
id,
|
|
30
|
+
stored,
|
|
31
|
+
current,
|
|
32
|
+
reason
|
|
33
|
+
}, source) => reason ?? `"${source}" read the row "${id}" stored at schema version ${JSON.stringify(stored)}, and its entity is at version ${current}.`
|
|
34
|
+
});
|
|
35
|
+
export const InvalidRepositoryData = exceptionOf({
|
|
36
|
+
layer: ExceptionLayers.Repository,
|
|
37
|
+
name: "InvalidRepositoryData",
|
|
38
|
+
message: (_details, source) => `"${source}" read a value that its entity rejects.`
|
|
39
|
+
});
|
|
40
|
+
export const RepositoryUniqueViolation = exceptionOf({
|
|
41
|
+
layer: ExceptionLayers.Repository,
|
|
42
|
+
name: "RepositoryUniqueViolation",
|
|
43
|
+
message: ({ key }, source) => `"${source}" already holds an entity with this "${key}".`
|
|
44
|
+
});
|
|
45
|
+
export const RepositoryUsedOutsideApplication = exceptionOf({
|
|
46
|
+
layer: ExceptionLayers.Repository,
|
|
47
|
+
name: "RepositoryUsedOutsideApplication",
|
|
48
|
+
message: ({
|
|
49
|
+
application,
|
|
50
|
+
repository
|
|
51
|
+
}, source) => `"${source}" is bound to the application "${application}", and its declaration no longer reads or writes: use app.repositories.${repository}, run its use cases with app.run, or give the repository of the application to execute.`
|
|
52
|
+
});
|
|
53
|
+
export const RepositoryEntityNotFound = exceptionOf({
|
|
54
|
+
layer: ExceptionLayers.Repository,
|
|
55
|
+
name: "RepositoryEntityNotFound",
|
|
56
|
+
message: ({ id }, source) => `"${source}" holds no entity of id "${id}".`
|
|
57
|
+
});
|
|
58
|
+
export const RepositoryConcurrencyConflict = exceptionOf({
|
|
59
|
+
layer: ExceptionLayers.Repository,
|
|
60
|
+
name: "RepositoryConcurrencyConflict",
|
|
61
|
+
message: ({
|
|
62
|
+
id,
|
|
63
|
+
expected
|
|
64
|
+
}, source) => `"${source}" holds another version of "${id}" than ${expected}: it changed since it was read.`
|
|
65
|
+
});
|
|
66
|
+
export const InvalidRepositoryIncrement = exceptionOf({
|
|
67
|
+
layer: ExceptionLayers.Repository,
|
|
68
|
+
name: "InvalidRepositoryIncrement",
|
|
69
|
+
message: ({
|
|
70
|
+
reason
|
|
71
|
+
}) => reason
|
|
72
|
+
});
|