@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.
Files changed (186) hide show
  1. package/README.md +62 -189
  2. package/dist/adapters.js +31 -55
  3. package/dist/core/application/application.d.ts +30 -0
  4. package/dist/core/application/application.js +1291 -0
  5. package/dist/core/application/exceptions.js +85 -0
  6. package/dist/core/application/migrate.js +67 -0
  7. package/dist/core/application/providers.js +108 -0
  8. package/dist/core/application/registry.js +23 -0
  9. package/dist/core/application/schema.js +398 -0
  10. package/dist/core/application/start.js +162 -0
  11. package/dist/core/application/types/application.interface.d.ts +23 -3
  12. package/dist/core/auth/jwt.js +200 -0
  13. package/dist/core/auth/types/jwt.interface.d.ts +10 -2
  14. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  15. package/dist/core/blueprint/blueprint.js +146 -0
  16. package/dist/core/blueprint/callable.js +13 -0
  17. package/dist/core/blueprint/exceptions.js +20 -0
  18. package/dist/core/blueprint/is-blueprint.js +4 -0
  19. package/dist/core/blueprint/list.js +19 -0
  20. package/dist/core/blueprint/optional.js +69 -0
  21. package/dist/core/blueprint/record-field.js +14 -0
  22. package/dist/core/blueprint/slice.js +127 -0
  23. package/dist/core/blueprint/types/blueprint.interface.d.ts +16 -0
  24. package/dist/core/blueprint/variant.js +72 -0
  25. package/dist/core/command/command.js +428 -0
  26. package/dist/core/command/exceptions.js +30 -0
  27. package/dist/core/command/types/command.interface.d.ts +10 -1
  28. package/dist/core/entity/abstract-entity.d.ts +19 -0
  29. package/dist/core/entity/abstract-entity.js +181 -0
  30. package/dist/core/entity/entity.js +209 -0
  31. package/dist/core/entity/event.d.ts +39 -0
  32. package/dist/core/entity/event.js +80 -0
  33. package/dist/core/entity/exceptions.js +27 -0
  34. package/dist/core/entity/trust.js +7 -0
  35. package/dist/core/entity/version.js +13 -0
  36. package/dist/core/environment/environment.d.ts +26 -3
  37. package/dist/core/environment/environment.js +71 -0
  38. package/dist/core/event-policy/event-policy.js +76 -0
  39. package/dist/core/event-policy/exceptions.js +12 -0
  40. package/dist/core/event-policy/publish.js +98 -0
  41. package/dist/core/event-policy/publishers.js +18 -0
  42. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  43. package/dist/core/exception/enums/exception-layers.js +15 -0
  44. package/dist/core/exception/exception.d.ts +15 -0
  45. package/dist/core/exception/exception.js +128 -0
  46. package/dist/core/exception/exported.js +49 -0
  47. package/dist/core/exception/guard.d.ts +20 -1
  48. package/dist/core/exception/guard.js +16 -0
  49. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  50. package/dist/core/json.js +19 -0
  51. package/dist/core/logger/exceptions.js +7 -0
  52. package/dist/core/logger/logger.d.ts +14 -1
  53. package/dist/core/logger/logger.js +178 -0
  54. package/dist/core/openapi/exceptions.js +7 -0
  55. package/dist/core/openapi/openapi.js +132 -0
  56. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  57. package/dist/core/output/access.js +66 -0
  58. package/dist/core/output/coerce.js +38 -0
  59. package/dist/core/output/exceptions.js +22 -0
  60. package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
  61. package/dist/core/output/openapi-sink.d.ts +10 -2
  62. package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
  63. package/dist/core/output/output.js +192 -0
  64. package/dist/core/output/problem.js +30 -0
  65. package/dist/core/output/select.js +28 -0
  66. package/dist/core/output/stage.js +111 -0
  67. package/dist/core/output/status.js +52 -0
  68. package/dist/core/output/types/http.interface.d.ts +5 -1
  69. package/dist/core/output/types/output.interface.d.ts +24 -0
  70. package/dist/core/parse/issue.js +31 -0
  71. package/dist/core/parse/parse.d.ts +8 -1
  72. package/dist/core/parse/parse.js +323 -0
  73. package/dist/core/parse/strip.js +48 -0
  74. package/dist/core/property/abstract-property.js +37 -0
  75. package/dist/core/property/define-property.js +209 -0
  76. package/dist/core/property/enums/property-states.js +8 -0
  77. package/dist/core/property/exceptions.js +12 -0
  78. package/dist/core/property/input.js +10 -0
  79. package/dist/core/property/is-property.js +3 -0
  80. package/dist/core/property/lineage.js +8 -0
  81. package/dist/core/property/property.d.ts +27 -0
  82. package/dist/core/property/property.js +46 -0
  83. package/dist/core/property/refine.js +103 -0
  84. package/dist/core/record/abstract-record.d.ts +2 -1
  85. package/dist/core/record/abstract-record.js +307 -0
  86. package/dist/core/record/exceptions.js +49 -0
  87. package/dist/core/record/marks.js +7 -0
  88. package/dist/core/record/mask.js +60 -0
  89. package/dist/core/record/name.js +23 -0
  90. package/dist/core/record/plugin-catalog.js +115 -0
  91. package/dist/core/record/plugin.js +198 -0
  92. package/dist/core/record/record.js +117 -0
  93. package/dist/core/record/types/record.interface.d.ts +5 -0
  94. package/dist/core/repository/cursor.js +83 -0
  95. package/dist/core/repository/exceptions.js +72 -0
  96. package/dist/core/repository/in-memory.js +196 -0
  97. package/dist/core/repository/migrate.js +254 -0
  98. package/dist/core/repository/plan.js +27 -0
  99. package/dist/core/repository/provider.js +29 -0
  100. package/dist/core/repository/query.js +153 -0
  101. package/dist/core/repository/references.js +196 -0
  102. package/dist/core/repository/repository.d.ts +22 -2
  103. package/dist/core/repository/repository.js +720 -0
  104. package/dist/core/repository/state.js +138 -0
  105. package/dist/core/repository/types/query.interface.d.ts +13 -0
  106. package/dist/core/repository/types/repository.interface.d.ts +20 -8
  107. package/dist/core/repository/types/schema.interface.d.ts +8 -2
  108. package/dist/core/repository/upcast.js +179 -0
  109. package/dist/core/schema/schema.d.ts +9 -0
  110. package/dist/core/schema/schema.js +542 -0
  111. package/dist/core/standard/standard-schema.js +1 -0
  112. package/dist/core/standard/standard.js +23 -0
  113. package/dist/core/symbols/demo.d.ts +4 -0
  114. package/dist/core/symbols/demo.js +1 -0
  115. package/dist/core/symbols/kind.js +1 -0
  116. package/dist/core/symbols/meta.js +1 -0
  117. package/dist/core/symbols/state.d.ts +5 -0
  118. package/dist/core/symbols/state.js +1 -0
  119. package/dist/core/tracing/exceptions.d.ts +4 -0
  120. package/dist/core/tracing/exceptions.js +7 -0
  121. package/dist/core/tracing/tracing.d.ts +72 -0
  122. package/dist/core/tracing/tracing.js +101 -0
  123. package/dist/core/use-case/access.js +56 -0
  124. package/dist/core/use-case/decorators.js +65 -0
  125. package/dist/core/use-case/exceptions.d.ts +5 -1
  126. package/dist/core/use-case/exceptions.js +48 -0
  127. package/dist/core/use-case/plugin-catalog.js +68 -0
  128. package/dist/core/use-case/plugin.js +133 -0
  129. package/dist/core/use-case/route.js +200 -0
  130. package/dist/core/use-case/scope.d.ts +5 -0
  131. package/dist/core/use-case/scope.js +4 -0
  132. package/dist/core/use-case/steps/describe.d.ts +5 -1
  133. package/dist/core/use-case/steps/describe.js +24 -0
  134. package/dist/core/use-case/steps/references.js +105 -0
  135. package/dist/core/use-case/steps/run.js +139 -0
  136. package/dist/core/use-case/steps/types/check.interface.d.ts +27 -8
  137. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  138. package/dist/core/use-case/steps/validate.js +368 -0
  139. package/dist/core/use-case/target.js +100 -0
  140. package/dist/core/use-case/types/route.interface.d.ts +25 -5
  141. package/dist/core/use-case/use-case.d.ts +77 -2
  142. package/dist/core/use-case/use-case.js +493 -0
  143. package/dist/experimental.d.ts +5 -0
  144. package/dist/experimental.js +18 -161
  145. package/dist/index.d.ts +7 -5
  146. package/dist/index.js +213 -367
  147. package/dist/std/blueprint-plugin.js +42 -0
  148. package/dist/std/blueprint.d.ts +21 -0
  149. package/dist/std/blueprint.js +3 -0
  150. package/dist/std/command.js +3 -0
  151. package/dist/std/entity.d.ts +20 -1
  152. package/dist/std/entity.js +30 -0
  153. package/dist/std/event-policies.js +3 -0
  154. package/dist/std/properties/contact.d.ts +3 -0
  155. package/dist/std/properties/contact.js +29 -0
  156. package/dist/std/properties/enum.js +26 -0
  157. package/dist/std/properties/exceptions.js +14 -0
  158. package/dist/std/properties/headless.js +45 -0
  159. package/dist/std/properties/identity.js +28 -0
  160. package/dist/std/properties/json.js +161 -0
  161. package/dist/std/properties/password.js +79 -0
  162. package/dist/std/properties/primitives.d.ts +4 -0
  163. package/dist/std/properties/primitives.js +28 -0
  164. package/dist/std/properties/time.js +55 -0
  165. package/dist/std/record-plugins.js +73 -0
  166. package/dist/std/record.d.ts +20 -1
  167. package/dist/std/record.js +3 -0
  168. package/dist/std/resource.js +39 -0
  169. package/dist/std/runtime.js +193 -0
  170. package/dist/std/stages.js +4 -0
  171. package/dist/std/use-cases/auth.js +193 -0
  172. package/dist/std/use-cases/crud.js +261 -0
  173. package/dist/std/use-cases/index.js +12 -0
  174. package/dist/std/use-cases/lifecycle.js +97 -0
  175. package/dist/std/use-cases/shared.js +232 -0
  176. package/dist/std/use-cases/types.d.ts +5 -1
  177. package/dist/std/use-cases/types.js +0 -0
  178. package/dist/testing/http-provider-contract.js +344 -0
  179. package/dist/testing/index.js +3 -1435
  180. package/dist/testing/memory-publisher.js +12 -0
  181. package/dist/testing/provider-contract.d.ts +12 -3
  182. package/dist/testing/provider-contract.js +1071 -0
  183. package/package.json +17 -5
  184. package/dist/index-3aae5jfq.js +0 -2997
  185. package/dist/index-ny3as79r.js +0 -4567
  186. 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
+ });