graphddb 0.5.1 → 0.5.3
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 +10 -0
- package/dist/cdc/index.d.ts +37 -0
- package/dist/cdc/index.js +29 -0
- package/dist/{chunk-B3GWIWT6.js → chunk-CCIVET5K.js} +74 -1026
- package/dist/{chunk-EMJHTUA4.js → chunk-M6URQOAW.js} +4 -1
- package/dist/chunk-MMVHOUM4.js +24 -0
- package/dist/chunk-PDUVTYC5.js +992 -0
- package/dist/{chunk-FI63YKK5.js → chunk-Y7XV5QL2.js} +3564 -3549
- package/dist/cli.js +1174 -21
- package/dist/from-change-DQK2Jm9R.d.ts +327 -0
- package/dist/index-CtDBo8Se.d.ts +690 -0
- package/dist/index.d.ts +18 -1113
- package/dist/index.js +33 -41
- package/dist/linter/index.d.ts +126 -0
- package/dist/linter/index.js +36 -0
- package/dist/{types-B9rJ1z3H.d.ts → maintenance-view-adapter-D5t9taTE.d.ts} +304 -182
- package/dist/registry-BD_5Rm5C.d.ts +76 -0
- package/dist/relation-depth-DLkhG0xX.d.ts +36 -0
- package/dist/spec/index.d.ts +4 -0
- package/dist/spec/index.js +54 -0
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +5 -3
- package/docs/doc-sample.md +263 -0
- package/docs/docs-generation.md +183 -0
- package/package.json +49 -3
|
@@ -1,103 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
get(_target, prop) {
|
|
12
|
-
if (typeof prop !== "string") return void 0;
|
|
13
|
-
let col = cache.get(prop);
|
|
14
|
-
if (!col) {
|
|
15
|
-
col = { [COLUMN_REF]: true, name: prop };
|
|
16
|
-
cache.set(prop, col);
|
|
17
|
-
}
|
|
18
|
-
return col;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// src/decorators/key.ts
|
|
25
|
-
var KEY_MARKER = /* @__PURE__ */ Symbol("graphddb:key");
|
|
26
|
-
var KEY_SEGMENT = /* @__PURE__ */ Symbol("graphddb:keySegment");
|
|
27
|
-
function isKeySegment(value) {
|
|
28
|
-
return typeof value === "object" && value !== null && value[KEY_SEGMENT] === true;
|
|
29
|
-
}
|
|
30
|
-
function k(literals, ...columns) {
|
|
31
|
-
for (let i = 0; i < columns.length; i++) {
|
|
32
|
-
if (!isColumn(columns[i])) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`k\`...\`: interpolation #${i} must be a typed column reference (e.g. \`c.userId\`), not ${typeof columns[i]}. Key segments name fields via column references so they stay type-safe.`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
[KEY_SEGMENT]: true,
|
|
40
|
-
literals: [...literals],
|
|
41
|
-
columns
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function isKeyDefinition(value) {
|
|
45
|
-
return typeof value === "object" && value !== null && KEY_MARKER in value;
|
|
46
|
-
}
|
|
47
|
-
function normalizeSegments(spec) {
|
|
48
|
-
if (spec === void 0) return [];
|
|
49
|
-
const list = Array.isArray(spec) ? spec : [spec];
|
|
50
|
-
for (const seg of list) {
|
|
51
|
-
if (!isKeySegment(seg)) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
"key()/gsi(): pk and sk must be built with the `k` tagged template (e.g. k`USER#${c.userId}`) or an array of them."
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return list;
|
|
58
|
-
}
|
|
59
|
-
function segmentFieldNames(segments) {
|
|
60
|
-
const seen = /* @__PURE__ */ new Set();
|
|
61
|
-
const out = [];
|
|
62
|
-
for (const seg of segments) {
|
|
63
|
-
for (const col of seg.columns) {
|
|
64
|
-
if (!seen.has(col.name)) {
|
|
65
|
-
seen.add(col.name);
|
|
66
|
-
out.push(col.name);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
function buildSegmentedKey(structure) {
|
|
73
|
-
const pkSegments = normalizeSegments(structure.pk);
|
|
74
|
-
const skSegments = normalizeSegments(structure.sk);
|
|
75
|
-
if (pkSegments.length === 0) {
|
|
76
|
-
throw new Error("key()/gsi(): pk must define at least one segment.");
|
|
77
|
-
}
|
|
78
|
-
const seen = /* @__PURE__ */ new Set();
|
|
79
|
-
const inputFieldNames = [];
|
|
80
|
-
for (const name of [
|
|
81
|
-
...segmentFieldNames(pkSegments),
|
|
82
|
-
...segmentFieldNames(skSegments)
|
|
83
|
-
]) {
|
|
84
|
-
if (!seen.has(name)) {
|
|
85
|
-
seen.add(name);
|
|
86
|
-
inputFieldNames.push(name);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { segmented: { pkSegments, skSegments }, inputFieldNames };
|
|
90
|
-
}
|
|
91
|
-
function key(builder) {
|
|
92
|
-
const accessor = createColumnMap();
|
|
93
|
-
const structure = builder(accessor);
|
|
94
|
-
const { segmented, inputFieldNames } = buildSegmentedKey(structure);
|
|
95
|
-
return {
|
|
96
|
-
[KEY_MARKER]: true,
|
|
97
|
-
segmented,
|
|
98
|
-
inputFieldNames
|
|
99
|
-
};
|
|
100
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
TableMapping,
|
|
3
|
+
buildSegmentedKey,
|
|
4
|
+
createColumnMap,
|
|
5
|
+
createDefaultLinter,
|
|
6
|
+
isColumn,
|
|
7
|
+
isKeyDefinition,
|
|
8
|
+
resolveKey,
|
|
9
|
+
segmentFieldNames
|
|
10
|
+
} from "./chunk-PDUVTYC5.js";
|
|
101
11
|
|
|
102
12
|
// src/decorators/gsi.ts
|
|
103
13
|
var GSI_MARKER = /* @__PURE__ */ Symbol("graphddb:gsi");
|
|
@@ -113,879 +23,20 @@ function gsi(indexName, builder, options) {
|
|
|
113
23
|
indexName,
|
|
114
24
|
segmented,
|
|
115
25
|
inputFieldNames,
|
|
116
|
-
unique: options?.unique ?? false
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// src/linter/linter.ts
|
|
121
|
-
var Linter = class {
|
|
122
|
-
rules = [];
|
|
123
|
-
addRule(rule) {
|
|
124
|
-
this.rules.push(rule);
|
|
125
|
-
}
|
|
126
|
-
run(metadata, registry) {
|
|
127
|
-
const results = [];
|
|
128
|
-
for (const rule of this.rules) {
|
|
129
|
-
results.push(...rule.check(metadata, registry));
|
|
130
|
-
}
|
|
131
|
-
return results;
|
|
132
|
-
}
|
|
133
|
-
runAll(registry) {
|
|
134
|
-
const results = [];
|
|
135
|
-
const all = registry.getAll();
|
|
136
|
-
for (const [, metadata] of all) {
|
|
137
|
-
results.push(...this.run(metadata, registry));
|
|
138
|
-
}
|
|
139
|
-
return results;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
// src/linter/rules/no-scan.ts
|
|
144
|
-
var noScanRule = {
|
|
145
|
-
id: "no-scan",
|
|
146
|
-
severity: "error",
|
|
147
|
-
check(metadata) {
|
|
148
|
-
if (metadata.primaryKey === null && metadata.gsiDefinitions.length === 0) {
|
|
149
|
-
return [
|
|
150
|
-
{
|
|
151
|
-
ruleId: "no-scan",
|
|
152
|
-
severity: "error",
|
|
153
|
-
message: `Entity '${metadata.prefix}' has no primaryKey or GSI defined. All reads would require a full table scan.`,
|
|
154
|
-
entity: metadata.prefix
|
|
155
|
-
}
|
|
156
|
-
];
|
|
157
|
-
}
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// src/linter/rules/require-limit.ts
|
|
163
|
-
var requireLimitRule = {
|
|
164
|
-
id: "require-limit",
|
|
165
|
-
severity: "warning",
|
|
166
|
-
check(metadata) {
|
|
167
|
-
const results = [];
|
|
168
|
-
for (const relation of metadata.relations) {
|
|
169
|
-
if (relation.type !== "hasMany") continue;
|
|
170
|
-
if (relation.options?.versioned) continue;
|
|
171
|
-
const limit = relation.options?.limit;
|
|
172
|
-
if (!limit || limit.default === void 0 || limit.max === void 0) {
|
|
173
|
-
const kb = Object.keys(relation.keyBinding).join(", ");
|
|
174
|
-
results.push({
|
|
175
|
-
ruleId: "require-limit",
|
|
176
|
-
severity: "warning",
|
|
177
|
-
message: `hasMany relation (keyBinding: [${kb}]) on entity '${metadata.prefix}' is missing limit.default or limit.max. Unbounded queries may consume excessive RCU.`,
|
|
178
|
-
entity: metadata.prefix
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return results;
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// src/linter/rules/gsi-ambiguity.ts
|
|
187
|
-
function isSubsetOf(a, b) {
|
|
188
|
-
const setB = new Set(b);
|
|
189
|
-
return a.length > 0 && a.every((f) => setB.has(f));
|
|
190
|
-
}
|
|
191
|
-
var gsiAmbiguityRule = {
|
|
192
|
-
id: "gsi-ambiguity",
|
|
193
|
-
severity: "error",
|
|
194
|
-
check(metadata) {
|
|
195
|
-
const results = [];
|
|
196
|
-
const patterns = [];
|
|
197
|
-
if (metadata.primaryKey) {
|
|
198
|
-
patterns.push({
|
|
199
|
-
label: "primaryKey",
|
|
200
|
-
fieldNames: metadata.primaryKey.inputFieldNames
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
for (const gsi2 of metadata.gsiDefinitions) {
|
|
204
|
-
patterns.push({ label: gsi2.indexName, fieldNames: gsi2.inputFieldNames });
|
|
205
|
-
}
|
|
206
|
-
for (let i = 0; i < patterns.length; i++) {
|
|
207
|
-
for (let j = i + 1; j < patterns.length; j++) {
|
|
208
|
-
const a = patterns[i];
|
|
209
|
-
const b = patterns[j];
|
|
210
|
-
if (isSubsetOf(a.fieldNames, b.fieldNames) || isSubsetOf(b.fieldNames, a.fieldNames)) {
|
|
211
|
-
const smaller = a.fieldNames.length <= b.fieldNames.length ? a : b;
|
|
212
|
-
results.push({
|
|
213
|
-
ruleId: "gsi-ambiguity",
|
|
214
|
-
severity: "error",
|
|
215
|
-
message: `Ambiguous GSI resolution: field set [${smaller.fieldNames.join(", ")}] matches both '${a.label}' and '${b.label}'. Each access pattern must have a unique field combination.`,
|
|
216
|
-
entity: metadata.prefix,
|
|
217
|
-
field: smaller.fieldNames.join(", ")
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return results;
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// src/planner/key-resolver.ts
|
|
227
|
-
function fieldSetsEqual(a, b) {
|
|
228
|
-
if (a.length !== b.length) return false;
|
|
229
|
-
const sorted = [...b].sort();
|
|
230
|
-
return [...a].sort().every((v, i) => v === sorted[i]);
|
|
231
|
-
}
|
|
232
|
-
function classifySegmentMatch(segmented, queryFields) {
|
|
233
|
-
const pkFields = segmentFieldNames(segmented.pkSegments);
|
|
234
|
-
const present = new Set(queryFields);
|
|
235
|
-
const allFields = /* @__PURE__ */ new Set([
|
|
236
|
-
...pkFields,
|
|
237
|
-
...segmentFieldNames(segmented.skSegments)
|
|
238
|
-
]);
|
|
239
|
-
for (const f of queryFields) {
|
|
240
|
-
if (!allFields.has(f)) return null;
|
|
241
|
-
}
|
|
242
|
-
for (const f of pkFields) {
|
|
243
|
-
if (!present.has(f)) return null;
|
|
244
|
-
}
|
|
245
|
-
const sk = segmented.skSegments;
|
|
246
|
-
let consumed = 0;
|
|
247
|
-
let truncated = false;
|
|
248
|
-
for (let i = 0; i < sk.length; i++) {
|
|
249
|
-
const fields = sk[i].columns.map((c) => c.name);
|
|
250
|
-
const all = fields.every((f) => present.has(f));
|
|
251
|
-
const any = fields.some((f) => present.has(f));
|
|
252
|
-
if (all && fields.length > 0) {
|
|
253
|
-
if (truncated) return null;
|
|
254
|
-
consumed = i + 1;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
if (fields.length === 0) {
|
|
258
|
-
if (!truncated) consumed = i + 1;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
if (any) return null;
|
|
262
|
-
truncated = true;
|
|
263
|
-
}
|
|
264
|
-
const skFieldCount = segmentFieldNames(sk).length;
|
|
265
|
-
const allSkPresent = segmentFieldNames(sk).every((f) => present.has(f));
|
|
266
|
-
if (skFieldCount === 0 || allSkPresent && consumed === sk.length) {
|
|
267
|
-
return "full";
|
|
268
|
-
}
|
|
269
|
-
return "partial";
|
|
270
|
-
}
|
|
271
|
-
function resolveKey(queryFields, metadata) {
|
|
272
|
-
if (metadata.primaryKey && fieldSetsEqual(queryFields, metadata.primaryKey.inputFieldNames)) {
|
|
273
|
-
return {
|
|
274
|
-
type: "pk",
|
|
275
|
-
partial: false,
|
|
276
|
-
inputFieldNames: metadata.primaryKey.inputFieldNames
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
for (const gsi2 of metadata.gsiDefinitions) {
|
|
280
|
-
if (fieldSetsEqual(queryFields, gsi2.inputFieldNames)) {
|
|
281
|
-
return {
|
|
282
|
-
type: "gsi",
|
|
283
|
-
indexName: gsi2.indexName,
|
|
284
|
-
unique: gsi2.unique,
|
|
285
|
-
partial: false,
|
|
286
|
-
inputFieldNames: gsi2.inputFieldNames
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (metadata.primaryKey) {
|
|
291
|
-
const match = classifySegmentMatch(metadata.primaryKey.segmented, queryFields);
|
|
292
|
-
if (match === "partial") {
|
|
293
|
-
return { type: "pk", partial: true, inputFieldNames: queryFields };
|
|
294
|
-
}
|
|
295
|
-
if (match === "full") {
|
|
296
|
-
return { type: "pk", partial: false, inputFieldNames: queryFields };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
for (const gsi2 of metadata.gsiDefinitions) {
|
|
300
|
-
const match = classifySegmentMatch(gsi2.segmented, queryFields);
|
|
301
|
-
if (match === "partial") {
|
|
302
|
-
return {
|
|
303
|
-
type: "gsi",
|
|
304
|
-
indexName: gsi2.indexName,
|
|
305
|
-
unique: false,
|
|
306
|
-
partial: true,
|
|
307
|
-
inputFieldNames: queryFields
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
if (match === "full") {
|
|
311
|
-
return {
|
|
312
|
-
type: "gsi",
|
|
313
|
-
indexName: gsi2.indexName,
|
|
314
|
-
unique: gsi2.unique,
|
|
315
|
-
partial: false,
|
|
316
|
-
inputFieldNames: queryFields
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
throw new Error(
|
|
321
|
-
`No access pattern found for fields [${queryFields.join(", ")}]. Available: primaryKey([${metadata.primaryKey?.inputFieldNames.join(", ") ?? ""}])` + metadata.gsiDefinitions.map((g) => `, ${g.indexName}([${g.inputFieldNames.join(", ")}])`).join("")
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/linter/rules/missing-gsi.ts
|
|
326
|
-
var missingGsiRule = {
|
|
327
|
-
id: "missing-gsi",
|
|
328
|
-
severity: "error",
|
|
329
|
-
check(metadata, registry) {
|
|
330
|
-
const results = [];
|
|
331
|
-
for (const relation of metadata.relations) {
|
|
332
|
-
const targetFields = Object.keys(relation.keyBinding);
|
|
333
|
-
if (targetFields.length === 0) {
|
|
334
|
-
continue;
|
|
335
|
-
}
|
|
336
|
-
let targetMeta;
|
|
337
|
-
try {
|
|
338
|
-
const targetClass = relation.targetFactory();
|
|
339
|
-
targetMeta = registry.get(targetClass);
|
|
340
|
-
} catch {
|
|
341
|
-
results.push({
|
|
342
|
-
ruleId: "missing-gsi",
|
|
343
|
-
severity: "error",
|
|
344
|
-
message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' targets an entity that is not registered in MetadataRegistry.`,
|
|
345
|
-
entity: metadata.prefix,
|
|
346
|
-
field: relation.propertyName
|
|
347
|
-
});
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
try {
|
|
351
|
-
resolveKey(targetFields, targetMeta);
|
|
352
|
-
} catch {
|
|
353
|
-
results.push({
|
|
354
|
-
ruleId: "missing-gsi",
|
|
355
|
-
severity: "error",
|
|
356
|
-
message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' requires access pattern [${targetFields.join(", ")}] on target entity '${targetMeta.prefix}', but no matching primary key or GSI is defined.`,
|
|
357
|
-
entity: metadata.prefix,
|
|
358
|
-
field: relation.propertyName
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return results;
|
|
363
|
-
}
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// src/linter/rules/relation-depth.ts
|
|
367
|
-
var DEFAULT_MAX_DEPTH = 1;
|
|
368
|
-
function computeRelationDepth(metadata, registry, visited = /* @__PURE__ */ new Set()) {
|
|
369
|
-
if (metadata.relations.length === 0) {
|
|
370
|
-
return 0;
|
|
371
|
-
}
|
|
372
|
-
let maxDepth = 0;
|
|
373
|
-
for (const relation of metadata.relations) {
|
|
374
|
-
let targetClass;
|
|
375
|
-
try {
|
|
376
|
-
targetClass = relation.targetFactory();
|
|
377
|
-
} catch {
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
if (visited.has(targetClass)) {
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
let targetMeta;
|
|
384
|
-
try {
|
|
385
|
-
targetMeta = registry.get(targetClass);
|
|
386
|
-
} catch {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
const nextVisited = new Set(visited);
|
|
390
|
-
nextVisited.add(targetClass);
|
|
391
|
-
const childDepth = 1 + computeRelationDepth(targetMeta, registry, nextVisited);
|
|
392
|
-
maxDepth = Math.max(maxDepth, childDepth);
|
|
393
|
-
}
|
|
394
|
-
return maxDepth;
|
|
395
|
-
}
|
|
396
|
-
var relationDepthRule = {
|
|
397
|
-
id: "relation-depth",
|
|
398
|
-
severity: "warning",
|
|
399
|
-
check(metadata, registry) {
|
|
400
|
-
const depth = computeRelationDepth(metadata, registry);
|
|
401
|
-
if (depth <= DEFAULT_MAX_DEPTH) {
|
|
402
|
-
return [];
|
|
403
|
-
}
|
|
404
|
-
return [
|
|
405
|
-
{
|
|
406
|
-
ruleId: "relation-depth",
|
|
407
|
-
severity: "warning",
|
|
408
|
-
message: `Entity '${metadata.prefix}' has relation paths exceeding default maxDepth ${DEFAULT_MAX_DEPTH} (detected depth: ${depth}). Deep traversal requires explicit maxDepth at runtime.`,
|
|
409
|
-
entity: metadata.prefix
|
|
410
|
-
}
|
|
411
|
-
];
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
// src/client/TableMapping.ts
|
|
416
|
-
var TableMapping = class {
|
|
417
|
-
static mapping = {};
|
|
418
|
-
static set(mapping) {
|
|
419
|
-
this.mapping = { ...mapping };
|
|
420
|
-
}
|
|
421
|
-
static resolve(declaredName) {
|
|
422
|
-
return this.mapping[declaredName] ?? declaredName;
|
|
423
|
-
}
|
|
424
|
-
/** @internal — for testing only */
|
|
425
|
-
static reset() {
|
|
426
|
-
this.mapping = {};
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
// src/linter/rules/same-partition-preset.ts
|
|
431
|
-
var samePartitionPresetRule = {
|
|
432
|
-
id: "same-partition-preset",
|
|
433
|
-
severity: "error",
|
|
434
|
-
check(metadata, registry) {
|
|
435
|
-
const results = [];
|
|
436
|
-
for (const relation of metadata.relations) {
|
|
437
|
-
if (relation.options?.pattern !== "samePartition") continue;
|
|
438
|
-
results.push(...checkRelation(metadata, relation, registry));
|
|
439
|
-
}
|
|
440
|
-
return results;
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
function err(metadata, relation, message) {
|
|
444
|
-
return {
|
|
445
|
-
ruleId: "same-partition-preset",
|
|
446
|
-
severity: "error",
|
|
447
|
-
message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' declares pattern 'samePartition' but ${message}`,
|
|
448
|
-
entity: metadata.prefix,
|
|
449
|
-
field: relation.propertyName
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
function checkRelation(metadata, relation, registry) {
|
|
453
|
-
if (relation.type !== "hasMany") {
|
|
454
|
-
return [
|
|
455
|
-
err(
|
|
456
|
-
metadata,
|
|
457
|
-
relation,
|
|
458
|
-
`is a '${relation.type}'. 'samePartition' lowers a parent\u2192children collection read (begins_with on a segmented sort key) and is only valid on @hasMany.`
|
|
459
|
-
)
|
|
460
|
-
];
|
|
461
|
-
}
|
|
462
|
-
const results = [];
|
|
463
|
-
const maintenanceOptions = ["read", "write", "projection"].filter(
|
|
464
|
-
(k2) => relation.options?.[k2] !== void 0
|
|
465
|
-
);
|
|
466
|
-
if (maintenanceOptions.length > 0) {
|
|
467
|
-
results.push(
|
|
468
|
-
err(
|
|
469
|
-
metadata,
|
|
470
|
-
relation,
|
|
471
|
-
`also sets [${maintenanceOptions.join(", ")}]. 'samePartition' has no maintained materialisation \u2014 children are read live by begins_with, so it carries no read/write/projection effect. Drop these options (or use a maintained preset such as 'embeddedSnapshot').`
|
|
472
|
-
)
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
const targetFields = Object.keys(relation.keyBinding);
|
|
476
|
-
if (targetFields.length === 0) {
|
|
477
|
-
results.push(
|
|
478
|
-
err(metadata, relation, `has an empty keyBinding; nothing to lower.`)
|
|
479
|
-
);
|
|
480
|
-
return results;
|
|
481
|
-
}
|
|
482
|
-
let targetMeta;
|
|
483
|
-
try {
|
|
484
|
-
const targetClass = relation.targetFactory();
|
|
485
|
-
targetMeta = registry.get(targetClass);
|
|
486
|
-
} catch {
|
|
487
|
-
results.push(
|
|
488
|
-
err(
|
|
489
|
-
metadata,
|
|
490
|
-
relation,
|
|
491
|
-
`targets an entity that is not registered in MetadataRegistry.`
|
|
492
|
-
)
|
|
493
|
-
);
|
|
494
|
-
return results;
|
|
495
|
-
}
|
|
496
|
-
let resolved;
|
|
497
|
-
try {
|
|
498
|
-
resolved = resolveKey(targetFields, targetMeta);
|
|
499
|
-
} catch {
|
|
500
|
-
results.push(
|
|
501
|
-
err(
|
|
502
|
-
metadata,
|
|
503
|
-
relation,
|
|
504
|
-
`binds [${targetFields.join(", ")}] on target '${targetMeta.prefix}', which matches no access pattern. A same-partition relation must bind a contiguous prefix of the target's primary key.`
|
|
505
|
-
)
|
|
506
|
-
);
|
|
507
|
-
return results;
|
|
508
|
-
}
|
|
509
|
-
if (resolved.type !== "pk") {
|
|
510
|
-
results.push(
|
|
511
|
-
err(
|
|
512
|
-
metadata,
|
|
513
|
-
relation,
|
|
514
|
-
`binds [${targetFields.join(", ")}] to a ${resolved.type.toUpperCase()} on target '${targetMeta.prefix}'. A GSI is a different partition; 'samePartition' requires the binding to resolve on the target's primary key (same PK, segmented-SK begins_with).`
|
|
515
|
-
)
|
|
516
|
-
);
|
|
517
|
-
return results;
|
|
518
|
-
}
|
|
519
|
-
if (!resolved.partial) {
|
|
520
|
-
results.push(
|
|
521
|
-
err(
|
|
522
|
-
metadata,
|
|
523
|
-
relation,
|
|
524
|
-
`binds the target's full primary key on '${targetMeta.prefix}', which is a point read of a single item, not a child collection. 'samePartition' requires a partial key \u2014 the parent fields fully supply the PK and a proper prefix of the segmented SK, leaving a begins_with boundary for the children.`
|
|
525
|
-
)
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
return results;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// src/linter/rules/maintenance-shared.ts
|
|
532
|
-
function isMaintainedRelation(relation) {
|
|
533
|
-
const triggers = relation.options?.write?.maintainedOn;
|
|
534
|
-
return Array.isArray(triggers) && triggers.length > 0;
|
|
535
|
-
}
|
|
536
|
-
function maintainedRelations(metadata) {
|
|
537
|
-
return metadata.relations.filter(isMaintainedRelation);
|
|
538
|
-
}
|
|
539
|
-
function logicalName(metadata) {
|
|
540
|
-
return metadata.prefix.endsWith("#") ? metadata.prefix.slice(0, -1) : metadata.prefix;
|
|
541
|
-
}
|
|
542
|
-
function payloadAttributes(meta) {
|
|
543
|
-
const attrs = /* @__PURE__ */ new Set();
|
|
544
|
-
for (const f of meta.fields) attrs.add(f.propertyName);
|
|
545
|
-
if (meta.primaryKey) {
|
|
546
|
-
for (const f of segmentFieldNames(meta.primaryKey.segmented.pkSegments)) {
|
|
547
|
-
attrs.add(f);
|
|
548
|
-
}
|
|
549
|
-
for (const f of segmentFieldNames(meta.primaryKey.segmented.skSegments)) {
|
|
550
|
-
attrs.add(f);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
for (const gsi2 of meta.gsiDefinitions) {
|
|
554
|
-
for (const f of segmentFieldNames(gsi2.segmented.pkSegments)) attrs.add(f);
|
|
555
|
-
for (const f of segmentFieldNames(gsi2.segmented.skSegments)) attrs.add(f);
|
|
556
|
-
}
|
|
557
|
-
return attrs;
|
|
558
|
-
}
|
|
559
|
-
function isPayloadRooted(value) {
|
|
560
|
-
return value.startsWith("$.input.") || value.startsWith("$.entity.");
|
|
561
|
-
}
|
|
562
|
-
function projectionSourceAttribute(value) {
|
|
563
|
-
const path = typeof value === "string" ? value : value.path;
|
|
564
|
-
if (typeof value === "string") {
|
|
565
|
-
if (!isPayloadRooted(path)) return path;
|
|
566
|
-
} else if (!isPayloadRooted(path)) {
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
const stripped = path.replace(/^\$\.(input|entity)\./, "");
|
|
570
|
-
return stripped.split(".")[0] ?? stripped;
|
|
571
|
-
}
|
|
572
|
-
function projectionEntries(projection) {
|
|
573
|
-
return projection ? Object.entries(projection) : [];
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
// src/linter/rules/missing-context.ts
|
|
577
|
-
var missingContextRule = {
|
|
578
|
-
id: "missing-context",
|
|
579
|
-
severity: "error",
|
|
580
|
-
check(metadata, registry) {
|
|
581
|
-
const results = [];
|
|
582
|
-
for (const relation of maintainedRelations(metadata)) {
|
|
583
|
-
results.push(...checkRelation2(metadata, relation, registry));
|
|
584
|
-
}
|
|
585
|
-
return results;
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
function err2(metadata, relation, message) {
|
|
589
|
-
return {
|
|
590
|
-
ruleId: "missing-context",
|
|
591
|
-
severity: "error",
|
|
592
|
-
message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' ${message}`,
|
|
593
|
-
entity: metadata.prefix,
|
|
594
|
-
field: relation.propertyName
|
|
26
|
+
unique: options?.unique ?? false,
|
|
27
|
+
// Purely-documentary description (issue #166); emitted only when declared so an
|
|
28
|
+
// undescribed index carries no `description` key (byte-identical to pre-#166).
|
|
29
|
+
...options?.description !== void 0 ? { description: options.description } : {}
|
|
595
30
|
};
|
|
596
31
|
}
|
|
597
|
-
function checkRelation2(metadata, relation, registry) {
|
|
598
|
-
let sourceMeta;
|
|
599
|
-
try {
|
|
600
|
-
sourceMeta = registry.get(relation.targetFactory());
|
|
601
|
-
} catch {
|
|
602
|
-
return [];
|
|
603
|
-
}
|
|
604
|
-
const sourceName = logicalName(sourceMeta);
|
|
605
|
-
const payload = payloadAttributes(sourceMeta);
|
|
606
|
-
const available = `available: ${[...payload].sort().map((f) => `'${f}'`).join(", ")}`;
|
|
607
|
-
const results = [];
|
|
608
|
-
for (const sourceField of Object.keys(relation.keyBinding)) {
|
|
609
|
-
if (!payload.has(sourceField)) {
|
|
610
|
-
results.push(
|
|
611
|
-
err2(
|
|
612
|
-
metadata,
|
|
613
|
-
relation,
|
|
614
|
-
`resolves its destination row from source field '${sourceField}', but the source entity '${sourceName}' carries no such attribute on its write payload (${available}). Phase 1 maintenance is payload \u540C\u68B1 only (no write-time fetch) \u2014 include '${sourceField}' on '${sourceName}' or bind a key field it does carry.`
|
|
615
|
-
)
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
for (const [attr, value] of projectionEntries(relation.options?.projection)) {
|
|
620
|
-
const path = typeof value === "string" ? value : value.path;
|
|
621
|
-
const srcAttr = projectionSourceAttribute(value);
|
|
622
|
-
if (srcAttr === null || typeof value !== "string" && !isPayloadRooted(path)) {
|
|
623
|
-
results.push(
|
|
624
|
-
err2(
|
|
625
|
-
metadata,
|
|
626
|
-
relation,
|
|
627
|
-
`projects '${attr}' from '${path}', which is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). Phase 1 maintenance projects only from the source payload (payload \u540C\u68B1); there is no fetch / re-projection.`
|
|
628
|
-
)
|
|
629
|
-
);
|
|
630
|
-
continue;
|
|
631
|
-
}
|
|
632
|
-
if (!payload.has(srcAttr)) {
|
|
633
|
-
results.push(
|
|
634
|
-
err2(
|
|
635
|
-
metadata,
|
|
636
|
-
relation,
|
|
637
|
-
`projects '${attr}' from source attribute '${srcAttr}', but the source entity '${sourceName}' carries no such attribute on its write payload (${available}). Phase 1 maintenance is payload \u540C\u68B1 only (no write-time fetch / re-projection) \u2014 an attribute the source row does not carry cannot be projected.`
|
|
638
|
-
)
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
return results;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// src/linter/rules/embedded-snapshot-size.ts
|
|
646
|
-
var ITEM_LIMIT_BYTES = 400 * 1024;
|
|
647
|
-
var SOFT_BUDGET_BYTES = ITEM_LIMIT_BYTES / 2;
|
|
648
|
-
var DEFAULT_STRING_BYTES = 256;
|
|
649
|
-
var ATTR_NAME_OVERHEAD_BYTES = 16;
|
|
650
|
-
var embeddedSnapshotSizeRule = {
|
|
651
|
-
id: "400kb",
|
|
652
|
-
severity: "warning",
|
|
653
|
-
check(metadata, _registry) {
|
|
654
|
-
const results = [];
|
|
655
|
-
for (const relation of maintainedRelations(metadata)) {
|
|
656
|
-
const result = checkRelation3(metadata, relation);
|
|
657
|
-
if (result) results.push(result);
|
|
658
|
-
}
|
|
659
|
-
return results;
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
function estimateItemBytes(relation) {
|
|
663
|
-
let bytes = 0;
|
|
664
|
-
for (const [attr, value] of projectionEntries(relation.options?.projection)) {
|
|
665
|
-
bytes += ATTR_NAME_OVERHEAD_BYTES + attr.length;
|
|
666
|
-
bytes += attributeValueBytes(value);
|
|
667
|
-
}
|
|
668
|
-
return bytes;
|
|
669
|
-
}
|
|
670
|
-
function attributeValueBytes(value) {
|
|
671
|
-
if (typeof value !== "string" && value.op === "preview") {
|
|
672
|
-
const [n] = value.args;
|
|
673
|
-
if (typeof n === "number" && Number.isFinite(n)) return n;
|
|
674
|
-
}
|
|
675
|
-
return DEFAULT_STRING_BYTES;
|
|
676
|
-
}
|
|
677
|
-
function checkRelation3(metadata, relation) {
|
|
678
|
-
const itemBytes = estimateItemBytes(relation);
|
|
679
|
-
if (itemBytes === 0) return null;
|
|
680
|
-
const isCollection = relation.type === "hasMany";
|
|
681
|
-
const maxItems = relation.options?.read?.maxItems;
|
|
682
|
-
if (isCollection && maxItems === void 0) return null;
|
|
683
|
-
const count = isCollection ? maxItems : 1;
|
|
684
|
-
const estimate = itemBytes * count;
|
|
685
|
-
const shape = isCollection ? `${count} item(s) \xD7 ~${itemBytes} B` : `~${itemBytes} B snapshot`;
|
|
686
|
-
if (estimate >= ITEM_LIMIT_BYTES) {
|
|
687
|
-
return {
|
|
688
|
-
ruleId: "400kb",
|
|
689
|
-
severity: "error",
|
|
690
|
-
message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' materializes ${shape} \u2248 ${estimate} B onto the owner row, which meets or exceeds DynamoDB's 400 KB item limit (${ITEM_LIMIT_BYTES} B). The maintenance write would fail once the collection fills \u2014 reduce \`read.maxItems\` or the projected attributes (e.g. tighter \`preview(n)\` bounds).`,
|
|
691
|
-
entity: metadata.prefix,
|
|
692
|
-
field: relation.propertyName
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
if (estimate >= SOFT_BUDGET_BYTES) {
|
|
696
|
-
return {
|
|
697
|
-
ruleId: "400kb",
|
|
698
|
-
severity: "warning",
|
|
699
|
-
message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' materializes ${shape} \u2248 ${estimate} B onto the owner row, over half of DynamoDB's 400 KB item limit (${ITEM_LIMIT_BYTES} B). Little headroom remains for the rest of the item \u2014 consider a tighter \`read.maxItems\` / projection.`,
|
|
700
|
-
entity: metadata.prefix,
|
|
701
|
-
field: relation.propertyName
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
return null;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// src/linter/rules/hot-partition.ts
|
|
708
|
-
var HOT_TRIGGER_FANIN = 2;
|
|
709
|
-
var hotPartitionRule = {
|
|
710
|
-
id: "hot-partition",
|
|
711
|
-
severity: "warning",
|
|
712
|
-
check(metadata, _registry) {
|
|
713
|
-
const results = [];
|
|
714
|
-
const maintained = maintainedRelations(metadata);
|
|
715
|
-
for (const relation of maintained) {
|
|
716
|
-
if (relation.type !== "hasMany") continue;
|
|
717
|
-
if (relation.options?.read?.maxItems === void 0) {
|
|
718
|
-
results.push(unboundedWarning(metadata, relation));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
const byTrigger = /* @__PURE__ */ new Map();
|
|
722
|
-
for (const relation of maintained) {
|
|
723
|
-
for (const trigger of relation.options?.write?.maintainedOn ?? []) {
|
|
724
|
-
const bucket = byTrigger.get(trigger);
|
|
725
|
-
if (bucket) bucket.push(relation.propertyName);
|
|
726
|
-
else byTrigger.set(trigger, [relation.propertyName]);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
for (const [trigger, props] of byTrigger) {
|
|
730
|
-
if (props.length > HOT_TRIGGER_FANIN) {
|
|
731
|
-
results.push(fanInWarning(metadata, trigger, props));
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return results;
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
function unboundedWarning(metadata, relation) {
|
|
738
|
-
return {
|
|
739
|
-
ruleId: "hot-partition",
|
|
740
|
-
severity: "warning",
|
|
741
|
-
message: `Maintained collection '${relation.propertyName}' on '${logicalName(metadata)}' has no \`read.maxItems\` bound: every maintained source row appends to the same owner ("root") item, so that single partition takes unbounded write traffic and the item trends toward the 400 KB limit. Declare a \`read.maxItems\` window (e.g. latest-N) or read the children from their own partition (\`samePartition\`) instead of inlining a snapshot.`,
|
|
742
|
-
entity: metadata.prefix,
|
|
743
|
-
field: relation.propertyName
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
function fanInWarning(metadata, trigger, props) {
|
|
747
|
-
return {
|
|
748
|
-
ruleId: "hot-partition",
|
|
749
|
-
severity: "warning",
|
|
750
|
-
message: `${props.length} maintained relations on '${logicalName(metadata)}' ([${props.map((p) => `'${p}'`).join(", ")}]) all fire on trigger '${trigger}', concentrating that many maintenance writes onto the single owner ("root") row in one mutation. This amplifies per-source-row write cost on one partition \u2014 consider splitting the maintained shapes across rows / partitions.`,
|
|
751
|
-
entity: metadata.prefix
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// src/linter/rules/fan-out.ts
|
|
756
|
-
var MAX_MAINTENANCE_HOPS = 1;
|
|
757
|
-
var fanOutRule = {
|
|
758
|
-
id: "fan-out",
|
|
759
|
-
severity: "warning",
|
|
760
|
-
check(metadata, registry) {
|
|
761
|
-
const results = [];
|
|
762
|
-
for (const relation of maintainedRelations(metadata)) {
|
|
763
|
-
const point = checkDestinationFanOut(metadata, relation);
|
|
764
|
-
if (point) results.push(point);
|
|
765
|
-
}
|
|
766
|
-
const chain = maintenanceChain(metadata, registry, /* @__PURE__ */ new Set([modelKey(metadata)]));
|
|
767
|
-
if (chain.hops > MAX_MAINTENANCE_HOPS) {
|
|
768
|
-
results.push({
|
|
769
|
-
ruleId: "fan-out",
|
|
770
|
-
severity: "warning",
|
|
771
|
-
message: `Entity '${chain.root}' is the root of a maintenance chain ${chain.hops} hops deep, terminating at '${logicalName(metadata)}': a single '${chain.root}' lifecycle event cascades through ${chain.hops} maintenance writes \u2014 each maintained row is itself the trigger source of the next hop \u2014 until it reaches '${logicalName(metadata)}'. A multi-step maintenance fan-out amplifies one write into many; flatten the chain or move downstream hops to a stream (\`updateMode: 'stream'\`).`,
|
|
772
|
-
entity: metadata.prefix
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
return results;
|
|
776
|
-
}
|
|
777
|
-
};
|
|
778
|
-
function checkDestinationFanOut(metadata, relation) {
|
|
779
|
-
const ownerKeyFields = Object.values(relation.keyBinding);
|
|
780
|
-
if (ownerKeyFields.length === 0) return null;
|
|
781
|
-
let resolved;
|
|
782
|
-
try {
|
|
783
|
-
resolved = resolveKey(ownerKeyFields, metadata);
|
|
784
|
-
} catch {
|
|
785
|
-
return null;
|
|
786
|
-
}
|
|
787
|
-
const isPoint = !resolved.partial && (resolved.type === "pk" || resolved.type === "gsi" && resolved.unique);
|
|
788
|
-
if (isPoint) return null;
|
|
789
|
-
const shape = resolved.partial ? `a partial ${resolved.type.toUpperCase()} (a begins_with range of rows)` : `a non-unique ${resolved.type.toUpperCase()} (many matching rows)`;
|
|
790
|
-
return {
|
|
791
|
-
ruleId: "fan-out",
|
|
792
|
-
severity: "warning",
|
|
793
|
-
message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' binds its destination row by [${ownerKeyFields.map((f) => `'${f}'`).join(", ")}], which resolves to ${shape} on '${logicalName(metadata)}'. A single source event then fans its maintenance write out across every matching destination row in one mutation (unbounded fan-out). Bind the destination's full primary key (or a unique GSI) so each source event maintains exactly one row.`,
|
|
794
|
-
entity: metadata.prefix,
|
|
795
|
-
field: relation.propertyName
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
function modelKey(metadata) {
|
|
799
|
-
return logicalName(metadata);
|
|
800
|
-
}
|
|
801
|
-
function maintenanceChain(metadata, registry, visited) {
|
|
802
|
-
const maintained = maintainedRelations(metadata);
|
|
803
|
-
if (maintained.length === 0) return { hops: 0, root: logicalName(metadata) };
|
|
804
|
-
let best = { hops: 0, root: logicalName(metadata) };
|
|
805
|
-
for (const relation of maintained) {
|
|
806
|
-
let targetMeta;
|
|
807
|
-
try {
|
|
808
|
-
targetMeta = registry.get(relation.targetFactory());
|
|
809
|
-
} catch {
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
const key2 = modelKey(targetMeta);
|
|
813
|
-
if (visited.has(key2)) continue;
|
|
814
|
-
const next = new Set(visited);
|
|
815
|
-
next.add(key2);
|
|
816
|
-
const upstream = maintenanceChain(targetMeta, registry, next);
|
|
817
|
-
if (1 + upstream.hops > best.hops) {
|
|
818
|
-
best = { hops: 1 + upstream.hops, root: upstream.root };
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
return best;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// src/linter/rules/multi-maintainer-same-row.ts
|
|
825
|
-
var multiMaintainerSameRowRule = {
|
|
826
|
-
id: "multi-maintainer-same-row",
|
|
827
|
-
severity: "error",
|
|
828
|
-
check(metadata, _registry) {
|
|
829
|
-
const maintained = maintainedRelations(metadata);
|
|
830
|
-
if (maintained.length < 2) return [];
|
|
831
|
-
const owner = logicalName(metadata);
|
|
832
|
-
const groups = /* @__PURE__ */ new Map();
|
|
833
|
-
for (const relation of maintained) {
|
|
834
|
-
const rowKey = destinationRowKey(owner, relation);
|
|
835
|
-
for (const trigger of relation.options?.write?.maintainedOn ?? []) {
|
|
836
|
-
const groupKey = `${trigger} ${rowKey}`;
|
|
837
|
-
const group = groups.get(groupKey);
|
|
838
|
-
if (group) group.props.push(relation.propertyName);
|
|
839
|
-
else groups.set(groupKey, { trigger, props: [relation.propertyName] });
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
const results = [];
|
|
843
|
-
for (const { trigger, props } of groups.values()) {
|
|
844
|
-
if (props.length < 2) continue;
|
|
845
|
-
results.push({
|
|
846
|
-
ruleId: "multi-maintainer-same-row",
|
|
847
|
-
severity: "error",
|
|
848
|
-
message: `Maintained relations [${props.map((p) => `'${p}'`).join(", ")}] on '${owner}' all fire on trigger '${trigger}' and write the SAME destination row. Phase 1 maintenance is "1 mutation \xD7 1 target row = 1 effect" (\u8AD6\u70B92 = b): a single mutation cannot touch one row with ${props.length} maintain effects \u2014 DynamoDB rejects two writes to the same key in one TransactWriteItems (cf. #96). Merge the projections into one maintained relation, or split them onto distinct destination rows / triggers.`,
|
|
849
|
-
entity: metadata.prefix,
|
|
850
|
-
field: props[0]
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
return results;
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
function destinationRowKey(owner, relation) {
|
|
857
|
-
const parts = Object.entries(relation.keyBinding).map(([sourceField, ownerField]) => `${ownerField}=$.entity.${sourceField}`).sort();
|
|
858
|
-
return `${owner}#${parts.join("&")}`;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// src/linter/rules/cfn-schema-consistency.ts
|
|
862
|
-
var MAX_GSIS_PER_TABLE = 20;
|
|
863
|
-
function gsiShape(gsi2) {
|
|
864
|
-
const hasSk = gsi2.segmented.skSegments.length > 0;
|
|
865
|
-
return { indexName: gsi2.indexName, hasSk };
|
|
866
|
-
}
|
|
867
|
-
function err3(entity, message) {
|
|
868
|
-
return {
|
|
869
|
-
ruleId: "cfn-schema-consistency",
|
|
870
|
-
severity: "error",
|
|
871
|
-
message,
|
|
872
|
-
entity
|
|
873
|
-
};
|
|
874
|
-
}
|
|
875
|
-
var cfnSchemaConsistencyRule = {
|
|
876
|
-
id: "cfn-schema-consistency",
|
|
877
|
-
severity: "error",
|
|
878
|
-
check(metadata, registry) {
|
|
879
|
-
return checkAllTables(metadata, registry);
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
function checkAllTables(current, registry) {
|
|
883
|
-
const seenMeta = /* @__PURE__ */ new Set();
|
|
884
|
-
const byPhysical = /* @__PURE__ */ new Map();
|
|
885
|
-
const add = (meta) => {
|
|
886
|
-
if (seenMeta.has(meta)) return;
|
|
887
|
-
seenMeta.add(meta);
|
|
888
|
-
const physical = TableMapping.resolve(meta.tableName);
|
|
889
|
-
const list = byPhysical.get(physical) ?? [];
|
|
890
|
-
list.push(meta);
|
|
891
|
-
byPhysical.set(physical, list);
|
|
892
|
-
};
|
|
893
|
-
add(current);
|
|
894
|
-
for (const [, meta] of registry.getFinalized()) add(meta);
|
|
895
|
-
const currentPhysical = TableMapping.resolve(current.tableName);
|
|
896
|
-
const entities = byPhysical.get(currentPhysical);
|
|
897
|
-
if (!entities) return [];
|
|
898
|
-
return checkTable(currentPhysical, entities);
|
|
899
|
-
}
|
|
900
|
-
function checkTable(physical, entities) {
|
|
901
|
-
const ordered = [...entities].sort((a, b) => a.prefix.localeCompare(b.prefix));
|
|
902
|
-
const results = [];
|
|
903
|
-
results.push(...checkBaseKeySchema(physical, ordered));
|
|
904
|
-
results.push(...checkGsiUnion(physical, ordered));
|
|
905
|
-
return results;
|
|
906
|
-
}
|
|
907
|
-
function checkBaseKeySchema(physical, entities) {
|
|
908
|
-
const withSk = [];
|
|
909
|
-
const withoutSk = [];
|
|
910
|
-
for (const meta of entities) {
|
|
911
|
-
if (!meta.primaryKey) continue;
|
|
912
|
-
const hasSk = meta.primaryKey.segmented.skSegments.length > 0;
|
|
913
|
-
(hasSk ? withSk : withoutSk).push(meta.prefix);
|
|
914
|
-
}
|
|
915
|
-
if (withSk.length > 0 && withoutSk.length > 0) {
|
|
916
|
-
const offender = [...withSk, ...withoutSk].sort()[0];
|
|
917
|
-
return [
|
|
918
|
-
err3(
|
|
919
|
-
offender,
|
|
920
|
-
`CloudFormation schema consistency: physical table '${physical}' has an ambiguous base KeySchema \u2014 some entities declare a sort key and some do not (with SK: [${withSk.join(", ")}]; without SK: [${withoutSk.join(", ")}]). A single DynamoDB table has exactly one KeySchema; every entity sharing a table must agree on whether the base key has an SK.`
|
|
921
|
-
)
|
|
922
|
-
];
|
|
923
|
-
}
|
|
924
|
-
return [];
|
|
925
|
-
}
|
|
926
|
-
function checkGsiUnion(physical, entities) {
|
|
927
|
-
const results = [];
|
|
928
|
-
const seen = /* @__PURE__ */ new Map();
|
|
929
|
-
for (const meta of entities) {
|
|
930
|
-
for (const gsi2 of meta.gsiDefinitions) {
|
|
931
|
-
const shape = gsiShape(gsi2);
|
|
932
|
-
const existing = seen.get(shape.indexName);
|
|
933
|
-
if (!existing) {
|
|
934
|
-
seen.set(shape.indexName, { shape, entity: meta.prefix });
|
|
935
|
-
continue;
|
|
936
|
-
}
|
|
937
|
-
const conflict = describeConflict(existing.shape, shape);
|
|
938
|
-
if (conflict) {
|
|
939
|
-
results.push(
|
|
940
|
-
err3(
|
|
941
|
-
meta.prefix,
|
|
942
|
-
`CloudFormation schema consistency: GSI '${shape.indexName}' on physical table '${physical}' is declared inconsistently across entities that share it \u2014 ${conflict} (first declared by '${existing.entity}', conflicting declaration on '${meta.prefix}'). A single DynamoDB table has exactly one GSI per index name; entities may share an index name (projecting different logical fields into the shared physical key), but only when its physical key structure (sort-key presence) is identical.`
|
|
943
|
-
)
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
if (seen.size > MAX_GSIS_PER_TABLE) {
|
|
949
|
-
const names = [...seen.keys()].sort();
|
|
950
|
-
const offender = [...entities].map((e) => e.prefix).sort()[0];
|
|
951
|
-
results.push(
|
|
952
|
-
err3(
|
|
953
|
-
offender,
|
|
954
|
-
`CloudFormation schema consistency: physical table '${physical}' would have ${seen.size} global secondary indexes after unioning across entities, exceeding the DynamoDB limit of ${MAX_GSIS_PER_TABLE}. Distinct index names: [${names.join(", ")}]. Reduce the number of distinct GSIs on this table.`
|
|
955
|
-
)
|
|
956
|
-
);
|
|
957
|
-
}
|
|
958
|
-
return results;
|
|
959
|
-
}
|
|
960
|
-
function describeConflict(a, b) {
|
|
961
|
-
if (a.hasSk !== b.hasSk) {
|
|
962
|
-
return `one declaration has a sort key and the other does not (${a.hasSk ? "with" : "without"} SK vs. ${b.hasSk ? "with" : "without"} SK)`;
|
|
963
|
-
}
|
|
964
|
-
return null;
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// src/linter/default-linter.ts
|
|
968
|
-
function createDefaultLinter() {
|
|
969
|
-
const linter = new Linter();
|
|
970
|
-
linter.addRule(noScanRule);
|
|
971
|
-
linter.addRule(requireLimitRule);
|
|
972
|
-
linter.addRule(gsiAmbiguityRule);
|
|
973
|
-
linter.addRule(missingGsiRule);
|
|
974
|
-
linter.addRule(relationDepthRule);
|
|
975
|
-
linter.addRule(samePartitionPresetRule);
|
|
976
|
-
linter.addRule(missingContextRule);
|
|
977
|
-
linter.addRule(embeddedSnapshotSizeRule);
|
|
978
|
-
linter.addRule(hotPartitionRule);
|
|
979
|
-
linter.addRule(fanOutRule);
|
|
980
|
-
linter.addRule(multiMaintainerSameRowRule);
|
|
981
|
-
linter.addRule(cfnSchemaConsistencyRule);
|
|
982
|
-
return linter;
|
|
983
|
-
}
|
|
984
32
|
|
|
985
33
|
// src/metadata/registry.ts
|
|
34
|
+
function defaultLinterForEnv() {
|
|
35
|
+
return process.env.NODE_ENV === "production" ? null : createDefaultLinter();
|
|
36
|
+
}
|
|
986
37
|
var MetadataRegistry = class {
|
|
987
38
|
static store = /* @__PURE__ */ new Map();
|
|
988
|
-
static _linter =
|
|
39
|
+
static _linter = defaultLinterForEnv();
|
|
989
40
|
/**
|
|
990
41
|
* A monotonically-increasing counter bumped on every change to the set of
|
|
991
42
|
* registered models ({@link register} / {@link clear}). It lets a consumer that
|
|
@@ -1056,14 +107,21 @@ var MetadataRegistry = class {
|
|
|
1056
107
|
static set linter(linter) {
|
|
1057
108
|
this._linter = linter;
|
|
1058
109
|
}
|
|
1059
|
-
/** Resets linter to the default built-in rule set. */
|
|
110
|
+
/** Resets linter to the environment default (built-in rule set outside production). */
|
|
1060
111
|
static resetLinter() {
|
|
1061
|
-
this._linter =
|
|
112
|
+
this._linter = defaultLinterForEnv();
|
|
1062
113
|
}
|
|
1063
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* @internal — for testing only. Resets to a no-op linter (no rules): matches
|
|
116
|
+
* the pre-#189 `new Linter()` behaviour where a cleared registry does not lint
|
|
117
|
+
* on re-registration. `null` is finalize-equivalent to an empty linter (the
|
|
118
|
+
* `finalize` guard skips a null linter), and nothing reads the linter getter
|
|
119
|
+
* to invoke it, so this preserves existing test semantics without importing
|
|
120
|
+
* the `Linter` class as a value (keeping it out of a production bundle).
|
|
121
|
+
*/
|
|
1064
122
|
static clear() {
|
|
1065
123
|
this.store.clear();
|
|
1066
|
-
this._linter =
|
|
124
|
+
this._linter = null;
|
|
1067
125
|
this._generation++;
|
|
1068
126
|
}
|
|
1069
127
|
static finalize(target, meta) {
|
|
@@ -1084,12 +142,17 @@ var MetadataRegistry = class {
|
|
|
1084
142
|
indexName: value.indexName,
|
|
1085
143
|
segmented: value.segmented,
|
|
1086
144
|
inputFieldNames: value.inputFieldNames,
|
|
1087
|
-
unique: value.unique
|
|
145
|
+
unique: value.unique,
|
|
146
|
+
// Purely-documentary GSI description (issue #166); copied only when declared
|
|
147
|
+
// so an undescribed index is byte-identical to the pre-#166 metadata.
|
|
148
|
+
...value.description !== void 0 ? { description: value.description } : {}
|
|
1088
149
|
});
|
|
1089
150
|
}
|
|
1090
151
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
152
|
+
if (this._linter) {
|
|
153
|
+
const results = this._linter.run(meta, this);
|
|
154
|
+
this.handleLintResults(results);
|
|
155
|
+
}
|
|
1093
156
|
meta._finalized = true;
|
|
1094
157
|
}
|
|
1095
158
|
static handleLintResults(results) {
|
|
@@ -1953,7 +1016,7 @@ function isOperatorObject(value) {
|
|
|
1953
1016
|
}
|
|
1954
1017
|
const keys = Object.keys(value);
|
|
1955
1018
|
if (keys.length === 0) return false;
|
|
1956
|
-
return keys.every((
|
|
1019
|
+
return keys.every((k) => OPERATOR_KEYS.has(k));
|
|
1957
1020
|
}
|
|
1958
1021
|
function joinAnd(clauses) {
|
|
1959
1022
|
if (clauses.length === 1) return clauses[0];
|
|
@@ -2053,25 +1116,25 @@ function compileNode(ctx, node) {
|
|
|
2053
1116
|
return compileRaw(ctx, node);
|
|
2054
1117
|
}
|
|
2055
1118
|
const clauses = [];
|
|
2056
|
-
for (const [
|
|
1119
|
+
for (const [key, value] of Object.entries(node)) {
|
|
2057
1120
|
if (value === void 0) continue;
|
|
2058
|
-
if (
|
|
1121
|
+
if (key === "and" || key === "or") {
|
|
2059
1122
|
const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
|
|
2060
1123
|
if (parts.length === 0) continue;
|
|
2061
1124
|
if (parts.length === 1) {
|
|
2062
1125
|
clauses.push(parts[0]);
|
|
2063
1126
|
} else {
|
|
2064
|
-
const sep =
|
|
1127
|
+
const sep = key === "and" ? " AND " : " OR ";
|
|
2065
1128
|
clauses.push(`(${parts.map((p) => wrap(p)).join(sep)})`);
|
|
2066
1129
|
}
|
|
2067
1130
|
continue;
|
|
2068
1131
|
}
|
|
2069
|
-
if (
|
|
1132
|
+
if (key === "not") {
|
|
2070
1133
|
const inner = compileNode(ctx, value);
|
|
2071
1134
|
if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
|
|
2072
1135
|
continue;
|
|
2073
1136
|
}
|
|
2074
|
-
const clause = compileField(ctx,
|
|
1137
|
+
const clause = compileField(ctx, key, value);
|
|
2075
1138
|
if (clause.length > 0) clauses.push(clause);
|
|
2076
1139
|
}
|
|
2077
1140
|
return joinAnd(clauses);
|
|
@@ -2089,11 +1152,11 @@ function resolveLeaf(value, resolveParam) {
|
|
|
2089
1152
|
function resolveNode(node, resolveParam) {
|
|
2090
1153
|
const obj = node;
|
|
2091
1154
|
const out = {};
|
|
2092
|
-
for (const [
|
|
2093
|
-
if (
|
|
2094
|
-
out[
|
|
2095
|
-
} else if (
|
|
2096
|
-
out[
|
|
1155
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1156
|
+
if (key === "and" || key === "or") {
|
|
1157
|
+
out[key] = value.map((s) => resolveNode(s, resolveParam));
|
|
1158
|
+
} else if (key === "not") {
|
|
1159
|
+
out[key] = resolveNode(value, resolveParam);
|
|
2097
1160
|
} else {
|
|
2098
1161
|
const ops = value;
|
|
2099
1162
|
const rendered = {};
|
|
@@ -2114,7 +1177,7 @@ function resolveNode(node, resolveParam) {
|
|
|
2114
1177
|
rendered[op] = resolveLeaf(opVal, resolveParam);
|
|
2115
1178
|
}
|
|
2116
1179
|
}
|
|
2117
|
-
out[
|
|
1180
|
+
out[key] = rendered;
|
|
2118
1181
|
}
|
|
2119
1182
|
}
|
|
2120
1183
|
return out;
|
|
@@ -2533,14 +1596,14 @@ var MiddlewareRuntime = class {
|
|
|
2533
1596
|
* is the host's responsibility — a hook recovering a `query` should return an
|
|
2534
1597
|
* item / `null`, one recovering a `list` a `{ items, cursor }`.
|
|
2535
1598
|
*/
|
|
2536
|
-
async runRequestError(ctx,
|
|
1599
|
+
async runRequestError(ctx, err) {
|
|
2537
1600
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2538
1601
|
const onError = this.chain[i].read?.onError;
|
|
2539
1602
|
if (!onError) continue;
|
|
2540
|
-
const recovered = await onError(ctx,
|
|
1603
|
+
const recovered = await onError(ctx, err);
|
|
2541
1604
|
if (recovered !== void 0) return recovered;
|
|
2542
1605
|
}
|
|
2543
|
-
throw
|
|
1606
|
+
throw err;
|
|
2544
1607
|
}
|
|
2545
1608
|
/**
|
|
2546
1609
|
* Drive ONE physical op (root read or any fan-out fetch) through R2 → send →
|
|
@@ -2580,14 +1643,14 @@ var MiddlewareRuntime = class {
|
|
|
2580
1643
|
if (afterFetch) items = await afterFetch(ctx, items);
|
|
2581
1644
|
}
|
|
2582
1645
|
return items;
|
|
2583
|
-
} catch (
|
|
1646
|
+
} catch (err) {
|
|
2584
1647
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2585
1648
|
const onError = this.chain[i].read?.op?.onError;
|
|
2586
1649
|
if (!onError) continue;
|
|
2587
|
-
const recovered = await onError(ctx,
|
|
1650
|
+
const recovered = await onError(ctx, err);
|
|
2588
1651
|
if (recovered !== void 0) return recovered;
|
|
2589
1652
|
}
|
|
2590
|
-
throw
|
|
1653
|
+
throw err;
|
|
2591
1654
|
}
|
|
2592
1655
|
}
|
|
2593
1656
|
};
|
|
@@ -2651,14 +1714,14 @@ var WriteRuntime = class {
|
|
|
2651
1714
|
* short-circuits the chain); the value becomes the write's resolved value.
|
|
2652
1715
|
* Otherwise the original error rethrows (symmetric with the read `onError`).
|
|
2653
1716
|
*/
|
|
2654
|
-
async runWriteError(ctx,
|
|
1717
|
+
async runWriteError(ctx, err) {
|
|
2655
1718
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2656
1719
|
const onError = this.chain[i].write?.onError;
|
|
2657
1720
|
if (!onError) continue;
|
|
2658
|
-
const recovered = await onError(ctx,
|
|
1721
|
+
const recovered = await onError(ctx, err);
|
|
2659
1722
|
if (recovered !== void 0) return recovered;
|
|
2660
1723
|
}
|
|
2661
|
-
throw
|
|
1724
|
+
throw err;
|
|
2662
1725
|
}
|
|
2663
1726
|
/** Build a W3/W4/W5 persist context (pure — no hooks run). */
|
|
2664
1727
|
persistCtx(items, origins, transaction) {
|
|
@@ -2687,20 +1750,20 @@ var WriteRuntime = class {
|
|
|
2687
1750
|
if (before) await before(ctx);
|
|
2688
1751
|
}
|
|
2689
1752
|
results = await send(ctx.items);
|
|
2690
|
-
} catch (
|
|
1753
|
+
} catch (err) {
|
|
2691
1754
|
let recovered;
|
|
2692
1755
|
let didRecover = false;
|
|
2693
1756
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2694
1757
|
const onError = this.chain[i].write?.persist?.onError;
|
|
2695
1758
|
if (!onError) continue;
|
|
2696
|
-
const r = await onError(ctx,
|
|
1759
|
+
const r = await onError(ctx, err);
|
|
2697
1760
|
if (r !== void 0) {
|
|
2698
1761
|
recovered = r;
|
|
2699
1762
|
didRecover = true;
|
|
2700
1763
|
break;
|
|
2701
1764
|
}
|
|
2702
1765
|
}
|
|
2703
|
-
if (!didRecover) throw
|
|
1766
|
+
if (!didRecover) throw err;
|
|
2704
1767
|
results = recovered;
|
|
2705
1768
|
}
|
|
2706
1769
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
@@ -2921,8 +1984,8 @@ async function executeSingleWriteWithHooks(req, mw) {
|
|
|
2921
1984
|
await mw.runWriteBefore(ctx);
|
|
2922
1985
|
const change2 = await persistRewrittenWrite(req.modelClass, ctx, mw);
|
|
2923
1986
|
await mw.runWriteAfter(ctx, change2);
|
|
2924
|
-
} catch (
|
|
2925
|
-
await mw.runWriteError(ctx,
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
await mw.runWriteError(ctx, err);
|
|
2926
1989
|
}
|
|
2927
1990
|
}
|
|
2928
1991
|
async function persistRewrittenWrite(modelClass, ctx, mw) {
|
|
@@ -3500,12 +2563,12 @@ var TransactionContext = class {
|
|
|
3500
2563
|
keys: { pk: String(updateInput.Key.PK), sk: String(updateInput.Key.SK ?? "") }
|
|
3501
2564
|
});
|
|
3502
2565
|
}
|
|
3503
|
-
delete(model,
|
|
2566
|
+
delete(model, key, options) {
|
|
3504
2567
|
this.assertWithinLimit();
|
|
3505
2568
|
const modelClass = resolveModelClass(model);
|
|
3506
|
-
const deleteInput = buildDeleteInput(modelClass,
|
|
2569
|
+
const deleteInput = buildDeleteInput(modelClass, key, options);
|
|
3507
2570
|
this.items.push({ Delete: deleteInput });
|
|
3508
|
-
this.logicalOps.push({ kind: "delete", modelClass, key
|
|
2571
|
+
this.logicalOps.push({ kind: "delete", modelClass, key, options });
|
|
3509
2572
|
this.captureMeta.push({
|
|
3510
2573
|
modelName: modelClass.name,
|
|
3511
2574
|
op: "delete",
|
|
@@ -3525,7 +2588,7 @@ var TransactionContext = class {
|
|
|
3525
2588
|
* @throws if `options.condition` is missing — a ConditionCheck without an
|
|
3526
2589
|
* assertion is meaningless.
|
|
3527
2590
|
*/
|
|
3528
|
-
conditionCheck(model,
|
|
2591
|
+
conditionCheck(model, key, options) {
|
|
3529
2592
|
if (!options || !options.condition) {
|
|
3530
2593
|
throw new Error(
|
|
3531
2594
|
"TransactionContext.conditionCheck requires a `condition` (the read-only assertion it makes); e.g. `{ condition: { attributeExists: 'PK' } }`."
|
|
@@ -3533,7 +2596,7 @@ var TransactionContext = class {
|
|
|
3533
2596
|
}
|
|
3534
2597
|
this.assertWithinLimit();
|
|
3535
2598
|
const modelClass = resolveModelClass(model);
|
|
3536
|
-
const { TableName, Key } = buildDeleteInput(modelClass,
|
|
2599
|
+
const { TableName, Key } = buildDeleteInput(modelClass, key);
|
|
3537
2600
|
const cond2 = buildConditionExpression(options.condition);
|
|
3538
2601
|
const check = {
|
|
3539
2602
|
TableName,
|
|
@@ -3677,19 +2740,19 @@ function hydrateItem(raw, select, metadata, updatable = false) {
|
|
|
3677
2740
|
}
|
|
3678
2741
|
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
3679
2742
|
const result = {};
|
|
3680
|
-
for (const [
|
|
2743
|
+
for (const [key, selectValue] of Object.entries(select)) {
|
|
3681
2744
|
if (selectValue === true) {
|
|
3682
|
-
if (raw[
|
|
3683
|
-
result[
|
|
2745
|
+
if (raw[key] !== void 0) {
|
|
2746
|
+
result[key] = raw[key];
|
|
3684
2747
|
}
|
|
3685
2748
|
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
3686
|
-
const nested = raw[
|
|
2749
|
+
const nested = raw[key];
|
|
3687
2750
|
if (nested && typeof nested === "object") {
|
|
3688
|
-
result[
|
|
2751
|
+
result[key] = hydrateEmbedded(
|
|
3689
2752
|
nested,
|
|
3690
2753
|
selectValue,
|
|
3691
2754
|
_metadata,
|
|
3692
|
-
|
|
2755
|
+
key
|
|
3693
2756
|
);
|
|
3694
2757
|
}
|
|
3695
2758
|
}
|
|
@@ -4640,22 +3703,7 @@ function buildSubscribeHandler(handlers) {
|
|
|
4640
3703
|
}
|
|
4641
3704
|
|
|
4642
3705
|
export {
|
|
4643
|
-
isColumn,
|
|
4644
|
-
createColumnMap,
|
|
4645
|
-
isKeySegment,
|
|
4646
|
-
k,
|
|
4647
|
-
segmentFieldNames,
|
|
4648
|
-
key,
|
|
4649
3706
|
gsi,
|
|
4650
|
-
Linter,
|
|
4651
|
-
noScanRule,
|
|
4652
|
-
requireLimitRule,
|
|
4653
|
-
gsiAmbiguityRule,
|
|
4654
|
-
resolveKey,
|
|
4655
|
-
missingGsiRule,
|
|
4656
|
-
relationDepthRule,
|
|
4657
|
-
TableMapping,
|
|
4658
|
-
createDefaultLinter,
|
|
4659
3707
|
MetadataRegistry,
|
|
4660
3708
|
pkTemplate,
|
|
4661
3709
|
skTemplate,
|