@haddowg/json-api-codegen 1.0.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gregory Haddow
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,170 @@
1
+ # @haddowg/json-api-codegen
2
+
3
+ > **Part of the [jsonapi.rest](https://jsonapi.rest) suite** — a complete, spec-compliant
4
+ > JSON:API 1.1 stack: a framework-agnostic PHP [core](https://github.com/haddowg/json-api), a
5
+ > [Symfony bundle](https://github.com/haddowg/json-api-symfony), a
6
+ > [Laravel package](https://github.com/haddowg/json-api-laravel), and this **typed TypeScript
7
+ > client**, bound together by one conformance-tested OpenAPI 3.1 contract.
8
+ > Full documentation: **[haddowg.github.io/json-api-ts](https://haddowg.github.io/json-api-ts/)**.
9
+
10
+ The CLI (and programmatic API) that reads the
11
+ [`haddowg/json-api-symfony`](https://github.com/haddowg/json-api-symfony) OpenAPI 3.1 document and
12
+ emits a typed client: a runtime descriptor, per-type attribute interfaces, and a descriptor-bound
13
+ `createClient`. Optionally reads the bundle's JSON Schemas to emit a per-type schema map for the
14
+ [client's](../client) opt-in validation seam.
15
+
16
+ The OpenAPI document is fully self-describing for the runtime's needs — type identity
17
+ (`properties.type.const`), relationship cardinality and related types, the allowed `?include`
18
+ paths (a bounded, pre-expanded enum per endpoint), the create client-id policy, and paginator
19
+ kinds. The codegen only reads the spec; all real work is generic runtime machinery in
20
+ `@haddowg/json-api-client`.
21
+
22
+ ## Install
23
+
24
+ A dev-only dependency — the **generated output** has no runtime tie back to the codegen (it imports
25
+ `@haddowg/json-api-client`).
26
+
27
+ ```bash
28
+ pnpm add -D @haddowg/json-api-codegen
29
+ ```
30
+
31
+ Two bin names are installed: `json-api-codegen` and the short alias `japi`.
32
+
33
+ ## CLI
34
+
35
+ ```bash
36
+ json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]
37
+ ```
38
+
39
+ | Flag | Required | Description |
40
+ | ----------- | -------- | ------------------------------------------------------------------------------------- |
41
+ | `--input` | yes | OpenAPI document source — an http(s) URL or a local file path (JSON or YAML). |
42
+ | `--output` | yes | Output file for the generated client (e.g. `src/api/music.gen.ts`). |
43
+ | `--server` | no | Server name to target. Metadata-only — the served document is already per-server. |
44
+ | `--schemas` | no | JSON Schema source (URL or path) for the validation seam. Omit to skip schema output. |
45
+ | `--check` | no | Drift gate — regenerate in memory and exit non-zero if the committed output differs. |
46
+
47
+ Example — generate against a served API, with the validation schemas:
48
+
49
+ ```bash
50
+ json-api-codegen \
51
+ --input https://music.example/docs.json \
52
+ --output src/api/music.gen.ts \
53
+ --schemas https://music.example/schemas.json
54
+ ```
55
+
56
+ This writes:
57
+
58
+ - `src/api/music.gen.ts` — the typed client (descriptor + types + bound `createClient`);
59
+ - `src/api/music.schemas.gen.ts` — the per-type JSON Schema map (only with `--schemas`).
60
+
61
+ The schema artifact path is derived from `--output`: a `.gen.ts` suffix becomes `.schemas.gen.ts`,
62
+ a plain `.ts` becomes `.schemas.ts`.
63
+
64
+ > **Commit the generated files.** Like `openapi-typescript` / `graphql-codegen`, the output is one
65
+ > small, reviewable, diffable, versioned module in your repo — not a published package. Regenerate
66
+ > when the API changes.
67
+
68
+ ### Provenance stamp
69
+
70
+ Every generated artifact carries a provenance record in its header — the source spec identifier
71
+ (`<info.title> <info.version>`), the first declared server URL, and a stable 16-hex content hash of
72
+ the source document (the schema artifact carries a `Schema hash` of the schema bundle). It is
73
+ **timestamp-free and deterministic**: regenerating from the same spec yields byte-identical output.
74
+ The hash tells a reviewer which spec a committed client was generated from, and lets the drift gate
75
+ below compare regenerated output byte-for-byte.
76
+
77
+ ```
78
+ /**
79
+ * AUTO-GENERATED by @haddowg/json-api-codegen — do not edit by hand.
80
+ * Regenerate from the source OpenAPI document instead.
81
+ *
82
+ * Source spec: Music Catalog API 1.0.0
83
+ * Server: https://music.example
84
+ * Spec hash: 40e273dff0411f68
85
+ */
86
+ ```
87
+
88
+ ### Drift check (`--check`)
89
+
90
+ `--check` regenerates in memory and compares against the committed `--output` (and its schema
91
+ sibling when `--schemas` is set) **without writing anything**. It prints a per-artifact `ok`/`DRIFT`
92
+ line and exits non-zero if any committed file is missing or out of date. Wire it into CI — pinning
93
+ the spec to a committed fixture — so a stale generated client fails the build:
94
+
95
+ ```bash
96
+ json-api-codegen \
97
+ --input spec/music-catalog.openapi.json \
98
+ --output src/api/music.gen.ts \
99
+ --schemas spec/music-catalog.schemas.json \
100
+ --check
101
+ ```
102
+
103
+ ### Multiple servers
104
+
105
+ The bundle serves one OpenAPI document per server (e.g. `default` and `admin` differ in their
106
+ type set and path prefix). Generate one client per server by pointing `--input` at each server's
107
+ document — one typed client per server.
108
+
109
+ ## What it generates
110
+
111
+ The output module exports:
112
+
113
+ - `resourceMap` — the runtime descriptor (`as const`); what the [query](../query) package's
114
+ normalization needs.
115
+ - `ResourceMap` — `typeof resourceMap`.
116
+ - `createClient(options)` — the descriptor-bound factory (the descriptor and the server's `atomic`
117
+ capability are baked in; you supply only [`ClientOptions`](../client)).
118
+ - `Attributes` / `WriteAttributes` — the per-type attribute maps (read vs create/update).
119
+ - Per-type `…Attributes` / `…CreateAttributes` / `…UpdateAttributes` interfaces, enum types
120
+ (e.g. `AlbumStatus`), and action input/output types.
121
+
122
+ With `--schemas`, the sibling module exports `schemas` — the per-type JSON Schema 2020-12 map.
123
+
124
+ ```ts
125
+ import { createClient, type ResourceMap, resourceMap } from './api/music.gen'
126
+ import { schemas } from './api/music.schemas.gen'
127
+
128
+ const client = createClient({ baseUrl: 'https://music.example' })
129
+ ```
130
+
131
+ ### Verb / relation collisions
132
+
133
+ If a type has a relation named like a reserved verb on the resource handle (`get`, `update`,
134
+ `delete`, `rel`, `actions`, …), the codegen emits a **build-time warning** and that relation routes
135
+ through `.rel('name')` in the fluent client; the common case stays clean.
136
+
137
+ ## Programmatic API
138
+
139
+ The same generation, callable from a script (`japi.config.ts`, a build step, etc.):
140
+
141
+ ```ts
142
+ import { generate, type CodegenConfig } from '@haddowg/json-api-codegen'
143
+
144
+ const config: CodegenConfig = {
145
+ input: 'https://music.example/docs.json',
146
+ output: 'src/api/music.gen.ts',
147
+ schemas: 'https://music.example/schemas.json', // optional
148
+ server: 'default', // optional, metadata-only
149
+ }
150
+
151
+ await generate(config) // writes the output file(s); returns the client source string
152
+ ```
153
+
154
+ The drift gate is also programmatic — `check(config)` regenerates in memory and returns
155
+ `{ ok, artifacts }` (per-artifact `{ path, upToDate }`) without writing:
156
+
157
+ ```ts
158
+ import { check } from '@haddowg/json-api-codegen'
159
+
160
+ const result = await check({ ...config, check: true })
161
+ if (!result.ok) process.exit(1)
162
+ ```
163
+
164
+ Lower-level building blocks are also exported for advanced use: `readDocument` / `readSchemas`,
165
+ `buildDescriptor`, `emit`, `emitSchemas`, `detectVerbCollisions`, `schemasOutputPath`, and the
166
+ provenance helpers `deriveProvenance` / `provenanceLines` / `hashJson`.
167
+
168
+ ## License
169
+
170
+ MIT
package/dist/cli.d.mts ADDED
@@ -0,0 +1,14 @@
1
+ import { r as CodegenConfig } from "./index-C0becRSc.mjs";
2
+
3
+ //#region src/cli.d.ts
4
+ declare const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]";
5
+ /**
6
+ * Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the
7
+ * generator itself; this stub establishes the bin entry and the shape of invocation.
8
+ * `--check` is a boolean flag (drift gate) with no value.
9
+ */
10
+ declare function parseArgs(argv: string[]): Partial<CodegenConfig>;
11
+ declare function run(argv: string[]): Promise<number>;
12
+ //#endregion
13
+ export { USAGE, parseArgs, run };
14
+ //# sourceMappingURL=cli.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";;;cAIa,KAAA;AAAb;;;;AAAkB;AAAlB,iBAQgB,SAAA,CAAU,IAAA,aAAiB,OAAO,CAAC,aAAA;AAAA,iBA6B7B,GAAA,CAAI,IAAA,aAAiB,OAAO"}
package/dist/cli.mjs ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import { n as generate, t as check } from "./src-B24cYyjM.mjs";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/cli.ts
5
+ const USAGE = "Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]";
6
+ /**
7
+ * Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the
8
+ * generator itself; this stub establishes the bin entry and the shape of invocation.
9
+ * `--check` is a boolean flag (drift gate) with no value.
10
+ */
11
+ function parseArgs(argv) {
12
+ const config = {};
13
+ for (let i = 0; i < argv.length; i++) {
14
+ const arg = argv[i];
15
+ if (arg === "--check") {
16
+ config.check = true;
17
+ continue;
18
+ }
19
+ const next = argv[i + 1];
20
+ if (next === void 0) continue;
21
+ if (arg === "--input") {
22
+ config.input = next;
23
+ i++;
24
+ } else if (arg === "--output") {
25
+ config.output = next;
26
+ i++;
27
+ } else if (arg === "--server") {
28
+ config.server = next;
29
+ i++;
30
+ } else if (arg === "--schemas") {
31
+ config.schemas = next;
32
+ i++;
33
+ }
34
+ }
35
+ return config;
36
+ }
37
+ async function run(argv) {
38
+ const config = parseArgs(argv);
39
+ if (!config.input || !config.output) {
40
+ console.error(USAGE);
41
+ return 1;
42
+ }
43
+ if (config.check) {
44
+ const result = await check(config);
45
+ for (const { path, upToDate } of result.artifacts) console.error(`${upToDate ? "ok " : "DRIFT"} ${path}`);
46
+ if (!result.ok) {
47
+ console.error("Generated client is out of date with the source spec. Regenerate (drop --check) and commit the result.");
48
+ return 1;
49
+ }
50
+ return 0;
51
+ }
52
+ await generate(config);
53
+ return 0;
54
+ }
55
+ async function main() {
56
+ process.exitCode = await run(process.argv.slice(2));
57
+ }
58
+ const entry = process.argv[1];
59
+ if (entry !== void 0 && import.meta.url === pathToFileURL(entry).href) main().catch((error) => {
60
+ console.error(error instanceof Error ? error.message : error);
61
+ process.exitCode = 1;
62
+ });
63
+ //#endregion
64
+ export { USAGE, parseArgs, run };
65
+
66
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { pathToFileURL } from 'node:url'\nimport { check, generate, type CodegenConfig } from './index'\n\nexport const USAGE =\n 'Usage: json-api-codegen --input <url|file> --output <file> [--server <name>] [--schemas <url|file>] [--check]'\n\n/**\n * Thin CLI wrapper. Real arg parsing + `japi.config.ts` discovery land with the\n * generator itself; this stub establishes the bin entry and the shape of invocation.\n * `--check` is a boolean flag (drift gate) with no value.\n */\nexport function parseArgs(argv: string[]): Partial<CodegenConfig> {\n const config: Partial<CodegenConfig> = {}\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]\n if (arg === '--check') {\n config.check = true\n continue\n }\n const next = argv[i + 1]\n if (next === undefined) {\n continue\n }\n if (arg === '--input') {\n config.input = next\n i++\n } else if (arg === '--output') {\n config.output = next\n i++\n } else if (arg === '--server') {\n config.server = next\n i++\n } else if (arg === '--schemas') {\n config.schemas = next\n i++\n }\n }\n return config\n}\n\nexport async function run(argv: string[]): Promise<number> {\n const config = parseArgs(argv)\n if (!config.input || !config.output) {\n console.error(USAGE)\n return 1\n }\n if (config.check) {\n const result = await check(config as CodegenConfig)\n for (const { path, upToDate } of result.artifacts) {\n console.error(`${upToDate ? 'ok ' : 'DRIFT'} ${path}`)\n }\n if (!result.ok) {\n console.error(\n 'Generated client is out of date with the source spec. Regenerate (drop --check) and commit the result.',\n )\n return 1\n }\n return 0\n }\n await generate(config as CodegenConfig)\n return 0\n}\n\nasync function main(): Promise<void> {\n process.exitCode = await run(process.argv.slice(2))\n}\n\n// Only run when invoked as the bin, not when imported (e.g. by tests).\nconst entry = process.argv[1]\nif (entry !== undefined && import.meta.url === pathToFileURL(entry).href) {\n main().catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error)\n process.exitCode = 1\n })\n}\n"],"mappings":";;;;AAIA,MAAa,QACX;;;;;;AAOF,SAAgB,UAAU,MAAwC;CAChE,MAAM,SAAiC,CAAC;CACxC,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EACjB,IAAI,QAAQ,WAAW;GACrB,OAAO,QAAQ;GACf;EACF;EACA,MAAM,OAAO,KAAK,IAAI;EACtB,IAAI,SAAS,KAAA,GACX;EAEF,IAAI,QAAQ,WAAW;GACrB,OAAO,QAAQ;GACf;EACF,OAAO,IAAI,QAAQ,YAAY;GAC7B,OAAO,SAAS;GAChB;EACF,OAAO,IAAI,QAAQ,YAAY;GAC7B,OAAO,SAAS;GAChB;EACF,OAAO,IAAI,QAAQ,aAAa;GAC9B,OAAO,UAAU;GACjB;EACF;CACF;CACA,OAAO;AACT;AAEA,eAAsB,IAAI,MAAiC;CACzD,MAAM,SAAS,UAAU,IAAI;CAC7B,IAAI,CAAC,OAAO,SAAS,CAAC,OAAO,QAAQ;EACnC,QAAQ,MAAM,KAAK;EACnB,OAAO;CACT;CACA,IAAI,OAAO,OAAO;EAChB,MAAM,SAAS,MAAM,MAAM,MAAuB;EAClD,KAAK,MAAM,EAAE,MAAM,cAAc,OAAO,WACtC,QAAQ,MAAM,GAAG,WAAW,SAAS,QAAQ,GAAG,MAAM;EAExD,IAAI,CAAC,OAAO,IAAI;GACd,QAAQ,MACN,wGACF;GACA,OAAO;EACT;EACA,OAAO;CACT;CACA,MAAM,SAAS,MAAuB;CACtC,OAAO;AACT;AAEA,eAAe,OAAsB;CACnC,QAAQ,WAAW,MAAM,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC;AACpD;AAGA,MAAM,QAAQ,QAAQ,KAAK;AAC3B,IAAI,UAAU,KAAA,KAAa,OAAO,KAAK,QAAQ,cAAc,KAAK,CAAC,CAAC,MAClE,KAAK,CAAC,CAAC,OAAO,UAAmB;CAC/B,QAAQ,MAAM,iBAAiB,QAAQ,MAAM,UAAU,KAAK;CAC5D,QAAQ,WAAW;AACrB,CAAC"}