graphddb 0.7.8 → 0.7.10
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/dist/cdc/index.d.ts +3 -2
- package/dist/cdc/index.js +3 -2
- package/dist/chunk-3UD3XIF2.js +860 -0
- package/dist/{chunk-HFFIB77D.js → chunk-3ZU2VW3L.js} +2 -1189
- package/dist/{chunk-IA6MW2HP.js → chunk-AD6ZQTTE.js} +4 -2
- package/dist/chunk-DFUKGU2Q.js +1197 -0
- package/dist/{chunk-NZRCBEWS.js → chunk-EOJDN3SA.js} +302 -728
- package/dist/cli.js +389 -29
- package/dist/{from-change-w2Ih8fkm.d.ts → from-change-Ty95KA8C.d.ts} +1 -1
- package/dist/{index-Deugy2sa.d.ts → index-Dc7d8mWI.d.ts} +161 -6
- package/dist/index.d.ts +127 -115
- package/dist/index.js +196 -59
- package/dist/linter/index.d.ts +5 -4
- package/dist/{maintenance-view-adapter-BCbgKG5d.d.ts → maintenance-view-adapter-BAZ9uBGe.d.ts} +122 -1091
- package/dist/{registry-pAnFcc62.d.ts → registry-LWE54Sdc.d.ts} +1 -1
- package/dist/{relation-depth-C9t4s9bt.d.ts → relation-depth-BRS513Tq.d.ts} +1 -1
- package/dist/spec/index.d.ts +4 -3
- package/dist/spec/index.js +18 -3
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +3 -2
- package/dist/transform/index.d.ts +460 -1
- package/dist/transform/index.js +2084 -1
- package/dist/types-BQLzTEqh.d.ts +1207 -0
- package/docs/python-bridge.md +17 -1
- package/package.json +7 -1
|
@@ -0,0 +1,1197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MetadataRegistry,
|
|
3
|
+
attachHiddenKey
|
|
4
|
+
} from "./chunk-3ZU2VW3L.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveKey,
|
|
7
|
+
segmentFieldNames
|
|
8
|
+
} from "./chunk-PDUVTYC5.js";
|
|
9
|
+
|
|
10
|
+
// src/types/select-builder.ts
|
|
11
|
+
var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
|
|
12
|
+
var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
|
|
13
|
+
|
|
14
|
+
// src/select/builder.ts
|
|
15
|
+
var BuilderImpl = class _BuilderImpl {
|
|
16
|
+
[SELECT_BUILDER] = true;
|
|
17
|
+
/** The resolved spec, exposed (non-method) so normalizers can read it. */
|
|
18
|
+
[SELECT_SPEC];
|
|
19
|
+
constructor(spec) {
|
|
20
|
+
this[SELECT_SPEC] = spec;
|
|
21
|
+
}
|
|
22
|
+
clone(patch) {
|
|
23
|
+
return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
|
|
24
|
+
}
|
|
25
|
+
filter(filter) {
|
|
26
|
+
return this.clone({ filter });
|
|
27
|
+
}
|
|
28
|
+
limit(limit) {
|
|
29
|
+
return this.clone({ limit });
|
|
30
|
+
}
|
|
31
|
+
after(cursor) {
|
|
32
|
+
return this.clone({ after: cursor });
|
|
33
|
+
}
|
|
34
|
+
order(order) {
|
|
35
|
+
return this.clone({ order });
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
function isSelectBuilder(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
|
|
40
|
+
}
|
|
41
|
+
function fromBuilder(value) {
|
|
42
|
+
const spec = value[SELECT_SPEC];
|
|
43
|
+
return {
|
|
44
|
+
select: spec.select,
|
|
45
|
+
filter: spec.filter,
|
|
46
|
+
limit: spec.limit,
|
|
47
|
+
after: spec.after,
|
|
48
|
+
order: spec.order
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function normalizeSelectSpec(value) {
|
|
52
|
+
if (isSelectBuilder(value)) {
|
|
53
|
+
return fromBuilder(value);
|
|
54
|
+
}
|
|
55
|
+
const obj = value;
|
|
56
|
+
return {
|
|
57
|
+
select: obj.select,
|
|
58
|
+
filter: obj.filter,
|
|
59
|
+
limit: obj.limit,
|
|
60
|
+
after: obj.after,
|
|
61
|
+
order: obj.order
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function normalizeTopLevelSelect(value) {
|
|
65
|
+
if (isSelectBuilder(value)) {
|
|
66
|
+
return fromBuilder(value);
|
|
67
|
+
}
|
|
68
|
+
return { select: value ?? {} };
|
|
69
|
+
}
|
|
70
|
+
function buildProject(select) {
|
|
71
|
+
return new BuilderImpl({
|
|
72
|
+
[SELECT_BUILDER]: true,
|
|
73
|
+
select
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function buildRelation(select) {
|
|
77
|
+
return new BuilderImpl({
|
|
78
|
+
[SELECT_BUILDER]: true,
|
|
79
|
+
select
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/relation/detect.ts
|
|
84
|
+
function isRelationSpec(value) {
|
|
85
|
+
if (typeof value !== "object" || value === null) return false;
|
|
86
|
+
return "select" in value || isSelectBuilder(value);
|
|
87
|
+
}
|
|
88
|
+
function isInlineSnapshotSpec(value) {
|
|
89
|
+
return typeof value === "object" && value !== null && !isSelectBuilder(value) && value.inline === true && !("select" in value);
|
|
90
|
+
}
|
|
91
|
+
function detectRelationFields(select, metadata) {
|
|
92
|
+
const result = [];
|
|
93
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
94
|
+
if (isRelationSpec(value)) {
|
|
95
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
96
|
+
if (rel) {
|
|
97
|
+
result.push(rel);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function validateInlineSnapshotSelect(select, metadata, resolveTargetMeta) {
|
|
104
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
105
|
+
if (isInlineSnapshotSpec(value)) {
|
|
106
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
107
|
+
if (!rel) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation, but '${fieldName}' is not a relation on '${metadata.prefix}'. Use '${fieldName}: true' for a scalar / embedded field.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (rel.options?.pattern !== "embeddedSnapshot") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation. '${fieldName}' is a plain '${rel.type}' relation (no 'pattern: embeddedSnapshot'); use '{ ${fieldName}: { select: \u2026 } }' to traverse to its child rows instead.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (typeof value === "object" && value !== null && isRelationSpec(value)) {
|
|
120
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
121
|
+
if (!rel) continue;
|
|
122
|
+
const targetMeta = resolveTargetMeta(rel);
|
|
123
|
+
const nested = normalizeSelectSpec(value).select;
|
|
124
|
+
if (nested) {
|
|
125
|
+
validateInlineSnapshotSelect(nested, targetMeta, resolveTargetMeta);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function getImplicitKeyFields(select, metadata) {
|
|
131
|
+
const relations = detectRelationFields(select, metadata);
|
|
132
|
+
const needed = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const rel of relations) {
|
|
134
|
+
if (rel.type === "refs" && rel.refs) {
|
|
135
|
+
const listField = rel.refs.from;
|
|
136
|
+
if (!(listField in select) || select[listField] !== true) {
|
|
137
|
+
needed.add(listField);
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
for (const sourceField of Object.values(rel.keyBinding)) {
|
|
142
|
+
if (!(sourceField in select) || select[sourceField] !== true) {
|
|
143
|
+
needed.add(sourceField);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return [...needed];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/hydrator/hydrator.ts
|
|
151
|
+
function hydrate(rawItems, select, metadata, updatable = false) {
|
|
152
|
+
return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
|
|
153
|
+
}
|
|
154
|
+
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
155
|
+
const fieldMap = new Map(
|
|
156
|
+
metadata.fields.map((f) => [f.propertyName, f])
|
|
157
|
+
);
|
|
158
|
+
const embeddedMap = new Map(
|
|
159
|
+
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
160
|
+
);
|
|
161
|
+
const result = {};
|
|
162
|
+
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
163
|
+
if (selectValue === true) {
|
|
164
|
+
const value = raw[fieldName];
|
|
165
|
+
if (value !== void 0) {
|
|
166
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
167
|
+
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
168
|
+
}
|
|
169
|
+
} else if (isInlineSnapshotSpec(selectValue)) {
|
|
170
|
+
const value = raw[fieldName];
|
|
171
|
+
if (value !== void 0) {
|
|
172
|
+
result[fieldName] = value;
|
|
173
|
+
}
|
|
174
|
+
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
175
|
+
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const rawEmb = raw[fieldName];
|
|
179
|
+
if (rawEmb && typeof rawEmb === "object") {
|
|
180
|
+
result[fieldName] = hydrateEmbedded(
|
|
181
|
+
rawEmb,
|
|
182
|
+
selectValue,
|
|
183
|
+
metadata,
|
|
184
|
+
fieldName
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (updatable) {
|
|
190
|
+
attachHiddenKey(result, raw);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
195
|
+
const result = {};
|
|
196
|
+
for (const [key, selectValue] of Object.entries(select)) {
|
|
197
|
+
if (selectValue === true) {
|
|
198
|
+
if (raw[key] !== void 0) {
|
|
199
|
+
result[key] = raw[key];
|
|
200
|
+
}
|
|
201
|
+
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
202
|
+
const nested = raw[key];
|
|
203
|
+
if (nested && typeof nested === "object") {
|
|
204
|
+
result[key] = hydrateEmbedded(
|
|
205
|
+
nested,
|
|
206
|
+
selectValue,
|
|
207
|
+
_metadata,
|
|
208
|
+
key
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
function deserializeValue(value, fieldMeta) {
|
|
216
|
+
if (!fieldMeta) return value;
|
|
217
|
+
if (fieldMeta.options?.deserialize) {
|
|
218
|
+
return fieldMeta.options.deserialize(value);
|
|
219
|
+
}
|
|
220
|
+
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
221
|
+
return new Date(value);
|
|
222
|
+
}
|
|
223
|
+
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
224
|
+
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/define/entity-writes.ts
|
|
230
|
+
var MAINTAIN_TRIGGER_RE = /^[^.\s$[\]*]+\.(?:created|updated|removed)$/;
|
|
231
|
+
function maintainTrigger(value) {
|
|
232
|
+
if (typeof value !== "string" || !MAINTAIN_TRIGGER_RE.test(value)) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`maintainTrigger: a maintenance trigger must be an \`Entity.event\` string where event is one of \`created\` / \`updated\` / \`removed\` (e.g. \`"Post.created"\`), but received ${JSON.stringify(value)}.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
function isMaintainTrigger(value) {
|
|
240
|
+
return typeof value === "string" && MAINTAIN_TRIGGER_RE.test(value);
|
|
241
|
+
}
|
|
242
|
+
function makeProjectionTransform(path, op, args) {
|
|
243
|
+
if (op === "preview") {
|
|
244
|
+
const [n] = args;
|
|
245
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`transform: the \`preview\` op requires a positive integer length bound (e.g. \`preview('$.body', 200)\`), but received ${JSON.stringify(n)}.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return { kind: "transform", path, op, args: [n] };
|
|
251
|
+
}
|
|
252
|
+
if (op === "identity") {
|
|
253
|
+
if (args.length > 0) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`transform: the \`identity\` op takes no arguments, but received ${JSON.stringify(args)}.`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return { kind: "transform", path, op, args: [] };
|
|
259
|
+
}
|
|
260
|
+
throw new Error(
|
|
261
|
+
`transform: unknown projection op ${JSON.stringify(op)} (expected \`identity\` or \`preview\`).`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
function pathOf(value) {
|
|
265
|
+
return typeof value === "string" ? value : value.path;
|
|
266
|
+
}
|
|
267
|
+
function preview(path, n) {
|
|
268
|
+
return makeProjectionTransform(pathOf(path), "preview", [n]);
|
|
269
|
+
}
|
|
270
|
+
function identity(path) {
|
|
271
|
+
return makeProjectionTransform(pathOf(path), "identity", []);
|
|
272
|
+
}
|
|
273
|
+
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
274
|
+
"graphddb:lifecycleContract"
|
|
275
|
+
);
|
|
276
|
+
function isLifecycleContract(value) {
|
|
277
|
+
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
278
|
+
}
|
|
279
|
+
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
280
|
+
function isEntityWritesDefinition(value) {
|
|
281
|
+
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
282
|
+
}
|
|
283
|
+
function freezeEffects(effects) {
|
|
284
|
+
const out = {};
|
|
285
|
+
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
286
|
+
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
287
|
+
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
288
|
+
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
289
|
+
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
290
|
+
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
var recorder = {
|
|
294
|
+
lifecycle(effects = {}) {
|
|
295
|
+
return {
|
|
296
|
+
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
297
|
+
effects: freezeEffects(effects)
|
|
298
|
+
};
|
|
299
|
+
},
|
|
300
|
+
exists(targetFactory, keys) {
|
|
301
|
+
return { kind: "requires", targetFactory, keys };
|
|
302
|
+
},
|
|
303
|
+
unique(spec) {
|
|
304
|
+
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
305
|
+
},
|
|
306
|
+
putEdge(targetFactory, relationProperty) {
|
|
307
|
+
return { kind: "putEdge", targetFactory, relationProperty };
|
|
308
|
+
},
|
|
309
|
+
deleteEdge(targetFactory, relationProperty) {
|
|
310
|
+
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
311
|
+
},
|
|
312
|
+
dualEdge(forward, inverse) {
|
|
313
|
+
return {
|
|
314
|
+
kind: "putEdge",
|
|
315
|
+
targetFactory: forward.target,
|
|
316
|
+
relationProperty: forward.relationProperty,
|
|
317
|
+
inverse: {
|
|
318
|
+
adjacencyFactory: inverse.adjacency,
|
|
319
|
+
targetFactory: inverse.target,
|
|
320
|
+
relationProperty: inverse.relationProperty
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
increment(targetFactory, keys, attribute, amount) {
|
|
325
|
+
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
326
|
+
},
|
|
327
|
+
transform(path, op, ...args) {
|
|
328
|
+
return makeProjectionTransform(path, op, args);
|
|
329
|
+
},
|
|
330
|
+
event(name, payload) {
|
|
331
|
+
return { kind: "event", name, payload };
|
|
332
|
+
},
|
|
333
|
+
idempotentBy(token) {
|
|
334
|
+
return { kind: "idempotency", token };
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
function entityWrites(builder) {
|
|
338
|
+
const shape = builder(recorder);
|
|
339
|
+
for (const phase of ["create", "update", "remove"]) {
|
|
340
|
+
const contract = shape[phase];
|
|
341
|
+
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
[ENTITY_WRITES_MARKER]: true,
|
|
354
|
+
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
355
|
+
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
356
|
+
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function getEntityWrites(modelClass) {
|
|
360
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
361
|
+
let value;
|
|
362
|
+
try {
|
|
363
|
+
value = modelClass[name];
|
|
364
|
+
} catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (isEntityWritesDefinition(value)) return value;
|
|
368
|
+
}
|
|
369
|
+
return void 0;
|
|
370
|
+
}
|
|
371
|
+
function lifecyclePhaseForIntent(intent) {
|
|
372
|
+
return intent;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/relation/maintenance-view-adapter.ts
|
|
376
|
+
function logicalNameOf(meta) {
|
|
377
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
378
|
+
}
|
|
379
|
+
function resolveClass(factory) {
|
|
380
|
+
return factory();
|
|
381
|
+
}
|
|
382
|
+
function sourceLogicalName(sourceClass) {
|
|
383
|
+
const meta = MetadataRegistry.get(sourceClass);
|
|
384
|
+
return logicalNameOf(meta);
|
|
385
|
+
}
|
|
386
|
+
function triggersFor(sourceLogical, on) {
|
|
387
|
+
return on.map((ev) => `${sourceLogical}.${ev}`);
|
|
388
|
+
}
|
|
389
|
+
function assertNoCrossSliceCollision(viewName, decls) {
|
|
390
|
+
const projectionOwner = /* @__PURE__ */ new Map();
|
|
391
|
+
const collectionOwner = /* @__PURE__ */ new Map();
|
|
392
|
+
let keyFieldShape;
|
|
393
|
+
decls.forEach((d, idx) => {
|
|
394
|
+
for (const attr of Object.keys(d.project)) {
|
|
395
|
+
const prev = projectionOwner.get(attr);
|
|
396
|
+
if (prev !== void 0 && prev !== idx) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`View '${viewName}': projection target '${attr}' is maintained by more than one \`@maintainedFrom\` declaration. A view field must be filled by exactly one source \u2014 the declaration order carries no meaning, so this is ambiguous. Remove '${attr}' from one of the declarations (or split it onto a distinct view field).`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
projectionOwner.set(attr, idx);
|
|
402
|
+
}
|
|
403
|
+
if (d.collectionField !== void 0) {
|
|
404
|
+
const prev = collectionOwner.get(d.collectionField);
|
|
405
|
+
if (prev !== void 0 && prev !== idx) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`View '${viewName}': collection field '${d.collectionField}' is maintained by more than one \`@maintainedFrom\` declaration. A maintained collection has exactly one source \u2014 split the second source onto a distinct collection field.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
collectionOwner.set(d.collectionField, idx);
|
|
411
|
+
}
|
|
412
|
+
const shape = Object.keys(d.keyBind).slice().sort().join(",");
|
|
413
|
+
if (keyFieldShape === void 0) {
|
|
414
|
+
keyFieldShape = shape;
|
|
415
|
+
} else if (keyFieldShape !== shape) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`View '${viewName}': \`@maintainedFrom\` declarations bind different view-key field SETS ([${keyFieldShape}] vs [${shape}]). All sources must resolve the SAME view row identity \u2014 they must bind the same set of view-key fields (the source field a given key reads from may differ per source, but the key SHAPE may not). A divergent key shape would split the row.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
function viewFromMaintainedFrom(viewClass, meta, kind) {
|
|
423
|
+
const viewName = viewClass.name ?? logicalNameOf(meta);
|
|
424
|
+
const decls = meta.maintainedFrom ?? [];
|
|
425
|
+
const pattern = kind === "sparseView" ? "sparseView" : "materializedView";
|
|
426
|
+
for (const d of decls) {
|
|
427
|
+
if (kind === "sparseView" && !d.when) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`View '${viewName}': \`kind: 'sparseView'\` requires every \`@maintainedFrom\` to declare a \`when\` membership predicate (e.g. \`whenMember(source.status, 'eq', 'active')\`) \u2014 the predicate decides PUT (holds) vs DELETE (flips false).`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (kind !== "sparseView" && d.when) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`View '${viewName}': a \`@maintainedFrom\` declares a \`when\` membership predicate but the view \`kind\` is '${kind}'. Declare \`kind: 'sparseView'\` to maintain a predicate-gated put/delete row.`
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
assertNoCrossSliceCollision(
|
|
439
|
+
viewName,
|
|
440
|
+
decls.map((d) => ({
|
|
441
|
+
keyBind: d.keyBind,
|
|
442
|
+
project: d.project,
|
|
443
|
+
collectionField: d.collection?.field
|
|
444
|
+
}))
|
|
445
|
+
);
|
|
446
|
+
let updateMode;
|
|
447
|
+
let consistency;
|
|
448
|
+
for (const d of decls) {
|
|
449
|
+
if (d.updateMode !== void 0) {
|
|
450
|
+
if (updateMode !== void 0 && updateMode !== d.updateMode) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`View '${viewName}': conflicting \`updateMode\` across \`@maintainedFrom\` declarations ('${updateMode}' vs '${d.updateMode}'). A view row has a single write path \u2014 declare the same \`updateMode\` on every source.`
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
updateMode = d.updateMode;
|
|
456
|
+
}
|
|
457
|
+
if (d.consistency !== void 0) {
|
|
458
|
+
if (consistency !== void 0 && consistency !== d.consistency) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`View '${viewName}': conflicting \`consistency\` across \`@maintainedFrom\` declarations ('${consistency}' vs '${d.consistency}').`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
consistency = d.consistency;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const slices = decls.map((d) => sliceFromDeclaration(d));
|
|
467
|
+
return {
|
|
468
|
+
name: viewName,
|
|
469
|
+
viewClass,
|
|
470
|
+
pattern,
|
|
471
|
+
slices,
|
|
472
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
473
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function sliceFromDeclaration(d) {
|
|
477
|
+
const sourceClass = resolveClass(d.sourceFactory);
|
|
478
|
+
const sourceLogical = sourceLogicalName(sourceClass);
|
|
479
|
+
const collection = d.collection ? {
|
|
480
|
+
field: d.collection.field,
|
|
481
|
+
...d.collection.maxItems !== void 0 ? { maxItems: d.collection.maxItems } : {},
|
|
482
|
+
...d.collection.orderBy !== void 0 ? { orderBy: d.collection.orderBy, orderDir: d.collection.order ?? "DESC" } : {}
|
|
483
|
+
} : void 0;
|
|
484
|
+
return {
|
|
485
|
+
sourceClass,
|
|
486
|
+
maintainedOn: triggersFor(sourceLogical, d.on),
|
|
487
|
+
keys: d.keyBind,
|
|
488
|
+
project: d.project,
|
|
489
|
+
...collection ? { collection } : {},
|
|
490
|
+
...d.when ? { predicate: d.when } : {}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function versionedViews(ownerClass, ownerMeta) {
|
|
494
|
+
const ownerLogical = logicalNameOf(ownerMeta);
|
|
495
|
+
const rels = [];
|
|
496
|
+
for (const rel of ownerMeta.relations) {
|
|
497
|
+
const v = rel.options?.versioned;
|
|
498
|
+
if (!v) continue;
|
|
499
|
+
const targetClass = resolveClass(rel.targetFactory);
|
|
500
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
501
|
+
if (!targetMeta.primaryKey) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`Versioned relation '${rel.propertyName}' on '${ownerLogical}': the target model '${targetClass.name}' has no \`key\` \u2014 a versioned latest/history row needs a key to resolve which row to overwrite / append.`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
const keyFields = [
|
|
507
|
+
...segmentFieldNames(targetMeta.primaryKey.segmented.pkSegments),
|
|
508
|
+
...segmentFieldNames(targetMeta.primaryKey.segmented.skSegments)
|
|
509
|
+
];
|
|
510
|
+
rels.push({
|
|
511
|
+
propertyName: rel.propertyName,
|
|
512
|
+
mode: v.mode,
|
|
513
|
+
on: v.on,
|
|
514
|
+
project: v.project,
|
|
515
|
+
...v.updateMode !== void 0 ? { updateMode: v.updateMode } : {},
|
|
516
|
+
...v.consistency !== void 0 ? { consistency: v.consistency } : {},
|
|
517
|
+
targetClass,
|
|
518
|
+
targetMeta,
|
|
519
|
+
keyFields
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
assertVersionedDiscriminator(ownerLogical, rels);
|
|
523
|
+
const out = [];
|
|
524
|
+
for (const rel of rels) {
|
|
525
|
+
const keys = {};
|
|
526
|
+
for (const f of rel.keyFields) keys[f] = `$.entity.${f}`;
|
|
527
|
+
const targetName = rel.targetClass.name ?? logicalNameOf(rel.targetMeta);
|
|
528
|
+
out.push({
|
|
529
|
+
name: targetName,
|
|
530
|
+
viewClass: rel.targetClass,
|
|
531
|
+
pattern: "materializedView",
|
|
532
|
+
slices: [
|
|
533
|
+
{
|
|
534
|
+
sourceClass: ownerClass,
|
|
535
|
+
maintainedOn: triggersFor(ownerLogical, rel.on),
|
|
536
|
+
keys,
|
|
537
|
+
project: rel.project
|
|
538
|
+
}
|
|
539
|
+
],
|
|
540
|
+
...rel.updateMode !== void 0 ? { updateMode: rel.updateMode } : {},
|
|
541
|
+
...rel.consistency !== void 0 ? { consistency: rel.consistency } : {}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return out;
|
|
545
|
+
}
|
|
546
|
+
function assertVersionedDiscriminator(ownerLogical, rels) {
|
|
547
|
+
const latests = rels.filter((r) => r.mode === "latest");
|
|
548
|
+
const histories = rels.filter((r) => r.mode === "history");
|
|
549
|
+
if (latests.length === 0 || histories.length === 0) return;
|
|
550
|
+
for (const latest of latests) {
|
|
551
|
+
const latestKeyFields = new Set(latest.keyFields);
|
|
552
|
+
for (const history of histories) {
|
|
553
|
+
const discriminators = history.keyFields.filter((f) => !latestKeyFields.has(f));
|
|
554
|
+
if (discriminators.length === 0) {
|
|
555
|
+
const latestName = latest.targetClass.name ?? "<latest>";
|
|
556
|
+
const historyName = history.targetClass.name ?? "<history>";
|
|
557
|
+
throw new Error(
|
|
558
|
+
`Versioned relation pair on '${ownerLogical}': the \`versionedHistory\` target '${historyName}' (\`${history.propertyName}\`) is keyed by [${history.keyFields.map((f) => `'${f}'`).join(", ")}], which carries NO version discriminator beyond the \`versionedLatest\` target '${latestName}' (\`${latest.propertyName}\`) keyed by [${[...latestKeyFields].map((f) => `'${f}'`).join(", ")}]. The history row must be keyed by a per-version field (e.g. add a \`version\` segment to '${historyName}'s \`@key\`) so each revision is a NEW row; otherwise every revision overwrites one row (a latest pointer, not an append-only history).`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function collectViewDefinitions(registry) {
|
|
565
|
+
const resolved = registry ?? MetadataRegistry.getAll();
|
|
566
|
+
const out = [];
|
|
567
|
+
for (const [klass, meta] of resolved) {
|
|
568
|
+
const kind = meta.kind ?? "entity";
|
|
569
|
+
if (kind === "materializedView" || kind === "sparseView") {
|
|
570
|
+
out.push(viewFromMaintainedFrom(klass, meta, kind));
|
|
571
|
+
}
|
|
572
|
+
if (meta.relations?.some((r) => r.options?.versioned)) {
|
|
573
|
+
out.push(...versionedViews(klass, meta));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/relation/maintenance-graph.ts
|
|
580
|
+
var COUNT_DELTA_FOR_EVENT = {
|
|
581
|
+
created: 1,
|
|
582
|
+
removed: -1,
|
|
583
|
+
updated: 0
|
|
584
|
+
};
|
|
585
|
+
var MAINTAIN_EVENTS = ["created", "updated", "removed"];
|
|
586
|
+
function isPathRooted(value) {
|
|
587
|
+
return value.startsWith("$.input.") || value.startsWith("$.entity.");
|
|
588
|
+
}
|
|
589
|
+
function sourceAttributeOf(path) {
|
|
590
|
+
const stripped = path.replace(/^\$\.(input|entity)\./, "");
|
|
591
|
+
return stripped.split(".")[0] ?? stripped;
|
|
592
|
+
}
|
|
593
|
+
function normalizeProjectionEntry(value) {
|
|
594
|
+
if (typeof value === "string") {
|
|
595
|
+
const path = isPathRooted(value) ? value : `$.entity.${value}`;
|
|
596
|
+
return identity(path);
|
|
597
|
+
}
|
|
598
|
+
return value;
|
|
599
|
+
}
|
|
600
|
+
function buildProjectionMap(projection, ownerEntity, relationProperty) {
|
|
601
|
+
const entries = projection ? Object.entries(projection) : [];
|
|
602
|
+
if (entries.length === 0) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Relation '${relationProperty}' on '${ownerEntity}' declares maintenance (\`write.maintainedOn\`) but no \`projection\`: a maintained snapshot / collection with no captured attributes projects nothing. Declare a \`projection\` map (e.g. \`{ postId: 'postId', textPreview: preview('body', 120) }\`).`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const out = {};
|
|
608
|
+
for (const [attr, value] of entries) {
|
|
609
|
+
out[attr] = normalizeProjectionEntry(value);
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
612
|
+
}
|
|
613
|
+
function buildDestinationKeys(keyBinding) {
|
|
614
|
+
const keys = {};
|
|
615
|
+
const sourceFields = [];
|
|
616
|
+
for (const [sourceField, ownerField] of Object.entries(keyBinding)) {
|
|
617
|
+
keys[ownerField] = `$.entity.${sourceField}`;
|
|
618
|
+
sourceFields.push(sourceField);
|
|
619
|
+
}
|
|
620
|
+
return { keys, sourceFields };
|
|
621
|
+
}
|
|
622
|
+
function sourcePayloadAttributes(meta) {
|
|
623
|
+
const attrs = /* @__PURE__ */ new Set();
|
|
624
|
+
for (const f of meta.fields) attrs.add(f.propertyName);
|
|
625
|
+
if (meta.primaryKey) {
|
|
626
|
+
for (const f of segmentFieldNames(meta.primaryKey.segmented.pkSegments)) attrs.add(f);
|
|
627
|
+
for (const f of segmentFieldNames(meta.primaryKey.segmented.skSegments)) attrs.add(f);
|
|
628
|
+
}
|
|
629
|
+
for (const gsi of meta.gsiDefinitions) {
|
|
630
|
+
for (const f of segmentFieldNames(gsi.segmented.pkSegments)) attrs.add(f);
|
|
631
|
+
for (const f of segmentFieldNames(gsi.segmented.skSegments)) attrs.add(f);
|
|
632
|
+
}
|
|
633
|
+
return attrs;
|
|
634
|
+
}
|
|
635
|
+
function assertMaintenanceRoundTrips(args) {
|
|
636
|
+
const {
|
|
637
|
+
ownerEntity,
|
|
638
|
+
relationProperty,
|
|
639
|
+
ownerMeta,
|
|
640
|
+
sourceEntity,
|
|
641
|
+
sourceMeta,
|
|
642
|
+
destinationKeyFields,
|
|
643
|
+
keyBindingSourceFields,
|
|
644
|
+
projection
|
|
645
|
+
} = args;
|
|
646
|
+
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
647
|
+
try {
|
|
648
|
+
resolveKey([...destinationKeyFields], ownerMeta);
|
|
649
|
+
} catch (cause) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`Maintenance declaration for relation '${relationProperty}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The maintained snapshot / collection row could not be resolved, so the snapshot key would not point at a real target row \u2014 bind the partition-key field(s) of the index the maintained row lives on. (${cause.message})`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
for (const field of keyBindingSourceFields) {
|
|
655
|
+
if (!sourceAttrs.has(field)) {
|
|
656
|
+
throw new Error(
|
|
657
|
+
`Maintenance key binding for relation '${relationProperty}' on '${ownerEntity}' reads source field '${field}' to resolve the destination row, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). The destination key could not be built from the source payload \u2014 reject.`
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const [attr, transform] of Object.entries(projection)) {
|
|
662
|
+
if (!isPathRooted(transform.path)) {
|
|
663
|
+
throw new Error(
|
|
664
|
+
`Maintenance projection for relation '${relationProperty}' on '${ownerEntity}' captures attribute '${attr}' from '${transform.path}', which is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The maintenance graph projects only from the source payload (payload \u540C\u68B1) \u2014 there is no fetch / re-projection.`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const srcAttr = sourceAttributeOf(transform.path);
|
|
668
|
+
if (!sourceAttrs.has(srcAttr)) {
|
|
669
|
+
throw new Error(
|
|
670
|
+
`Maintenance projection for relation '${relationProperty}' on '${ownerEntity}' captures '${attr}' from '${transform.path}', but the source entity '${sourceEntity}' carries no attribute '${srcAttr}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). Projection is payload \u540C\u68B1 only (no fetch / re-projection): an attribute the source row does not carry cannot be projected \u2014 reject.`
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
function assertCounterRoundTrips(args) {
|
|
676
|
+
const {
|
|
677
|
+
ownerEntity,
|
|
678
|
+
aggregateField,
|
|
679
|
+
ownerMeta,
|
|
680
|
+
sourceEntity,
|
|
681
|
+
sourceMeta,
|
|
682
|
+
destinationKeyFields,
|
|
683
|
+
keyBindingSourceFields,
|
|
684
|
+
valueSourceFields
|
|
685
|
+
} = args;
|
|
686
|
+
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
687
|
+
try {
|
|
688
|
+
resolveKey([...destinationKeyFields], ownerMeta);
|
|
689
|
+
} catch (cause) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The counter row could not be resolved \u2014 bind the partition-key field(s) of the index the counter row lives on. (${cause.message})`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
for (const field of keyBindingSourceFields) {
|
|
695
|
+
if (!sourceAttrs.has(field)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' reads source field '${field}' to resolve the counter row, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). The destination key could not be built from the source payload \u2014 reject.`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
for (const field of valueSourceFields) {
|
|
702
|
+
if (!sourceAttrs.has(field)) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`Counter aggregate '${aggregateField}' on '${ownerEntity}' tracks \`max('${field}')\`, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). A counter aggregates only the source payload (payload \u540C\u68B1) \u2014 reject.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function logicalNameOf2(meta) {
|
|
710
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
711
|
+
}
|
|
712
|
+
function resolveEntityByName(name, registry) {
|
|
713
|
+
for (const [klass, meta] of registry) {
|
|
714
|
+
if (logicalNameOf2(meta) === name) return klass;
|
|
715
|
+
}
|
|
716
|
+
return void 0;
|
|
717
|
+
}
|
|
718
|
+
function parseTrigger(raw) {
|
|
719
|
+
const idx = raw.lastIndexOf(".");
|
|
720
|
+
return { entity: raw.slice(0, idx), event: raw.slice(idx + 1) };
|
|
721
|
+
}
|
|
722
|
+
function buildEffect(relation, ownerFactory, trigger, keys, project, sourceMeta, ownerEntity) {
|
|
723
|
+
const write = relation.options?.write;
|
|
724
|
+
const updateMode = write?.updateMode;
|
|
725
|
+
const consistency = write?.consistency;
|
|
726
|
+
const base = {
|
|
727
|
+
trigger,
|
|
728
|
+
targetFactory: ownerFactory,
|
|
729
|
+
keys,
|
|
730
|
+
project,
|
|
731
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
732
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
733
|
+
};
|
|
734
|
+
if (relation.type === "hasMany") {
|
|
735
|
+
const read = relation.options?.read;
|
|
736
|
+
const order = resolveCollectionOrder(relation, sourceMeta, ownerEntity, project);
|
|
737
|
+
const collection = {
|
|
738
|
+
field: relation.propertyName,
|
|
739
|
+
...read?.maxItems !== void 0 ? { maxItems: read.maxItems } : {},
|
|
740
|
+
...order !== void 0 ? { orderBy: order.orderBy, orderDir: order.orderDir } : {}
|
|
741
|
+
};
|
|
742
|
+
return { kind: "collection", ...base, collection };
|
|
743
|
+
}
|
|
744
|
+
return { kind: "snapshot", ...base };
|
|
745
|
+
}
|
|
746
|
+
function viewSliceProperty(viewName, sliceIndex, slice) {
|
|
747
|
+
return slice.collection ? `${viewName}#${slice.collection.field}` : `${viewName}#${sliceIndex}`;
|
|
748
|
+
}
|
|
749
|
+
function buildViewEffect(view, slice, trigger) {
|
|
750
|
+
const targetFactory = () => view.viewClass;
|
|
751
|
+
const base = {
|
|
752
|
+
trigger,
|
|
753
|
+
targetFactory,
|
|
754
|
+
keys: slice.keys,
|
|
755
|
+
project: slice.project,
|
|
756
|
+
...view.updateMode !== void 0 ? { updateMode: view.updateMode } : {},
|
|
757
|
+
...view.consistency !== void 0 ? { consistency: view.consistency } : {}
|
|
758
|
+
};
|
|
759
|
+
if (slice.predicate) {
|
|
760
|
+
return { kind: "membership", ...base, predicate: slice.predicate };
|
|
761
|
+
}
|
|
762
|
+
if (slice.collection) {
|
|
763
|
+
const collection = {
|
|
764
|
+
field: slice.collection.field,
|
|
765
|
+
...slice.collection.maxItems !== void 0 ? { maxItems: slice.collection.maxItems } : {},
|
|
766
|
+
...slice.collection.orderBy !== void 0 ? {
|
|
767
|
+
orderBy: slice.collection.orderBy,
|
|
768
|
+
orderDir: slice.collection.orderDir ?? "DESC"
|
|
769
|
+
} : {}
|
|
770
|
+
};
|
|
771
|
+
return { kind: "collection", ...base, collection };
|
|
772
|
+
}
|
|
773
|
+
return { kind: "snapshot", ...base };
|
|
774
|
+
}
|
|
775
|
+
function resolveCollectionOrder(relation, sourceMeta, ownerEntity, project) {
|
|
776
|
+
const order = relation.options?.read?.order;
|
|
777
|
+
if (order === void 0) return void 0;
|
|
778
|
+
const skFields = sourceMeta.primaryKey ? segmentFieldNames(sourceMeta.primaryKey.segmented.skSegments) : [];
|
|
779
|
+
const orderField = skFields[skFields.length - 1];
|
|
780
|
+
if (orderField === void 0) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, but its source entity '${logicalNameOf2(sourceMeta)}' has no sort key to order by. A maintained collection orders by the source's sort-key field (the field the live read sorts on); declare a sort key on '${logicalNameOf2(sourceMeta)}' or drop \`read.order\`.`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (!Object.prototype.hasOwnProperty.call(project, orderField)) {
|
|
786
|
+
throw new Error(
|
|
787
|
+
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, which orders by the source entity '${logicalNameOf2(sourceMeta)}'s sort-key field '${orderField}', but that field is not in the relation's \`projection\` (projected: ${Object.keys(project).map((a) => `'${a}'`).join(", ")}). The maintained collection sorts its projected items, so the order field must be projected \u2014 add '${orderField}' to the projection (e.g. \`{ ${orderField}: '${orderField}', \u2026 }\`) or drop \`read.order\`.`
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
return { orderBy: `$.entity.${orderField}`, orderDir: order };
|
|
791
|
+
}
|
|
792
|
+
function buildCounterEffect(aggregate, ownerFactory, trigger, event, keys) {
|
|
793
|
+
const write = aggregate.options.write;
|
|
794
|
+
const updateMode = write?.updateMode;
|
|
795
|
+
const consistency = write?.consistency;
|
|
796
|
+
const value = aggregate.options.value.op === "count" ? { op: "count" } : { op: "max", field: aggregate.options.value.field };
|
|
797
|
+
return {
|
|
798
|
+
kind: "counter",
|
|
799
|
+
trigger,
|
|
800
|
+
targetFactory: ownerFactory,
|
|
801
|
+
keys,
|
|
802
|
+
attribute: aggregate.propertyName,
|
|
803
|
+
value,
|
|
804
|
+
...value.op === "count" ? { delta: COUNT_DELTA_FOR_EVENT[event] } : {},
|
|
805
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
806
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function destinationRowKeyOf(ownerEntity, keys) {
|
|
810
|
+
const parts = Object.entries(keys).map(([field, path]) => `${field}=${path}`).sort();
|
|
811
|
+
return `${ownerEntity}#${parts.join("&")}`;
|
|
812
|
+
}
|
|
813
|
+
function assertNoCycle(items) {
|
|
814
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
815
|
+
for (const item of items) {
|
|
816
|
+
if (!adjacency.has(item.sourceEntity)) adjacency.set(item.sourceEntity, /* @__PURE__ */ new Set());
|
|
817
|
+
adjacency.get(item.sourceEntity).add(item.ownerEntity);
|
|
818
|
+
}
|
|
819
|
+
const WHITE = 0;
|
|
820
|
+
const GRAY = 1;
|
|
821
|
+
const BLACK = 2;
|
|
822
|
+
const color = /* @__PURE__ */ new Map();
|
|
823
|
+
for (const node of adjacency.keys()) color.set(node, WHITE);
|
|
824
|
+
for (const start of adjacency.keys()) {
|
|
825
|
+
if (color.get(start) !== WHITE) continue;
|
|
826
|
+
const stack = [];
|
|
827
|
+
const path = [];
|
|
828
|
+
color.set(start, GRAY);
|
|
829
|
+
path.push(start);
|
|
830
|
+
stack.push({ node: start, iter: (adjacency.get(start) ?? /* @__PURE__ */ new Set()).values() });
|
|
831
|
+
while (stack.length > 0) {
|
|
832
|
+
const top = stack[stack.length - 1];
|
|
833
|
+
const next = top.iter.next();
|
|
834
|
+
if (next.done) {
|
|
835
|
+
color.set(top.node, BLACK);
|
|
836
|
+
stack.pop();
|
|
837
|
+
path.pop();
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
const child = next.value;
|
|
841
|
+
const childColor = color.get(child) ?? WHITE;
|
|
842
|
+
if (childColor === GRAY) {
|
|
843
|
+
const from = path.indexOf(child);
|
|
844
|
+
const cycle = [...path.slice(from), child].join(" \u2192 ");
|
|
845
|
+
throw new Error(
|
|
846
|
+
`Maintenance dependency cycle detected: ${cycle}. A maintenance trigger chain that loops back on itself is an unbounded cascade \u2014 break the cycle (an entity's maintained shape must not, transitively, be triggered by its own maintenance).`
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
if (childColor === WHITE) {
|
|
850
|
+
color.set(child, GRAY);
|
|
851
|
+
path.push(child);
|
|
852
|
+
stack.push({ node: child, iter: (adjacency.get(child) ?? /* @__PURE__ */ new Set()).values() });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function buildMaintenanceGraph(registry, views) {
|
|
858
|
+
const resolvedRegistry = registry ?? MetadataRegistry.getAll();
|
|
859
|
+
const resolvedViews = views ?? collectViewDefinitions(resolvedRegistry);
|
|
860
|
+
return buildMaintenanceGraphImpl(resolvedRegistry, resolvedViews);
|
|
861
|
+
}
|
|
862
|
+
function buildMaintenanceGraphImpl(registry, views) {
|
|
863
|
+
const items = [];
|
|
864
|
+
const owners = [...registry.entries()].map(([klass, meta]) => ({ klass, meta })).sort((a, b) => a.klass.name < b.klass.name ? -1 : a.klass.name > b.klass.name ? 1 : 0);
|
|
865
|
+
for (const { klass: ownerClass, meta: ownerMeta } of owners) {
|
|
866
|
+
const ownerEntity = logicalNameOf2(ownerMeta);
|
|
867
|
+
const relations = [...ownerMeta.relations].sort(
|
|
868
|
+
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
869
|
+
);
|
|
870
|
+
for (const relation of relations) {
|
|
871
|
+
const maintainedOn = relation.options?.write?.maintainedOn;
|
|
872
|
+
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
873
|
+
const sourceClass = relation.targetFactory();
|
|
874
|
+
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
875
|
+
const relationTargetEntity = logicalNameOf2(sourceMeta);
|
|
876
|
+
const project = buildProjectionMap(
|
|
877
|
+
relation.options?.projection,
|
|
878
|
+
ownerEntity,
|
|
879
|
+
relation.propertyName
|
|
880
|
+
);
|
|
881
|
+
const { keys, sourceFields } = buildDestinationKeys(relation.keyBinding);
|
|
882
|
+
const destinationKeyFields = Object.keys(keys);
|
|
883
|
+
for (const raw of maintainedOn) {
|
|
884
|
+
if (!isMaintainTrigger(raw)) {
|
|
885
|
+
const { event } = parseTrigger(raw);
|
|
886
|
+
throw new Error(
|
|
887
|
+
`Maintenance trigger ${JSON.stringify(raw)} on relation '${relation.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event}'). Fix the \`write.maintainedOn\` entry.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
const trigger = raw;
|
|
891
|
+
const { entity: triggerEntity } = parseTrigger(trigger);
|
|
892
|
+
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (triggerEntity !== relationTargetEntity) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the relation's target (the projected source) is '${relationTargetEntity}'. #124 projects only from the relation target's payload (payload \u540C\u68B1); a trigger on a different entity needs a write-time context fetch, which is Phase 2 \u2014 restrict \`maintainedOn\` to '${relationTargetEntity}.*' triggers.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
assertMaintenanceRoundTrips({
|
|
903
|
+
ownerEntity,
|
|
904
|
+
relationProperty: relation.propertyName,
|
|
905
|
+
ownerMeta,
|
|
906
|
+
sourceEntity: triggerEntity,
|
|
907
|
+
sourceMeta,
|
|
908
|
+
destinationKeyFields,
|
|
909
|
+
keyBindingSourceFields: sourceFields,
|
|
910
|
+
projection: project
|
|
911
|
+
});
|
|
912
|
+
const effect = buildEffect(
|
|
913
|
+
relation,
|
|
914
|
+
() => ownerClass,
|
|
915
|
+
trigger,
|
|
916
|
+
keys,
|
|
917
|
+
project,
|
|
918
|
+
sourceMeta,
|
|
919
|
+
ownerEntity
|
|
920
|
+
);
|
|
921
|
+
items.push({
|
|
922
|
+
trigger,
|
|
923
|
+
sourceEntity: triggerEntity,
|
|
924
|
+
sourceClass,
|
|
925
|
+
ownerEntity,
|
|
926
|
+
ownerClass,
|
|
927
|
+
relationProperty: relation.propertyName,
|
|
928
|
+
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
929
|
+
effect
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const aggregates = [...ownerMeta.aggregates].sort(
|
|
934
|
+
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
935
|
+
);
|
|
936
|
+
for (const agg of aggregates) {
|
|
937
|
+
const maintainedOn = agg.options.write?.maintainedOn;
|
|
938
|
+
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
939
|
+
const sourceClass = agg.targetFactory();
|
|
940
|
+
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
941
|
+
const aggSourceEntity = logicalNameOf2(sourceMeta);
|
|
942
|
+
const { keys, sourceFields } = buildDestinationKeys(agg.keyBinding);
|
|
943
|
+
const destinationKeyFields = Object.keys(keys);
|
|
944
|
+
const valueSourceFields = agg.options.value.op === "max" ? [agg.options.value.field] : [];
|
|
945
|
+
for (const raw of maintainedOn) {
|
|
946
|
+
if (!isMaintainTrigger(raw)) {
|
|
947
|
+
const { event: event2 } = parseTrigger(raw);
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Maintenance trigger ${JSON.stringify(raw)} on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event2}'). Fix the \`write.maintainedOn\` entry.`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
const trigger = raw;
|
|
953
|
+
const parsed = parseTrigger(trigger);
|
|
954
|
+
const triggerEntity = parsed.entity;
|
|
955
|
+
const event = parsed.event;
|
|
956
|
+
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
957
|
+
throw new Error(
|
|
958
|
+
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
if (triggerEntity !== aggSourceEntity) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the aggregate's source (the entity being counted) is '${aggSourceEntity}'. A counter aggregates only the rows of its own source (payload \u540C\u68B1); a trigger on a different entity is Phase 2 \u2014 restrict \`maintainedOn\` to '${aggSourceEntity}.*' triggers.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (agg.options.value.op === "count" && COUNT_DELTA_FOR_EVENT[event] === 0) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`\`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is a \`count()\` counter maintained on '${trigger}', but a 'updated' event does not change a row count (it adds 0). A \`count()\` counter is maintained on 'created' (+1) and 'removed' (-1); drop the 'updated' trigger.`
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
assertCounterRoundTrips({
|
|
972
|
+
ownerEntity,
|
|
973
|
+
aggregateField: agg.propertyName,
|
|
974
|
+
ownerMeta,
|
|
975
|
+
sourceEntity: triggerEntity,
|
|
976
|
+
sourceMeta,
|
|
977
|
+
destinationKeyFields,
|
|
978
|
+
keyBindingSourceFields: sourceFields,
|
|
979
|
+
valueSourceFields
|
|
980
|
+
});
|
|
981
|
+
const effect = buildCounterEffect(agg, () => ownerClass, trigger, event, keys);
|
|
982
|
+
items.push({
|
|
983
|
+
trigger,
|
|
984
|
+
sourceEntity: triggerEntity,
|
|
985
|
+
sourceClass,
|
|
986
|
+
ownerEntity,
|
|
987
|
+
ownerClass,
|
|
988
|
+
relationProperty: agg.propertyName,
|
|
989
|
+
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
990
|
+
effect
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
for (const view of [...views].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
996
|
+
const ownerClass = view.viewClass;
|
|
997
|
+
const ownerMeta = MetadataRegistry.get(ownerClass);
|
|
998
|
+
const ownerEntity = logicalNameOf2(ownerMeta);
|
|
999
|
+
view.slices.forEach((slice, sliceIndex) => {
|
|
1000
|
+
const sourceClass = slice.sourceClass;
|
|
1001
|
+
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
1002
|
+
const sourceEntity = logicalNameOf2(sourceMeta);
|
|
1003
|
+
const keys = slice.keys;
|
|
1004
|
+
const destinationKeyFields = Object.keys(keys);
|
|
1005
|
+
const keyBindingSourceFields = Object.values(keys).map((p) => sourceAttributeOf(p));
|
|
1006
|
+
for (const trigger of slice.maintainedOn) {
|
|
1007
|
+
const { entity: triggerEntity } = parseTrigger(trigger);
|
|
1008
|
+
if (triggerEntity !== sourceEntity) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`defineView '${view.name}': source slice trigger '${trigger}' names entity '${triggerEntity}', but the slice's source entity is '${sourceEntity}'. A view source projects only from its own payload (payload \u540C\u68B1); a trigger on a different entity needs a write-time context fetch (RFC \xA74.A.6), which is a deferred follow-up \u2014 restrict the slice's \`maintainedOn\` to '${sourceEntity}.*' triggers.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
assertMaintenanceRoundTrips({
|
|
1014
|
+
ownerEntity,
|
|
1015
|
+
relationProperty: viewSliceProperty(view.name, sliceIndex, slice),
|
|
1016
|
+
ownerMeta,
|
|
1017
|
+
sourceEntity,
|
|
1018
|
+
sourceMeta,
|
|
1019
|
+
destinationKeyFields,
|
|
1020
|
+
keyBindingSourceFields,
|
|
1021
|
+
projection: slice.project
|
|
1022
|
+
});
|
|
1023
|
+
if (slice.predicate) {
|
|
1024
|
+
if (!isPathRooted(slice.predicate.path)) {
|
|
1025
|
+
throw new Error(
|
|
1026
|
+
`defineView '${view.name}': sparse-view predicate path ${JSON.stringify(slice.predicate.path)} is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The drain evaluates the predicate against the source image \u2014 use a source attribute path.`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const predAttr = sourceAttributeOf(slice.predicate.path);
|
|
1030
|
+
if (!sourcePayloadAttributes(sourceMeta).has(predAttr)) {
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`defineView '${view.name}': sparse-view predicate reads source attribute '${predAttr}' (from '${slice.predicate.path}'), but the source entity '${sourceEntity}' carries no such attribute on its payload (available: ${[...sourcePayloadAttributes(sourceMeta)].sort().map((f) => `'${f}'`).join(", ")}). A membership predicate is payload \u540C\u68B1 only \u2014 reject.`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const effect = buildViewEffect(view, slice, trigger);
|
|
1037
|
+
items.push({
|
|
1038
|
+
trigger,
|
|
1039
|
+
sourceEntity,
|
|
1040
|
+
sourceClass,
|
|
1041
|
+
ownerEntity,
|
|
1042
|
+
ownerClass,
|
|
1043
|
+
relationProperty: viewSliceProperty(view.name, sliceIndex, slice),
|
|
1044
|
+
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
1045
|
+
effect
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
assertNoCycle(items);
|
|
1051
|
+
const byTrigger = /* @__PURE__ */ new Map();
|
|
1052
|
+
const sameRow = /* @__PURE__ */ new Map();
|
|
1053
|
+
for (const item of items) {
|
|
1054
|
+
const triggerBucket = byTrigger.get(item.trigger);
|
|
1055
|
+
if (triggerBucket) triggerBucket.push(item);
|
|
1056
|
+
else byTrigger.set(item.trigger, [item]);
|
|
1057
|
+
const rowKey = `${item.trigger}\0${item.destinationRowKey}`;
|
|
1058
|
+
const rowBucket = sameRow.get(rowKey);
|
|
1059
|
+
if (rowBucket) rowBucket.push(item);
|
|
1060
|
+
else sameRow.set(rowKey, [item]);
|
|
1061
|
+
}
|
|
1062
|
+
const multiMaintainerTargets = /* @__PURE__ */ new Map();
|
|
1063
|
+
for (const [rowKey, bucket] of sameRow) {
|
|
1064
|
+
if (bucket.length > 1) multiMaintainerTargets.set(rowKey, bucket);
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
items,
|
|
1068
|
+
byTrigger,
|
|
1069
|
+
effectsFor(trigger) {
|
|
1070
|
+
return byTrigger.get(trigger) ?? [];
|
|
1071
|
+
},
|
|
1072
|
+
multiMaintainerTargets
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/cdc/from-change.ts
|
|
1077
|
+
function buildFullSelect(metadata) {
|
|
1078
|
+
const select = {};
|
|
1079
|
+
for (const field of metadata.fields) {
|
|
1080
|
+
select[field.propertyName] = true;
|
|
1081
|
+
}
|
|
1082
|
+
return select;
|
|
1083
|
+
}
|
|
1084
|
+
function eventOwnedBy(event, metadata, modelName) {
|
|
1085
|
+
if (event.model !== void 0) {
|
|
1086
|
+
return event.model === modelName;
|
|
1087
|
+
}
|
|
1088
|
+
return metadata.prefix.length > 0 && event.keys.pk.startsWith(metadata.prefix);
|
|
1089
|
+
}
|
|
1090
|
+
function hydrateImage(image, select, metadata, modelClass) {
|
|
1091
|
+
if (image === void 0) return null;
|
|
1092
|
+
const [hydrated] = hydrate([image], select, metadata);
|
|
1093
|
+
const instance = new modelClass();
|
|
1094
|
+
Object.assign(instance, hydrated);
|
|
1095
|
+
for (const emb of metadata.embeddedFields) {
|
|
1096
|
+
const rawEmb = image[emb.propertyName];
|
|
1097
|
+
if (rawEmb !== void 0 && rawEmb !== null) {
|
|
1098
|
+
instance[emb.propertyName] = rawEmb;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return instance;
|
|
1102
|
+
}
|
|
1103
|
+
function parseChange(event, metadata, modelName, modelClass) {
|
|
1104
|
+
if (!eventOwnedBy(event, metadata, modelName)) {
|
|
1105
|
+
return [null, null];
|
|
1106
|
+
}
|
|
1107
|
+
const select = buildFullSelect(metadata);
|
|
1108
|
+
const oldRecord = hydrateImage(event.oldImage, select, metadata, modelClass);
|
|
1109
|
+
const newRecord = hydrateImage(event.newImage, select, metadata, modelClass);
|
|
1110
|
+
return [oldRecord, newRecord];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/cdc/subscribe.ts
|
|
1114
|
+
function resolveRoutableModels(handlers) {
|
|
1115
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1116
|
+
for (const [cls, meta] of MetadataRegistry.getAll()) {
|
|
1117
|
+
byName.set(cls.name, { cls, meta });
|
|
1118
|
+
}
|
|
1119
|
+
const routable = /* @__PURE__ */ new Map();
|
|
1120
|
+
for (const modelName of Object.keys(handlers)) {
|
|
1121
|
+
const entry = byName.get(modelName);
|
|
1122
|
+
if (!entry) {
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
`DDBModel.subscribe: no model named '${modelName}' is registered. Did you import the model module (so its @model decorator ran) and spell the handler key exactly as the class name?`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (!entry.meta.cdcProjected) {
|
|
1128
|
+
throw new Error(
|
|
1129
|
+
`DDBModel.subscribe: model '${modelName}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model, or remove its handler.`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
routable.set(modelName, {
|
|
1133
|
+
modelClass: entry.cls,
|
|
1134
|
+
metadata: entry.meta,
|
|
1135
|
+
modelName
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
return routable;
|
|
1139
|
+
}
|
|
1140
|
+
async function dispatchEvent(event, routable, handlers) {
|
|
1141
|
+
for (const { modelClass, metadata, modelName } of routable.values()) {
|
|
1142
|
+
const [oldRecord, newRecord] = parseChange(
|
|
1143
|
+
event,
|
|
1144
|
+
metadata,
|
|
1145
|
+
modelName,
|
|
1146
|
+
modelClass
|
|
1147
|
+
);
|
|
1148
|
+
if (oldRecord === null && newRecord === null) continue;
|
|
1149
|
+
const handler = handlers[modelName];
|
|
1150
|
+
await handler(oldRecord, newRecord);
|
|
1151
|
+
return true;
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
function buildSubscribeHandler(handlers) {
|
|
1156
|
+
const routable = resolveRoutableModels(handlers);
|
|
1157
|
+
return async (batch) => {
|
|
1158
|
+
const batchItemFailures = [];
|
|
1159
|
+
for (const event of batch.records) {
|
|
1160
|
+
try {
|
|
1161
|
+
await dispatchEvent(event, routable, handlers);
|
|
1162
|
+
} catch {
|
|
1163
|
+
batchItemFailures.push(event.sequenceNumber);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return { batchItemFailures };
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export {
|
|
1171
|
+
SELECT_SPEC,
|
|
1172
|
+
isSelectBuilder,
|
|
1173
|
+
normalizeSelectSpec,
|
|
1174
|
+
normalizeTopLevelSelect,
|
|
1175
|
+
buildProject,
|
|
1176
|
+
buildRelation,
|
|
1177
|
+
isInlineSnapshotSpec,
|
|
1178
|
+
detectRelationFields,
|
|
1179
|
+
validateInlineSnapshotSelect,
|
|
1180
|
+
getImplicitKeyFields,
|
|
1181
|
+
hydrate,
|
|
1182
|
+
maintainTrigger,
|
|
1183
|
+
isMaintainTrigger,
|
|
1184
|
+
preview,
|
|
1185
|
+
identity,
|
|
1186
|
+
LIFECYCLE_CONTRACT_MARKER,
|
|
1187
|
+
isLifecycleContract,
|
|
1188
|
+
ENTITY_WRITES_MARKER,
|
|
1189
|
+
isEntityWritesDefinition,
|
|
1190
|
+
entityWrites,
|
|
1191
|
+
getEntityWrites,
|
|
1192
|
+
lifecyclePhaseForIntent,
|
|
1193
|
+
collectViewDefinitions,
|
|
1194
|
+
buildMaintenanceGraph,
|
|
1195
|
+
parseChange,
|
|
1196
|
+
buildSubscribeHandler
|
|
1197
|
+
};
|