@afora/core 0.0.9 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -189
- 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 +17 -5
- package/dist/index-3aae5jfq.js +0 -2997
- package/dist/index-ny3as79r.js +0 -4567
- package/dist/index-xegv6a1p.js +0 -4291
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { isBlueprint } from "../blueprint/is-blueprint.js";
|
|
2
|
+
import {
|
|
3
|
+
isEnvironment,
|
|
4
|
+
loadEnvironments
|
|
5
|
+
} from "../environment/environment.js";
|
|
6
|
+
import { resolvePublisher } from "../event-policy/publishers.js";
|
|
7
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
8
|
+
import { Exception } from "../exception/exception.js";
|
|
9
|
+
import { guard } from "../exception/guard.js";
|
|
10
|
+
import {
|
|
11
|
+
clock,
|
|
12
|
+
elapsed,
|
|
13
|
+
logFailure,
|
|
14
|
+
resolveLogger,
|
|
15
|
+
silentLogger,
|
|
16
|
+
writeLog
|
|
17
|
+
} from "../logger/logger.js";
|
|
18
|
+
import { isRepository } from "../repository/repository.js";
|
|
19
|
+
import { findState } from "../repository/state.js";
|
|
20
|
+
import { Kind } from "../symbols/kind.js";
|
|
21
|
+
import {
|
|
22
|
+
endSpan,
|
|
23
|
+
failSpan,
|
|
24
|
+
resolveTracer,
|
|
25
|
+
runInSpan,
|
|
26
|
+
startSpan
|
|
27
|
+
} from "../tracing/tracing.js";
|
|
28
|
+
import { isUseCaseCatalog } from "../use-case/plugin-catalog.js";
|
|
29
|
+
import { ScopeKey } from "../use-case/scope.js";
|
|
30
|
+
import { isUseCaseClass } from "../use-case/use-case.js";
|
|
31
|
+
import {
|
|
32
|
+
CommandEventFailed,
|
|
33
|
+
InvalidCommandContext,
|
|
34
|
+
InvalidCommandDeclaration,
|
|
35
|
+
InvalidCommandEnvironment,
|
|
36
|
+
UnknownCommandOperation
|
|
37
|
+
} from "./exceptions.js";
|
|
38
|
+
const verifiers = new WeakMap;
|
|
39
|
+
export function verifyCommand(command) {
|
|
40
|
+
verifiers.get(command)?.();
|
|
41
|
+
}
|
|
42
|
+
export function commandTransactionProvider(command) {
|
|
43
|
+
return verifiers.get(command)?.();
|
|
44
|
+
}
|
|
45
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
46
|
+
function repositoryProvidersOf(useCases, declared) {
|
|
47
|
+
const providers = new Map;
|
|
48
|
+
const seen = new Set;
|
|
49
|
+
const add = (state) => {
|
|
50
|
+
if (declared && !state.explicitProvider)
|
|
51
|
+
return;
|
|
52
|
+
const { provider, name } = state;
|
|
53
|
+
const names = providers.get(provider) ?? [];
|
|
54
|
+
if (!names.includes(name))
|
|
55
|
+
names.push(name);
|
|
56
|
+
providers.set(provider, names);
|
|
57
|
+
};
|
|
58
|
+
const visit = (UseCase) => {
|
|
59
|
+
if (seen.has(UseCase))
|
|
60
|
+
return;
|
|
61
|
+
seen.add(UseCase);
|
|
62
|
+
for (const dependency of Object.values(UseCase.dependencies)) {
|
|
63
|
+
if (isUseCaseClass(dependency)) {
|
|
64
|
+
visit(dependency);
|
|
65
|
+
} else if (isRepository(dependency)) {
|
|
66
|
+
const state = findState(dependency);
|
|
67
|
+
if (!state)
|
|
68
|
+
continue;
|
|
69
|
+
add(state);
|
|
70
|
+
for (const related of state.relations.values())
|
|
71
|
+
add(related);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const UseCase of Object.values(useCases))
|
|
76
|
+
visit(UseCase);
|
|
77
|
+
return providers;
|
|
78
|
+
}
|
|
79
|
+
function transactionProviderOf(name, useCases, declared = false) {
|
|
80
|
+
const providers = repositoryProvidersOf(useCases, declared);
|
|
81
|
+
const invalid = (reason, repositories) => {
|
|
82
|
+
throw new InvalidCommandDeclaration({
|
|
83
|
+
source: name,
|
|
84
|
+
details: { reason, repositories }
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
if (providers.size > 1) {
|
|
88
|
+
const repositories = [...providers.values()].flat();
|
|
89
|
+
invalid(`A transaction needs every repository to share one provider; found ${providers.size} (${repositories.join(", ")}).`, repositories);
|
|
90
|
+
}
|
|
91
|
+
const [entry] = providers;
|
|
92
|
+
if (!entry)
|
|
93
|
+
return;
|
|
94
|
+
const [provider, repositories] = entry;
|
|
95
|
+
if (typeof provider.transaction !== "function" || provider.capabilities?.transactions === false) {
|
|
96
|
+
invalid(`The provider of ${repositories.join(", ")} does not support transactions.`, repositories);
|
|
97
|
+
}
|
|
98
|
+
return provider;
|
|
99
|
+
}
|
|
100
|
+
function narrowed(UseCase, context) {
|
|
101
|
+
if (!UseCase.context || typeof context !== "object" || context === null) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const picked = {};
|
|
105
|
+
for (const key of Object.keys(UseCase.context.properties)) {
|
|
106
|
+
if (Object.hasOwn(context, key)) {
|
|
107
|
+
picked[key] = context[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return picked;
|
|
111
|
+
}
|
|
112
|
+
function callEnvironment(source, environments, given) {
|
|
113
|
+
if (typeof given !== "object" || given === null || Array.isArray(given)) {
|
|
114
|
+
throw new InvalidCommandEnvironment({
|
|
115
|
+
source,
|
|
116
|
+
details: { keys: [], issues: [] }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const values = given;
|
|
120
|
+
const unknownKeys = Object.keys(values).filter((key) => !environments.some((environment) => Object.hasOwn(environment.blueprint.properties, key)));
|
|
121
|
+
if (unknownKeys.length > 0) {
|
|
122
|
+
throw new InvalidCommandEnvironment({
|
|
123
|
+
source,
|
|
124
|
+
details: { keys: unknownKeys, issues: [] }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const byEnvironment = new Map;
|
|
128
|
+
const merged = {};
|
|
129
|
+
for (const environment of environments) {
|
|
130
|
+
const keys = Object.keys(environment.blueprint.properties);
|
|
131
|
+
const picked = {};
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
if (Object.hasOwn(values, key))
|
|
134
|
+
picked[key] = values[key];
|
|
135
|
+
}
|
|
136
|
+
const complete = keys.every((key) => Object.hasOwn(picked, key));
|
|
137
|
+
const base = Object.keys(picked).length === 0 || !complete ? environment.load() : {};
|
|
138
|
+
if (Object.keys(picked).length === 0) {
|
|
139
|
+
Object.assign(merged, base);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const parsed = environment.blueprint.safeParse({ ...base, ...picked });
|
|
143
|
+
if (parsed.issues) {
|
|
144
|
+
throw new InvalidCommandEnvironment({
|
|
145
|
+
source,
|
|
146
|
+
details: {
|
|
147
|
+
keys: [
|
|
148
|
+
...new Set(parsed.issues.map((issue) => String(issue.path[0] ?? "")))
|
|
149
|
+
],
|
|
150
|
+
issues: parsed.issues
|
|
151
|
+
},
|
|
152
|
+
cause: parsed.issues.find((issue) => issue.cause)?.cause
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const value = Object.freeze(parsed.value);
|
|
156
|
+
byEnvironment.set(environment, value);
|
|
157
|
+
Object.assign(merged, value);
|
|
158
|
+
}
|
|
159
|
+
return { byEnvironment, merged: Object.freeze(merged) };
|
|
160
|
+
}
|
|
161
|
+
function declareCommand(given, generics) {
|
|
162
|
+
const { name, description, context, onEventError } = given;
|
|
163
|
+
const source = typeof name === "string" ? name : "commandOf";
|
|
164
|
+
const picked = given.useCases;
|
|
165
|
+
const useCases = typeof picked === "function" && !isUseCaseClass(picked) ? guard({ layer: ExceptionLayers.Command, source }, () => picked(generics)) : picked;
|
|
166
|
+
const settings = given;
|
|
167
|
+
const invalid = (reason) => {
|
|
168
|
+
throw new InvalidCommandDeclaration({ source, details: { reason } });
|
|
169
|
+
};
|
|
170
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
171
|
+
invalid("Expected name to be a non-empty string.");
|
|
172
|
+
}
|
|
173
|
+
if (description !== undefined && typeof description !== "string") {
|
|
174
|
+
invalid("Expected description to be a string.");
|
|
175
|
+
}
|
|
176
|
+
const tags = settings.tags === undefined ? [] : settings.tags;
|
|
177
|
+
if (!Array.isArray(tags) || tags.some((tag) => typeof tag !== "string") || new Set(tags).size !== tags.length) {
|
|
178
|
+
invalid("Expected tags to be distinct strings.");
|
|
179
|
+
}
|
|
180
|
+
if (context !== undefined && (!isBlueprint(context) || context.optional)) {
|
|
181
|
+
invalid("Expected context to be a non-optional blueprint.");
|
|
182
|
+
}
|
|
183
|
+
if (typeof useCases !== "object" || useCases === null || Array.isArray(useCases) || Reflect.ownKeys(useCases).length === 0) {
|
|
184
|
+
invalid("Expected useCases to be a non-empty object of use case classes, or a function returning one.");
|
|
185
|
+
}
|
|
186
|
+
for (const key of Reflect.ownKeys(useCases)) {
|
|
187
|
+
if (typeof key !== "string" || !IDENTIFIER.test(key) || !isUseCaseClass(useCases[key])) {
|
|
188
|
+
invalid("Expected the operations to be identifiers holding use case classes.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const transaction = settings.transaction === undefined ? false : settings.transaction;
|
|
192
|
+
if (typeof transaction !== "boolean") {
|
|
193
|
+
invalid("Expected transaction to be a boolean.");
|
|
194
|
+
}
|
|
195
|
+
const declaredEnvironments = settings.environmentDependencies;
|
|
196
|
+
const environments = Object.freeze(declaredEnvironments === undefined ? [] : isEnvironment(declaredEnvironments) ? [declaredEnvironments] : Array.isArray(declaredEnvironments) ? [...declaredEnvironments] : [null]);
|
|
197
|
+
if (!environments.every(isEnvironment) || new Set(environments).size !== environments.length) {
|
|
198
|
+
invalid("Expected environmentDependencies to be distinct environments made by environmentOf.");
|
|
199
|
+
}
|
|
200
|
+
const events = settings.events === undefined ? "after-commit" : settings.events;
|
|
201
|
+
if (events !== "after-commit" && events !== "manual" && typeof events !== "function") {
|
|
202
|
+
invalid('Expected events to be "after-commit", "manual" or a function.');
|
|
203
|
+
}
|
|
204
|
+
if (onEventError !== undefined) {
|
|
205
|
+
if (typeof onEventError !== "function") {
|
|
206
|
+
invalid("Expected onEventError to be a function.");
|
|
207
|
+
}
|
|
208
|
+
if (events !== "after-commit") {
|
|
209
|
+
invalid('onEventError only applies to the "after-commit" policy.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (transaction)
|
|
213
|
+
transactionProviderOf(source, useCases, true);
|
|
214
|
+
let resolved;
|
|
215
|
+
const providerOfCall = () => {
|
|
216
|
+
if (!transaction)
|
|
217
|
+
return;
|
|
218
|
+
resolved ??= { provider: transactionProviderOf(source, useCases) };
|
|
219
|
+
return resolved.provider;
|
|
220
|
+
};
|
|
221
|
+
const operations = Object.freeze(Object.keys(useCases));
|
|
222
|
+
const frozenUseCases = Object.freeze({ ...useCases });
|
|
223
|
+
const perform = async (op, input, options, keepSpan) => {
|
|
224
|
+
if (typeof op !== "string" || !Object.hasOwn(frozenUseCases, op)) {
|
|
225
|
+
throw new UnknownCommandOperation({
|
|
226
|
+
source,
|
|
227
|
+
details: { operation: String(op) }
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const where = `${source}.${op}`;
|
|
231
|
+
const UseCase = frozenUseCases[op];
|
|
232
|
+
const tracer = resolveTracer(undefined, UseCase);
|
|
233
|
+
const span = tracer === undefined ? undefined : startSpan(tracer, `command ${where}`, { "afora.command": where, "afora.operation": op }, undefined);
|
|
234
|
+
let callContext = options?.context;
|
|
235
|
+
let given = callContext;
|
|
236
|
+
let overrides;
|
|
237
|
+
try {
|
|
238
|
+
if (context) {
|
|
239
|
+
const checked = context.safeParse(callContext);
|
|
240
|
+
if (checked.issues) {
|
|
241
|
+
throw new InvalidCommandContext({
|
|
242
|
+
source: where,
|
|
243
|
+
details: { issues: checked.issues },
|
|
244
|
+
cause: checked.issues.find((issue) => issue.cause)?.cause
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
callContext = checked.value;
|
|
248
|
+
given = narrowed(UseCase, callContext);
|
|
249
|
+
}
|
|
250
|
+
overrides = options?.env === undefined ? undefined : callEnvironment(where, environments, options.env);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
failSpan(span, error, ExceptionLayers.Command);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
const logger = resolveLogger(undefined, UseCase);
|
|
256
|
+
const withEnvironments = (scope) => {
|
|
257
|
+
const environmentsOfCall = overrides === undefined || overrides.byEnvironment.size === 0 ? scope?.environments : overrides.byEnvironment;
|
|
258
|
+
if (!environmentsOfCall && !logger && !tracer)
|
|
259
|
+
return scope;
|
|
260
|
+
return {
|
|
261
|
+
...scope,
|
|
262
|
+
providers: scope?.providers ?? new Map,
|
|
263
|
+
...environmentsOfCall ? { environments: environmentsOfCall } : {},
|
|
264
|
+
...logger ? { logger } : {},
|
|
265
|
+
...tracer ? { tracer, ...span ? { span } : {} } : {}
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
const run = async (declared) => {
|
|
269
|
+
const scope = withEnvironments(declared);
|
|
270
|
+
const useCase = new UseCase(input, { context: given, [ScopeKey]: scope });
|
|
271
|
+
const value = await useCase.execute();
|
|
272
|
+
return { value, events: useCase.pullEvents() };
|
|
273
|
+
};
|
|
274
|
+
const provider = providerOfCall();
|
|
275
|
+
const started = clock();
|
|
276
|
+
const outcome = await guard({ layer: ExceptionLayers.Command, source: where }, span === undefined || tracer === undefined ? () => provider?.transaction ? provider.transaction((scoped) => run({ providers: new Map([[provider, scoped]]) })) : run(undefined) : () => runInSpan(tracer, span, () => provider?.transaction ? provider.transaction((scoped) => run({ providers: new Map([[provider, scoped]]) })) : run(undefined))).catch((error) => {
|
|
277
|
+
logFailure(logger, "command-failed", { command: where, ms: elapsed(started) }, error);
|
|
278
|
+
failSpan(span, error, ExceptionLayers.Command);
|
|
279
|
+
throw error;
|
|
280
|
+
});
|
|
281
|
+
writeLog(logger, "debug", "command", {
|
|
282
|
+
command: where,
|
|
283
|
+
ms: elapsed(started)
|
|
284
|
+
});
|
|
285
|
+
if (!keepSpan)
|
|
286
|
+
endSpan(span);
|
|
287
|
+
return {
|
|
288
|
+
...outcome,
|
|
289
|
+
context: callContext,
|
|
290
|
+
env: () => overrides?.merged ?? loadEnvironments(environments),
|
|
291
|
+
logger: logger ?? silentLogger,
|
|
292
|
+
tracer,
|
|
293
|
+
span
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
const command = {
|
|
297
|
+
[Kind]: "command",
|
|
298
|
+
commandName: name,
|
|
299
|
+
description,
|
|
300
|
+
tags: Object.freeze([...tags]),
|
|
301
|
+
context,
|
|
302
|
+
useCases: frozenUseCases,
|
|
303
|
+
transaction,
|
|
304
|
+
environments,
|
|
305
|
+
get env() {
|
|
306
|
+
return loadEnvironments(environments);
|
|
307
|
+
},
|
|
308
|
+
events,
|
|
309
|
+
onEventError,
|
|
310
|
+
operations,
|
|
311
|
+
async execute(op, input, options) {
|
|
312
|
+
const { value, events } = await perform(op, input, options, false);
|
|
313
|
+
return { value, events };
|
|
314
|
+
},
|
|
315
|
+
async run(op, input, options) {
|
|
316
|
+
const {
|
|
317
|
+
value,
|
|
318
|
+
events: raised,
|
|
319
|
+
context: callContext,
|
|
320
|
+
env,
|
|
321
|
+
logger,
|
|
322
|
+
tracer,
|
|
323
|
+
span
|
|
324
|
+
} = await perform(op, input, options, true);
|
|
325
|
+
const call = {
|
|
326
|
+
operation: op,
|
|
327
|
+
raised,
|
|
328
|
+
context: callContext,
|
|
329
|
+
env,
|
|
330
|
+
logger
|
|
331
|
+
};
|
|
332
|
+
if (span === undefined || tracer === undefined) {
|
|
333
|
+
await applyPolicy(command, events, onEventError, call);
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await runInSpan(tracer, span, () => applyPolicy(command, events, onEventError, call));
|
|
338
|
+
} catch (error) {
|
|
339
|
+
failSpan(span, error, ExceptionLayers.Command);
|
|
340
|
+
throw error;
|
|
341
|
+
}
|
|
342
|
+
endSpan(span);
|
|
343
|
+
return value;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
Object.freeze(command);
|
|
347
|
+
verifiers.set(command, providerOfCall);
|
|
348
|
+
return command;
|
|
349
|
+
}
|
|
350
|
+
export function commandBuilderOf(...catalogs) {
|
|
351
|
+
if (!catalogs.every(isUseCaseCatalog)) {
|
|
352
|
+
throw new InvalidCommandDeclaration({
|
|
353
|
+
source: "commandBuilderOf",
|
|
354
|
+
details: {
|
|
355
|
+
reason: "Expected catalogs made by useCaseCatalogOf."
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const generics = Object.freeze(Object.assign({}, ...catalogs.map((catalog) => catalog.context)));
|
|
360
|
+
return (settings) => declareCommand(settings, generics);
|
|
361
|
+
}
|
|
362
|
+
export const commandOf = commandBuilderOf();
|
|
363
|
+
async function applyPolicy(command, policy, onEventError, call) {
|
|
364
|
+
const { operation, raised } = call;
|
|
365
|
+
const source = `${command.commandName}.${operation}`;
|
|
366
|
+
if (policy === "manual")
|
|
367
|
+
return;
|
|
368
|
+
const info = {
|
|
369
|
+
command,
|
|
370
|
+
operation,
|
|
371
|
+
context: call.context,
|
|
372
|
+
get env() {
|
|
373
|
+
return call.env();
|
|
374
|
+
},
|
|
375
|
+
logger: call.logger,
|
|
376
|
+
publisher: (name) => resolvePublisher(command, name, source)
|
|
377
|
+
};
|
|
378
|
+
if (typeof policy === "function") {
|
|
379
|
+
try {
|
|
380
|
+
await policy(raised, info);
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
if (cause instanceof Exception && cause.name === "CommandEventFailed") {
|
|
383
|
+
throw cause;
|
|
384
|
+
}
|
|
385
|
+
throw new CommandEventFailed({
|
|
386
|
+
source,
|
|
387
|
+
cause,
|
|
388
|
+
details: { operation, handled: 0, remaining: raised.length }
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const [index, event] of raised.entries()) {
|
|
394
|
+
try {
|
|
395
|
+
await event.handle({ logger: call.logger });
|
|
396
|
+
} catch (cause) {
|
|
397
|
+
if (!onEventError) {
|
|
398
|
+
throw new CommandEventFailed({
|
|
399
|
+
source,
|
|
400
|
+
cause,
|
|
401
|
+
details: {
|
|
402
|
+
operation,
|
|
403
|
+
event: event.name,
|
|
404
|
+
handled: index,
|
|
405
|
+
remaining: raised.length - index - 1
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
await onEventError(cause, event, info);
|
|
411
|
+
} catch (failure) {
|
|
412
|
+
throw new CommandEventFailed({
|
|
413
|
+
source,
|
|
414
|
+
cause: failure,
|
|
415
|
+
details: {
|
|
416
|
+
operation,
|
|
417
|
+
event: event.name,
|
|
418
|
+
handled: index,
|
|
419
|
+
remaining: raised.length - index - 1
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
export function isCommand(value) {
|
|
427
|
+
return typeof value === "object" && value !== null && value[Kind] === "command";
|
|
428
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ExceptionLayers } from "../exception/enums/exception-layers.js";
|
|
2
|
+
import { exceptionOf } from "../exception/exception.js";
|
|
3
|
+
export const InvalidCommandDeclaration = exceptionOf({
|
|
4
|
+
layer: ExceptionLayers.Command,
|
|
5
|
+
name: "InvalidCommandDeclaration",
|
|
6
|
+
message: ({ reason }) => reason
|
|
7
|
+
});
|
|
8
|
+
export const UnknownCommandOperation = exceptionOf({
|
|
9
|
+
layer: ExceptionLayers.Command,
|
|
10
|
+
name: "UnknownCommandOperation",
|
|
11
|
+
message: ({ operation }) => `Unknown command operation "${operation}".`
|
|
12
|
+
});
|
|
13
|
+
export const InvalidCommandContext = exceptionOf({
|
|
14
|
+
layer: ExceptionLayers.Command,
|
|
15
|
+
name: "InvalidCommandContext",
|
|
16
|
+
message: (_details) => "Invalid Command context."
|
|
17
|
+
});
|
|
18
|
+
export const InvalidCommandEnvironment = exceptionOf({
|
|
19
|
+
layer: ExceptionLayers.Command,
|
|
20
|
+
name: "InvalidCommandEnvironment",
|
|
21
|
+
message: (_details) => "Invalid Command environment."
|
|
22
|
+
});
|
|
23
|
+
export const CommandEventFailed = exceptionOf({
|
|
24
|
+
layer: ExceptionLayers.Command,
|
|
25
|
+
name: "CommandEventFailed",
|
|
26
|
+
message: ({
|
|
27
|
+
operation,
|
|
28
|
+
event
|
|
29
|
+
}) => event === undefined ? `The event handler of "${operation}" failed after the call succeeded.` : `The event "${event}" of "${operation}" failed after the call succeeded.`
|
|
30
|
+
});
|
|
@@ -37,6 +37,11 @@ export type CommandEventHandler = (events: readonly EntityEvent[], info: Command
|
|
|
37
37
|
export type CommandEvents = "after-commit" | "manual" | CommandEventHandler;
|
|
38
38
|
/** Called when an event fails with the `"after-commit"` policy; the next event still runs. */
|
|
39
39
|
export type CommandEventErrorHandler = (error: unknown, event: EntityEvent, info: CommandEventInfo) => void | Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* What {@link commandOf} takes: the `name` and the `useCases` of the command,
|
|
42
|
+
* the `context` a call supplies, and how a call runs (`transaction`,
|
|
43
|
+
* `environmentDependencies`, `events` and `onEventError`).
|
|
44
|
+
*/
|
|
40
45
|
export interface CommandSettings<UseCases extends CommandUseCases, Context extends IBlueprintCollection | undefined = undefined, Environment extends EnvironmentDependencies | undefined = undefined> {
|
|
41
46
|
readonly name: string;
|
|
42
47
|
readonly description?: string;
|
|
@@ -44,7 +49,11 @@ export interface CommandSettings<UseCases extends CommandUseCases, Context exten
|
|
|
44
49
|
/** What the caller supplies with each call; validated once by this blueprint. */
|
|
45
50
|
readonly context?: IBlueprint<Exclude<Context, undefined>>;
|
|
46
51
|
readonly useCases: UseCases;
|
|
47
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Runs every call in a transaction of the provider of the repositories.
|
|
54
|
+
*
|
|
55
|
+
* @default false
|
|
56
|
+
*/
|
|
48
57
|
readonly transaction?: boolean;
|
|
49
58
|
/**
|
|
50
59
|
* One environment, or a list merged in order: read on first use as
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlueprintInput, BlueprintValue, IBlueprintCollection } from "../blueprint/types/blueprint.interface.js";
|
|
2
|
+
import type { Complete } from "../parse/parse.js";
|
|
2
3
|
import { AbstractRecord } from "../record/abstract-record.js";
|
|
3
4
|
import { type EntityEvent, type EventDefinition } from "./event.js";
|
|
4
5
|
import type { EntityIdentityKey } from "./types/entity.interface.js";
|
|
@@ -43,6 +44,24 @@ export declare function identityKeysOf(entityClass: unknown): readonly string[];
|
|
|
43
44
|
* a write succeeded. @internal
|
|
44
45
|
*/
|
|
45
46
|
export declare function storeEntityVersion(entity: object, version: number): void;
|
|
47
|
+
/**
|
|
48
|
+
* Under this key, a class made by `entityOf` holds what makes its id, its
|
|
49
|
+
* `createdAt` and its `expiresAt` once its value is known to be valid.
|
|
50
|
+
*
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export declare const EntityComplete: unique symbol;
|
|
54
|
+
/**
|
|
55
|
+
* What an entity class does with the values its builder vouched for and left
|
|
56
|
+
* out, after the rest of its value is known to be valid: `createdAt` first,
|
|
57
|
+
* then `id`, as they were made before; and, for a class that expires, an
|
|
58
|
+
* `expiresAt` `lifetime` milliseconds after `createdAt`, which is still
|
|
59
|
+
* checked.
|
|
60
|
+
*/
|
|
61
|
+
export declare function completeOf(clock: {
|
|
62
|
+
readonly generateId: () => string;
|
|
63
|
+
readonly now: () => string;
|
|
64
|
+
}, lifetime: number | undefined): Complete;
|
|
46
65
|
/**
|
|
47
66
|
* A record with identity: an `id` and a `createdAt`, made when it is built
|
|
48
67
|
* and never changed after, an `updatedAt` set by each change, and a
|
|
@@ -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
|
+
}
|