@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.
Files changed (226) hide show
  1. package/README.md +149 -23
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +32 -0
  4. package/dist/core/application/application.d.ts +30 -0
  5. package/dist/core/application/application.js +1291 -0
  6. package/dist/core/application/exceptions.d.ts +17 -0
  7. package/dist/core/application/exceptions.js +85 -0
  8. package/dist/core/application/migrate.d.ts +19 -0
  9. package/dist/core/application/migrate.js +67 -0
  10. package/dist/core/application/providers.js +108 -0
  11. package/dist/core/application/registry.js +23 -0
  12. package/dist/core/application/schema.d.ts +30 -0
  13. package/dist/core/application/schema.js +398 -0
  14. package/dist/core/application/start.js +162 -0
  15. package/dist/core/application/types/application.interface.d.ts +60 -3
  16. package/dist/core/auth/jwt.d.ts +30 -0
  17. package/dist/core/auth/jwt.js +200 -0
  18. package/dist/core/auth/types/jwt.interface.d.ts +74 -0
  19. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  20. package/dist/core/blueprint/blueprint.js +146 -0
  21. package/dist/core/blueprint/callable.js +13 -0
  22. package/dist/core/blueprint/exceptions.d.ts +5 -0
  23. package/dist/core/blueprint/exceptions.js +20 -0
  24. package/dist/core/blueprint/is-blueprint.js +4 -0
  25. package/dist/core/blueprint/list.js +19 -0
  26. package/dist/core/blueprint/optional.d.ts +3 -1
  27. package/dist/core/blueprint/optional.js +69 -0
  28. package/dist/core/blueprint/record-field.js +14 -0
  29. package/dist/core/blueprint/slice.js +127 -0
  30. package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
  31. package/dist/core/blueprint/variant.d.ts +32 -0
  32. package/dist/core/blueprint/variant.js +72 -0
  33. package/dist/core/command/command.js +428 -0
  34. package/dist/core/command/exceptions.js +30 -0
  35. package/dist/core/command/types/command.interface.d.ts +10 -1
  36. package/dist/core/entity/abstract-entity.d.ts +24 -0
  37. package/dist/core/entity/abstract-entity.js +181 -0
  38. package/dist/core/entity/entity.d.ts +1 -1
  39. package/dist/core/entity/entity.js +209 -0
  40. package/dist/core/entity/event.d.ts +39 -0
  41. package/dist/core/entity/event.js +80 -0
  42. package/dist/core/entity/exceptions.d.ts +9 -0
  43. package/dist/core/entity/exceptions.js +27 -0
  44. package/dist/core/entity/trust.d.ts +24 -0
  45. package/dist/core/entity/trust.js +7 -0
  46. package/dist/core/entity/types/entity.interface.d.ts +30 -0
  47. package/dist/core/entity/version.js +13 -0
  48. package/dist/core/environment/environment.d.ts +26 -3
  49. package/dist/core/environment/environment.js +71 -0
  50. package/dist/core/event-policy/event-policy.js +76 -0
  51. package/dist/core/event-policy/exceptions.js +12 -0
  52. package/dist/core/event-policy/publish.js +98 -0
  53. package/dist/core/event-policy/publishers.js +18 -0
  54. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  55. package/dist/core/exception/enums/exception-layers.js +15 -0
  56. package/dist/core/exception/exception.d.ts +15 -0
  57. package/dist/core/exception/exception.js +128 -0
  58. package/dist/core/exception/exported.js +49 -0
  59. package/dist/core/exception/guard.d.ts +20 -1
  60. package/dist/core/exception/guard.js +16 -0
  61. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  62. package/dist/core/json.js +19 -0
  63. package/dist/core/logger/exceptions.js +7 -0
  64. package/dist/core/logger/logger.d.ts +14 -1
  65. package/dist/core/logger/logger.js +178 -0
  66. package/dist/core/openapi/exceptions.js +7 -0
  67. package/dist/core/openapi/openapi.js +132 -0
  68. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  69. package/dist/core/output/access.d.ts +16 -0
  70. package/dist/core/output/access.js +66 -0
  71. package/dist/core/output/coerce.js +38 -0
  72. package/dist/core/output/exceptions.d.ts +16 -0
  73. package/dist/core/output/exceptions.js +22 -0
  74. package/dist/core/output/http.d.ts +36 -0
  75. package/dist/core/output/http.js +351 -0
  76. package/dist/core/output/openapi-sink.d.ts +33 -4
  77. package/dist/core/output/openapi-sink.js +571 -0
  78. package/dist/core/output/output.js +192 -0
  79. package/dist/core/output/problem.js +30 -0
  80. package/dist/core/output/select.js +28 -0
  81. package/dist/core/output/stage.d.ts +7 -2
  82. package/dist/core/output/stage.js +111 -0
  83. package/dist/core/output/status.d.ts +1 -1
  84. package/dist/core/output/status.js +52 -0
  85. package/dist/core/output/types/http.interface.d.ts +134 -0
  86. package/dist/core/output/types/output.interface.d.ts +49 -5
  87. package/dist/core/parse/issue.d.ts +28 -3
  88. package/dist/core/parse/issue.js +31 -0
  89. package/dist/core/parse/parse.d.ts +16 -0
  90. package/dist/core/parse/parse.js +323 -0
  91. package/dist/core/parse/strip.js +48 -0
  92. package/dist/core/property/abstract-property.d.ts +9 -0
  93. package/dist/core/property/abstract-property.js +37 -0
  94. package/dist/core/property/define-property.d.ts +34 -6
  95. package/dist/core/property/define-property.js +209 -0
  96. package/dist/core/property/enums/property-states.js +8 -0
  97. package/dist/core/property/exceptions.js +12 -0
  98. package/dist/core/property/input.js +10 -0
  99. package/dist/core/property/is-property.js +3 -0
  100. package/dist/core/property/lineage.js +8 -0
  101. package/dist/core/property/property.d.ts +27 -0
  102. package/dist/core/property/property.js +46 -0
  103. package/dist/core/property/refine.js +103 -0
  104. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  105. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  106. package/dist/core/record/abstract-record.d.ts +8 -0
  107. package/dist/core/record/abstract-record.js +307 -0
  108. package/dist/core/record/exceptions.js +49 -0
  109. package/dist/core/record/marks.js +7 -0
  110. package/dist/core/record/mask.js +60 -0
  111. package/dist/core/record/name.js +23 -0
  112. package/dist/core/record/plugin-catalog.js +115 -0
  113. package/dist/core/record/plugin.js +198 -0
  114. package/dist/core/record/record.js +117 -0
  115. package/dist/core/record/types/record.interface.d.ts +17 -1
  116. package/dist/core/repository/cursor.js +83 -0
  117. package/dist/core/repository/exceptions.d.ts +13 -0
  118. package/dist/core/repository/exceptions.js +72 -0
  119. package/dist/core/repository/in-memory.d.ts +2 -2
  120. package/dist/core/repository/in-memory.js +196 -0
  121. package/dist/core/repository/migrate.d.ts +20 -0
  122. package/dist/core/repository/migrate.js +254 -0
  123. package/dist/core/repository/plan.js +27 -0
  124. package/dist/core/repository/provider.js +29 -0
  125. package/dist/core/repository/query.js +153 -0
  126. package/dist/core/repository/references.js +196 -0
  127. package/dist/core/repository/repository.d.ts +22 -2
  128. package/dist/core/repository/repository.js +720 -0
  129. package/dist/core/repository/state.d.ts +2 -0
  130. package/dist/core/repository/state.js +138 -0
  131. package/dist/core/repository/types/query.interface.d.ts +13 -0
  132. package/dist/core/repository/types/repository.interface.d.ts +50 -5
  133. package/dist/core/repository/types/schema.interface.d.ts +132 -0
  134. package/dist/core/repository/upcast.d.ts +64 -0
  135. package/dist/core/repository/upcast.js +179 -0
  136. package/dist/core/schema/schema.d.ts +37 -0
  137. package/dist/core/schema/schema.js +542 -0
  138. package/dist/core/standard/standard-schema.js +1 -0
  139. package/dist/core/standard/standard.js +23 -0
  140. package/dist/core/symbols/demo.d.ts +4 -0
  141. package/dist/core/symbols/demo.js +1 -0
  142. package/dist/core/symbols/kind.d.ts +1 -1
  143. package/dist/core/symbols/kind.js +1 -0
  144. package/dist/core/symbols/meta.js +1 -0
  145. package/dist/core/symbols/state.d.ts +5 -0
  146. package/dist/core/symbols/state.js +1 -0
  147. package/dist/core/tracing/exceptions.d.ts +4 -0
  148. package/dist/core/tracing/exceptions.js +7 -0
  149. package/dist/core/tracing/tracing.d.ts +72 -0
  150. package/dist/core/tracing/tracing.js +101 -0
  151. package/dist/core/use-case/access.d.ts +12 -0
  152. package/dist/core/use-case/access.js +56 -0
  153. package/dist/core/use-case/decorators.d.ts +40 -0
  154. package/dist/core/use-case/decorators.js +65 -0
  155. package/dist/core/use-case/exceptions.d.ts +5 -1
  156. package/dist/core/use-case/exceptions.js +48 -0
  157. package/dist/core/use-case/plugin-catalog.js +68 -0
  158. package/dist/core/use-case/plugin.js +133 -0
  159. package/dist/core/use-case/route.d.ts +18 -0
  160. package/dist/core/use-case/route.js +200 -0
  161. package/dist/core/use-case/scope.d.ts +5 -0
  162. package/dist/core/use-case/scope.js +4 -0
  163. package/dist/core/use-case/steps/describe.d.ts +5 -1
  164. package/dist/core/use-case/steps/describe.js +24 -0
  165. package/dist/core/use-case/steps/references.js +105 -0
  166. package/dist/core/use-case/steps/run.js +139 -0
  167. package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
  168. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  169. package/dist/core/use-case/steps/validate.js +368 -0
  170. package/dist/core/use-case/target.js +100 -0
  171. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  172. package/dist/core/use-case/types/route.interface.d.ts +86 -0
  173. package/dist/core/use-case/use-case.d.ts +127 -2
  174. package/dist/core/use-case/use-case.js +493 -0
  175. package/dist/experimental.d.ts +18 -1
  176. package/dist/experimental.js +18 -123
  177. package/dist/index.d.ts +49 -51
  178. package/dist/index.js +213 -443
  179. package/dist/std/blueprint-plugin.js +42 -0
  180. package/dist/std/blueprint.d.ts +21 -0
  181. package/dist/std/blueprint.js +3 -0
  182. package/dist/std/command.js +3 -0
  183. package/dist/std/entity.d.ts +20 -1
  184. package/dist/std/entity.js +30 -0
  185. package/dist/std/event-policies.js +3 -0
  186. package/dist/std/properties/contact.d.ts +3 -0
  187. package/dist/std/properties/contact.js +29 -0
  188. package/dist/std/properties/enum.js +26 -0
  189. package/dist/std/properties/exceptions.js +14 -0
  190. package/dist/std/properties/headless.js +45 -0
  191. package/dist/std/properties/identity.js +28 -0
  192. package/dist/std/properties/json.js +161 -0
  193. package/dist/std/properties/password.d.ts +0 -2
  194. package/dist/std/properties/password.js +79 -0
  195. package/dist/std/properties/primitives.d.ts +4 -0
  196. package/dist/std/properties/primitives.js +28 -0
  197. package/dist/std/properties/time.js +55 -0
  198. package/dist/std/record-plugins.d.ts +2 -2
  199. package/dist/std/record-plugins.js +73 -0
  200. package/dist/std/record.d.ts +20 -1
  201. package/dist/std/record.js +3 -0
  202. package/dist/std/resource.d.ts +31 -0
  203. package/dist/std/resource.js +39 -0
  204. package/dist/std/runtime.d.ts +6 -0
  205. package/dist/std/runtime.js +193 -0
  206. package/dist/std/stages.js +4 -0
  207. package/dist/std/use-cases/auth.d.ts +5 -1
  208. package/dist/std/use-cases/auth.js +193 -0
  209. package/dist/std/use-cases/crud.js +261 -0
  210. package/dist/std/use-cases/index.d.ts +1 -1
  211. package/dist/std/use-cases/index.js +12 -0
  212. package/dist/std/use-cases/lifecycle.js +97 -0
  213. package/dist/std/use-cases/shared.d.ts +2 -2
  214. package/dist/std/use-cases/shared.js +232 -0
  215. package/dist/std/use-cases/types.d.ts +102 -2
  216. package/dist/std/use-cases/types.js +0 -0
  217. package/dist/testing/http-provider-contract.d.ts +32 -0
  218. package/dist/testing/http-provider-contract.js +344 -0
  219. package/dist/testing/index.d.ts +2 -0
  220. package/dist/testing/index.js +3 -944
  221. package/dist/testing/memory-publisher.js +12 -0
  222. package/dist/testing/provider-contract.d.ts +26 -1
  223. package/dist/testing/provider-contract.js +1071 -0
  224. package/package.json +23 -3
  225. package/dist/index-qxfkyd6k.js +0 -1721
  226. package/dist/index-t5qwtyj5.js +0 -8194
