@haddowg/json-api-codegen 1.8.1 → 1.10.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 CHANGED
@@ -1,4 +1,4 @@
1
- import { r as CodegenConfig } from "./index-COQQrn7a.mjs";
1
+ import { r as CodegenConfig } from "./index-DqeWPRAD.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--examples] [--check]";
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { i as generateFiles, t as check } from "./src-AWTGjMiX.mjs";
2
+ import { i as generateFiles, t as check } from "./src-C3noZURK.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/cli.ts
@@ -56,6 +56,12 @@ interface OperationObject {
56
56
  parameters?: readonly ParameterObject[];
57
57
  requestBody?: RequestBodyObject;
58
58
  responses?: Record<string, ResponseObject>;
59
+ /** What a custom action declares about its effects (generator contract 15). */
60
+ 'x-safety'?: {
61
+ readOnly?: unknown;
62
+ destructive?: unknown;
63
+ idempotent?: unknown;
64
+ };
59
65
  }
60
66
  type HttpMethod = 'get' | 'post' | 'patch' | 'put' | 'delete';
61
67
  type PathItemObject = Partial<Record<HttpMethod, OperationObject>>;
@@ -152,9 +158,12 @@ declare class DescriptorBuilder {
152
158
  * `{collection}/{id}/-actions/{name}` paths. The action name is the last segment; scope
153
159
  * is `resource` when the path carries `/{id}/`, else `collection`. `input` is `document`
154
160
  * (a JSON:API requestBody), `none` (no requestBody) or `raw` (a non-JSON:API body);
155
- * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`).
161
+ * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`). An action
162
+ * answering to several methods gives one entry per method (see {@link pickOperations}).
156
163
  */
157
164
  private actions;
165
+ /** The descriptor for one operation of a custom action. */
166
+ private action;
158
167
  /**
159
168
  * The non-JSON:API media types an action answers with under its 2xx responses, each once, in
160
169
  * the document's order, with how the client reads its body.
@@ -295,7 +304,7 @@ declare function buildExamples(doc: OpenApiDocument, descriptor: ApiDescriptor):
295
304
  /** Below this the document predates the structures this codegen reads. */
296
305
  declare const MINIMUM_CONTRACT = 1;
297
306
  /** Above this the document carries structures this codegen does not read. */
298
- declare const MAXIMUM_CONTRACT = 13;
307
+ declare const MAXIMUM_CONTRACT = 15;
299
308
  /**
300
309
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
301
310
  * document older than the range throws; a newer one returns a warning, because its new
@@ -346,6 +355,8 @@ declare class Emitter {
346
355
  private readonly rawBodyAliases;
347
356
  /** JSDoc for entries of the `resourceMap` literal, keyed by their path joined with `/`. */
348
357
  private readonly literalDocs;
358
+ /** Wire type -> the links it declares, for the types that declare any. */
359
+ private readonly declaredLinks;
349
360
  /** Provenance of the source document, stamped into the generated header for drift tracking. */
350
361
  private readonly provenance;
351
362
  private readonly doc;
