@haddowg/json-api-codegen 1.0.1 → 1.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/dist/cli.d.mts +8 -2
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +20 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/{index-C0becRSc.d.mts → index-JD2FpQHX.d.mts} +71 -9
- package/dist/index-JD2FpQHX.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-B24cYyjM.mjs → src-CDV4z7In.mjs} +303 -42
- package/dist/src-CDV4z7In.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/index-C0becRSc.d.mts.map +0 -1
- package/dist/src-B24cYyjM.mjs.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import { ATOMIC_EXT, HANDLE_RESERVED } from "@haddowg/json-api-client";
|
|
3
|
+
import { ATOMIC_EXT, CANONICAL_PAGE_KEYS, HANDLE_RESERVED } from "@haddowg/json-api-client";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { parse } from "yaml";
|
|
6
6
|
//#region src/build-descriptor.ts
|
|
7
|
-
const REF_PREFIX$
|
|
7
|
+
const REF_PREFIX$2 = "#/components/schemas/";
|
|
8
8
|
const JSON_API_MEDIA_TYPE$1 = "application/vnd.api+json";
|
|
9
9
|
/** Sort a record's keys for deterministic, snapshot-stable output. */
|
|
10
10
|
function sortRecord(record) {
|
|
@@ -14,9 +14,9 @@ function sortRecord(record) {
|
|
|
14
14
|
}
|
|
15
15
|
/** Resolve a local schema name from a `#/components/schemas/X` ref. */
|
|
16
16
|
function refName$1(ref) {
|
|
17
|
-
return ref.startsWith(REF_PREFIX$
|
|
17
|
+
return ref.startsWith(REF_PREFIX$2) ? ref.slice(21) : void 0;
|
|
18
18
|
}
|
|
19
|
-
function isSchema$
|
|
19
|
+
function isSchema$2(value) {
|
|
20
20
|
return typeof value === "object" && value !== null;
|
|
21
21
|
}
|
|
22
22
|
/** The set of `type` keys a JSON type node declares, with `"null"` removed. */
|
|
@@ -99,28 +99,28 @@ var DescriptorBuilder = class {
|
|
|
99
99
|
resourceType(name, schema) {
|
|
100
100
|
if (!name.endsWith("Resource") || name.endsWith("ResourceIdentifier")) return;
|
|
101
101
|
const constValue = schema.properties?.["type"];
|
|
102
|
-
if (isSchema$
|
|
102
|
+
if (isSchema$2(constValue) && typeof constValue.const === "string") return constValue.const;
|
|
103
103
|
}
|
|
104
104
|
/** Follow a `$ref` to its identifier/resource schema and read `properties.type.const`. */
|
|
105
105
|
resolveConst(ref) {
|
|
106
106
|
const target = refName$1(ref);
|
|
107
107
|
if (target === void 0) return;
|
|
108
108
|
const typeProp = this.schemas[target]?.properties?.["type"];
|
|
109
|
-
return isSchema$
|
|
109
|
+
return isSchema$2(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
|
|
110
110
|
}
|
|
111
111
|
attributes(base) {
|
|
112
112
|
const props = this.schemas[`${base}Attributes`]?.properties;
|
|
113
113
|
if (props === void 0) return {};
|
|
114
114
|
const out = {};
|
|
115
|
-
for (const [name, prop] of Object.entries(props)) if (isSchema$
|
|
115
|
+
for (const [name, prop] of Object.entries(props)) if (isSchema$2(prop)) out[name] = formatHint(prop);
|
|
116
116
|
return sortRecord(out);
|
|
117
117
|
}
|
|
118
118
|
relations(resource, collection) {
|
|
119
|
-
const relProps = isSchema$
|
|
119
|
+
const relProps = isSchema$2(resource.properties?.["relationships"]) ? resource.properties["relationships"].properties : void 0;
|
|
120
120
|
if (relProps === void 0) return {};
|
|
121
121
|
const out = {};
|
|
122
122
|
for (const [name, prop] of Object.entries(relProps)) {
|
|
123
|
-
if (!isSchema$
|
|
123
|
+
if (!isSchema$2(prop) || typeof prop.$ref !== "string") continue;
|
|
124
124
|
const component = this.schemas[refName$1(prop.$ref) ?? ""];
|
|
125
125
|
const descriptor = component ? this.relationFromComponent(component) : void 0;
|
|
126
126
|
if (descriptor === void 0) {
|
|
@@ -171,10 +171,10 @@ var DescriptorBuilder = class {
|
|
|
171
171
|
const countable = withCountParam(relatedGet) ?? withCountParam(this.operation(`${collection}/{id}/relationships/${name}`, "get"));
|
|
172
172
|
if (countable !== void 0) out.countable = countable;
|
|
173
173
|
if (descriptor.cardinality === "many") {
|
|
174
|
-
const
|
|
174
|
+
const relatedPaginator = this.paginatorOf(relatedGet) === "none" ? this.paginatorOf(this.operation(`${collection}/{id}/relationships/${name}`, "get")) : this.paginatorOf(relatedGet);
|
|
175
175
|
const relatedType = descriptor.types[0];
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
176
|
+
const typePaginator = relatedType !== void 0 ? this.typePaginator(relatedType) : "none";
|
|
177
|
+
if (relatedPaginator !== "none" && JSON.stringify(relatedPaginator) !== JSON.stringify(typePaginator)) out.paginator = relatedPaginator;
|
|
178
178
|
}
|
|
179
179
|
return out;
|
|
180
180
|
}
|
|
@@ -243,8 +243,8 @@ var DescriptorBuilder = class {
|
|
|
243
243
|
if (typeof ref !== "string") return;
|
|
244
244
|
const component = refName$1(ref);
|
|
245
245
|
const data = component === void 0 ? void 0 : this.schemas[component]?.properties?.["data"];
|
|
246
|
-
if (!isSchema$
|
|
247
|
-
if (isSchema$
|
|
246
|
+
if (!isSchema$2(data)) return;
|
|
247
|
+
if (isSchema$2(data.items)) {
|
|
248
248
|
const itemRef = data.items.$ref;
|
|
249
249
|
const many = typeof itemRef === "string" ? this.resolveConst(itemRef) : void 0;
|
|
250
250
|
return many === void 0 ? void 0 : {
|
|
@@ -262,7 +262,7 @@ var DescriptorBuilder = class {
|
|
|
262
262
|
dataConst(data) {
|
|
263
263
|
if (typeof data.$ref === "string") return this.resolveConst(data.$ref);
|
|
264
264
|
const typeProp = data.properties?.["type"];
|
|
265
|
-
return isSchema$
|
|
265
|
+
return isSchema$2(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
|
|
266
266
|
}
|
|
267
267
|
/** The `$ref` of the first 2xx JSON:API response body on an operation (the output document). */
|
|
268
268
|
firstOkJsonApiRef(op) {
|
|
@@ -275,7 +275,7 @@ var DescriptorBuilder = class {
|
|
|
275
275
|
/** Resolve a relationship component's `data` into a {@link RelationDescriptor}. */
|
|
276
276
|
relationFromComponent(component) {
|
|
277
277
|
const data = component.properties?.["data"];
|
|
278
|
-
if (!isSchema$
|
|
278
|
+
if (!isSchema$2(data)) return;
|
|
279
279
|
if (data.type === "array") {
|
|
280
280
|
const items = data.items;
|
|
281
281
|
if (items === void 0) return;
|
|
@@ -340,11 +340,11 @@ var DescriptorBuilder = class {
|
|
|
340
340
|
pivotFieldsFromAllOf(allOf) {
|
|
341
341
|
for (const entry of allOf) {
|
|
342
342
|
const meta = entry.properties?.["meta"];
|
|
343
|
-
const pivot = isSchema$
|
|
344
|
-
const props = isSchema$
|
|
343
|
+
const pivot = isSchema$2(meta) ? meta.properties?.["pivot"] : void 0;
|
|
344
|
+
const props = isSchema$2(pivot) ? pivot.properties : void 0;
|
|
345
345
|
if (props === void 0) continue;
|
|
346
346
|
const out = {};
|
|
347
|
-
for (const [name, schema] of Object.entries(props)) if (isSchema$
|
|
347
|
+
for (const [name, schema] of Object.entries(props)) if (isSchema$2(schema)) out[name] = formatHint(schema);
|
|
348
348
|
return Object.keys(out).length > 0 ? sortRecord(out) : void 0;
|
|
349
349
|
}
|
|
350
350
|
}
|
|
@@ -366,7 +366,7 @@ var DescriptorBuilder = class {
|
|
|
366
366
|
}
|
|
367
367
|
clientId(base) {
|
|
368
368
|
const create = this.schemas[`${base}CreateRequest`];
|
|
369
|
-
const data = isSchema$
|
|
369
|
+
const data = isSchema$2(create?.properties?.["data"]) ? create.properties["data"] : void 0;
|
|
370
370
|
if (data === void 0) return "forbidden";
|
|
371
371
|
if (data.properties?.["id"] === false) return "forbidden";
|
|
372
372
|
return data.required?.includes("id") ? "required" : "optional";
|
|
@@ -412,24 +412,55 @@ var DescriptorBuilder = class {
|
|
|
412
412
|
const prefix = `${collection}/{id}/relationships/`;
|
|
413
413
|
return Object.keys(this.paths).some((path) => path.startsWith(prefix) && !path.slice(prefix.length).includes("/"));
|
|
414
414
|
}
|
|
415
|
-
/** Detect the paginator
|
|
415
|
+
/** Detect the paginator from the fetchMany operation's page parameters. */
|
|
416
416
|
paginator(base) {
|
|
417
417
|
const collection = this.collectionPath(base);
|
|
418
|
-
return this.
|
|
418
|
+
return this.paginatorOf(this.operation(collection ?? "", "get"), collection);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Detect the paginator an operation advertises. A page schema stamped with `x-paginator`
|
|
422
|
+
* (contract 3) names its strategy and the wire key for each role, so it decides both. Without
|
|
423
|
+
* the stamp, the strategy is inferred from canonical member names (see {@link kindOfMembers}).
|
|
424
|
+
* Anything unrecognised warns and generates as unpaginated. `label` (a path) names the endpoint
|
|
425
|
+
* in the warning; without it nothing is logged.
|
|
426
|
+
*/
|
|
427
|
+
paginatorOf(op, label) {
|
|
428
|
+
const warn = (problem) => {
|
|
429
|
+
if (label !== void 0) console.warn(`json-api codegen: endpoint "${label}" ${problem}; treating it as unpaginated.`);
|
|
430
|
+
return "none";
|
|
431
|
+
};
|
|
432
|
+
const schema = op?.parameters?.find((param) => param.name === "page")?.schema;
|
|
433
|
+
if (schema?.["x-paginator"] !== void 0) return stampedPaginator(schema, warn);
|
|
434
|
+
const { members, menu } = this.pageMembers(op);
|
|
435
|
+
if (menu === void 0) {
|
|
436
|
+
const kind = kindOfMembers(members);
|
|
437
|
+
if (kind !== void 0) return kind;
|
|
438
|
+
}
|
|
439
|
+
if (menu !== void 0) return warn(`advertises a selectable page strategy menu (${menu.map((arm) => kindOfMembers(arm) ?? sortedList(arm)).join(" | ")}) without an x-paginator stamp naming its default`);
|
|
440
|
+
return members.size > 0 ? warn(`advertises page parameters matching no known paginator kind (members ${sortedList(members)})`) : "none";
|
|
419
441
|
}
|
|
420
442
|
/**
|
|
421
|
-
*
|
|
422
|
-
* `
|
|
423
|
-
*
|
|
424
|
-
*
|
|
443
|
+
* The `page` members an operation accepts, from either wire form: one `page` parameter
|
|
444
|
+
* (`style: deepObject`) whose schema `properties` name the members, as `haddowg/json-api` v1
|
|
445
|
+
* projects it, or the older flattened `page[number]`/`page[size]` parameters. A `oneOf` page
|
|
446
|
+
* schema (a MultiPaginator menu) yields each arm's members in `menu` instead, minus the
|
|
447
|
+
* `kind` discriminator.
|
|
425
448
|
*/
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
449
|
+
pageMembers(op) {
|
|
450
|
+
const members = /* @__PURE__ */ new Set();
|
|
451
|
+
let menu;
|
|
452
|
+
for (const param of op?.parameters ?? []) {
|
|
453
|
+
const name = param.name;
|
|
454
|
+
if (name === "page") {
|
|
455
|
+
const arms = param.schema?.oneOf;
|
|
456
|
+
if (arms !== void 0) menu = arms.map((arm) => new Set(Object.keys(arm.properties ?? {}).filter((m) => m !== "kind")));
|
|
457
|
+
for (const member of Object.keys(param.schema?.properties ?? {})) members.add(member);
|
|
458
|
+
} else if (name?.startsWith("page[") === true && name.endsWith("]")) members.add(name.slice(5, -1));
|
|
459
|
+
}
|
|
460
|
+
return menu === void 0 ? { members } : {
|
|
461
|
+
members,
|
|
462
|
+
menu
|
|
463
|
+
};
|
|
433
464
|
}
|
|
434
465
|
/**
|
|
435
466
|
* The `withCount` capability (Countable profile) for a type's COLLECTION read (`list`): the
|
|
@@ -487,11 +518,6 @@ var DescriptorBuilder = class {
|
|
|
487
518
|
}
|
|
488
519
|
return keys.length > 0 ? keys.sort() : void 0;
|
|
489
520
|
}
|
|
490
|
-
queryParamNames(op) {
|
|
491
|
-
const names = /* @__PURE__ */ new Set();
|
|
492
|
-
for (const param of op?.parameters ?? []) if (typeof param.name === "string") names.add(param.name);
|
|
493
|
-
return names;
|
|
494
|
-
}
|
|
495
521
|
operation(path, method) {
|
|
496
522
|
return this.paths[path]?.[method];
|
|
497
523
|
}
|
|
@@ -613,6 +639,234 @@ function buildDescriptor(doc) {
|
|
|
613
639
|
function buildAtomic(doc) {
|
|
614
640
|
return new DescriptorBuilder(doc).buildAtomic();
|
|
615
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Classify a page member set: `number`+`size` = `page`; `offset`+`limit` = `offset`; either
|
|
644
|
+
* cursor bound (`after`/`before`) = `cursor` (link-driven, D44); `number` alone = `fixed` (the
|
|
645
|
+
* server fixes the size); no members = `none`; anything else = `undefined` (unrecognised).
|
|
646
|
+
*/
|
|
647
|
+
function kindOfMembers(members) {
|
|
648
|
+
if (members.has("number") && members.has("size")) return "page";
|
|
649
|
+
if (members.has("offset") && members.has("limit")) return "offset";
|
|
650
|
+
if (members.has("after") || members.has("before")) return "cursor";
|
|
651
|
+
if (members.size === 1 && members.has("number")) return "fixed";
|
|
652
|
+
return members.size === 0 ? "none" : void 0;
|
|
653
|
+
}
|
|
654
|
+
const sortedList = (members) => [...members].sort().join(", ");
|
|
655
|
+
const STRATEGIES = [
|
|
656
|
+
"page",
|
|
657
|
+
"offset",
|
|
658
|
+
"cursor",
|
|
659
|
+
"fixed"
|
|
660
|
+
];
|
|
661
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
662
|
+
/**
|
|
663
|
+
* Read a page schema's `x-paginator` stamp. A strategy keeps the wire key the stamp names for
|
|
664
|
+
* each of its roles (a missing role keeps its canonical name) and collapses to the bare kind when
|
|
665
|
+
* every key is canonical. A `multi` stamp reads each `oneOf` arm by its `kind` const: an arm's own
|
|
666
|
+
* stamp decides it, an unstamped arm falls back to name inference, and an arm neither recognises
|
|
667
|
+
* is left out of the menu.
|
|
668
|
+
*/
|
|
669
|
+
function stampedPaginator(schema, warn) {
|
|
670
|
+
const stamp = schema["x-paginator"];
|
|
671
|
+
const strategy = isRecord(stamp) ? stamp["strategy"] : void 0;
|
|
672
|
+
if (!isRecord(stamp) || typeof strategy !== "string") return warn("stamps a malformed x-paginator");
|
|
673
|
+
if (strategy === "multi") return stampedMenu(schema, stamp, warn);
|
|
674
|
+
if (!STRATEGIES.includes(strategy)) return warn(`stamps an x-paginator strategy "${strategy}" this codegen does not know`);
|
|
675
|
+
const kind = strategy;
|
|
676
|
+
const canonical = CANONICAL_PAGE_KEYS[kind];
|
|
677
|
+
const stamped = isRecord(stamp["keys"]) ? stamp["keys"] : {};
|
|
678
|
+
const keys = {};
|
|
679
|
+
let renamed = false;
|
|
680
|
+
for (const [role, fallback] of Object.entries(canonical)) {
|
|
681
|
+
const key = typeof stamped[role] === "string" ? stamped[role] : fallback;
|
|
682
|
+
keys[role] = key;
|
|
683
|
+
renamed ||= key !== fallback;
|
|
684
|
+
}
|
|
685
|
+
return renamed ? {
|
|
686
|
+
strategy: kind,
|
|
687
|
+
keys
|
|
688
|
+
} : kind;
|
|
689
|
+
}
|
|
690
|
+
function stampedMenu(schema, stamp, warn) {
|
|
691
|
+
const arms = {};
|
|
692
|
+
for (const arm of schema.oneOf ?? []) {
|
|
693
|
+
const discriminator = arm.properties?.["kind"];
|
|
694
|
+
const kind = isRecord(discriminator) ? discriminator["const"] : void 0;
|
|
695
|
+
if (typeof kind !== "string") {
|
|
696
|
+
warn("offers a page strategy menu arm without a kind const, which is left out");
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
const paginator = arm["x-paginator"] !== void 0 ? stampedPaginator(arm, (problem) => warn(`offers a menu arm "${kind}" that ${problem}, which is left out`)) : kindOfMembers(new Set(Object.keys(arm.properties ?? {}).filter((m) => m !== "kind"))) ?? warn(`offers a menu arm "${kind}" matching no known paginator kind, which is left out`);
|
|
700
|
+
if (typeof paginator === "object" && paginator.strategy === "multi") {
|
|
701
|
+
warn(`nests a page strategy menu in arm "${kind}", which is left out`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (paginator !== "none") arms[kind] = paginator;
|
|
705
|
+
}
|
|
706
|
+
const fallback = stamp["default"];
|
|
707
|
+
if (typeof fallback !== "string" || arms[fallback] === void 0) return warn(`stamps a page strategy menu whose default ${typeof fallback === "string" ? `"${fallback}"` : "(missing)"} is not one of its generated arms`);
|
|
708
|
+
return {
|
|
709
|
+
strategy: "multi",
|
|
710
|
+
default: fallback,
|
|
711
|
+
arms
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
//#endregion
|
|
715
|
+
//#region src/contract.ts
|
|
716
|
+
/** Below this the document predates the structures this codegen reads. */
|
|
717
|
+
const MINIMUM_CONTRACT = 1;
|
|
718
|
+
/** Above this the document carries structures this codegen does not read. */
|
|
719
|
+
const MAXIMUM_CONTRACT = 4;
|
|
720
|
+
/**
|
|
721
|
+
* Check the document's `info.x-generator.contract` against the range this codegen reads. A
|
|
722
|
+
* document older than the range throws; a newer one returns a warning, because its new
|
|
723
|
+
* structures would go unread and the client would silently miss what the server offers. A
|
|
724
|
+
* document with no contract (json-api before 1.1.0) passes.
|
|
725
|
+
*/
|
|
726
|
+
function checkContract(doc) {
|
|
727
|
+
const contract = doc.info?.["x-generator"]?.contract;
|
|
728
|
+
if (typeof contract !== "number") return;
|
|
729
|
+
if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 4). Use an older codegen release, or regenerate the document from a newer server.`);
|
|
730
|
+
if (contract > 4) return `the document declares contract ${contract} and this codegen reads up to 4. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
|
|
731
|
+
}
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/error-codes.ts
|
|
734
|
+
const REF_PREFIX$1 = "#/components/schemas/";
|
|
735
|
+
const ERROR_DOCUMENT = /^ErrorDocument(\d{3})?$/;
|
|
736
|
+
function isSchema$1(value) {
|
|
737
|
+
return typeof value === "object" && value !== null;
|
|
738
|
+
}
|
|
739
|
+
/** A schema node and its `allOf` members, where a code variant keeps its narrowing. */
|
|
740
|
+
function layers(schema) {
|
|
741
|
+
return [schema, ...(schema.allOf ?? []).filter((member) => member.$ref === void 0)];
|
|
742
|
+
}
|
|
743
|
+
function property(schema, name) {
|
|
744
|
+
for (const layer of layers(schema)) {
|
|
745
|
+
const prop = layer.properties?.[name];
|
|
746
|
+
if (isSchema$1(prop)) return prop;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function examplesOf(detail) {
|
|
750
|
+
if (detail === void 0) return [];
|
|
751
|
+
const examples = Array.isArray(detail["examples"]) ? [...detail["examples"]] : [];
|
|
752
|
+
if ("example" in detail && !examples.includes(detail["example"])) examples.unshift(detail["example"]);
|
|
753
|
+
return examples;
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Read the documented error codes off a document's error documents. `ErrorDocument` and each
|
|
757
|
+
* `ErrorDocument<NNN>` offer their codes as `errors.items.anyOf` branches that `$ref` a
|
|
758
|
+
* `<Code>Error` component; a branch with a `code` const names a code, and the generic `Error`
|
|
759
|
+
* branch (no const) names none.
|
|
760
|
+
*/
|
|
761
|
+
function readErrorCatalogue(doc) {
|
|
762
|
+
const schemas = doc.components?.schemas ?? {};
|
|
763
|
+
const codes = /* @__PURE__ */ new Map();
|
|
764
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
765
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
766
|
+
const match = ERROR_DOCUMENT.exec(name);
|
|
767
|
+
if (match === null) continue;
|
|
768
|
+
const items = property(schema, "errors")?.items;
|
|
769
|
+
const branches = items === void 0 ? [] : [...items.anyOf ?? [], ...items.oneOf ?? []];
|
|
770
|
+
const documentCodes = /* @__PURE__ */ new Set();
|
|
771
|
+
for (const branch of branches) {
|
|
772
|
+
const component = typeof branch.$ref === "string" && branch.$ref.startsWith(REF_PREFIX$1) ? schemas[branch.$ref.slice(21)] : branch;
|
|
773
|
+
const code = component === void 0 ? void 0 : property(component, "code")?.const;
|
|
774
|
+
if (component === void 0 || typeof code !== "string") continue;
|
|
775
|
+
documentCodes.add(code);
|
|
776
|
+
if (!codes.has(code)) {
|
|
777
|
+
const description = layers(component).map((layer) => layer.description?.trim()).find((text) => text !== void 0 && text !== "");
|
|
778
|
+
codes.set(code, {
|
|
779
|
+
code,
|
|
780
|
+
...description ? { description } : {},
|
|
781
|
+
examples: examplesOf(property(component, "detail"))
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (match[1] !== void 0) byStatus.set(Number(match[1]), [...documentCodes].sort());
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
codes: [...codes.values()].sort((a, b) => a.code < b.code ? -1 : a.code > b.code ? 1 : 0),
|
|
789
|
+
byStatus: new Map([...byStatus].sort(([a], [b]) => a - b))
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
/** A TSDoc block for one code: its description, then each example detail as an `@example`. */
|
|
793
|
+
function codeDoc(entry, indent) {
|
|
794
|
+
const lines = [];
|
|
795
|
+
if (entry.description !== void 0) lines.push(...entry.description.split("\n"));
|
|
796
|
+
for (const example of entry.examples) {
|
|
797
|
+
if (lines.length > 0) lines.push("");
|
|
798
|
+
const text = typeof example === "string" ? example : JSON.stringify(example);
|
|
799
|
+
lines.push(...`@example ${text}`.split("\n"));
|
|
800
|
+
}
|
|
801
|
+
if (lines.length === 0) return;
|
|
802
|
+
if (lines.length === 1) return `${indent}/** ${lines[0].replace(/\*\//g, "* /")} */`;
|
|
803
|
+
return [
|
|
804
|
+
`${indent}/**`,
|
|
805
|
+
...lines.map((line) => `${indent} * ${line.replace(/\*\//g, "* /")}`.trimEnd()),
|
|
806
|
+
`${indent} */`
|
|
807
|
+
].join("\n");
|
|
808
|
+
}
|
|
809
|
+
/** A bare object key when the code is a valid identifier, else a quoted one. */
|
|
810
|
+
function codeKey(code) {
|
|
811
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(code) ? code : JSON.stringify(code);
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Emit `ErrorCodes`, a const object keyed by every documented code with its TSDoc on the key
|
|
815
|
+
* (editors show TSDoc on an object key, not on a string-literal union member), the `ErrorCode`
|
|
816
|
+
* union derived from it, and `ErrorCodeByStatus`, the codes each status's error document offers.
|
|
817
|
+
* A document with no code catalogue gets an empty `ErrorCodes` and `ErrorCode = string`, so the
|
|
818
|
+
* bound guard still takes any code.
|
|
819
|
+
*/
|
|
820
|
+
function emitErrorCodes(catalogue) {
|
|
821
|
+
const codeTypes = catalogue.codes.length === 0 ? [
|
|
822
|
+
"/** The documented error codes. This document catalogues none. */",
|
|
823
|
+
"export const ErrorCodes = {} as const",
|
|
824
|
+
"",
|
|
825
|
+
"/** The documented error codes. This document catalogues none, so any code is accepted. */",
|
|
826
|
+
"export type ErrorCode = string"
|
|
827
|
+
].join("\n") : [
|
|
828
|
+
"/**",
|
|
829
|
+
" * Every error code this server documents, keyed by itself. Hover a key for what the code",
|
|
830
|
+
" * means: `hasErrorCode(error, ErrorCodes.RESOURCE_NOT_FOUND)`.",
|
|
831
|
+
" */",
|
|
832
|
+
"export const ErrorCodes = {",
|
|
833
|
+
...catalogue.codes.flatMap((entry) => {
|
|
834
|
+
const doc = codeDoc(entry, " ");
|
|
835
|
+
const member = ` ${codeKey(entry.code)}: ${JSON.stringify(entry.code)},`;
|
|
836
|
+
return doc === void 0 ? [member] : [doc, member];
|
|
837
|
+
}),
|
|
838
|
+
"} as const",
|
|
839
|
+
"",
|
|
840
|
+
"/**",
|
|
841
|
+
" * Every error code this server documents. An error may still carry a code that is not",
|
|
842
|
+
" * listed: the catalogue is open.",
|
|
843
|
+
" */",
|
|
844
|
+
"export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]"
|
|
845
|
+
].join("\n");
|
|
846
|
+
const statusLines = [...catalogue.byStatus].map(([status, codes]) => {
|
|
847
|
+
const members = codes.map((c) => JSON.stringify(c));
|
|
848
|
+
const line = ` ${status}: ${members.length === 0 ? "never" : members.join(" | ")}`;
|
|
849
|
+
return line.length <= 100 ? line : ` ${status}:\n${members.map((m) => ` | ${m}`).join("\n")}`;
|
|
850
|
+
});
|
|
851
|
+
return [
|
|
852
|
+
codeTypes,
|
|
853
|
+
[
|
|
854
|
+
"/**",
|
|
855
|
+
" * The documented error codes per HTTP status. `never` marks a status whose error document",
|
|
856
|
+
" * documents no specific code.",
|
|
857
|
+
" */",
|
|
858
|
+
statusLines.length === 0 ? "export interface ErrorCodeByStatus {}" : `export interface ErrorCodeByStatus {\n${statusLines.join("\n")}\n}`
|
|
859
|
+
].join("\n"),
|
|
860
|
+
[
|
|
861
|
+
"/**",
|
|
862
|
+
" * True when `err` is a `JsonApiError` and any of its error objects carries `code`. Only takes a",
|
|
863
|
+
" * documented code; match an undocumented one with `hasErrorCode` from",
|
|
864
|
+
" * '@haddowg/json-api-client'.",
|
|
865
|
+
" */",
|
|
866
|
+
"export const hasErrorCode = hasErrorCodeRuntime as ErrorCodeGuard<ErrorCode>"
|
|
867
|
+
].join("\n")
|
|
868
|
+
].join("\n\n");
|
|
869
|
+
}
|
|
616
870
|
//#endregion
|
|
617
871
|
//#region src/provenance.ts
|
|
618
872
|
/**
|
|
@@ -786,6 +1040,7 @@ var Emitter = class {
|
|
|
786
1040
|
this.resourceMap(),
|
|
787
1041
|
"export type ResourceMap = typeof resourceMap",
|
|
788
1042
|
this.atomicConst(),
|
|
1043
|
+
emitErrorCodes(readErrorCatalogue(this.doc)),
|
|
789
1044
|
this.boundFactory()
|
|
790
1045
|
].join("\n\n")}\n`;
|
|
791
1046
|
}
|
|
@@ -805,6 +1060,8 @@ var Emitter = class {
|
|
|
805
1060
|
" type ApiDescriptor,",
|
|
806
1061
|
" type ClientOptions,",
|
|
807
1062
|
" createClient as createClientRuntime,",
|
|
1063
|
+
" type ErrorCodeGuard,",
|
|
1064
|
+
" hasErrorCode as hasErrorCodeRuntime,",
|
|
808
1065
|
"} from '@haddowg/json-api-client'"
|
|
809
1066
|
].join("\n");
|
|
810
1067
|
}
|
|
@@ -1083,10 +1340,11 @@ var Emitter = class {
|
|
|
1083
1340
|
"/**",
|
|
1084
1341
|
" * Descriptor-bound client factory; wraps the generic runtime with this API’s `resourceMap`.",
|
|
1085
1342
|
" * The server-level `atomic` capability is threaded in by default so `client.atomic` is wired",
|
|
1086
|
-
" * (a caller may still override it via `options.atomic`).",
|
|
1343
|
+
" * (a caller may still override it via `options.atomic`). The client type carries `ErrorCode`,",
|
|
1344
|
+
" * so bindings that only see the client (the Angular refs) type its errors.",
|
|
1087
1345
|
" */",
|
|
1088
1346
|
"export const createClient = (options: ClientOptions) =>",
|
|
1089
|
-
" createClientRuntime<typeof resourceMap, Attributes, WriteAttributes, ActionTypes>(",
|
|
1347
|
+
" createClientRuntime<typeof resourceMap, Attributes, WriteAttributes, ActionTypes, ErrorCode>(",
|
|
1090
1348
|
" resourceMap,",
|
|
1091
1349
|
" {",
|
|
1092
1350
|
" atomic,",
|
|
@@ -1211,8 +1469,11 @@ function schemasOutputPath(output) {
|
|
|
1211
1469
|
*/
|
|
1212
1470
|
async function render(config) {
|
|
1213
1471
|
const doc = await readDocument(config.input);
|
|
1472
|
+
const contractWarning = checkContract(doc);
|
|
1473
|
+
if (contractWarning !== void 0) console.warn(`[json-api-codegen] ${contractWarning}`);
|
|
1214
1474
|
const descriptor = buildDescriptor(doc);
|
|
1215
1475
|
for (const { type, relation } of detectVerbCollisions(descriptor)) console.warn(`[json-api-codegen] "${type}" has a relation named "${relation}" which collides with a reserved verb on the resource handle; it will be reachable via .rel("${relation}") in the fluent client.`);
|
|
1476
|
+
if (Object.keys(descriptor).length === 0) console.warn(`[json-api-codegen] found no JSON:API resource types in ${config.input}; the generated client is empty.`);
|
|
1216
1477
|
const artifacts = [{
|
|
1217
1478
|
path: config.output,
|
|
1218
1479
|
content: emit(doc, descriptor)
|
|
@@ -1273,6 +1534,6 @@ async function check(config) {
|
|
|
1273
1534
|
};
|
|
1274
1535
|
}
|
|
1275
1536
|
//#endregion
|
|
1276
|
-
export { readSchemas as a, detectVerbCollisions as c, hashJson as d, provenanceLines as f,
|
|
1537
|
+
export { checkContract as _, readSchemas as a, buildDescriptor as b, detectVerbCollisions as c, hashJson as d, provenanceLines as f, MINIMUM_CONTRACT as g, MAXIMUM_CONTRACT as h, readDocument as i, emit as l, readErrorCatalogue as m, generate as n, emitSchemas as o, emitErrorCodes as p, schemasOutputPath as r, Emitter as s, check as t, deriveProvenance as u, DescriptorBuilder as v, buildAtomic as y };
|
|
1277
1538
|
|
|
1278
|
-
//# sourceMappingURL=src-
|
|
1539
|
+
//# sourceMappingURL=src-CDV4z7In.mjs.map
|