@afora/core 0.0.8 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -23
- package/dist/adapters.d.ts +25 -0
- package/dist/adapters.js +32 -0
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.d.ts +17 -0
- package/dist/core/application/exceptions.js +85 -0
- package/dist/core/application/migrate.d.ts +19 -0
- package/dist/core/application/migrate.js +67 -0
- package/dist/core/application/providers.js +108 -0
- package/dist/core/application/registry.js +23 -0
- package/dist/core/application/schema.d.ts +30 -0
- package/dist/core/application/schema.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +60 -3
- package/dist/core/auth/jwt.d.ts +30 -0
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +74 -0
- package/dist/core/blueprint/blueprint-plugin.js +7 -0
- package/dist/core/blueprint/blueprint.js +146 -0
- package/dist/core/blueprint/callable.js +13 -0
- package/dist/core/blueprint/exceptions.d.ts +5 -0
- package/dist/core/blueprint/exceptions.js +20 -0
- package/dist/core/blueprint/is-blueprint.js +4 -0
- package/dist/core/blueprint/list.js +19 -0
- package/dist/core/blueprint/optional.d.ts +3 -1
- package/dist/core/blueprint/optional.js +69 -0
- package/dist/core/blueprint/record-field.js +14 -0
- package/dist/core/blueprint/slice.js +127 -0
- package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
- package/dist/core/blueprint/variant.d.ts +32 -0
- package/dist/core/blueprint/variant.js +72 -0
- package/dist/core/command/command.js +428 -0
- package/dist/core/command/exceptions.js +30 -0
- package/dist/core/command/types/command.interface.d.ts +10 -1
- package/dist/core/entity/abstract-entity.d.ts +24 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- package/dist/core/entity/entity.d.ts +1 -1
- package/dist/core/entity/entity.js +209 -0
- package/dist/core/entity/event.d.ts +39 -0
- package/dist/core/entity/event.js +80 -0
- package/dist/core/entity/exceptions.d.ts +9 -0
- package/dist/core/entity/exceptions.js +27 -0
- package/dist/core/entity/trust.d.ts +24 -0
- package/dist/core/entity/trust.js +7 -0
- package/dist/core/entity/types/entity.interface.d.ts +30 -0
- package/dist/core/entity/version.js +13 -0
- package/dist/core/environment/environment.d.ts +26 -3
- package/dist/core/environment/environment.js +71 -0
- package/dist/core/event-policy/event-policy.js +76 -0
- package/dist/core/event-policy/exceptions.js +12 -0
- package/dist/core/event-policy/publish.js +98 -0
- package/dist/core/event-policy/publishers.js +18 -0
- package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
- package/dist/core/exception/enums/exception-layers.js +15 -0
- package/dist/core/exception/exception.d.ts +15 -0
- package/dist/core/exception/exception.js +128 -0
- package/dist/core/exception/exported.js +49 -0
- package/dist/core/exception/guard.d.ts +20 -1
- package/dist/core/exception/guard.js +16 -0
- package/dist/core/exception/types/exception.interface.d.ts +13 -0
- package/dist/core/json.js +19 -0
- package/dist/core/logger/exceptions.js +7 -0
- package/dist/core/logger/logger.d.ts +14 -1
- package/dist/core/logger/logger.js +178 -0
- package/dist/core/openapi/exceptions.js +7 -0
- package/dist/core/openapi/openapi.js +132 -0
- package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
- package/dist/core/output/access.d.ts +16 -0
- package/dist/core/output/access.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.d.ts +16 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/core/output/http.d.ts +36 -0
- package/dist/core/output/http.js +351 -0
- package/dist/core/output/openapi-sink.d.ts +33 -4
- package/dist/core/output/openapi-sink.js +571 -0
- package/dist/core/output/output.js +192 -0
- package/dist/core/output/problem.js +30 -0
- package/dist/core/output/select.js +28 -0
- package/dist/core/output/stage.d.ts +7 -2
- package/dist/core/output/stage.js +111 -0
- package/dist/core/output/status.d.ts +1 -1
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +134 -0
- package/dist/core/output/types/output.interface.d.ts +49 -5
- package/dist/core/parse/issue.d.ts +28 -3
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +16 -0
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.d.ts +9 -0
- package/dist/core/property/abstract-property.js +37 -0
- package/dist/core/property/define-property.d.ts +34 -6
- package/dist/core/property/define-property.js +209 -0
- package/dist/core/property/enums/property-states.js +8 -0
- package/dist/core/property/exceptions.js +12 -0
- package/dist/core/property/input.js +10 -0
- package/dist/core/property/is-property.js +3 -0
- package/dist/core/property/lineage.js +8 -0
- package/dist/core/property/property.d.ts +27 -0
- package/dist/core/property/property.js +46 -0
- package/dist/core/property/refine.js +103 -0
- package/dist/core/property/types/property-options.interface.d.ts +10 -0
- package/dist/core/property/types/property-type.interface.d.ts +4 -0
- package/dist/core/record/abstract-record.d.ts +8 -0
- package/dist/core/record/abstract-record.js +307 -0
- package/dist/core/record/exceptions.js +49 -0
- package/dist/core/record/marks.js +7 -0
- package/dist/core/record/mask.js +60 -0
- package/dist/core/record/name.js +23 -0
- package/dist/core/record/plugin-catalog.js +115 -0
- package/dist/core/record/plugin.js +198 -0
- package/dist/core/record/record.js +117 -0
- package/dist/core/record/types/record.interface.d.ts +17 -1
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.d.ts +13 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.d.ts +2 -2
- package/dist/core/repository/in-memory.js +196 -0
- package/dist/core/repository/migrate.d.ts +20 -0
- package/dist/core/repository/migrate.js +254 -0
- package/dist/core/repository/plan.js +27 -0
- package/dist/core/repository/provider.js +29 -0
- package/dist/core/repository/query.js +153 -0
- package/dist/core/repository/references.js +196 -0
- package/dist/core/repository/repository.d.ts +22 -2
- package/dist/core/repository/repository.js +720 -0
- package/dist/core/repository/state.d.ts +2 -0
- package/dist/core/repository/state.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +50 -5
- package/dist/core/repository/types/schema.interface.d.ts +132 -0
- package/dist/core/repository/upcast.d.ts +64 -0
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +37 -0
- package/dist/core/schema/schema.js +542 -0
- package/dist/core/standard/standard-schema.js +1 -0
- package/dist/core/standard/standard.js +23 -0
- package/dist/core/symbols/demo.d.ts +4 -0
- package/dist/core/symbols/demo.js +1 -0
- package/dist/core/symbols/kind.d.ts +1 -1
- package/dist/core/symbols/kind.js +1 -0
- package/dist/core/symbols/meta.js +1 -0
- package/dist/core/symbols/state.d.ts +5 -0
- package/dist/core/symbols/state.js +1 -0
- package/dist/core/tracing/exceptions.d.ts +4 -0
- package/dist/core/tracing/exceptions.js +7 -0
- package/dist/core/tracing/tracing.d.ts +72 -0
- package/dist/core/tracing/tracing.js +101 -0
- package/dist/core/use-case/access.d.ts +12 -0
- package/dist/core/use-case/access.js +56 -0
- package/dist/core/use-case/decorators.d.ts +40 -0
- package/dist/core/use-case/decorators.js +65 -0
- package/dist/core/use-case/exceptions.d.ts +5 -1
- package/dist/core/use-case/exceptions.js +48 -0
- package/dist/core/use-case/plugin-catalog.js +68 -0
- package/dist/core/use-case/plugin.js +133 -0
- package/dist/core/use-case/route.d.ts +18 -0
- package/dist/core/use-case/route.js +200 -0
- package/dist/core/use-case/scope.d.ts +5 -0
- package/dist/core/use-case/scope.js +4 -0
- package/dist/core/use-case/steps/describe.d.ts +5 -1
- package/dist/core/use-case/steps/describe.js +24 -0
- package/dist/core/use-case/steps/references.js +105 -0
- package/dist/core/use-case/steps/run.js +139 -0
- package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
- package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
- package/dist/core/use-case/steps/validate.js +368 -0
- package/dist/core/use-case/target.js +100 -0
- package/dist/core/use-case/types/access.interface.d.ts +39 -0
- package/dist/core/use-case/types/route.interface.d.ts +86 -0
- package/dist/core/use-case/use-case.d.ts +127 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +18 -1
- package/dist/experimental.js +18 -123
- package/dist/index.d.ts +49 -51
- package/dist/index.js +213 -443
- package/dist/std/blueprint-plugin.js +42 -0
- package/dist/std/blueprint.d.ts +21 -0
- package/dist/std/blueprint.js +3 -0
- package/dist/std/command.js +3 -0
- package/dist/std/entity.d.ts +20 -1
- package/dist/std/entity.js +30 -0
- package/dist/std/event-policies.js +3 -0
- package/dist/std/properties/contact.d.ts +3 -0
- package/dist/std/properties/contact.js +29 -0
- package/dist/std/properties/enum.js +26 -0
- package/dist/std/properties/exceptions.js +14 -0
- package/dist/std/properties/headless.js +45 -0
- package/dist/std/properties/identity.js +28 -0
- package/dist/std/properties/json.js +161 -0
- package/dist/std/properties/password.d.ts +0 -2
- package/dist/std/properties/password.js +79 -0
- package/dist/std/properties/primitives.d.ts +4 -0
- package/dist/std/properties/primitives.js +28 -0
- package/dist/std/properties/time.js +55 -0
- package/dist/std/record-plugins.d.ts +2 -2
- package/dist/std/record-plugins.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.d.ts +31 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.d.ts +6 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.d.ts +5 -1
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.d.ts +1 -1
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.d.ts +2 -2
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +102 -2
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.d.ts +32 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +3 -944
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +26 -1
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +23 -3
- package/dist/index-qxfkyd6k.js +0 -1721
- package/dist/index-t5qwtyj5.js +0 -8194
|
@@ -0,0 +1,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[] = []>(
|
|
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;
|
|
@@ -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
|
+
}));
|