@lovrozagar/oat 0.1.0
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/LICENSE +21 -0
- package/README.md +401 -0
- package/bin/oat.js +2 -0
- package/dist/cli.js +386 -0
- package/dist/config/define-config.d.ts +165 -0
- package/dist/config/define-config.d.ts.map +1 -0
- package/dist/config/define-config.js +24 -0
- package/dist/config/define-config.js.map +1 -0
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +61 -0
- package/dist/config/load.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +9 -0
- package/dist/reference/defects.d.ts +254 -0
- package/dist/reference/defects.d.ts.map +1 -0
- package/dist/reference/defects.js +265 -0
- package/dist/reference/defects.js.map +1 -0
- package/dist/reference/dialect.d.ts +104 -0
- package/dist/reference/dialect.d.ts.map +1 -0
- package/dist/reference/dialect.js +174 -0
- package/dist/reference/dialect.js.map +1 -0
- package/dist/reference/http.d.ts +79 -0
- package/dist/reference/http.d.ts.map +1 -0
- package/dist/reference/http.js +889 -0
- package/dist/reference/http.js.map +1 -0
- package/dist/reference/model.d.ts +56 -0
- package/dist/reference/model.d.ts.map +1 -0
- package/dist/reference/model.js +156 -0
- package/dist/reference/model.js.map +1 -0
- package/dist/reference/query.d.ts +45 -0
- package/dist/reference/query.d.ts.map +1 -0
- package/dist/reference/query.js +353 -0
- package/dist/reference/query.js.map +1 -0
- package/dist/reference/spec.d.ts +13 -0
- package/dist/reference/spec.d.ts.map +1 -0
- package/dist/reference/spec.js +547 -0
- package/dist/reference/spec.js.map +1 -0
- package/dist/reference/store-api.d.ts +59 -0
- package/dist/reference/store-api.d.ts.map +1 -0
- package/dist/reference/store-api.js +76 -0
- package/dist/reference/store-api.js.map +1 -0
- package/dist/reference/stores/memory.d.ts +27 -0
- package/dist/reference/stores/memory.d.ts.map +1 -0
- package/dist/reference/stores/memory.js +97 -0
- package/dist/reference/stores/memory.js.map +1 -0
- package/dist/reference/stores/postgres.d.ts +61 -0
- package/dist/reference/stores/postgres.d.ts.map +1 -0
- package/dist/reference/stores/postgres.js +406 -0
- package/dist/reference/stores/postgres.js.map +1 -0
- package/dist/reference/stores/sqlite-driver.d.ts +54 -0
- package/dist/reference/stores/sqlite-driver.d.ts.map +1 -0
- package/dist/reference/stores/sqlite-driver.js +99 -0
- package/dist/reference/stores/sqlite-driver.js.map +1 -0
- package/dist/reference/stores/sqlite.d.ts +68 -0
- package/dist/reference/stores/sqlite.d.ts.map +1 -0
- package/dist/reference/stores/sqlite.js +464 -0
- package/dist/reference/stores/sqlite.js.map +1 -0
- package/dist/report/console.d.ts +33 -0
- package/dist/report/console.d.ts.map +1 -0
- package/dist/report/console.js +237 -0
- package/dist/report/console.js.map +1 -0
- package/dist/report/matrix.d.ts +131 -0
- package/dist/report/matrix.d.ts.map +1 -0
- package/dist/report/matrix.js +865 -0
- package/dist/report/matrix.js.map +1 -0
- package/dist/report/render.d.ts +66 -0
- package/dist/report/render.d.ts.map +1 -0
- package/dist/report/render.js +423 -0
- package/dist/report/render.js.map +1 -0
- package/dist/runtime/async.d.ts +34 -0
- package/dist/runtime/async.d.ts.map +1 -0
- package/dist/runtime/async.js +130 -0
- package/dist/runtime/async.js.map +1 -0
- package/dist/runtime/auth.d.ts +46 -0
- package/dist/runtime/auth.d.ts.map +1 -0
- package/dist/runtime/auth.js +236 -0
- package/dist/runtime/auth.js.map +1 -0
- package/dist/runtime/checks.d.ts +89 -0
- package/dist/runtime/checks.d.ts.map +1 -0
- package/dist/runtime/checks.js +3400 -0
- package/dist/runtime/checks.js.map +1 -0
- package/dist/runtime/client.d.ts +65 -0
- package/dist/runtime/client.d.ts.map +1 -0
- package/dist/runtime/client.js +122 -0
- package/dist/runtime/client.js.map +1 -0
- package/dist/runtime/finding.d.ts +50 -0
- package/dist/runtime/finding.d.ts.map +1 -0
- package/dist/runtime/finding.js +50 -0
- package/dist/runtime/finding.js.map +1 -0
- package/dist/runtime/fixture.d.ts +20 -0
- package/dist/runtime/fixture.d.ts.map +1 -0
- package/dist/runtime/fixture.js +175 -0
- package/dist/runtime/fixture.js.map +1 -0
- package/dist/runtime/progress.d.ts +41 -0
- package/dist/runtime/progress.d.ts.map +1 -0
- package/dist/runtime/progress.js +186 -0
- package/dist/runtime/progress.js.map +1 -0
- package/dist/runtime/run.d.ts +69 -0
- package/dist/runtime/run.d.ts.map +1 -0
- package/dist/runtime/run.js +575 -0
- package/dist/runtime/run.js.map +1 -0
- package/dist/runtime/teardown.d.ts +38 -0
- package/dist/runtime/teardown.d.ts.map +1 -0
- package/dist/runtime/teardown.js +129 -0
- package/dist/runtime/teardown.js.map +1 -0
- package/dist/runtime/validate.d.ts +31 -0
- package/dist/runtime/validate.d.ts.map +1 -0
- package/dist/runtime/validate.js +111 -0
- package/dist/runtime/validate.js.map +1 -0
- package/dist/runtime/world.d.ts +49 -0
- package/dist/runtime/world.d.ts.map +1 -0
- package/dist/runtime/world.js +119 -0
- package/dist/runtime/world.js.map +1 -0
- package/dist/spec/collection.d.ts +31 -0
- package/dist/spec/collection.d.ts.map +1 -0
- package/dist/spec/collection.js +121 -0
- package/dist/spec/collection.js.map +1 -0
- package/dist/spec/conventions.d.ts +103 -0
- package/dist/spec/conventions.d.ts.map +1 -0
- package/dist/spec/conventions.js +275 -0
- package/dist/spec/conventions.js.map +1 -0
- package/dist/spec/extensions.d.ts +74 -0
- package/dist/spec/extensions.d.ts.map +1 -0
- package/dist/spec/extensions.js +335 -0
- package/dist/spec/extensions.js.map +1 -0
- package/dist/spec/graph.d.ts +89 -0
- package/dist/spec/graph.d.ts.map +1 -0
- package/dist/spec/graph.js +318 -0
- package/dist/spec/graph.js.map +1 -0
- package/dist/spec/load.d.ts +36 -0
- package/dist/spec/load.d.ts.map +1 -0
- package/dist/spec/load.js +166 -0
- package/dist/spec/load.js.map +1 -0
- package/dist/spec/types.d.ts +57 -0
- package/dist/spec/types.d.ts.map +1 -0
- package/dist/spec/types.js +24 -0
- package/dist/spec/types.js.map +1 -0
- package/labs/README.md +130 -0
- package/labs/annotated-openapi.yaml +311 -0
- package/labs/anyrow.config.ts +107 -0
- package/labs/demo.mjs +124 -0
- package/labs/local.config.ts +41 -0
- package/labs/minimal.config.ts +32 -0
- package/package.json +80 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teardown of everything a run created.
|
|
3
|
+
*
|
|
4
|
+
* A tester that leaves its fixtures behind is a tester nobody runs twice against anything real.
|
|
5
|
+
* Records are unwound newest-first so children go before the parents they depend on, and every
|
|
6
|
+
* failure is collected rather than thrown — a teardown that aborts halfway is worse than one that
|
|
7
|
+
* reports exactly what it could not remove.
|
|
8
|
+
*/
|
|
9
|
+
import { parseRouteRef } from "../spec/load.js";
|
|
10
|
+
import { fillPath } from "./world.js";
|
|
11
|
+
/**
|
|
12
|
+
* Registry of what a run brought into existence. Entities are recorded in creation order so the
|
|
13
|
+
* unwind can reverse it.
|
|
14
|
+
*/
|
|
15
|
+
export class Ledger {
|
|
16
|
+
items = [];
|
|
17
|
+
record(entity, id, scope) {
|
|
18
|
+
if (id === "" || id === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
this.items.push({ entity, id, scope: { ...scope } });
|
|
21
|
+
}
|
|
22
|
+
get size() {
|
|
23
|
+
return this.items.length;
|
|
24
|
+
}
|
|
25
|
+
entries() {
|
|
26
|
+
return this.items;
|
|
27
|
+
}
|
|
28
|
+
async unwind(model, client, headers, onItem) {
|
|
29
|
+
const report = { failed: [], removed: 0, unsupported: [] };
|
|
30
|
+
const unsupported = new Set();
|
|
31
|
+
/* Newest first: a child created inside a parent must be removed before the parent, and
|
|
32
|
+
* creation order already encodes that dependency. */
|
|
33
|
+
const queue = [...this.items].reverse();
|
|
34
|
+
let done = 0;
|
|
35
|
+
for (const item of queue) {
|
|
36
|
+
onItem?.(done, queue.length, item);
|
|
37
|
+
const deleteOp = deleteOperationFor(item.entity, model);
|
|
38
|
+
if (deleteOp === null) {
|
|
39
|
+
unsupported.add(item.entity);
|
|
40
|
+
done += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const param = deleteOp.pathParams.at(-1);
|
|
44
|
+
const scope = param === undefined ? item.scope : { ...item.scope, [param]: item.id };
|
|
45
|
+
let path;
|
|
46
|
+
try {
|
|
47
|
+
path = fillPath(deleteOp.path, scope);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
report.failed.push({
|
|
51
|
+
entity: item.entity,
|
|
52
|
+
id: item.id,
|
|
53
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
54
|
+
});
|
|
55
|
+
done += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const exchange = await client.request("DELETE", path, { headers: headers() });
|
|
60
|
+
/* 404 means it is already gone — the goal is absence, not a successful call. */
|
|
61
|
+
if (exchange.status < 300 || exchange.status === 404 || exchange.status === 410) {
|
|
62
|
+
report.removed += 1;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
report.failed.push({
|
|
66
|
+
entity: item.entity,
|
|
67
|
+
id: item.id,
|
|
68
|
+
reason: `DELETE ${path} returned ${exchange.status}`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
report.failed.push({
|
|
74
|
+
entity: item.entity,
|
|
75
|
+
id: item.id,
|
|
76
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
done += 1;
|
|
80
|
+
onItem?.(done, queue.length, item);
|
|
81
|
+
}
|
|
82
|
+
report.unsupported = [...unsupported];
|
|
83
|
+
return report;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The operation that removes an instance: the entity's own delete, or an explicit `x-cleanup`
|
|
88
|
+
* route for entities whose removal lives somewhere the graph does not connect.
|
|
89
|
+
*/
|
|
90
|
+
function deleteOperationFor(entityName, model) {
|
|
91
|
+
const entity = model.entities.get(entityName);
|
|
92
|
+
if (entity?.delete !== undefined) {
|
|
93
|
+
const op = model.byOperationId.get(entity.delete);
|
|
94
|
+
if (op !== undefined)
|
|
95
|
+
return op;
|
|
96
|
+
}
|
|
97
|
+
for (const op of model.operations) {
|
|
98
|
+
if (op.entity !== entityName || op.cleanup === null)
|
|
99
|
+
continue;
|
|
100
|
+
const parsed = parseRouteRef(op.cleanup);
|
|
101
|
+
if (parsed === null)
|
|
102
|
+
continue;
|
|
103
|
+
const target = model.byRoute.get(`${parsed.method} ${parsed.path}`);
|
|
104
|
+
if (target !== undefined)
|
|
105
|
+
return target;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
export function renderTeardown(report, created) {
|
|
110
|
+
if (created === 0)
|
|
111
|
+
return [];
|
|
112
|
+
const lines = [];
|
|
113
|
+
if (report.failed.length === 0 && report.unsupported.length === 0) {
|
|
114
|
+
lines.push(` cleaned up ${report.removed}/${created} created record(s)`);
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
lines.push(` cleanup: removed ${report.removed}/${created} created record(s)`);
|
|
118
|
+
if (report.unsupported.length > 0) {
|
|
119
|
+
lines.push(` ${report.unsupported.join(", ")} — no delete operation in the document, so records ` +
|
|
120
|
+
"created for these entities remain. Declare x-cleanup to make them removable.");
|
|
121
|
+
}
|
|
122
|
+
for (const failure of report.failed.slice(0, 5)) {
|
|
123
|
+
lines.push(` ${failure.entity} ${failure.id}: ${failure.reason}`);
|
|
124
|
+
}
|
|
125
|
+
if (report.failed.length > 5)
|
|
126
|
+
lines.push(` … and ${report.failed.length - 5} more`);
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.js","sourceRoot":"","sources":["../../src/runtime/teardown.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response-schema validation — the floor beneath the behavioural checks.
|
|
3
|
+
*
|
|
4
|
+
* Schemas are compiled per operation+status and reused. `additionalProperties: false` is honoured
|
|
5
|
+
* as written: a document that declares a closed object and a backend that returns extra fields
|
|
6
|
+
* genuinely disagree, and which one is wrong is a decision for the reader, not for oat.
|
|
7
|
+
*/
|
|
8
|
+
import type { OperationObject } from "../spec/types.ts";
|
|
9
|
+
/** Minimal surface oat uses — avoids depending on AJV's CJS/ESM type shape. */
|
|
10
|
+
export interface ValidateFunction {
|
|
11
|
+
(data: unknown): boolean;
|
|
12
|
+
errors?: Array<{
|
|
13
|
+
instancePath?: string;
|
|
14
|
+
message?: string;
|
|
15
|
+
}> | null;
|
|
16
|
+
}
|
|
17
|
+
export interface ValidationResult {
|
|
18
|
+
ok: boolean;
|
|
19
|
+
errors: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare class SchemaValidator {
|
|
22
|
+
private readonly ajv;
|
|
23
|
+
private readonly cache;
|
|
24
|
+
constructor();
|
|
25
|
+
/** Compiles the schema documented for this operation and status, if there is one. */
|
|
26
|
+
private compile;
|
|
27
|
+
validate(operationId: string, op: OperationObject, status: number, body: unknown): ValidationResult;
|
|
28
|
+
/** True when the document actually documents this status for this operation. */
|
|
29
|
+
documents(op: OperationObject, status: number): boolean;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/runtime/validate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,kBAAkB,CAAA;AAErE,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAChC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACxB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAA;CAClE;AAQD,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,MAAM,EAAE,CAAA;CAChB;AAWD,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6C;;IAanE,qFAAqF;IACrF,OAAO,CAAC,OAAO;IAqBf,QAAQ,CACP,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,GACX,gBAAgB;IAWnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO;CAGvD"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response-schema validation — the floor beneath the behavioural checks.
|
|
3
|
+
*
|
|
4
|
+
* Schemas are compiled per operation+status and reused. `additionalProperties: false` is honoured
|
|
5
|
+
* as written: a document that declares a closed object and a backend that returns extra fields
|
|
6
|
+
* genuinely disagree, and which one is wrong is a decision for the reader, not for oat.
|
|
7
|
+
*/
|
|
8
|
+
import ajvModule from "ajv/dist/2020.js";
|
|
9
|
+
import formatsModule from "ajv-formats";
|
|
10
|
+
const OK = { errors: [], ok: true };
|
|
11
|
+
/* Both packages ship CJS with an interop default, so the callable lives on `.default` under
|
|
12
|
+
* NodeNext resolution — but only at runtime, hence the cast. */
|
|
13
|
+
const Ajv2020 = (ajvModule.default ?? ajvModule);
|
|
14
|
+
const addFormats = (formatsModule.default ?? formatsModule);
|
|
15
|
+
export class SchemaValidator {
|
|
16
|
+
ajv;
|
|
17
|
+
cache = new Map();
|
|
18
|
+
constructor() {
|
|
19
|
+
this.ajv = new Ajv2020({
|
|
20
|
+
allErrors: true,
|
|
21
|
+
/* Specs in the wild carry annotations AJV does not know; refusing to compile over a
|
|
22
|
+
* vocabulary quibble would make oat useless on real documents. */
|
|
23
|
+
strict: false,
|
|
24
|
+
validateFormats: true,
|
|
25
|
+
});
|
|
26
|
+
addFormats(this.ajv);
|
|
27
|
+
}
|
|
28
|
+
/** Compiles the schema documented for this operation and status, if there is one. */
|
|
29
|
+
compile(op, key, status) {
|
|
30
|
+
const cached = this.cache.get(key);
|
|
31
|
+
if (cached !== undefined)
|
|
32
|
+
return cached;
|
|
33
|
+
const schema = schemaFor(op, status);
|
|
34
|
+
if (schema === null) {
|
|
35
|
+
this.cache.set(key, null);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const validate = this.ajv.compile(sanitise(schema));
|
|
40
|
+
this.cache.set(key, validate);
|
|
41
|
+
return validate;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* An uncompilable schema is a spec defect, surfaced by the caller as a gap rather than
|
|
45
|
+
* crashing the run. */
|
|
46
|
+
this.cache.set(key, null);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
validate(operationId, op, status, body) {
|
|
51
|
+
const validate = this.compile(op, `${operationId}:${status}`, status);
|
|
52
|
+
if (validate === null)
|
|
53
|
+
return OK;
|
|
54
|
+
if (validate(body) === true)
|
|
55
|
+
return OK;
|
|
56
|
+
const errors = (validate.errors ?? []).map((error) => {
|
|
57
|
+
const at = error.instancePath === undefined || error.instancePath === "" ? "(root)" : error.instancePath;
|
|
58
|
+
return `${at} ${error.message ?? "is invalid"}`;
|
|
59
|
+
});
|
|
60
|
+
return { errors: [...new Set(errors)].slice(0, 12), ok: false };
|
|
61
|
+
}
|
|
62
|
+
/** True when the document actually documents this status for this operation. */
|
|
63
|
+
documents(op, status) {
|
|
64
|
+
return schemaFor(op, status) !== null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function schemaFor(op, status) {
|
|
68
|
+
const responses = op.responses ?? {};
|
|
69
|
+
const candidates = [String(status), `${Math.floor(status / 100)}XX`, "default"];
|
|
70
|
+
for (const code of candidates) {
|
|
71
|
+
const content = responses[code]?.content;
|
|
72
|
+
if (content === undefined)
|
|
73
|
+
continue;
|
|
74
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
75
|
+
if (mediaType.includes("json") && media.schema !== undefined)
|
|
76
|
+
return media.schema;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Strips constructs that break AJV but carry no validation meaning here: OpenAPI's `nullable`
|
|
83
|
+
* without a sibling `type` (invalid JSON Schema, emitted by several generators), and `example`
|
|
84
|
+
* keys that collide with the `examples` keyword in draft 2020-12.
|
|
85
|
+
*/
|
|
86
|
+
function sanitise(schema) {
|
|
87
|
+
const walk = (node) => {
|
|
88
|
+
if (Array.isArray(node))
|
|
89
|
+
return node.map(walk);
|
|
90
|
+
if (node === null || typeof node !== "object")
|
|
91
|
+
return node;
|
|
92
|
+
const out = {};
|
|
93
|
+
for (const [key, value] of Object.entries(node)) {
|
|
94
|
+
if (key === "nullable")
|
|
95
|
+
continue;
|
|
96
|
+
if (key === "example")
|
|
97
|
+
continue;
|
|
98
|
+
if (key === "discriminator")
|
|
99
|
+
continue;
|
|
100
|
+
out[key] = walk(value);
|
|
101
|
+
}
|
|
102
|
+
/* `nullable: true` with a concrete type widens the type rather than being dropped. */
|
|
103
|
+
const original = node;
|
|
104
|
+
if (original.nullable === true && typeof original.type === "string") {
|
|
105
|
+
out.type = [original.type, "null"];
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
};
|
|
109
|
+
return walk(schema);
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/runtime/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds the world once per run: resolves path parameters top-down, then creates a discriminating
|
|
3
|
+
* cohort per entity.
|
|
4
|
+
*
|
|
5
|
+
* Prior art resolved fixtures per test case, which meant one unseedable resource produced dozens
|
|
6
|
+
* of unrelated failures. Here a resolution failure is recorded once and everything downstream is
|
|
7
|
+
* reported as BLOCKED against that single cause.
|
|
8
|
+
*/
|
|
9
|
+
import type { OperationModel, SpecModel } from "../spec/graph.ts";
|
|
10
|
+
import type { Client } from "./client.ts";
|
|
11
|
+
import { type CohortMember } from "./fixture.ts";
|
|
12
|
+
export type Record_ = Record<string, unknown>;
|
|
13
|
+
export interface Scope {
|
|
14
|
+
values: Record<string, string>;
|
|
15
|
+
/** Records created while resolving ancestors, newest last — unwound in reverse at teardown. */
|
|
16
|
+
created: Array<{
|
|
17
|
+
entity: string;
|
|
18
|
+
path: string;
|
|
19
|
+
id: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export declare class SeedError extends Error {
|
|
23
|
+
readonly cause_: string;
|
|
24
|
+
/** Status the failing request returned, when the failure was an HTTP response. */
|
|
25
|
+
readonly status?: number | undefined;
|
|
26
|
+
constructor(cause_: string, message: string,
|
|
27
|
+
/** Status the failing request returned, when the failure was an HTTP response. */
|
|
28
|
+
status?: number | undefined);
|
|
29
|
+
}
|
|
30
|
+
export declare function fillPath(template: string, values: Record<string, string>): string;
|
|
31
|
+
export interface WorldOptions {
|
|
32
|
+
roots: Record<string, string>;
|
|
33
|
+
seed: number;
|
|
34
|
+
cohortSize?: number;
|
|
35
|
+
authHeaders: () => Record<string, string>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves every path parameter an operation needs. Roots come from config; everything else is
|
|
39
|
+
* created through the owning entity's create operation, depth-first.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveScope(op: OperationModel, model: SpecModel, client: Client, options: WorldOptions, scope?: Scope): Promise<Scope>;
|
|
42
|
+
export declare function requestSchemaOf(op: OperationModel, model: SpecModel): Record<string, unknown> | null;
|
|
43
|
+
export interface SeededCohort {
|
|
44
|
+
members: CohortMember[];
|
|
45
|
+
records: Record_[];
|
|
46
|
+
}
|
|
47
|
+
/** Creates the cohort and returns the server's view of each instance. */
|
|
48
|
+
export declare function seedCohort(createOp: OperationModel, model: SpecModel, client: Client, options: WorldOptions, scope: Scope): Promise<SeededCohort>;
|
|
49
|
+
//# sourceMappingURL=world.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.d.ts","sourceRoot":"","sources":["../../src/runtime/world.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,cAAc,CAAA;AAE7D,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAE7C,MAAM,WAAW,KAAK;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,+FAA+F;IAC/F,OAAO,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC5D;AAED,qBAAa,SAAU,SAAQ,KAAK;IAElC,QAAQ,CAAC,MAAM,EAAE,MAAM;IAEvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM;gBAHf,MAAM,EAAE,MAAM,EACvB,OAAO,EAAE,MAAM;IACf,kFAAkF;IACzE,MAAM,CAAC,EAAE,MAAM,YAAA;CAIzB;AAED,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAMjF;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzC;AAED;;;GAGG;AACH,wBAAsB,YAAY,CACjC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,EACrB,KAAK,GAAE,KAAmC,GACxC,OAAO,CAAC,KAAK,CAAC,CAuChB;AAyBD,wBAAgB,eAAe,CAC9B,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,SAAS,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAQhC;AAED,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,YAAY,EAAE,CAAA;IACvB,OAAO,EAAE,OAAO,EAAE,CAAA;CAClB;AAED,yEAAyE;AACzE,wBAAsB,UAAU,CAC/B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,KAAK,GACV,OAAO,CAAC,YAAY,CAAC,CA4BvB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds the world once per run: resolves path parameters top-down, then creates a discriminating
|
|
3
|
+
* cohort per entity.
|
|
4
|
+
*
|
|
5
|
+
* Prior art resolved fixtures per test case, which meant one unseedable resource produced dozens
|
|
6
|
+
* of unrelated failures. Here a resolution failure is recorded once and everything downstream is
|
|
7
|
+
* reported as BLOCKED against that single cause.
|
|
8
|
+
*/
|
|
9
|
+
import { owningEntityName } from "../spec/graph.js";
|
|
10
|
+
import { buildCohort } from "./fixture.js";
|
|
11
|
+
export class SeedError extends Error {
|
|
12
|
+
cause_;
|
|
13
|
+
status;
|
|
14
|
+
constructor(cause_, message,
|
|
15
|
+
/** Status the failing request returned, when the failure was an HTTP response. */
|
|
16
|
+
status) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.cause_ = cause_;
|
|
19
|
+
this.status = status;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function fillPath(template, values) {
|
|
23
|
+
return template.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
24
|
+
const value = values[name];
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
throw new SeedError(name, `no value for path parameter {${name}}`);
|
|
27
|
+
return encodeURIComponent(value);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolves every path parameter an operation needs. Roots come from config; everything else is
|
|
32
|
+
* created through the owning entity's create operation, depth-first.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveScope(op, model, client, options, scope = { created: [], values: {} }) {
|
|
35
|
+
for (const param of op.pathParams) {
|
|
36
|
+
if (scope.values[param] !== undefined)
|
|
37
|
+
continue;
|
|
38
|
+
const fromRoot = options.roots[param];
|
|
39
|
+
if (fromRoot !== undefined) {
|
|
40
|
+
scope.values[param] = fromRoot;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const owner = owningEntityName(op.path, param);
|
|
44
|
+
if (owner === null) {
|
|
45
|
+
throw new SeedError(param, `path parameter {${param}} names no entity oat can create`);
|
|
46
|
+
}
|
|
47
|
+
const entity = model.entities.get(owner);
|
|
48
|
+
if (entity?.create === undefined) {
|
|
49
|
+
throw new SeedError(param, `{${param}} identifies a "${owner}", which has no create operation — supply it as a root`);
|
|
50
|
+
}
|
|
51
|
+
const createOp = model.byOperationId.get(entity.create);
|
|
52
|
+
if (createOp === undefined)
|
|
53
|
+
throw new SeedError(param, `missing operation ${entity.create}`);
|
|
54
|
+
await resolveScope(createOp, model, client, options, scope);
|
|
55
|
+
const created = await createOne(createOp, model, client, options, scope);
|
|
56
|
+
const identity = entity.identity ?? "id";
|
|
57
|
+
const id = created[identity];
|
|
58
|
+
if (typeof id !== "string" && typeof id !== "number") {
|
|
59
|
+
throw new SeedError(param, `create for "${owner}" returned no usable "${identity}"`);
|
|
60
|
+
}
|
|
61
|
+
scope.values[param] = String(id);
|
|
62
|
+
scope.created.push({
|
|
63
|
+
entity: owner,
|
|
64
|
+
id: String(id),
|
|
65
|
+
path: fillPath(createOp.path, scope.values),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return scope;
|
|
69
|
+
}
|
|
70
|
+
async function createOne(createOp, model, client, options, scope) {
|
|
71
|
+
const schema = requestSchemaOf(createOp, model);
|
|
72
|
+
const [member] = buildCohort(schema ?? {}, options.seed, ["baseline"]);
|
|
73
|
+
const exchange = await client.request("POST", fillPath(createOp.path, scope.values), {
|
|
74
|
+
body: member?.body ?? {},
|
|
75
|
+
headers: options.authHeaders(),
|
|
76
|
+
});
|
|
77
|
+
if (exchange.status >= 300) {
|
|
78
|
+
throw new SeedError(createOp.operationId, `${createOp.operationId} returned ${exchange.status}: ${JSON.stringify(exchange.responseBody).slice(0, 300)}`, exchange.status);
|
|
79
|
+
}
|
|
80
|
+
return (exchange.responseBody ?? {});
|
|
81
|
+
}
|
|
82
|
+
export function requestSchemaOf(op, model) {
|
|
83
|
+
const raw = model.rawOperations.get(op.operationId);
|
|
84
|
+
const content = raw?.requestBody?.content;
|
|
85
|
+
if (content === undefined)
|
|
86
|
+
return null;
|
|
87
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
88
|
+
if (mediaType.includes("json") && media.schema !== undefined)
|
|
89
|
+
return media.schema;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** Creates the cohort and returns the server's view of each instance. */
|
|
94
|
+
export async function seedCohort(createOp, model, client, options, scope) {
|
|
95
|
+
const schema = requestSchemaOf(createOp, model);
|
|
96
|
+
if (schema === null) {
|
|
97
|
+
throw new SeedError(createOp.operationId, `${createOp.operationId} declares no JSON request body`);
|
|
98
|
+
}
|
|
99
|
+
const members = buildCohort(schema, options.seed).slice(0, options.cohortSize ?? 7);
|
|
100
|
+
const records = [];
|
|
101
|
+
const path = fillPath(createOp.path, scope.values);
|
|
102
|
+
for (const member of members) {
|
|
103
|
+
const exchange = await client.request("POST", path, {
|
|
104
|
+
body: member.body,
|
|
105
|
+
headers: options.authHeaders(),
|
|
106
|
+
});
|
|
107
|
+
if (exchange.status >= 300) {
|
|
108
|
+
/* Partial cohorts are still useful — a single rejected variant should not cost the
|
|
109
|
+
* entity all of its coverage. Only a completely empty cohort is fatal. */
|
|
110
|
+
if (records.length > 0)
|
|
111
|
+
break;
|
|
112
|
+
throw new SeedError(createOp.operationId, `seeding "${member.variant}" returned ${exchange.status}: ` +
|
|
113
|
+
JSON.stringify(exchange.responseBody).slice(0, 300), exchange.status);
|
|
114
|
+
}
|
|
115
|
+
records.push((exchange.responseBody ?? {}));
|
|
116
|
+
}
|
|
117
|
+
return { members, records };
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=world.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world.js","sourceRoot":"","sources":["../../src/runtime/world.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives collection shape from the response schema rather than guessing at wrapper key names.
|
|
3
|
+
*
|
|
4
|
+
* Hardcoding `["data","items","results","records"]` — as prior art does — returns null on any
|
|
5
|
+
* API whose envelopes are named after the resource (`{ tables: [...] }`), which silently
|
|
6
|
+
* disables every list-based assertion. The schema already states the answer; read it.
|
|
7
|
+
*/
|
|
8
|
+
import type { OperationObject, SchemaObject } from "./types.ts";
|
|
9
|
+
export interface CollectionShape {
|
|
10
|
+
/** Property holding the array, or null when the response *is* the array. */
|
|
11
|
+
key: string | null;
|
|
12
|
+
itemSchema: SchemaObject | null;
|
|
13
|
+
/** Sibling properties of the collection — `count`, `hasMore`, `nextCursor`, `page`. */
|
|
14
|
+
envelopeKeys: string[];
|
|
15
|
+
pagination: {
|
|
16
|
+
countKey?: string;
|
|
17
|
+
hasMoreKey?: string;
|
|
18
|
+
cursorKey?: string;
|
|
19
|
+
pageKey?: string;
|
|
20
|
+
limitKey?: string;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export declare function successSchema(op: OperationObject): SchemaObject | null;
|
|
24
|
+
export declare function requestSchema(op: OperationObject): SchemaObject | null;
|
|
25
|
+
export declare function deriveCollectionShape(schema: SchemaObject | null): CollectionShape | null;
|
|
26
|
+
/**
|
|
27
|
+
* Finds the property that identifies an instance. Prefers a required identity-like property,
|
|
28
|
+
* then any identity-like property, then the trailing path parameter's implied name.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deriveIdentity(itemSchema: SchemaObject | null, pathParamHint?: string): string | null;
|
|
31
|
+
//# sourceMappingURL=collection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../../src/spec/collection.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE/D,MAAM,WAAW,eAAe;IAC/B,4EAA4E;IAC5E,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,UAAU,EAAE,YAAY,GAAG,IAAI,CAAA;IAC/B,uFAAuF;IACvF,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,UAAU,EAAE;QACX,QAAQ,CAAC,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,CAAA;QACnB,SAAS,CAAC,EAAE,MAAM,CAAA;QAClB,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;KACjB,CAAA;CACD;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,eAAe,GAAG,YAAY,GAAG,IAAI,CAWtE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,eAAe,GAAG,YAAY,GAAG,IAAI,CAOtE;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,GAAG,eAAe,GAAG,IAAI,CA+CzF;AAQD;;;GAGG;AACH,wBAAgB,cAAc,CAC7B,UAAU,EAAE,YAAY,GAAG,IAAI,EAC/B,aAAa,CAAC,EAAE,MAAM,GACpB,MAAM,GAAG,IAAI,CAsBf"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives collection shape from the response schema rather than guessing at wrapper key names.
|
|
3
|
+
*
|
|
4
|
+
* Hardcoding `["data","items","results","records"]` — as prior art does — returns null on any
|
|
5
|
+
* API whose envelopes are named after the resource (`{ tables: [...] }`), which silently
|
|
6
|
+
* disables every list-based assertion. The schema already states the answer; read it.
|
|
7
|
+
*/
|
|
8
|
+
const COUNT_KEYS = ["count", "total", "totalCount", "total_count", "totalItems"];
|
|
9
|
+
const HAS_MORE_KEYS = ["hasMore", "has_more", "hasNextPage", "more"];
|
|
10
|
+
const CURSOR_KEYS = ["nextCursor", "next_cursor", "cursor", "next", "endCursor"];
|
|
11
|
+
const PAGE_KEYS = ["page", "pageNumber", "page_number", "offset"];
|
|
12
|
+
const LIMIT_KEYS = ["limit", "perPage", "per_page", "pageSize", "page_size"];
|
|
13
|
+
export function successSchema(op) {
|
|
14
|
+
const responses = op.responses ?? {};
|
|
15
|
+
for (const code of ["200", "201", "202", "2XX", "default"]) {
|
|
16
|
+
const content = responses[code]?.content;
|
|
17
|
+
if (content === undefined)
|
|
18
|
+
continue;
|
|
19
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
20
|
+
if (!mediaType.includes("json"))
|
|
21
|
+
continue;
|
|
22
|
+
if (media.schema !== undefined)
|
|
23
|
+
return media.schema;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
export function requestSchema(op) {
|
|
29
|
+
const content = op.requestBody?.content;
|
|
30
|
+
if (content === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
for (const [mediaType, media] of Object.entries(content)) {
|
|
33
|
+
if (mediaType.includes("json") && media.schema !== undefined)
|
|
34
|
+
return media.schema;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
export function deriveCollectionShape(schema) {
|
|
39
|
+
if (schema === null)
|
|
40
|
+
return null;
|
|
41
|
+
if (schema.type === "array") {
|
|
42
|
+
return {
|
|
43
|
+
envelopeKeys: [],
|
|
44
|
+
itemSchema: asSchema(schema.items),
|
|
45
|
+
key: null,
|
|
46
|
+
pagination: {},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const props = schema.properties;
|
|
50
|
+
if (props === null || typeof props !== "object")
|
|
51
|
+
return null;
|
|
52
|
+
const entries = Object.entries(props);
|
|
53
|
+
const arrayProps = entries.filter(([, value]) => value?.type === "array");
|
|
54
|
+
if (arrayProps.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
/* With several array properties, the collection is the one whose items are objects and
|
|
57
|
+
* whose name is not an obvious sidecar (`errors`, `warnings`, `meta`). Ties break toward
|
|
58
|
+
* the property with the richest item schema — the real payload. */
|
|
59
|
+
const candidates = arrayProps
|
|
60
|
+
.filter(([name]) => !/^(errors?|warnings?|messages?|meta|links)$/i.test(name))
|
|
61
|
+
.map(([name, value]) => ({ item: asSchema(value.items), name }))
|
|
62
|
+
.filter((c) => c.item !== null);
|
|
63
|
+
const chosen = candidates.find((c) => c.item?.type === "object" || c.item?.properties !== undefined) ??
|
|
64
|
+
candidates[0];
|
|
65
|
+
if (chosen === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
const siblingNames = entries.map(([name]) => name).filter((name) => name !== chosen.name);
|
|
68
|
+
const pagination = {};
|
|
69
|
+
const pick = (keys) => siblingNames.find((n) => keys.includes(n));
|
|
70
|
+
const countKey = pick(COUNT_KEYS);
|
|
71
|
+
const hasMoreKey = pick(HAS_MORE_KEYS);
|
|
72
|
+
const cursorKey = pick(CURSOR_KEYS);
|
|
73
|
+
const pageKey = pick(PAGE_KEYS);
|
|
74
|
+
const limitKey = pick(LIMIT_KEYS);
|
|
75
|
+
if (countKey !== undefined)
|
|
76
|
+
pagination.countKey = countKey;
|
|
77
|
+
if (hasMoreKey !== undefined)
|
|
78
|
+
pagination.hasMoreKey = hasMoreKey;
|
|
79
|
+
if (cursorKey !== undefined)
|
|
80
|
+
pagination.cursorKey = cursorKey;
|
|
81
|
+
if (pageKey !== undefined)
|
|
82
|
+
pagination.pageKey = pageKey;
|
|
83
|
+
if (limitKey !== undefined)
|
|
84
|
+
pagination.limitKey = limitKey;
|
|
85
|
+
return { envelopeKeys: siblingNames, itemSchema: chosen.item, key: chosen.name, pagination };
|
|
86
|
+
}
|
|
87
|
+
function asSchema(value) {
|
|
88
|
+
return value !== null && typeof value === "object" ? value : null;
|
|
89
|
+
}
|
|
90
|
+
const IDENTITY_CANDIDATES = ["id", "uuid", "slug", "key", "name"];
|
|
91
|
+
/**
|
|
92
|
+
* Finds the property that identifies an instance. Prefers a required identity-like property,
|
|
93
|
+
* then any identity-like property, then the trailing path parameter's implied name.
|
|
94
|
+
*/
|
|
95
|
+
export function deriveIdentity(itemSchema, pathParamHint) {
|
|
96
|
+
if (itemSchema !== null) {
|
|
97
|
+
const props = itemSchema.properties;
|
|
98
|
+
if (props !== null && typeof props === "object") {
|
|
99
|
+
const names = Object.keys(props);
|
|
100
|
+
const required = Array.isArray(itemSchema.required)
|
|
101
|
+
? itemSchema.required
|
|
102
|
+
: [];
|
|
103
|
+
for (const candidate of IDENTITY_CANDIDATES) {
|
|
104
|
+
if (required.includes(candidate))
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
for (const candidate of IDENTITY_CANDIDATES) {
|
|
108
|
+
if (names.includes(candidate))
|
|
109
|
+
return candidate;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (pathParamHint !== undefined) {
|
|
114
|
+
/* `{table_id}` on `/tables/{table_id}` implies the item's own key is `id`. */
|
|
115
|
+
const stripped = pathParamHint.replace(/^.*[_.]/, "");
|
|
116
|
+
if (IDENTITY_CANDIDATES.includes(stripped))
|
|
117
|
+
return stripped;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection.js","sourceRoot":"","sources":["../../src/spec/collection.ts"],"names":[],"mappings":"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"}
|