@@ -360,6 +371,22 @@ declare class Emitter {
360
371
  emit(): string;
361
372
  private header;
362
373
  private imports;
374
+ /**
375
+ * The links interface of a type that declares links, e.g. `ImagesLinks`: each declared link an
376
+ * optional member, since the server leaves a link out when its href resolves to `null`. It
377
+ * extends the loose `ResourceLinks`, so `self` and any link the type doesn't declare stay
378
+ * reachable as before.
379
+ */
380
+ private linksInterface;
381
+ /**
382
+ * A declared link object as a multi-line type: `href`, each static member as its `const`, and a
383
+ * `meta` object whose members are typed as attributes are, each with its own TSDoc.
384
+ */
385
+ private linkObjectType;
386
+ /** An object type with one line, and its TSDoc, per property. */
387
+ private documentedObject;
388
+ /** A static link member's `const` as a literal type, else its schema's type. */
389
+ private constOrType;
363
390
  /** Per-type attribute interface, e.g. `export interface AlbumsAttributes { ... }`. */
364
391
  private attributeInterface;
365
392
  /**
@@ -458,6 +485,13 @@ declare class Emitter {
458
485
  * argument to the runtime factory).
459
486
  */
460
487
  private attributesMap;
488
+ /**
489
+ * Emit the `Links` interface mapping each type that declares links to its links interface. The
490
+ * runtime threads it beside the `Attributes` map, so a read result's `$links` carries the
491
+ * declared links. A type absent from it keeps the loose `ResourceLinks`. Passed as the sixth
492
+ * type argument to the runtime factory.
493
+ */
494
+ private linksMap;
461
495
  /**
462
496
  * Emit the `WriteAttributes` interface mapping each writable wire type to its
463
497
  * `{ create; update }` attribute pair. A read-only type (no create/update component)
@@ -658,4 +692,4 @@ declare function generateFiles(config: CodegenConfig): Promise<string[]>;
658
692
  declare function check(config: CodegenConfig): Promise<CheckResult>;
659
693
  //#endregion
660
694
  export { operationExamples as A, PathItemObject as B, VerbCollision as C, MINIMUM_CONTRACT as D, MAXIMUM_CONTRACT as E, InfoObject as F, ResponseObject as H, MediaTypeObject as I, OpenApiDocument as L, buildAtomic as M, buildDescriptor as N, checkContract as O, HttpMethod as P, OperationObject as R, Emitter as S, emit as T, SchemaObject as U, RequestBodyObject as V, SchemaOrBool as W, emitExamples as _, examplesOutputPath as a, emitErrorCodes as b, schemasOutputPath as c, hashJson as d, provenanceLines as f, readSchemas as g, readDocument as h, check as i, DescriptorBuilder as j, buildExamples as k, Provenance as l, SchemaBundle as m, CheckResult as n, generate as o, emitSchemas as p, CodegenConfig as r, generateFiles as s, ArtifactDrift as t, deriveProvenance as u, DocumentedErrorCode as v, detectVerbCollisions as w, readErrorCatalogue as x, ErrorCatalogue as y, ParameterObject as z };
661
- //# sourceMappingURL=index-COQQrn7a.d.mts.map
695
+ //# sourceMappingURL=index-DqeWPRAD.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-DqeWPRAD.d.mts","names":[],"sources":["../src/openapi.ts","../src/build-descriptor.ts","../src/build-examples.ts","../src/contract.ts","../src/emit.ts","../src/error-codes.ts","../src/emit-examples.ts","../src/reader.ts","../src/emit-schemas.ts","../src/provenance.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as operationExamples, B as PathItemObject, C as VerbCollision, D as MINIMUM_CONTRACT, E as MAXIMUM_CONTRACT, F as InfoObject, H as ResponseObject, I as MediaTypeObject, L as OpenApiDocument, M as buildAtomic, N as buildDescriptor, O as checkContract, P as HttpMethod, R as OperationObject, S as Emitter, T as emit, U as SchemaObject, V as RequestBodyObject, W as SchemaOrBool, _ as emitExamples, a as examplesOutputPath, b as emitErrorCodes, c as schemasOutputPath, d as hashJson, f as provenanceLines, g as readSchemas, h as readDocument, i as check, j as DescriptorBuilder, k as buildExamples, l as Provenance, m as SchemaBundle, n as CheckResult, o as generate, p as emitSchemas, r as CodegenConfig, s as generateFiles, t as ArtifactDrift, u as deriveProvenance, v as DocumentedErrorCode, w as detectVerbCollisions, x as readErrorCatalogue, y as ErrorCatalogue, z as ParameterObject } from "./index-COQQrn7a.mjs";
1
+ import { A as operationExamples, B as PathItemObject, C as VerbCollision, D as MINIMUM_CONTRACT, E as MAXIMUM_CONTRACT, F as InfoObject, H as ResponseObject, I as MediaTypeObject, L as OpenApiDocument, M as buildAtomic, N as buildDescriptor, O as checkContract, P as HttpMethod, R as OperationObject, S as Emitter, T as emit, U as SchemaObject, V as RequestBodyObject, W as SchemaOrBool, _ as emitExamples, a as examplesOutputPath, b as emitErrorCodes, c as schemasOutputPath, d as hashJson, f as provenanceLines, g as readSchemas, h as readDocument, i as check, j as DescriptorBuilder, k as buildExamples, l as Provenance, m as SchemaBundle, n as CheckResult, o as generate, p as emitSchemas, r as CodegenConfig, s as generateFiles, t as ArtifactDrift, u as deriveProvenance, v as DocumentedErrorCode, w as detectVerbCollisions, x as readErrorCatalogue, y as ErrorCatalogue, z as ParameterObject } from "./index-DqeWPRAD.mjs";
2
2
  export { ArtifactDrift, CheckResult, CodegenConfig, DescriptorBuilder, type DocumentedErrorCode, Emitter, type ErrorCatalogue, type HttpMethod, type InfoObject, MAXIMUM_CONTRACT, MINIMUM_CONTRACT, type MediaTypeObject, type OpenApiDocument, type OperationObject, type ParameterObject, type PathItemObject, type Provenance, type RequestBodyObject, type ResponseObject, type SchemaBundle, type SchemaObject, type SchemaOrBool, type VerbCollision, buildAtomic, buildDescriptor, buildExamples, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitExamples, emitSchemas, examplesOutputPath, generate, generateFiles, hashJson, operationExamples, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as DescriptorBuilder, S as operationExamples, T as buildDescriptor, _ as readErrorCatalogue, a as schemasOutputPath, b as checkContract, c as emitSchemas, d as detectVerbCollisions, f as emit, g as emitErrorCodes, h as provenanceLines, i as generateFiles, l as emitExamples, m as hashJson, n as examplesOutputPath, o as readDocument, p as deriveProvenance, r as generate, s as readSchemas, t as check, u as Emitter, v as MAXIMUM_CONTRACT, w as buildAtomic, x as buildExamples, y as MINIMUM_CONTRACT } from "./src-AWTGjMiX.mjs";
1
+ import { C as DescriptorBuilder, S as operationExamples, T as buildDescriptor, _ as readErrorCatalogue, a as schemasOutputPath, b as checkContract, c as emitSchemas, d as detectVerbCollisions, f as emit, g as emitErrorCodes, h as provenanceLines, i as generateFiles, l as emitExamples, m as hashJson, n as examplesOutputPath, o as readDocument, p as deriveProvenance, r as generate, s as readSchemas, t as check, u as Emitter, v as MAXIMUM_CONTRACT, w as buildAtomic, x as buildExamples, y as MINIMUM_CONTRACT } from "./src-C3noZURK.mjs";
2
2
  export { DescriptorBuilder, Emitter, MAXIMUM_CONTRACT, MINIMUM_CONTRACT, buildAtomic, buildDescriptor, buildExamples, check, checkContract, deriveProvenance, detectVerbCollisions, emit, emitErrorCodes, emitExamples, emitSchemas, examplesOutputPath, generate, generateFiles, hashJson, operationExamples, provenanceLines, readDocument, readErrorCatalogue, readSchemas, schemasOutputPath };
@@ -74,7 +74,7 @@ function sortRecord(record) {
74
74
  function refName$1(ref) {
75
75
  return ref.startsWith(REF_PREFIX$2) ? ref.slice(21) : void 0;
76
76
  }
77
- function isSchema$2(value) {
77
+ function isSchema$3(value) {
78
78
  return typeof value === "object" && value !== null;
79
79
  }
80
80
  /** The operationId prefixes of a relation's related read and its relationship endpoint. */
@@ -164,20 +164,20 @@ var DescriptorBuilder = class {
164
164
  resourceType(name, schema) {
165
165
  if (!name.endsWith("Resource") || name.endsWith("ResourceIdentifier")) return;
166
166
  const constValue = schema.properties?.["type"];
167
- if (isSchema$2(constValue) && typeof constValue.const === "string") return constValue.const;
167
+ if (isSchema$3(constValue) && typeof constValue.const === "string") return constValue.const;
168
168
  }
169
169
  /** Follow a `$ref` to its identifier/resource schema and read `properties.type.const`. */
170
170
  resolveConst(ref) {
171
171
  const target = refName$1(ref);
172
172
  if (target === void 0) return;
173
173
  const typeProp = this.schemas[target]?.properties?.["type"];
174
- return isSchema$2(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
174
+ return isSchema$3(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
175
175
  }
176
176
  attributes(base) {
177
177
  const props = this.schemas[`${base}Attributes`]?.properties;
178
178
  if (props === void 0) return {};
179
179
  const out = {};
180
- for (const [name, prop] of Object.entries(props)) if (isSchema$2(prop)) out[name] = formatHint(prop);
180
+ for (const [name, prop] of Object.entries(props)) if (isSchema$3(prop)) out[name] = formatHint(prop);
181
181
  return sortRecord(out);
182
182
  }
183
183
  /**
@@ -188,7 +188,7 @@ var DescriptorBuilder = class {
188
188
  * request pivot off the write bodies.
189
189
  */
190
190
  relations(wireType, base, resource, collection) {
191
- const relProps = isSchema$2(resource.properties?.["relationships"]) ? resource.properties["relationships"].properties ?? {} : {};
191
+ const relProps = isSchema$3(resource.properties?.["relationships"]) ? resource.properties["relationships"].properties ?? {} : {};
192
192
  const writes = this.writeRelationships(base);
193
193
  const endpoints = this.relationEndpoints(wireType);
194
194
  const out = {};
@@ -210,7 +210,7 @@ var DescriptorBuilder = class {
210
210
  out[name] = collection === void 0 ? descriptor : this.withRelationExposure(descriptor, collection, name, endpoints.get(name));
211
211
  };
212
212
  for (const [name, prop] of Object.entries(relProps)) {
213
- if (!isSchema$2(prop) || typeof prop.$ref !== "string" || writes.get(name)?.writeOnly) continue;
213
+ if (!isSchema$3(prop) || typeof prop.$ref !== "string" || writes.get(name)?.writeOnly) continue;
214
214
  add(name, this.schemas[refName$1(prop.$ref) ?? ""], false);
215
215
  }
216
216
  for (const [name, write] of writes) if (write.writeOnly || !(name in relProps)) add(name, this.schemas[refName$1(write.ref) ?? ""], true);
@@ -224,10 +224,10 @@ var DescriptorBuilder = class {
224
224
  const out = /* @__PURE__ */ new Map();
225
225
  for (const request of [`${base}CreateRequest`, `${base}UpdateRequest`]) {
226
226
  const data = this.schemas[request]?.properties?.["data"];
227
- const relationships = isSchema$2(data) ? data.properties?.["relationships"] : void 0;
228
- const props = isSchema$2(relationships) ? relationships.properties ?? {} : {};
227
+ const relationships = isSchema$3(data) ? data.properties?.["relationships"] : void 0;
228
+ const props = isSchema$3(relationships) ? relationships.properties ?? {} : {};
229
229
  for (const [name, prop] of Object.entries(props)) {
230
- if (!isSchema$2(prop) || typeof prop.$ref !== "string") continue;
230
+ if (!isSchema$3(prop) || typeof prop.$ref !== "string") continue;
231
231
  const writeOnly = prop["writeOnly"] === true || out.get(name)?.writeOnly === true;
232
232
  out.set(name, {
233
233
  ref: out.get(name)?.ref ?? prop.$ref,
@@ -327,7 +327,8 @@ var DescriptorBuilder = class {
327
327
  * `{collection}/{id}/-actions/{name}` paths. The action name is the last segment; scope
328
328
  * is `resource` when the path carries `/{id}/`, else `collection`. `input` is `document`
329
329
  * (a JSON:API requestBody), `none` (no requestBody) or `raw` (a non-JSON:API body);
330
- * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`).
330
+ * `output` is `document` (a 2xx returns a JSON:API document) or `none` (only `204`). An action
331
+ * answering to several methods gives one entry per method (see {@link pickOperations}).
331
332
  */
332
333
  actions(collection) {
333
334
  const out = {};
@@ -344,47 +345,66 @@ var DescriptorBuilder = class {
344
345
  name = path.slice(collectionPrefix.length);
345
346
  } else continue;
346
347
  if (name.length === 0 || name.includes("/")) continue;
347
- const picked = pickOperation(item);
348
- if (picked === void 0) {
348
+ const picked = pickOperations(item);
349
+ if (picked.length === 0) {
349
350
  console.warn(`json-api codegen: custom action path "${path}" advertises no usable operation; skipping.`);
350
351
  continue;
351
352
  }
352
- const { method, op } = picked;
353
- const input = actionInput(op);
354
- const output = actionOutput(op);
355
- const action = {
356
- scope,
357
- path,
358
- input,
359
- output
360
- };
361
- if (method !== "post") action.method = method.toUpperCase();
362
- if (input === "document") {
363
- const inputType = this.documentPrimary(actionRequestRef(op))?.type;
364
- if (inputType !== void 0) action.inputType = inputType;
365
- }
366
- if (output === "document") {
367
- const primary = this.documentPrimary(this.firstOkJsonApiRef(op));
368
- if (primary !== void 0) {
369
- action.outputType = primary.type;
370
- action.outputCardinality = primary.cardinality;
353
+ for (const [index, { method, op }] of picked.entries()) {
354
+ const key = index === 0 ? name : methodActionName(name, method);
355
+ if (key in out) {
356
+ console.warn(`json-api codegen: the ${method.toUpperCase()} operation of custom action path "${path}" would be named "${key}", which another action already uses; skipping.`);
357
+ continue;
371
358
  }
359
+ out[key] = this.action(scope, path, method, op);
372
360
  }
373
- if (input === "raw") {
374
- const contentType = rawContentType(op);
375
- if (contentType !== void 0) action.contentType = contentType;
376
- }
377
- const rawOutput = this.rawOutput(op);
378
- if (rawOutput.length > 0) action.rawOutput = rawOutput;
379
- const requestContent = Object.entries(op.requestBody?.content ?? {});
380
- if (requestContent.length > 1) action.inputMedia = requestContent.map(([mediaType, media]) => ({
381
- mediaType,
382
- kind: this.inputMediaKind(mediaType, media.schema)
383
- }));
384
- out[name] = action;
385
361
  }
386
362
  return sortRecord(out);
387
363
  }
364
+ /** The descriptor for one operation of a custom action. */
365
+ action(scope, path, method, op) {
366
+ const input = actionInput(op);
367
+ const output = actionOutput(op);
368
+ const action = {
369
+ scope,
370
+ path,
371
+ input,
372
+ output
373
+ };
374
+ if (method !== "post") action.method = method.toUpperCase();
375
+ if (input === "document") {
376
+ const inputType = this.documentPrimary(actionRequestRef(op))?.type;
377
+ if (inputType !== void 0) action.inputType = inputType;
378
+ }
379
+ if (output === "document") {
380
+ const primary = this.documentPrimary(this.firstOkJsonApiRef(op));
381
+ if (primary !== void 0) {
382
+ action.outputType = primary.type;
383
+ action.outputCardinality = primary.cardinality;
384
+ }
385
+ }
386
+ if (input === "raw") {
387
+ const contentType = rawContentType(op);
388
+ if (contentType !== void 0) action.contentType = contentType;
389
+ }
390
+ const rawOutput = this.rawOutput(op);
391
+ if (rawOutput.length > 0) action.rawOutput = rawOutput;
392
+ const requestContent = Object.entries(op.requestBody?.content ?? {});
393
+ if (requestContent.length > 1) action.inputMedia = requestContent.map(([mediaType, media]) => ({
394
+ mediaType,
395
+ kind: this.inputMediaKind(mediaType, media.schema)
396
+ }));
397
+ const safety = op["x-safety"];
398
+ for (const flag of [
399
+ "readOnly",
400
+ "destructive",
401
+ "idempotent"
402
+ ]) {
403
+ const value = safety?.[flag];
404
+ if (typeof value === "boolean") action[flag] = value;
405
+ }
406
+ return action;
407
+ }
388
408
  /**
389
409
  * The non-JSON:API media types an action answers with under its 2xx responses, each once, in
390
410
  * the document's order, with how the client reads its body.
@@ -439,8 +459,8 @@ var DescriptorBuilder = class {
439
459
  if (typeof ref !== "string") return;
440
460
  const component = refName$1(ref);
441
461
  const data = component === void 0 ? void 0 : this.schemas[component]?.properties?.["data"];
442
- if (!isSchema$2(data)) return;
443
- if (isSchema$2(data.items)) {
462
+ if (!isSchema$3(data)) return;
463
+ if (isSchema$3(data.items)) {
444
464
  const itemRef = data.items.$ref;
445
465
  const many = typeof itemRef === "string" ? this.resolveConst(itemRef) : void 0;
446
466
  return many === void 0 ? void 0 : {
@@ -458,7 +478,7 @@ var DescriptorBuilder = class {
458
478
  dataConst(data) {
459
479
  if (typeof data.$ref === "string") return this.resolveConst(data.$ref);
460
480
  const typeProp = data.properties?.["type"];
461
- return isSchema$2(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
481
+ return isSchema$3(typeProp) && typeof typeProp.const === "string" ? typeProp.const : void 0;
462
482
  }
463
483
  /** The `$ref` of the first 2xx JSON:API response body on an operation (the output document). */
464
484
  firstOkJsonApiRef(op) {
@@ -471,7 +491,7 @@ var DescriptorBuilder = class {
471
491
  /** Resolve a relationship component's `data` into a {@link RelationDescriptor}. */
472
492
  relationFromComponent(component) {
473
493
  const data = component.properties?.["data"];
474
- if (!isSchema$2(data)) return;
494
+ if (!isSchema$3(data)) return;
475
495
  if (data.type === "array") {
476
496
  const items = data.items;
477
497
  if (items === void 0) return;
@@ -550,7 +570,7 @@ var DescriptorBuilder = class {
550
570
  const fields = {};
551
571
  const nullable = [];
552
572
  for (const [name, prop] of Object.entries(schema?.properties ?? {})) {
553
- if (!isSchema$2(prop) || prop[side === "read" ? "writeOnly" : "readOnly"] === true) continue;
573
+ if (!isSchema$3(prop) || prop[side === "read" ? "writeOnly" : "readOnly"] === true) continue;
554
574
  fields[name] = formatHint(prop);
555
575
  if (isNullable(prop)) nullable.push(name);
556
576
  }
@@ -580,7 +600,7 @@ var DescriptorBuilder = class {
580
600
  }
581
601
  clientId(base) {
582
602
  const create = this.schemas[`${base}CreateRequest`];
583
- const data = isSchema$2(create?.properties?.["data"]) ? create.properties["data"] : void 0;
603
+ const data = isSchema$3(create?.properties?.["data"]) ? create.properties["data"] : void 0;
584
604
  if (data === void 0) return "forbidden";
585
605
  if (data.properties?.["id"] === false) return "forbidden";
586
606
  return data.required?.includes("id") ? "required" : "optional";
@@ -746,8 +766,8 @@ var DescriptorBuilder = class {
746
766
  function pivotNodeFromAllOf(allOf) {
747
767
  for (const entry of allOf) {
748
768
  const meta = entry.properties?.["meta"];
749
- const pivot = isSchema$2(meta) ? meta.properties?.["pivot"] : void 0;
750
- if (isSchema$2(pivot)) return pivot;
769
+ const pivot = isSchema$3(meta) ? meta.properties?.["pivot"] : void 0;
770
+ if (isSchema$3(pivot)) return pivot;
751
771
  }
752
772
  }
753
773
  /**
@@ -756,7 +776,7 @@ function pivotNodeFromAllOf(allOf) {
756
776
  */
757
777
  function pivotNodeOf(component) {
758
778
  const data = component.properties?.["data"];
759
- if (!isSchema$2(data)) return;
779
+ if (!isSchema$3(data)) return;
760
780
  if (Array.isArray(data.items?.allOf)) return pivotNodeFromAllOf(data.items.allOf);
761
781
  for (const entry of data.anyOf ?? []) if (Array.isArray(entry.allOf)) return pivotNodeFromAllOf(entry.allOf);
762
782
  }
@@ -837,18 +857,39 @@ const ACTION_METHODS = [
837
857
  "delete"
838
858
  ];
839
859
  /**
840
- * The single operation an action is invoked over: POST when advertised (the common default), else
841
- * the first advertised method. `undefined` when the path item carries no operation at all.
860
+ * The operations an action is invoked over, the preferred one first: POST when advertised (the
861
+ * common default), else the first advertised method. The other methods follow only when their
862
+ * `operationId` differs from the preferred one's, as contract 15 makes it for an action answering
863
+ * to several methods. An older document gave every method one id, and its client exposed only the
864
+ * preferred one. Empty when the path item carries no operation at all.
842
865
  */
843
- function pickOperation(item) {
866
+ function pickOperations(item) {
844
867
  const available = ACTION_METHODS.filter((method) => item[method] !== void 0);
845
- const method = available.includes("post") ? "post" : available[0];
846
- if (method === void 0) return;
847
- const op = item[method];
848
- return op === void 0 ? void 0 : {
849
- method,
850
- op
851
- };
868
+ const preferred = available.includes("post") ? "post" : available[0];
869
+ if (preferred === void 0) return [];
870
+ const first = item[preferred];
871
+ const out = [{
872
+ method: preferred,
873
+ op: first
874
+ }];
875
+ for (const method of available) {
876
+ const op = item[method];
877
+ if (method !== preferred && op.operationId !== first.operationId) out.push({
878
+ method,
879
+ op
880
+ });
881
+ }
882
+ return out;
883
+ }
884
+ /**
885
+ * The name a multi-method action's other methods go under: the action's name and the method,
886
+ * joined the way the name joins its words. So `export` gives `exportGet` and `bulk-import` gives
887
+ * `bulk-import-get`. Core refuses action ids that differ only in case or punctuation, so the
888
+ * name can't clash with another action's.
889
+ */
890
+ function methodActionName(name, method) {
891
+ const separator = /[-_.]/.exec(name)?.[0];
892
+ return separator === void 0 ? `${name}${method[0].toUpperCase()}${method.slice(1)}` : `${name}${separator}${method}`;
852
893
  }
853
894
  /** The body shape a custom action accepts: a JSON:API document, none, or a raw payload. */
854
895
  function actionInput(op) {
@@ -1044,7 +1085,7 @@ function buildExamples(doc, descriptor) {
1044
1085
  /** Below this the document predates the structures this codegen reads. */
1045
1086
  const MINIMUM_CONTRACT = 1;
1046
1087
  /** Above this the document carries structures this codegen does not read. */
1047
- const MAXIMUM_CONTRACT = 13;
1088
+ const MAXIMUM_CONTRACT = 15;
1048
1089
  /**
1049
1090
  * Check the document's `info.x-generator.contract` against the range this codegen reads. A
1050
1091
  * document older than the range throws; a newer one returns a warning, because its new
@@ -1054,8 +1095,38 @@ const MAXIMUM_CONTRACT = 13;
1054
1095
  function checkContract(doc) {
1055
1096
  const contract = doc.info?.["x-generator"]?.contract;
1056
1097
  if (typeof contract !== "number") return;
1057
- if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 13). Use an older codegen release, or regenerate the document from a newer server.`);
1058
- if (contract > 13) return `the document declares contract ${contract} and this codegen reads up to 13. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
1098
+ if (contract < 1) throw new Error(`the document declares contract ${contract}, which predates what this codegen reads (it supports 1 to 15). Use an older codegen release, or regenerate the document from a newer server.`);
1099
+ if (contract > 15) return `the document declares contract ${contract} and this codegen reads up to 15. It describes capabilities that were not generated; upgrade the codegen to pick them up.`;
1100
+ }
1101
+ //#endregion
1102
+ //#region src/declared-links.ts
1103
+ const LINK_OBJECT_REF = "#/components/schemas/LinkObject";
1104
+ function isSchema$2(value) {
1105
+ return typeof value === "object" && value !== null;
1106
+ }
1107
+ /**
1108
+ * The declared links on a `<Type>ResourceLinks` component, in document order. `self` is never
1109
+ * declared, and a link the server builds itself, such as an action's link, points at
1110
+ * `RenderedLink`, so neither is read. A server that renders its links as strings points those at
1111
+ * `LinkObject` instead, so they read as bare links, which is the shape they render in. Expects
1112
+ * the document's single-entry `allOf` wrappers already read as their `$ref`.
1113
+ */
1114
+ function readDeclaredLinks(component) {
1115
+ const out = [];
1116
+ for (const [name, schema] of Object.entries(component?.properties ?? {})) {
1117
+ if (name === "self" || !isSchema$2(schema)) continue;
1118
+ if (schema.$ref === LINK_OBJECT_REF) out.push({
1119
+ name,
1120
+ kind: "bare",
1121
+ schema
1122
+ });
1123
+ else if (schema.$ref === void 0 && isSchema$2(schema.properties?.["href"])) out.push({
1124
+ name,
1125
+ kind: "object",
1126
+ schema
1127
+ });
1128
+ }
1129
+ return out;
1059
1130
  }
1060
1131
  //#endregion
1061
1132
  //#region src/source.ts
@@ -1397,7 +1468,9 @@ const MODULE_NAMES = [
1397
1468
  "ErrorCodeByStatus",
1398
1469
  "ErrorCodeGuard",
1399
1470
  "ErrorCodes",
1471
+ "Links",
1400
1472
  "Record",
1473
+ "ResourceLinks",
1401
1474
  "ResourceMap",
1402
1475
  "WriteAttributes"
1403
1476
  ];
@@ -1489,6 +1562,19 @@ function actionDoc(name, op) {
1489
1562
  if (description && !generated.test(description)) parts.push(description);
1490
1563
  return parts.length === 0 ? void 0 : parts.join("\n\n");
1491
1564
  }
1565
+ /**
1566
+ * What an action declares about its effects (generator contract 15), as one sentence, or
1567
+ * `undefined` when it declares nothing.
1568
+ */
1569
+ function safetyDoc(action) {
1570
+ const words = [
1571
+ [action.readOnly, "read-only"],
1572
+ [action.destructive, "destructive"],
1573
+ [action.idempotent, "idempotent"]
1574
+ ].filter(([value]) => value !== void 0).map(([value, word]) => value === true ? word : `not ${word}`);
1575
+ if (words.length === 0) return;
1576
+ return `The server declares this action ${words.length === 1 ? words[0] : `${words.slice(0, -1).join(", ")} and ${words.at(-1)}`}.`;
1577
+ }
1492
1578
  /** Whether a media type carries JSON: JSON:API, `application/json` or a `+json` suffix. */
1493
1579
  function isJsonMedia(mediaType) {
1494
1580
  const essence = mediaType.split(";")[0].trim().toLowerCase();
@@ -1564,6 +1650,8 @@ var Emitter = class {
1564
1650
  rawBodyAliases = /* @__PURE__ */ new Map();
1565
1651
  /** JSDoc for entries of the `resourceMap` literal, keyed by their path joined with `/`. */
1566
1652
  literalDocs = /* @__PURE__ */ new Map();
1653
+ /** Wire type -> the links it declares, for the types that declare any. */
1654
+ declaredLinks = /* @__PURE__ */ new Map();
1567
1655
  /** Provenance of the source document, stamped into the generated header for drift tracking. */
1568
1656
  provenance;
1569
1657
  doc;
@@ -1575,12 +1663,18 @@ var Emitter = class {
1575
1663
  this.baseByType = this.indexBases();
1576
1664
  this.provenance = deriveProvenance(source);
1577
1665
  this.takenNames = new Set(MODULE_NAMES);
1666
+ for (const type of Object.keys(descriptor)) {
1667
+ const base = this.baseByType.get(type);
1668
+ const links = base === void 0 ? [] : readDeclaredLinks(this.schemas[`${base}ResourceLinks`]);
1669
+ if (links.length > 0) this.declaredLinks.set(type, links);
1670
+ }
1578
1671
  for (const type of Object.keys(descriptor)) {
1579
1672
  const stem = typeStem(type);
1580
1673
  for (const suffix of [
1581
1674
  "Attributes",
1582
1675
  "CreateAttributes",
1583
- "UpdateAttributes"
1676
+ "UpdateAttributes",
1677
+ "Links"
1584
1678
  ]) this.takenNames.add(`${stem}${suffix}`);
1585
1679
  for (const [name, action] of Object.entries(descriptor[type]?.actions ?? {})) {
1586
1680
  this.takenNames.add(`${stem}${pascalCase(name)}Input`);
@@ -1590,7 +1684,11 @@ var Emitter = class {
1590
1684
  const single = action.contentType;
1591
1685
  if (single !== void 0 && (single.startsWith("multipart/") || single === "application/x-www-form-urlencoded")) this.takenNames.add(`${stem}${pascalCase(name)}${pascalCase(single)}Input`);
1592
1686
  const op = actionOperation(this.doc, action);
1593
- const parts = [actionDoc(name, op), exampleDoc(op)].filter((part) => part !== void 0);
1687
+ const parts = [
1688
+ actionDoc(action.path.slice(action.path.lastIndexOf("/") + 1), op),
1689
+ safetyDoc(action),
1690
+ exampleDoc(op)
1691
+ ].filter((part) => part !== void 0);
1594
1692
  const doc = parts.length === 0 ? void 0 : parts.join("\n\n");
1595
1693
  if (doc !== void 0) this.literalDocs.set(`${type}/actions/${name}`, doc);
1596
1694
  }
@@ -1629,6 +1727,7 @@ var Emitter = class {
1629
1727
  emit() {
1630
1728
  const interfaces = Object.keys(this.descriptor).map((type) => this.attributeInterface(type));
1631
1729
  const writeInterfaces = Object.keys(this.descriptor).flatMap((type) => this.writeAttributeInterfaces(type));
1730
+ const linkInterfaces = [...this.declaredLinks].map(([type, links]) => this.linksInterface(type, links));
1632
1731
  const actionAliases = this.actionTypeAliases();
1633
1732
  const enums = this.enumAliases();
1634
1733
  return `${[
@@ -1637,9 +1736,11 @@ var Emitter = class {
1637
1736
  ...enums ? [enums] : [],
1638
1737
  ...interfaces,
1639
1738
  ...writeInterfaces,
1739
+ ...linkInterfaces,
1640
1740
  ...actionAliases,
1641
1741
  this.attributesMap(),
1642
1742
  this.writeAttributesMap(),
1743
+ this.linksMap(),
1643
1744
  this.actionTypesMap(),
1644
1745
  this.resourceMap(),
1645
1746
  "export type ResourceMap = typeof resourceMap",
@@ -1667,9 +1768,51 @@ var Emitter = class {
1667
1768
  " createClient as createClientRuntime,",
1668
1769
  " type ErrorCodeGuard,",
1669
1770
  " hasErrorCode as hasErrorCodeRuntime,",
1771
+ ...this.declaredLinks.size > 0 ? [" type ResourceLinks,"] : [],
1670
1772
  "} from '@haddowg/json-api-client'"
1671
1773
  ].join("\n");
1672
1774
  }
1775
+ /**
1776
+ * The links interface of a type that declares links, e.g. `ImagesLinks`: each declared link an
1777
+ * optional member, since the server leaves a link out when its href resolves to `null`. It
1778
+ * extends the loose `ResourceLinks`, so `self` and any link the type doesn't declare stay
1779
+ * reachable as before.
1780
+ */
1781
+ linksInterface(type, links) {
1782
+ const lines = [];
1783
+ for (const link of links) {
1784
+ const doc = jsDoc(memberDoc(link.schema), " ");
1785
+ if (doc !== void 0) lines.push(doc);
1786
+ const value = link.kind === "bare" ? "string" : this.linkObjectType(link.schema, " ");
1787
+ lines.push(` ${propertyKey(link.name)}?: ${value}`);
1788
+ }
1789
+ return `export interface ${typeStem(type)}Links extends ResourceLinks {\n${lines.join("\n")}\n}`;
1790
+ }
1791
+ /**
1792
+ * A declared link object as a multi-line type: `href`, each static member as its `const`, and a
1793
+ * `meta` object whose members are typed as attributes are, each with its own TSDoc.
1794
+ */
1795
+ linkObjectType(schema, indent) {
1796
+ return this.documentedObject(schema, indent, (name, member) => name === "meta" ? this.documentedObject(member, `${indent} `, (_, field) => this.tsType(field)) : this.constOrType(member));
1797
+ }
1798
+ /** An object type with one line, and its TSDoc, per property. */
1799
+ documentedObject(schema, indent, type) {
1800
+ const required = new Set(schema.required ?? []);
1801
+ const inner = `${indent} `;
1802
+ const lines = [];
1803
+ for (const [name, member] of Object.entries(schema.properties ?? {})) {
1804
+ if (!isSchema(member)) continue;
1805
+ const doc = jsDoc(memberDoc(member), inner);
1806
+ if (doc !== void 0) lines.push(doc);
1807
+ lines.push(`${inner}${propertyKey(name)}${required.has(name) ? "" : "?"}: ${type(name, member)}`);
1808
+ }
1809
+ return lines.length === 0 ? "Record<string, unknown>" : `{\n${lines.join("\n")}\n${indent}}`;
1810
+ }
1811
+ /** A static link member's `const` as a literal type, else its schema's type. */
1812
+ constOrType(schema) {
1813
+ const value = schema["const"];
1814
+ return typeof value === "string" ? stringLiteral(value) : this.tsType(schema);
1815
+ }
1673
1816
  /** Per-type attribute interface, e.g. `export interface AlbumsAttributes { ... }`. */
1674
1817
  attributeInterface(type) {
1675
1818
  const base = this.baseByType.get(type);
@@ -2067,6 +2210,17 @@ var Emitter = class {
2067
2210
  return `export interface Attributes {\n${Object.keys(this.descriptor).map((type) => ` ${propertyKey(type)}: ${typeStem(type)}Attributes`).join("\n")}\n}`;
2068
2211
  }
2069
2212
  /**
2213
+ * Emit the `Links` interface mapping each type that declares links to its links interface. The
2214
+ * runtime threads it beside the `Attributes` map, so a read result's `$links` carries the
2215
+ * declared links. A type absent from it keeps the loose `ResourceLinks`. Passed as the sixth
2216
+ * type argument to the runtime factory.
2217
+ */
2218
+ linksMap() {
2219
+ const lines = [...this.declaredLinks.keys()].map((type) => ` ${propertyKey(type)}: ${typeStem(type)}Links`);
2220
+ if (lines.length === 0) return "export interface Links {}";
2221
+ return `export interface Links {\n${lines.join("\n")}\n}`;
2222
+ }
2223
+ /**
2070
2224
  * Emit the `WriteAttributes` interface mapping each writable wire type to its
2071
2225
  * `{ create; update }` attribute pair. A read-only type (no create/update component)
2072
2226
  * contributes no entry, so the bound client only exposes writes where the API allows
@@ -2121,7 +2275,7 @@ var Emitter = class {
2121
2275
  " * so bindings that only see the client (the Angular refs) type its errors.",
2122
2276
  " */",
2123
2277
  "export const createClient = (options: ClientOptions) =>",
2124
- " createClientRuntime<typeof resourceMap, Attributes, WriteAttributes, ActionTypes, ErrorCode>(",
2278
+ " createClientRuntime<typeof resourceMap, Attributes, WriteAttributes, ActionTypes, ErrorCode, Links>(",
2125
2279
  " resourceMap,",
2126
2280
  " {",
2127
2281
  " atomic,",
@@ -2364,4 +2518,4 @@ async function check(config) {
2364
2518
  //#endregion
2365
2519
  export { DescriptorBuilder as C, operationExamples as S, buildDescriptor as T, readErrorCatalogue as _, schemasOutputPath as a, checkContract as b, emitSchemas as c, detectVerbCollisions as d, emit as f, emitErrorCodes as g, provenanceLines as h, generateFiles as i, emitExamples as l, hashJson as m, examplesOutputPath as n, readDocument as o, deriveProvenance as p, generate as r, readSchemas as s, check as t, Emitter as u, MAXIMUM_CONTRACT as v, buildAtomic as w, buildExamples as x, MINIMUM_CONTRACT as y };
2366
2520
 
2367
- //# sourceMappingURL=src-AWTGjMiX.mjs.map
2521
+ //# sourceMappingURL=src-C3noZURK.mjs.map