@afora/core 0.0.8 → 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 +149 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +32 -0
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.d.ts +19 -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.d.ts +30 -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 +60 -3
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +74 -0
- 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.d.ts +5 -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.d.ts +3 -1
- 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 +95 -5
- package/dist/core/blueprint/variant.d.ts +32 -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 +24 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.d.ts +1 -1
- 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.d.ts +9 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -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.d.ts +16 -0
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/http.js +351 -0
- package/dist/core/output/openapi-sink.d.ts +33 -4
- package/dist/core/output/openapi-sink.js +571 -0
- 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.d.ts +7 -2
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +134 -0
- package/dist/core/output/types/output.interface.d.ts +49 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +16 -0
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.d.ts +34 -6
- 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/property/types/property-options.interface.d.ts +10 -0
- package/dist/core/property/types/property-type.interface.d.ts +4 -0
- package/dist/core/record/abstract-record.d.ts +8 -0
- 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 +17 -1
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.d.ts +20 -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.d.ts +2 -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 +50 -5
- package/dist/core/repository/types/schema.interface.d.ts +132 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +37 -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.d.ts +1 -1
- 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.d.ts +12 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.d.ts +40 -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.d.ts +18 -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 +233 -0
- 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/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +86 -0
- package/dist/core/use-case/use-case.d.ts +127 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +18 -1
- package/dist/experimental.js +18 -123
- package/dist/index.d.ts +49 -51
- package/dist/index.js +213 -443
- 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.d.ts +0 -2
- 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.d.ts +2 -2
- 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.d.ts +31 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +102 -2
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +3 -944
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +26 -1
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +23 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
|
@@ -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;
|
|
@@ -120,7 +120,18 @@ type JsonField<Field, Data> = Field extends {
|
|
|
120
120
|
readonly properties: infer Properties extends IBlueprintCollection;
|
|
121
121
|
} ? Data extends object ? JsonKeys<Properties, Data, never> : Data : Field extends {
|
|
122
122
|
readonly item: infer Item;
|
|
123
|
-
} ? Data extends readonly (infer ItemData)[] ? JsonField<Item, ItemData>[] : Data :
|
|
123
|
+
} ? Data extends readonly (infer ItemData)[] ? JsonField<Item, ItemData>[] : Data : Field extends {
|
|
124
|
+
readonly key: infer Key extends string;
|
|
125
|
+
readonly variants: infer Variants;
|
|
126
|
+
} ? JsonVariant<Key, Variants, Data> : Data;
|
|
127
|
+
/** What `toJSON` gives for the value `Data` of a variant field: each variant by its own blueprint. */
|
|
128
|
+
type JsonVariant<Key extends string, Variants, Data> = Data extends {
|
|
129
|
+
readonly [Name in Key]: infer Tag extends keyof Variants;
|
|
130
|
+
} ? Variants[Tag] extends AnyRecordClass ? {
|
|
131
|
+
readonly [Name in Key]: Tag;
|
|
132
|
+
} & JsonOfRecord<Variants[Tag], Data> : Variants[Tag] extends {
|
|
133
|
+
readonly properties: infer Properties extends IBlueprintCollection;
|
|
134
|
+
} ? JsonKeys<Properties, Data, never> : Data : Data;
|
|
124
135
|
/** What the `toJSON` of the records of `Class` gives, in place of their plain value. */
|
|
125
136
|
type JsonOfRecord<Class extends AnyRecordClass, Data> = InstanceType<Class> extends {
|
|
126
137
|
toJSON(): infer Json;
|
|
@@ -144,6 +155,11 @@ type JsonValue<Properties extends IBlueprintCollection, Sensitive, Result = 0 ex
|
|
|
144
155
|
* lists `Sensitive`: the plain value, no key left out, sensitive values masked.
|
|
145
156
|
*/
|
|
146
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
|
+
*/
|
|
147
163
|
export interface IRecord<Properties extends IBlueprintCollection, Sensitive extends string = never> {
|
|
148
164
|
/** Declaration metadata, separate from domain keys. */
|
|
149
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
|
+
}
|
|
@@ -18,6 +18,19 @@ export declare const InvalidRepositoryDeclaration: import("../../index.js").Decl
|
|
|
18
18
|
}>;
|
|
19
19
|
/** A method of a repository got a value that is not an entity of it. */
|
|
20
20
|
export declare const InvalidRepositoryEntity: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* A stored row holds a schema version (`SCHEMA_VERSION_KEY`) that is not a
|
|
23
|
+
* positive integer, or that is above the `schemaVersion` of its entity (it
|
|
24
|
+
* was written by newer code), or an `upgrade` step did not give an object.
|
|
25
|
+
* `source` is the repository name; reads throw it as the cause of
|
|
26
|
+
* `InvalidRepositoryData`.
|
|
27
|
+
*/
|
|
28
|
+
export declare const InvalidStoredSchemaVersion: import("../../index.js").DeclaredExceptionClass<{
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly stored: unknown;
|
|
31
|
+
readonly current: number;
|
|
32
|
+
readonly reason?: string;
|
|
33
|
+
}>;
|
|
21
34
|
/** A provider gave a value its entity rejects; `source` is the repository name. */
|
|
22
35
|
export declare const InvalidRepositoryData: import("../../index.js").DeclaredExceptionClass<undefined>;
|
|
23
36
|
/** Another entity holds the value of a unique key; `source` is the repository name. */
|
|
@@ -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
|
+
});
|
|
@@ -13,8 +13,8 @@ export type InMemoryProvider = IRepositoryProvider & {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
16
|
-
* A provider that keeps the values in memory, one table per entity
|
|
17
|
-
* enforcing its unique keys. Each `repositoryOf` without a provider gets its
|
|
16
|
+
* A provider that keeps the values in memory, one table per entity name it
|
|
17
|
+
* declares (or per entity class, without one), enforcing its unique keys. Each `repositoryOf` without a provider gets its
|
|
18
18
|
* own; repositories given the same one share the tables of their entities.
|
|
19
19
|
* Its `transaction` works on isolated snapshots: commits on success, discards
|
|
20
20
|
* on rejection, runs one at a time and makes other operations wait; nested
|