@@ -0,0 +1,181 @@
1
+ import {
2
+ AbstractRecord,
3
+ runRecordEffects,
4
+ storeRecordValue,
5
+ vouchFor
6
+ } from "../record/abstract-record.js";
7
+ import { recordNameOf } from "../record/name.js";
8
+ import { Meta } from "../symbols/meta.js";
9
+ import {
10
+ enqueueEvent,
11
+ enqueueLifecycle,
12
+ pullEntityEvents
13
+ } from "./event.js";
14
+ import { DeletedEntity, ImmutableEntityKey } from "./exceptions.js";
15
+ export const ENTITY_IDENTITY_KEYS = Object.freeze(["id", "createdAt", "updatedAt", "deletedAt"]);
16
+ export const ENTITY_MEMBERS = Object.freeze([
17
+ "destroy",
18
+ "isDeleted",
19
+ "equals",
20
+ "onDestroy",
21
+ "emit",
22
+ "pullEvents"
23
+ ]);
24
+ export const RESERVED_ENTITY_KEYS = Object.freeze([
25
+ ...ENTITY_IDENTITY_KEYS,
26
+ ...ENTITY_MEMBERS
27
+ ]);
28
+ export const EntityClock = Symbol("entityClock");
29
+ export const EntityUniqueKeys = Symbol("entityUniqueKeys");
30
+ export const EntitySettings = Symbol("entitySettings");
31
+ export const EntityLifetime = Symbol("entityLifetime");
32
+ export const EXPIRES_AT = "expiresAt";
33
+ export const ENTITY_VERSION_KEY = "version";
34
+ export const SCHEMA_VERSION_KEY = "$v";
35
+ export function isVersionedEntityClass(entityClass) {
36
+ if (typeof entityClass !== "function")
37
+ return false;
38
+ return entityClass[EntitySettings]?.versioned === true;
39
+ }
40
+ export function identityKeysOf(entityClass) {
41
+ const keys = [...ENTITY_IDENTITY_KEYS];
42
+ if (isVersionedEntityClass(entityClass))
43
+ keys.push(ENTITY_VERSION_KEY);
44
+ if (typeof entityClass === "function" && entityClass[EntityLifetime] !== undefined) {
45
+ keys.push(EXPIRES_AT);
46
+ }
47
+ return keys;
48
+ }
49
+ function isIdentityKey(Entity, key) {
50
+ return identityKeysOf(Entity).includes(key);
51
+ }
52
+ export function storeEntityVersion(entity, version) {
53
+ storeRecordValue(entity, ENTITY_VERSION_KEY, version);
54
+ }
55
+ const ID_MADE = Object.freeze(["id"]);
56
+ const CREATED_AT_MADE = Object.freeze(["createdAt"]);
57
+ const BOTH_MADE = Object.freeze(["id", "createdAt"]);
58
+ const CREATED_AT_EXPIRES = Object.freeze([
59
+ "createdAt",
60
+ EXPIRES_AT
61
+ ]);
62
+ const BOTH_EXPIRES = Object.freeze([
63
+ "id",
64
+ "createdAt",
65
+ EXPIRES_AT
66
+ ]);
67
+ export const EntityComplete = Symbol("entityComplete");
68
+ export function completeOf(clock, lifetime) {
69
+ return (handled) => {
70
+ if (handled.createdAt === undefined)
71
+ handled.createdAt = clock.now();
72
+ if (handled.id === undefined)
73
+ handled.id = clock.generateId();
74
+ if (lifetime !== undefined && handled[EXPIRES_AT] === undefined) {
75
+ const created = handled.createdAt;
76
+ const start = typeof created === "string" ? Date.parse(created) : NaN;
77
+ if (!Number.isNaN(start)) {
78
+ handled[EXPIRES_AT] = new Date(start + lifetime).toISOString();
79
+ return [EXPIRES_AT];
80
+ }
81
+ }
82
+ return;
83
+ };
84
+ }
85
+ function identityOf(entity) {
86
+ return entity;
87
+ }
88
+ function clockOf(entity) {
89
+ return entity.constructor[EntityClock];
90
+ }
91
+ function assertAlive(entity) {
92
+ if (entity.isDeleted) {
93
+ throw new DeletedEntity({
94
+ source: recordNameOf(entity.constructor),
95
+ details: { id: identityOf(entity).id }
96
+ });
97
+ }
98
+ }
99
+ function withIdentity(value, clock, lifetime, versioned, complete) {
100
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
101
+ return value;
102
+ }
103
+ const { id, createdAt, expiresAt, version } = value;
104
+ const expiring = lifetime !== undefined && expiresAt === undefined;
105
+ const unversioned = versioned && version === undefined;
106
+ if (id !== undefined && createdAt !== undefined && !expiring && !unversioned)
107
+ return value;
108
+ const { makesValid } = clock;
109
+ const idLater = id === undefined && makesValid?.id() === true;
110
+ const createdAtLater = createdAt === undefined && makesValid?.createdAt() === true;
111
+ const expiresLater = expiring && createdAtLater;
112
+ const vouched = idLater && createdAtLater ? expiresLater ? BOTH_EXPIRES : BOTH_MADE : createdAtLater ? expiresLater ? CREATED_AT_EXPIRES : CREATED_AT_MADE : idLater ? ID_MADE : undefined;
113
+ if (!unversioned && !(expiring && !expiresLater) && (id !== undefined || idLater) && (createdAt !== undefined || createdAtLater)) {
114
+ if (vouched !== undefined)
115
+ vouchFor(vouched, complete);
116
+ return value;
117
+ }
118
+ const created = createdAt === undefined && !createdAtLater ? clock.now() : createdAt;
119
+ const identity = {
120
+ id: undefined,
121
+ createdAt: undefined,
122
+ ...value
123
+ };
124
+ identity.id = id === undefined && !idLater ? clock.generateId() : id;
125
+ identity.createdAt = created;
126
+ if (unversioned)
127
+ identity[ENTITY_VERSION_KEY] = 1;
128
+ if (expiring && !expiresLater) {
129
+ const start = typeof created === "string" ? Date.parse(created) : NaN;
130
+ if (!Number.isNaN(start)) {
131
+ identity[EXPIRES_AT] = new Date(start + lifetime).toISOString();
132
+ }
133
+ }
134
+ if (vouched !== undefined)
135
+ vouchFor(vouched, complete);
136
+ return identity;
137
+ }
138
+
139
+ export class AbstractEntity extends AbstractRecord {
140
+ constructor(value) {
141
+ super(withIdentity(value, new.target[EntityClock], new.target[EntityLifetime], isVersionedEntityClass(new.target), new.target[EntityComplete]));
142
+ const input = value;
143
+ if (input.id === undefined && input.createdAt === undefined) {
144
+ enqueueLifecycle(this, "onCreate");
145
+ }
146
+ }
147
+ emit(event) {
148
+ enqueueEvent(this, event);
149
+ }
150
+ pullEvents() {
151
+ return pullEntityEvents(this);
152
+ }
153
+ get isDeleted() {
154
+ return identityOf(this).deletedAt !== undefined;
155
+ }
156
+ equals(other) {
157
+ return other instanceof AbstractEntity && other[Meta].blueprint === this[Meta].blueprint && identityOf(other).id === identityOf(this).id;
158
+ }
159
+ destroy() {
160
+ assertAlive(this);
161
+ storeRecordValue(this, "deletedAt", clockOf(this).now());
162
+ enqueueLifecycle(this, "onDelete");
163
+ this.onDestroy();
164
+ return this;
165
+ }
166
+ set(key, value) {
167
+ const name = String(key);
168
+ if (isIdentityKey(this.constructor, name)) {
169
+ throw new ImmutableEntityKey({
170
+ source: recordNameOf(this.constructor),
171
+ details: { key: name }
172
+ });
173
+ }
174
+ assertAlive(this);
175
+ const { next, previous } = storeRecordValue(this, name, value);
176
+ storeRecordValue(this, "updatedAt", clockOf(this).now());
177
+ enqueueLifecycle(this, "onUpdate");
178
+ return runRecordEffects(this, key, next, previous);
179
+ }
180
+ onDestroy() {}
181
+ }
@@ -9,7 +9,7 @@ import type { EntityBuilder, EntityBuilderSettings, IEntityIdentity } from "./ty
9
9
  * `options.plugins` picks from the plugins of `plugins`, catalogs made by
