@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.
- package/README.md +52 -30
- package/dist/adapters.js +31 -55
- package/dist/core/application/application.d.ts +30 -0
- package/dist/core/application/application.js +1291 -0
- package/dist/core/application/exceptions.js +85 -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.js +398 -0
- package/dist/core/application/start.js +162 -0
- package/dist/core/application/types/application.interface.d.ts +23 -3
- package/dist/core/auth/jwt.js +200 -0
- package/dist/core/auth/types/jwt.interface.d.ts +10 -2
- 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.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.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 +16 -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 +19 -0
- package/dist/core/entity/abstract-entity.js +181 -0
- 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.js +27 -0
- package/dist/core/entity/trust.js +7 -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.js +66 -0
- package/dist/core/output/coerce.js +38 -0
- package/dist/core/output/exceptions.js +22 -0
- package/dist/{index-y280ew0k.js → core/output/http.js} +35 -80
- package/dist/core/output/openapi-sink.d.ts +10 -2
- package/dist/{index-hg1z2s2v.js → core/output/openapi-sink.js} +60 -544
- 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.js +111 -0
- package/dist/core/output/status.js +52 -0
- package/dist/core/output/types/http.interface.d.ts +5 -1
- package/dist/core/output/types/output.interface.d.ts +24 -0
- package/dist/core/parse/issue.js +31 -0
- package/dist/core/parse/parse.d.ts +8 -1
- package/dist/core/parse/parse.js +323 -0
- package/dist/core/parse/strip.js +48 -0
- package/dist/core/property/abstract-property.js +37 -0
- 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/record/abstract-record.d.ts +2 -1
- 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 +5 -0
- package/dist/core/repository/cursor.js +83 -0
- package/dist/core/repository/exceptions.js +72 -0
- package/dist/core/repository/in-memory.js +196 -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.js +138 -0
- package/dist/core/repository/types/query.interface.d.ts +13 -0
- package/dist/core/repository/types/repository.interface.d.ts +20 -8
- package/dist/core/repository/types/schema.interface.d.ts +8 -2
- package/dist/core/repository/upcast.js +179 -0
- package/dist/core/schema/schema.d.ts +9 -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.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.js +56 -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.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 +27 -8
- 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/route.interface.d.ts +25 -5
- package/dist/core/use-case/use-case.d.ts +77 -2
- package/dist/core/use-case/use-case.js +493 -0
- package/dist/experimental.d.ts +5 -0
- package/dist/experimental.js +18 -161
- package/dist/index.d.ts +7 -5
- package/dist/index.js +213 -367
- 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.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.js +73 -0
- package/dist/std/record.d.ts +20 -1
- package/dist/std/record.js +3 -0
- package/dist/std/resource.js +39 -0
- package/dist/std/runtime.js +193 -0
- package/dist/std/stages.js +4 -0
- package/dist/std/use-cases/auth.js +193 -0
- package/dist/std/use-cases/crud.js +261 -0
- package/dist/std/use-cases/index.js +12 -0
- package/dist/std/use-cases/lifecycle.js +97 -0
- package/dist/std/use-cases/shared.js +232 -0
- package/dist/std/use-cases/types.d.ts +5 -1
- package/dist/std/use-cases/types.js +0 -0
- package/dist/testing/http-provider-contract.js +344 -0
- package/dist/testing/index.js +3 -1435
- package/dist/testing/memory-publisher.js +12 -0
- package/dist/testing/provider-contract.d.ts +12 -3
- package/dist/testing/provider-contract.js +1071 -0
- package/package.json +10 -3
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- 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,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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
});
|