@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.
Files changed (186) hide show
  1. package/README.md +52 -30
  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 +10 -3
  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,209 @@
1
+ import { blueprintFromProperties } from "../blueprint/blueprint.js";
2
+ import { isRecordClass } from "../blueprint/record-field.js";
3
+ import { isProperty } from "../property/is-property.js";
4
+ import { isRecordName } from "../record/name.js";
5
+ import { IdentifiedRecord } from "../record/plugin.js";
6
+ import { declareRecord, pluginContextOf } from "../record/record.js";
7
+ import { rawTypeOf } from "../schema/schema.js";
8
+ import {
9
+ AbstractEntity,
10
+ completeOf,
11
+ ENTITY_IDENTITY_KEYS,
12
+ ENTITY_MEMBERS,
13
+ ENTITY_VERSION_KEY,
14
+ EntityClock,
15
+ EntityComplete,
16
+ EntityLifetime,
17
+ EntitySettings,
18
+ EntityUniqueKeys,
19
+ EXPIRES_AT,
20
+ RESERVED_ENTITY_KEYS,
21
+ SCHEMA_VERSION_KEY
22
+ } from "./abstract-entity.js";
23
+ import {
24
+ EntityClassBrand,
25
+ EntityEventSettings
26
+ } from "./event.js";
27
+ import {
28
+ ImmutableEntityKey,
29
+ InvalidEntityExpiry,
30
+ InvalidEntitySchemaVersion,
31
+ ReservedEntityKey
32
+ } from "./exceptions.js";
33
+ import { validMakersOf } from "./trust.js";
34
+ import { VersionIdentity } from "./version.js";
35
+ export function entityBuilderOf(settings) {
36
+ const { identity, generateId, now, expiresAt, plugins: catalogs } = settings;
37
+ const clock = Object.freeze({
38
+ generateId,
39
+ now,
40
+ makesValid: validMakersOf(settings)
41
+ });
42
+ const context = pluginContextOf(catalogs ?? []);
43
+ return (blueprint, options = {}) => {
44
+ const lifetime = lifetimeOf(blueprint, options);
45
+ const complete = completeOf(clock, lifetime);
46
+ const expiring = lifetime !== undefined;
47
+ const versioned = options.versioned === true;
48
+ const added = [
49
+ ...versioned ? [ENTITY_VERSION_KEY] : [],
50
+ ...expiring ? [EXPIRES_AT] : []
51
+ ];
52
+ if (expiring && expiresAt === undefined) {
53
+ throw new InvalidEntityExpiry({
54
+ source: options.name ?? "entity",
55
+ details: {
56
+ reason: "the entity builder has no expiresAt field, which expires: { after } needs."
57
+ }
58
+ });
59
+ }
60
+ const schemaVersioned = options.schemaVersion !== undefined;
61
+ checkSchemaOptions(options);
62
+ if (schemaVersioned)
63
+ added.push(SCHEMA_VERSION_KEY);
64
+ for (const key of Object.keys(blueprint.properties)) {
65
+ if (RESERVED_ENTITY_KEYS.includes(key) || added.includes(key)) {
66
+ throw new ReservedEntityKey({ source: "entity", details: { key } });
67
+ }
68
+ }
69
+ for (const key of options.change ?? []) {
70
+ if (ENTITY_IDENTITY_KEYS.includes(key) || added.includes(key)) {
71
+ throw new ImmutableEntityKey({ source: "entity", details: { key } });
72
+ }
73
+ }
74
+ const DeclaredEntity = declareRecord(AbstractEntity, identity.join(...versioned ? [VersionIdentity] : [], ...expiring ? [
75
+ blueprintFromProperties([], {
76
+ [EXPIRES_AT]: expiresAt
77
+ })
78
+ ] : [], blueprint), {
79
+ ...withoutStorage(options),
80
+ unique: ["id", ...options.unique ?? []]
81
+ }, "entity", ENTITY_MEMBERS, context);
82
+ Object.defineProperty(DeclaredEntity, IdentifiedRecord, { value: true });
83
+ Object.defineProperty(DeclaredEntity, EntityClock, { value: clock });
84
+ Object.defineProperty(DeclaredEntity, EntityClassBrand, { value: true });
85
+ Object.defineProperty(DeclaredEntity, EntityUniqueKeys, {
86
+ value: DeclaredEntity.options.unique
87
+ });
88
+ Object.defineProperty(DeclaredEntity, EntityLifetime, { value: lifetime });
89
+ Object.defineProperty(DeclaredEntity, EntityComplete, { value: complete });
90
+ Object.defineProperty(DeclaredEntity, EntitySettings, {
91
+ value: Object.freeze({
92
+ versioned,
93
+ expires: options.expires,
94
+ schemaVersioned,
95
+ schemaVersion: options.schemaVersion ?? 1,
96
+ upgrade: Object.freeze({ ...options.upgrade }),
97
+ previousNames: Object.freeze([...options.previousNames ?? []])
98
+ })
99
+ });
100
+ Object.defineProperty(DeclaredEntity, EntityEventSettings, {
101
+ value: Object.freeze({
102
+ onCreate: options.onCreate,
103
+ onUpdate: options.onUpdate,
104
+ onDelete: options.onDelete
105
+ })
106
+ });
107
+ return DeclaredEntity;
108
+ };
109
+ }
110
+ export function isEntityClass(value) {
111
+ return isRecordClass(value) && value.prototype instanceof AbstractEntity;
112
+ }
113
+ export function isEntity(value) {
114
+ return value instanceof AbstractEntity;
115
+ }
116
+ function checkSchemaOptions(options) {
117
+ const source = options.name ?? "entity";
118
+ const fail = (reason) => {
119
+ throw new InvalidEntitySchemaVersion({ source, details: { reason } });
120
+ };
121
+ const { schemaVersion = 1, upgrade, previousNames } = options;
122
+ if (!Number.isSafeInteger(schemaVersion) || schemaVersion < 1) {
123
+ fail("expected schemaVersion to be a positive integer.");
124
+ }
125
+ if (upgrade !== undefined) {
126
+ if (typeof upgrade !== "object" || upgrade === null) {
127
+ fail("expected upgrade to be an object of steps by version.");
128
+ }
129
+ for (const [key, step] of Object.entries(upgrade)) {
130
+ const version = Number(key);
131
+ if (!Number.isSafeInteger(version) || version < 2 || version > schemaVersion) {
132
+ fail(`the upgrade step "${key}" must reach a version from 2 to the schemaVersion (${schemaVersion}).`);
133
+ }
134
+ if (typeof step !== "function") {
135
+ fail(`the upgrade step ${key} is not a function.`);
136
+ }
137
+ }
138
+ }
139
+ if (previousNames !== undefined) {
140
+ if (!Array.isArray(previousNames)) {
141
+ fail("expected previousNames to be a list of names.");
142
+ }
143
+ for (const previous of previousNames) {
144
+ if (!isRecordName(previous) || previous === options.name) {
145
+ fail(`"${String(previous)}" cannot be a previous name: expected an identifier other than the name of the entity.`);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ function withoutStorage(options) {
151
+ const {
152
+ versioned: _versioned,
153
+ expires: _expires,
154
+ schemaVersion: _schemaVersion,
155
+ upgrade: _upgrade,
156
+ previousNames: _previousNames,
157
+ ...rest
158
+ } = options;
159
+ return rest;
160
+ }
161
+ const DURATION_UNITS = {
162
+ days: 86400000,
163
+ hours: 3600000,
164
+ minutes: 60000,
165
+ seconds: 1000
166
+ };
167
+ function isDateField(field) {
168
+ if (!isProperty(field))
169
+ return false;
170
+ const schema = rawTypeOf(field.type.schema);
171
+ return schema.type === "string" && (schema.format === "date-time" || schema.format === "date");
172
+ }
173
+ function lifetimeOf(blueprint, options) {
174
+ const { expires } = options;
175
+ if (expires === undefined)
176
+ return;
177
+ const fail = (reason) => {
178
+ throw new InvalidEntityExpiry({
179
+ source: options.name ?? "entity",
180
+ details: { reason }
181
+ });
182
+ };
183
+ const plain = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
184
+ if (!plain(expires) || Object.keys(expires).length !== 1)
185
+ return fail("expected { after: duration } or { at: key }.");
186
+ if ("at" in expires) {
187
+ const { at } = expires;
188
+ if (typeof at !== "string" || !Object.hasOwn(blueprint.properties, at))
189
+ return fail(`"${String(at)}" is not a key of the blueprint.`);
190
+ if (!isDateField(blueprint.properties[at]))
191
+ return fail(`"${at}" does not hold a date.`);
192
+ return;
193
+ }
194
+ if (!("after" in expires)) {
195
+ return fail("expected { after: duration } or { at: key }.");
196
+ }
197
+ const { after } = expires;
198
+ if (!plain(after) || Object.keys(after).length === 0)
199
+ return fail("after must be a non-empty duration, e.g. { days: 400 }.");
200
+ let total = 0;
201
+ for (const [unit, amount] of Object.entries(after)) {
202
+ if (!Object.hasOwn(DURATION_UNITS, unit))
203
+ return fail(`"${unit}" is not a unit of a duration (days, hours, minutes, seconds).`);
204
+ if (typeof amount !== "number" || !Number.isFinite(amount) || amount <= 0)
205
+ return fail(`${unit} must be a positive finite number.`);
206
+ total += amount * DURATION_UNITS[unit];
207
+ }
208
+ return total;
209
+ }
@@ -1,6 +1,10 @@
1
1
  import { type ILogger } from "../logger/logger.js";
2
2
  /** A class made by `entityOf`, including subclasses. */
3
3
  export declare const EntityClassBrand: unique symbol;
4
+ /**
5
+ * Any class made by {@link entityOf}, subclasses included: what {@link eventOf}
6
+ * takes as the entity of an event.
7
+ */
4
8
  export type AnyEntityClass = (abstract new (value: never) => object) & {
5
9
  readonly [EntityClassBrand]: true;
6
10
  };
@@ -25,7 +29,18 @@ export interface EntityEvent<Entity = object> {
25
29
  /** Runs the handler; `logger` replaces the one it would get. */
26
30
  handle(context?: Partial<EventHandlerContext>): Promise<void>;
27
31
  }
32
+ /**
33
+ * An event of the lifecycle of an entity, as `onCreate`, `onUpdate` and
34
+ * `onDelete` take it: a definition made by {@link eventOf}, a list of them, or
35
+ * a function that gives either, which lets the entity name an event declared
36
+ * after its class.
37
+ */
28
38
  export type EntityEventReference = EventDefinition | readonly EventDefinition[] | (() => EventDefinition | readonly EventDefinition[]);
39
+ /**
40
+ * The lifecycle events of an entity, the options of {@link entityOf}:
41
+ * `onCreate` is queued when a new entity is built, `onUpdate` after each
42
+ * change, and `onDelete` when it is destroyed.
43
+ */
29
44
  export interface EntityEvents {
30
45
  readonly onCreate?: EntityEventReference;
31
46
  readonly onUpdate?: EntityEventReference;
@@ -36,6 +51,30 @@ export declare const EntityEventSettings: unique symbol;
36
51
  * Defines an event without executing its handler. Without a handler, the
37
52
  * event only carries the entity, for a policy such as `publishEvents`, and
38
53
  * `handle()` does nothing.
54
+ *
55
+ * @throws {TypeError} when `entityClass` is not an entity class, or `handler` is given and is not a function.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { blueprintOf, type EventDefinition, entityOf, eventOf } from "@afora/core";
60
+ *
61
+ * class Post extends entityOf(blueprintOf((ctx) => ({ title: ctx.string }))) {
62
+ * publish(): void {
63
+ * this.emit(PostPublished);
64
+ * }
65
+ * }
66
+ *
67
+ * // The annotation breaks TypeScript's circular inference through the base class.
68
+ * const PostPublished: EventDefinition = eventOf(
69
+ * "post.published",
70
+ * Post,
71
+ * async (post, { logger }) => logger.info("post.published", { id: post.id }),
72
+ * );
73
+ *
74
+ * const post = new Post({ title: "Hi" });
75
+ * post.publish();
76
+ * for (const event of post.pullEvents()) await event.handle();
77
+ * ```
39
78
  */
40
79
  export declare function eventOf<Class extends AnyEntityClass>(name: string, entityClass: Class, handler?: (entity: InstanceType<Class>, context: EventHandlerContext) => Promise<void>): EventDefinition;
41
80
  /** Whether `event` was given a handler by `eventOf`. @internal */
@@ -0,0 +1,80 @@
1
+ import { resolveLogger, silentLogger } from "../logger/logger.js";
2
+ import { isEntityClass } from "./entity.js";
3
+ export const EntityClassBrand = Symbol("entityClass");
4
+ const EventBrand = Symbol("event");
5
+ export const EntityEventSettings = Symbol("entityEvents");
6
+ const definitions = new WeakMap;
7
+ const queues = new WeakMap;
8
+ const withHandler = new WeakSet;
9
+ export function eventOf(name, entityClass, handler) {
10
+ if (!isEntityClass(entityClass)) {
11
+ throw new TypeError("An event requires an entity class");
12
+ }
13
+ if (handler !== undefined && typeof handler !== "function") {
14
+ throw new TypeError(`The handler of event ${name} must be a function`);
15
+ }
16
+ const event = Object.freeze({
17
+ name,
18
+ [EventBrand]: true
19
+ });
20
+ if (handler)
21
+ withHandler.add(event);
22
+ definitions.set(event, (entity) => {
23
+ if (!(entity instanceof entityClass)) {
24
+ throw new TypeError(`Incompatible entity for event ${name}`);
25
+ }
26
+ return Object.freeze({
27
+ name,
28
+ entity,
29
+ async handle(context) {
30
+ if (!handler)
31
+ return;
32
+ await handler(entity, {
33
+ logger: context?.logger ?? resolveLogger(undefined, undefined) ?? silentLogger
34
+ });
35
+ }
36
+ });
37
+ });
38
+ return event;
39
+ }
40
+ export function eventHasHandler(event) {
41
+ return withHandler.has(event);
42
+ }
43
+ export function lifecycleEventsOf(entityClass) {
44
+ const settings = entityClass[EntityEventSettings];
45
+ const found = {};
46
+ for (const cycle of ["onCreate", "onUpdate", "onDelete"]) {
47
+ const reference = settings?.[cycle];
48
+ if (reference === undefined)
49
+ continue;
50
+ const events = typeof reference === "function" ? reference() : reference;
51
+ const list = Array.isArray(events) ? events : [events];
52
+ if (list.length > 0)
53
+ found[cycle] = list;
54
+ }
55
+ return found;
56
+ }
57
+ export function enqueueEvent(entity, event) {
58
+ const bind = definitions.get(event);
59
+ if (!bind)
60
+ throw new TypeError("Expected an eventOf definition");
61
+ const occurrence = bind(entity);
62
+ const queue = queues.get(entity) ?? [];
63
+ queue.push(occurrence);
64
+ queues.set(entity, queue);
65
+ }
66
+ export function pullEntityEvents(entity) {
67
+ const queue = queues.get(entity) ?? [];
68
+ queues.delete(entity);
69
+ return queue;
70
+ }
71
+ export function enqueueLifecycle(entity, cycle) {
72
+ const settings = entity.constructor[EntityEventSettings];
73
+ const reference = settings?.[cycle];
74
+ if (reference === undefined)
75
+ return;
76
+ const events = typeof reference === "function" ? reference() : reference;
77
+ for (const event of Array.isArray(events) ? events : [events]) {
78
+ enqueueEvent(entity, event);
79
+ }
80
+ }
@@ -0,0 +1,27 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ export const ReservedEntityKey = exceptionOf({
4
+ layer: ExceptionLayers.Entity,
5
+ name: "ReservedEntityKey",
6
+ message: ({ key }, source) => `"${key}" is a key or a member of every entity and cannot be a key of the blueprint of "${source}".`
7
+ });
8
+ export const ImmutableEntityKey = exceptionOf({
9
+ layer: ExceptionLayers.Entity,
10
+ name: "ImmutableEntityKey",
11
+ message: ({ key }, source) => `"${key}" of entity "${source}" cannot be changed.`
12
+ });
13
+ export const DeletedEntity = exceptionOf({
14
+ layer: ExceptionLayers.Entity,
15
+ name: "DeletedEntity",
16
+ message: ({ id }, source) => `Entity "${source}" of id "${id}" was destroyed and cannot change.`
17
+ });
18
+ export const InvalidEntityExpiry = exceptionOf({
19
+ layer: ExceptionLayers.Entity,
20
+ name: "InvalidEntityExpiry",
21
+ message: ({ reason }, source) => `Invalid expires of entity "${source}": ${reason}`
22
+ });
23
+ export const InvalidEntitySchemaVersion = exceptionOf({
24
+ layer: ExceptionLayers.Entity,
25
+ name: "InvalidEntitySchemaVersion",
26
+ message: ({ reason }, source) => `Invalid schema evolution options of entity "${source}": ${reason}`
27
+ });
@@ -0,0 +1,7 @@
1
+ export const MakesValid = Symbol("makesValid");
2
+ export function vouching(settings, makers) {
3
+ return Object.defineProperty({ ...settings }, MakesValid, { value: makers });
4
+ }
5
+ export function validMakersOf(settings) {
6
+ return settings[MakesValid];
7
+ }
@@ -0,0 +1,13 @@
1
+ import { Type } from "typebox";
2
+ import { blueprintBuilderOf } from "../blueprint/blueprint.js";
3
+ import { propertyOf } from "../property/property.js";
4
+
5
+ export class EntityVersion extends propertyOf({
6
+ name: "version",
7
+ schema: Type.Integer({ minimum: 1 }),
8
+ default: 1
9
+ }) {
10
+ }
11
+ export const VersionIdentity = blueprintBuilderOf()(() => ({
12
+ version: EntityVersion
13
+ }));
@@ -4,7 +4,11 @@ import { Kind } from "../symbols/kind.js";
4
4
  export type EnvironmentSource = () => Readonly<Record<string, string | undefined>>;
5
5
  /** What `environmentOf` takes, after the blueprint. */
6
6
  export interface EnvironmentOptions {
7
- /** The variables to read, e.g. `() => Bun.env`; `process.env` by default. */
7
+ /**
8
+ * The variables to read, e.g. `() => Bun.env`.
9
+ *
10
+ * @default `() => process.env`
11
+ */
8
12
  readonly source?: EnvironmentSource;
9
13
  }
10
14
  /**
@@ -15,7 +19,11 @@ export interface EnvironmentOptions {
15
19
  export interface IEnvironment<Properties extends IBlueprintCollection> {
16
20
  readonly [Kind]: "environment";
17
21
  readonly blueprint: IBlueprint<Properties>;
18
- /** The validated variables; throws `InvalidEnvironment` when any is rejected. */
22
+ /**
23
+ * The validated variables, read on the first call and the same after.
24
+ *
25
+ * @throws {@link InvalidEnvironment} when a variable is missing or rejected.
26
+ */
19
27
  load(): Readonly<BlueprintValue<Properties>>;
20
28
  }
21
29
  /** Any environment, whatever its blueprint. */
@@ -40,8 +48,23 @@ export declare const InvalidEnvironment: import("../../index.js").DeclaredExcept
40
48
  * boolean (`"true"`, `"false"`) or a number when its property takes one
41
49
  * instead of a string, validates them once and
42
50
  * gives the same value after. As a dependency of a repository it reaches the
43
- * provider as `context.env`; of a use case, the use case gets its value, e.g.
51
+ * provider as `context.env`; of a use case ({@link useCaseOf}), the use case gets its value, e.g.
44
52
  * `environmentOf(blueprintOf((ctx) => ({ DATABASE_URL: ctx.url })))`.
53
+ *
54
+ * @throws {TypeError} when `blueprint` is not a blueprint, or is an optional one.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { blueprintOf, environmentOf } from "@afora/core";
59
+ *
60
+ * const Env = environmentOf(
61
+ * blueprintOf((ctx) => ({ DATABASE_URL: ctx.url, PORT: ctx.integer })),
62
+ * { source: () => ({ DATABASE_URL: "https://db.example.com", PORT: "3000" }) },
63
+ * );
64
+ *
65
+ * Env.load().PORT; // 3000, converted from the string
66
+ * // Env.load() throws InvalidEnvironment when a variable is missing or invalid
67
+ * ```
45
68
  */
46
69
  export declare function environmentOf<Properties extends IBlueprintCollection>(blueprint: IBlueprint<Properties, boolean>, { source }?: EnvironmentOptions): IEnvironment<Properties>;
47
70
  /** The variables of every environment of `dependencies`, merged in order. */
@@ -0,0 +1,71 @@
1
+ import { isBlueprint } from "../blueprint/is-blueprint.js";
2
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
3
+ import { exceptionOf } from "../exception/exception.js";
4
+ import { isProperty } from "../property/is-property.js";
5
+ import { Kind } from "../symbols/kind.js";
6
+ export const InvalidEnvironment = exceptionOf({
7
+ layer: ExceptionLayers.Environment,
8
+ name: "InvalidEnvironment",
9
+ message: ({ keys }) => `Invalid environment variables: ${keys.join(", ")}.`
10
+ });
11
+ const processEnv = () => globalThis.process?.env ?? {};
12
+ export function environmentOf(blueprint, { source = processEnv } = {}) {
13
+ if (!isBlueprint(blueprint) || blueprint.optional) {
14
+ throw new TypeError("An environment requires a blueprint");
15
+ }
16
+ let value;
17
+ return Object.freeze({
18
+ [Kind]: "environment",
19
+ blueprint,
20
+ load() {
21
+ if (value !== undefined)
22
+ return value;
23
+ value = parseEnvironment(blueprint, source());
24
+ return value;
25
+ }
26
+ });
27
+ }
28
+ function parseEnvironment(blueprint, variables) {
29
+ const declared = {};
30
+ for (const [key, field] of Object.entries(blueprint.properties)) {
31
+ const variable = variables[key];
32
+ if (variable !== undefined)
33
+ declared[key] = convert(field, variable);
34
+ }
35
+ const parsed = blueprint.safeParse(declared);
36
+ if (!parsed.issues)
37
+ return Object.freeze(parsed.value);
38
+ const issues = parsed.issues;
39
+ throw new InvalidEnvironment({
40
+ source: "environment",
41
+ details: {
42
+ keys: [...new Set(issues.map((issue) => String(issue.path[0] ?? "")))]
43
+ },
44
+ cause: issues.find((issue) => issue.cause)?.cause
45
+ });
46
+ }
47
+ function convert(field, variable) {
48
+ if (!isProperty(field))
49
+ return variable;
50
+ const { schema } = field.type;
51
+ if (schema.matchShape(variable))
52
+ return variable;
53
+ const text = variable.trim();
54
+ if (text === "true" || text === "false") {
55
+ if (schema.matchShape(text === "true"))
56
+ return text === "true";
57
+ }
58
+ const number = text === "" ? Number.NaN : Number(text);
59
+ if (Number.isFinite(number) && schema.matchShape(number))
60
+ return number;
61
+ return variable;
62
+ }
63
+ export function loadEnvironments(dependencies) {
64
+ if (dependencies === undefined)
65
+ return Object.freeze({});
66
+ const list = isEnvironment(dependencies) ? [dependencies] : dependencies;
67
+ return Object.freeze(Object.assign({}, ...list.map((environment) => environment.load())));
68
+ }
69
+ export function isEnvironment(value) {
70
+ return typeof value === "object" && value !== null && value[Kind] === "environment";
71
+ }
@@ -0,0 +1,76 @@
1
+ import { frozenJson } from "../json.js";
2
+ import { Kind } from "../symbols/kind.js";
3
+ import { InvalidEventPolicy } from "./exceptions.js";
4
+ const described = new WeakMap;
5
+ const factories = new WeakMap;
6
+ function invalid(source, reason) {
7
+ throw new InvalidEventPolicy({ source, details: { reason } });
8
+ }
9
+ function optionsOf(name, given, validate) {
10
+ const options = frozenJson(given ?? {}, "Event policy options", (reason) => invalid(name, reason));
11
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
12
+ invalid(name, "Event policy options must be an object.");
13
+ }
14
+ const reason = validate?.(options);
15
+ if (reason !== undefined)
16
+ invalid(name, reason);
17
+ return options;
18
+ }
19
+ export function eventPolicyOf(settings) {
20
+ const name = settings?.name;
21
+ if (typeof name !== "string" || !/^[A-Za-z_$][\w$]*$/.test(name)) {
22
+ invalid("eventPolicyOf", "Expected the policy name to be an identifier.");
23
+ }
24
+ if (typeof settings.handle !== "function") {
25
+ invalid(name, "Expected handle to be a function.");
26
+ }
27
+ if (settings.publishers !== undefined && typeof settings.publishers !== "function") {
28
+ invalid(name, "Expected publishers to be a function.");
29
+ }
30
+ const { handle, validate, publishers } = settings;
31
+ const factory = (given) => {
32
+ const options = optionsOf(name, given, validate);
33
+ const handler = (events, info) => handle(events, info, options);
34
+ Object.defineProperty(handler, "name", { value: name });
35
+ described.set(handler, {
36
+ description: Object.freeze({
37
+ kind: "policy",
38
+ name,
39
+ options
40
+ }),
41
+ publishers: Object.freeze([...publishers?.(options) ?? []])
42
+ });
43
+ return handler;
44
+ };
45
+ factories.set(factory, name);
46
+ return factory;
47
+ }
48
+ export function eventPolicyDescriptionOf(handler) {
49
+ return typeof handler === "function" ? described.get(handler)?.description : undefined;
50
+ }
51
+ export function eventPolicyPublishersOf(handler) {
52
+ return typeof handler === "function" ? described.get(handler)?.publishers ?? [] : [];
53
+ }
54
+ export function eventPolicyCatalogOf(...policies) {
55
+ const byName = {};
56
+ for (const factory of policies) {
57
+ const name = factories.get(factory);
58
+ if (name === undefined) {
59
+ invalid("catalog", "A catalog takes factories made by `eventPolicyOf`.");
60
+ }
61
+ if (Object.hasOwn(byName, name)) {
62
+ invalid(name, "The catalog has another event policy with its name.");
63
+ }
64
+ byName[name] = factory;
65
+ }
66
+ return Object.freeze({
67
+ [Kind]: "event-policy-catalog",
68
+ policies: Object.freeze(byName),
69
+ resolve(name, options) {
70
+ const factory = Object.hasOwn(byName, name) ? byName[name] : undefined;
71
+ if (!factory)
72
+ invalid("catalog", `Unknown event policy "${name}".`);
73
+ return factory(options);
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,12 @@
1
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
2
+ import { exceptionOf } from "../exception/exception.js";
3
+ export const InvalidEventPolicy = exceptionOf({
4
+ layer: ExceptionLayers.Event,
5
+ name: "InvalidEventPolicy",
6
+ message: ({ reason }) => reason
7
+ });
8
+ export const PublisherNotBound = exceptionOf({
9
+ layer: ExceptionLayers.Application,
10
+ name: "PublisherNotBound",
11
+ message: ({ publisher }) => `No publisher "${publisher}" is declared: add it to applicationOf({ publishers }).`
12
+ });