10
10
  * `recordPluginCatalogOf`.
11
11
  */
12
- export declare function entityBuilderOf<Identity extends IEntityIdentity, const Catalogs extends readonly IRecordPluginCatalog[] = []>({ identity, generateId, now, expiresAt, plugins: catalogs, }: EntityBuilderSettings<Identity, Catalogs>): EntityBuilder<Identity, RecordPluginContext<Catalogs>>;
12
+ export declare function entityBuilderOf<Identity extends IEntityIdentity, const Catalogs extends readonly IRecordPluginCatalog[] = []>(settings: EntityBuilderSettings<Identity, Catalogs>): EntityBuilder<Identity, RecordPluginContext<Catalogs>>;
13
13
  /** Whether `value` is a class made by `entityOf`, or one extending it. */
14
14
  export declare function isEntityClass(value: unknown): value is AnyRecordClass & AnyEntityClass;
15
15
  /** Whether `value` is an entity, an instance of a class made by `entityOf`. */
@@ -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
+ }
@@ -25,3 +25,12 @@ export declare const DeletedEntity: import("../../index.js").DeclaredExceptionCl
25
25
  export declare const InvalidEntityExpiry: import("../../index.js").DeclaredExceptionClass<{
26
26
  readonly reason: string;
27
27
  }>;
28
+ /**
29
+ * The schema evolution options of an entity are invalid: `schemaVersion` is
30
+ * not a positive integer, a step of `upgrade` is not a function or reaches a
31
+ * version outside `2..schemaVersion`, or `previousNames` holds something
32
+ * that cannot name a record, or its own name; `source` is the entity name.
33
+ */
34
+ export declare const InvalidEntitySchemaVersion: import("../../index.js").DeclaredExceptionClass<{
35
+ readonly reason: string;
36
+ }>;
@@ -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,24 @@
1
+ /**
2
+ * Under this key, the settings of an entity builder tell when what their
3
+ * `generateId` and `now` make is known to be valid, so that an entity does
4
+ * not check again the id and the date it has just made. Only for a builder
5
+ * that knows its own makers, as `std` does of its defaults: a generator an
6
+ * application configures is never vouched for.
7
+ * @internal
8
+ */
9
+ export declare const MakesValid: unique symbol;
10
+ /** @internal */
11
+ export interface ValidMakers {
12
+ /** Whether the id `generateId` is about to make is one `id` accepts as it is. */
13
+ readonly id: () => boolean;
14
+ /** Whether the date `now` is about to make is one `createdAt` accepts as it is. */
15
+ readonly createdAt: () => boolean;
16
+ }
17
+ /**
18
+ * `settings` of an entity builder with `makers` vouching for its
19
+ * `generateId` and `now`.
20
+ * @internal
21
+ */
22
+ export declare function vouching<const Settings extends object>(settings: Settings, makers: ValidMakers): Settings;
23
+ /** The makers `settings` vouch for, if any. @internal */
24
+ export declare function validMakersOf(settings: object): ValidMakers | undefined;
@@ -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
+ }
@@ -75,6 +75,30 @@ export interface EntityStorageOptions<Key extends string = string, Expires exten
75
75
  * blueprint. A record with no date never expires.
76
76
  */
77
77
  readonly expires?: Expires;
78
+ /**
79
+ * The version of the stored shape of the entity, 1 when absent. When set,
80
+ * every write stores it in each row under `SCHEMA_VERSION_KEY`, and reads
81
+ * bring older rows up to it with `upgrade` before parsing them.
82
+ */
83
+ readonly schemaVersion?: number;
84
+ /**
85
+ * The steps that bring a stored row from the version before each key up
86
+ * to that key: `{ 2: (data) => ({ ...data, slug: data.path }) }`. Each
87
+ * gets the raw stored data, without the version key.
88
+ */
89
+ readonly upgrade?: EntityUpgrades;
90
+ /**
91
+ * The names the entity had before (its `name` option), so a backfill
92
+ * (`app.migrate`) moves their rows to the current name, and `start()`
93
+ * accepts the rename.
94
+ */
95
+ readonly previousNames?: readonly string[];
96
+ }
97
+ /** A step of `upgrade`: the stored data of a version, as the next one. */
98
+ export type EntityUpgrade = (data: Readonly<Record<string, unknown>>) => Record<string, unknown>;
99
+ /** The `upgrade` steps of an entity, keyed by the version each one reaches. */
100
+ export interface EntityUpgrades {
101
+ readonly [version: number]: EntityUpgrade;
78
102
  }
79
103
  /** A duration, e.g. `{ days: 400 }`. */
80
104
  export interface EntityDuration {
@@ -107,6 +131,12 @@ export type ExpiringIdentity<Identity extends IEntityIdentity, Expires> = Expire
107
131
  export interface EntityStorageSettings {
108
132
  readonly versioned: boolean;
109
133
  readonly expires?: EntityExpires;
134
+ /** Whether `schemaVersion` was declared, so rows carry the version key. */
135
+ readonly schemaVersioned: boolean;
136
+ /** `schemaVersion`, 1 when absent. */
137
+ readonly schemaVersion: number;
138
+ readonly upgrade: EntityUpgrades;
139
+ readonly previousNames: readonly string[];
110
140
  }
111
141
  /**
112
142
  * The unique keys of an entity: `id`, those listed in `options.unique`, and
@@ -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
+ }));