apiwork 0.0.0 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,311 @@
1
+ # apiwork-js
2
+
3
+ JavaScript toolkit for [Apiwork](https://apiwork.dev).
4
+
5
+ Provides a parser, TypeScript types, and code generators for TypeScript, [Zod](https://zod.dev), and [Sorbus](https://sorbus.dev).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install apiwork
11
+ # or
12
+ pnpm add apiwork
13
+ ```
14
+
15
+ ## Schema
16
+
17
+ The [Apiwork schema](https://apiwork.dev/guide/exports/apiwork) is a portable JSON description of your API. Parse it and work with typed, camelCased objects:
18
+
19
+ ```ts
20
+ import { parse } from 'apiwork';
21
+ import type { Schema, Resource, Action, Param } from 'apiwork';
22
+
23
+ // From raw JSON data
24
+ const schema = parse(data);
25
+
26
+ // From a URL
27
+ const schema = await parse.url('http://localhost:3000/api/v1/.apiwork');
28
+
29
+ // From a file
30
+ const schema = await parse.file('./apiwork.json');
31
+
32
+ for (const resource of schema.resources) {
33
+ console.log(resource.identifier);
34
+ }
35
+ ```
36
+
37
+ > `parse.url()` and `parse.file()` throw `ParseError` on failure, with the underlying error preserved on `error.cause`.
38
+
39
+ ## CLI
40
+
41
+ ```bash
42
+ apiwork typescript ./apiwork.json --outdir src/api/sorbus
43
+ apiwork zod ./apiwork.json --outdir src/api/sorbus
44
+ apiwork sorbus ./apiwork.json --outdir src/api/sorbus
45
+ ```
46
+
47
+ The source can be a local file or a URL:
48
+
49
+ ```bash
50
+ apiwork sorbus http://localhost:3000/api/v1/.apiwork --outdir src/api/sorbus
51
+ ```
52
+
53
+ ## Generators
54
+
55
+ ### TypeScript
56
+
57
+ Generates pure TypeScript types from representations and enums. Use this if you only need types without runtime validation.
58
+
59
+ ### Zod
60
+
61
+ Generates Zod validation schemas alongside TypeScript types. Use this if you need runtime validation without a typed HTTP client.
62
+
63
+ ### Sorbus
64
+
65
+ Generates a typed [Sorbus](https://sorbus.dev) client with operations, schemas, and endpoints. Includes everything from the TypeScript and Zod generators.
66
+
67
+ ## Options
68
+
69
+ All three generators share common options (`fileCase`, `importExtension`, `transformIdentifier`) plus their own `version` for the output they produce:
70
+
71
+ ```ts
72
+ import { generate } from 'apiwork/sorbus';
73
+
74
+ const files = generate(schema, {
75
+ version: 1, // Sorbus major version (default: 1)
76
+ zod: { version: 4 }, // Zod inline-schemas (default: 4)
77
+ typescript: { version: 5 }, // TypeScript types (default: 5)
78
+ fileCase: 'kebab',
79
+ importExtension: '.js',
80
+ transformIdentifier: (identifier, source) => {
81
+ if (source === 'api') return `Api${identifier}`;
82
+ if (source === 'client' && identifier === 'Client') return 'MyClient';
83
+ return identifier;
84
+ },
85
+ });
86
+ ```
87
+
88
+ ### `version`
89
+
90
+ Each generator targets a specific major version of its output:
91
+
92
+ | Generator | `version` | Default |
93
+ |---|---|---|
94
+ | `apiwork/typescript` | TypeScript major | `5` |
95
+ | `apiwork/zod` | Zod major | `4` |
96
+ | `apiwork/sorbus` | Sorbus major | `1` |
97
+
98
+ Generators that produce output for multiple libraries take nested configuration:
99
+
100
+ ```ts
101
+ // apiwork/typescript — just TS version
102
+ generate(schema, { version: 5 });
103
+
104
+ // apiwork/zod — Zod version + TS version for exported types
105
+ generate(schema, {
106
+ version: 4,
107
+ typescript: { version: 5 },
108
+ });
109
+
110
+ // apiwork/sorbus — Sorbus version + Zod + TS
111
+ generate(schema, {
112
+ version: 1,
113
+ zod: { version: 4 },
114
+ typescript: { version: 5 },
115
+ });
116
+ ```
117
+
118
+ ### `fileCase`
119
+
120
+ The file name case format. Accepts `'kebab'` (default), `'camel'`, `'pascal'`, or `'snake'`.
121
+
122
+ | Value | Example |
123
+ |---|---|
124
+ | `'kebab'` | `invoice-line-item.ts` |
125
+ | `'camel'` | `invoiceLineItem.ts` |
126
+ | `'pascal'` | `InvoiceLineItem.ts` |
127
+ | `'snake'` | `invoice_line_item.ts` |
128
+
129
+ ### `importExtension`
130
+
131
+ The extension appended to relative import paths. Accepts `''` (default), `'.js'`, or `'.ts'`. Use `'.js'` for Node ESM or Deno.
132
+
133
+ ```ts
134
+ // importExtension: '' → from '../api'
135
+ // importExtension: '.js' → from '../api.js'
136
+ ```
137
+
138
+ ### `transformIdentifier`
139
+
140
+ A callback invoked for every identifier the generator emits. Return the name unchanged to keep the default, or return a new name to rename it.
141
+
142
+ ```ts
143
+ transformIdentifier?: (identifier: string, source: IdentifierSource) => string;
144
+
145
+ type IdentifierSource = 'domain' | 'api' | 'endpoint' | 'client';
146
+ ```
147
+
148
+ | `source` | Covers |
149
+ |---|---|
150
+ | `'domain'` | Types and schemas in `domains/*` (scoped domain types). |
151
+ | `'api'` | Types and schemas in `api.ts` (global domain types — highest collision risk). |
152
+ | `'endpoint'` | Types and schemas in `endpoints/*` (request, response, definition). |
153
+ | `'client'` | Sorbus-specific symbols: `Client`, `createClient`, `contract`. |
154
+
155
+ Example — prefix all global types and rename the Sorbus client:
156
+
157
+ ```ts
158
+ transformIdentifier: (identifier, source) => {
159
+ if (source === 'api') return `Api${identifier}`;
160
+ if (source === 'client' && identifier === 'Client') return 'SkiftleClient';
161
+ if (source === 'client' && identifier === 'createClient') return 'createSkiftleClient';
162
+ return identifier;
163
+ }
164
+ ```
165
+
166
+ ## Generated output
167
+
168
+ Given this Apiwork schema:
169
+
170
+ ```json
171
+ {
172
+ "enums": [
173
+ { "name": "invoice_status", "values": ["draft", "sent", "paid"] }
174
+ ],
175
+ "types": [
176
+ {
177
+ "name": "invoice",
178
+ "type": "object",
179
+ "shape": [
180
+ { "name": "id", "type": "string" },
181
+ { "name": "number", "type": "string" },
182
+ { "name": "status", "type": "string", "enum": "invoice_status" },
183
+ { "name": "issuedOn", "type": "date", "nullable": true },
184
+ { "name": "createdAt", "type": "datetime" },
185
+ { "name": "updatedAt", "type": "datetime" }
186
+ ]
187
+ }
188
+ ],
189
+ "resources": [
190
+ {
191
+ "identifier": "invoices",
192
+ "path": "invoices",
193
+ "actions": [
194
+ { "name": "invoices.index", "method": "get", "path": "/invoices" },
195
+ { "name": "invoices.show", "method": "get", "path": "/invoices/:id" },
196
+ { "name": "invoices.create", "method": "post", "path": "/invoices" },
197
+ { "name": "invoices.destroy", "method": "delete", "path": "/invoices/:id" }
198
+ ]
199
+ }
200
+ ]
201
+ }
202
+ ```
203
+
204
+ `apiwork sorbus` generates:
205
+
206
+ ```
207
+ api.ts
208
+ domains/
209
+ index.ts
210
+ endpoints/
211
+ invoices.ts
212
+ index.ts
213
+ contract.ts
214
+ client.ts
215
+ ```
216
+
217
+ See the [full generated output](https://apiwork.dev/examples/representations#codegen) for a complete example.
218
+
219
+ ## Frontend integration
220
+
221
+ Add a script to generate the client from your API:
222
+
223
+ ```json
224
+ {
225
+ "scripts": {
226
+ "sorbus": "apiwork sorbus http://localhost:3000/api/v1/.apiwork --outdir src/api/sorbus"
227
+ }
228
+ }
229
+ ```
230
+
231
+ ```bash
232
+ pnpm sorbus
233
+ ```
234
+
235
+ Optionally, format the generated output:
236
+
237
+ ```json
238
+ {
239
+ "scripts": {
240
+ "sorbus": "apiwork sorbus http://localhost:3000/api/v1/.apiwork --outdir src/api/sorbus && biome check --write src/api/sorbus"
241
+ }
242
+ }
243
+ ```
244
+
245
+ Using Prettier or Oxfmt? Replace `biome check --write` with `prettier --write` or `oxfmt`.
246
+
247
+ Use the generated client:
248
+
249
+ ```ts
250
+ import { createClient } from './sorbus/client';
251
+
252
+ const api = createClient('http://localhost:3000/api/v1');
253
+
254
+ const { invoices } = await api.invoices.index();
255
+
256
+ const { invoice } = await api.invoices.show({ id: '1' });
257
+
258
+ const result = await api.invoices.create(
259
+ { invoice: { number: 'INV-001', customerId: '1' } },
260
+ { catch: [422] },
261
+ );
262
+
263
+ if (!result.ok) {
264
+ console.log(result.data);
265
+ }
266
+ ```
267
+
268
+ ## API Reference
269
+
270
+ ### `apiwork`
271
+
272
+ Parser and schema types.
273
+
274
+ **Values:**
275
+ - `parse(data)` — parse raw JSON data into a `Schema`
276
+ - `parse.url(url)` — fetch and parse from a URL
277
+ - `parse.file(path)` — read and parse from a file
278
+ - `ParseError` — thrown on parse failures, with `cause` on `error.cause`
279
+
280
+ **Types:**
281
+
282
+ `Action`, `ActionMethod`, `ActionRequest`, `ActionResponse`, `ArrayParam`, `BinaryParam`, `BooleanParam`, `DateParam`, `DatetimeParam`, `DecimalParam`, `Enum`, `ErrorCode`, `FileCase`, `GenerateOptions`, `IdentifierSource`, `Info`, `InfoContact`, `InfoLicense`, `InfoServer`, `IntegerParam`, `LiteralParam`, `NumberParam`, `ObjectParam`, `ObjectType`, `Param`, `RecordParam`, `ReferenceParam`, `Resource`, `Schema`, `StringParam`, `TimeParam`, `Type`, `UnionParam`, `UnionType`, `UnknownParam`, `UuidParam`
283
+
284
+ ### `apiwork/typescript`
285
+
286
+ TypeScript code generator — produces pure TypeScript types from the schema.
287
+
288
+ - `generate(schema, options?)` — returns a `Map<string, string>` of file paths to file contents
289
+ - `TypescriptGenerateOptions` — options type
290
+
291
+ ### `apiwork/zod`
292
+
293
+ Zod code generator — produces Zod schemas plus TypeScript types.
294
+
295
+ - `generate(schema, options?)` — returns a `Map<string, string>` of file paths to file contents
296
+ - `ZodGenerateOptions` — options type
297
+
298
+ ### `apiwork/sorbus`
299
+
300
+ Sorbus code generator — produces a typed Sorbus client, contract, Zod schemas, and TypeScript types.
301
+
302
+ - `generate(schema, options?)` — returns a `Map<string, string>` of file paths to file contents
303
+ - `SorbusGenerateOptions` — options type
304
+
305
+ ---
306
+
307
+ All exported symbols carry full TSDoc. Hover over any import in your IDE to see descriptions, parameter docs, and examples.
308
+
309
+ ## License
310
+
311
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ parse
4
+ } from "../chunk-7NDS6XB6.js";
5
+ import {
6
+ generate
7
+ } from "../chunk-ZP6ZWEPI.js";
8
+ import {
9
+ generate as generate2
10
+ } from "../chunk-4AJOYIJC.js";
11
+ import {
12
+ generate as generate3
13
+ } from "../chunk-NQPUJJ3O.js";
14
+ import {
15
+ writeFiles
16
+ } from "../chunk-QGKZLUZR.js";
17
+ import "../chunk-MWFEILAW.js";
18
+
19
+ // src/cli.ts
20
+ import { parseArgs } from "util";
21
+ var generators = {
22
+ sorbus: generate,
23
+ typescript: generate2,
24
+ zod: generate3
25
+ };
26
+ async function run(args) {
27
+ const { positionals, values } = parseArgs({
28
+ allowPositionals: true,
29
+ args,
30
+ options: {
31
+ outdir: {
32
+ short: "o",
33
+ type: "string"
34
+ }
35
+ }
36
+ });
37
+ const [command, url] = positionals;
38
+ if (!command || !url) {
39
+ console.error("Usage: apiwork <generator> <url> --outdir <dir>");
40
+ console.error("Generators: typescript, zod, sorbus");
41
+ process.exit(1);
42
+ }
43
+ const generator = generators[command];
44
+ if (!generator) {
45
+ console.error(`Unknown generator: ${command}`);
46
+ console.error(`Available: ${Object.keys(generators).join(", ")}`);
47
+ process.exit(1);
48
+ }
49
+ const outdir = values.outdir ?? ".";
50
+ const schema = url.startsWith("http://") || url.startsWith("https://") ? await parse.url(url) : await parse.file(url);
51
+ const files = generator(schema);
52
+ await writeFiles(outdir, files);
53
+ for (const filename of files.keys()) {
54
+ console.log(` ${filename}`);
55
+ }
56
+ }
57
+
58
+ // src/bin/apiwork.ts
59
+ run(process.argv.slice(2));
@@ -0,0 +1,37 @@
1
+ import {
2
+ buildEndpoints,
3
+ buildSchemas,
4
+ buildScopeIndex,
5
+ resolveGenerateOptions
6
+ } from "./chunk-QGKZLUZR.js";
7
+
8
+ // src/typescript/generate.ts
9
+ function generate(schema, options = {}) {
10
+ const resolvedOptions = resolveGenerateOptions(options);
11
+ const scopeIndex = buildScopeIndex(schema);
12
+ const files = /* @__PURE__ */ new Map();
13
+ for (const [filename, content] of buildSchemas(
14
+ schema,
15
+ { zod: false },
16
+ resolvedOptions
17
+ )) {
18
+ files.set(filename, content);
19
+ }
20
+ for (const [filename, content] of buildEndpoints(
21
+ schema,
22
+ scopeIndex,
23
+ {
24
+ endpoints: false,
25
+ reexportChildren: false,
26
+ zod: false
27
+ },
28
+ resolvedOptions
29
+ )) {
30
+ files.set(filename, content);
31
+ }
32
+ return files;
33
+ }
34
+
35
+ export {
36
+ generate
37
+ };
@@ -0,0 +1,57 @@
1
+ import {
2
+ camelCaseKeys
3
+ } from "./chunk-MWFEILAW.js";
4
+
5
+ // src/parse.ts
6
+ import { readFile } from "fs/promises";
7
+ var ParseError = class extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = "ParseError";
11
+ }
12
+ };
13
+ function parseData(data) {
14
+ return camelCaseKeys(data);
15
+ }
16
+ async function parseUrl(url) {
17
+ let response;
18
+ try {
19
+ response = await fetch(url);
20
+ } catch (cause) {
21
+ throw new ParseError(`Failed to fetch schema from ${url}`, { cause });
22
+ }
23
+ if (!response.ok) {
24
+ throw new ParseError(
25
+ `Failed to fetch schema from ${url}: ${response.status} ${response.statusText}`
26
+ );
27
+ }
28
+ let data;
29
+ try {
30
+ data = await response.json();
31
+ } catch (cause) {
32
+ throw new ParseError(`Schema at ${url} is not valid JSON`, { cause });
33
+ }
34
+ return parseData(data);
35
+ }
36
+ async function parseFile(path) {
37
+ let content;
38
+ try {
39
+ content = await readFile(path, "utf-8");
40
+ } catch (cause) {
41
+ throw new ParseError(`Failed to read schema from ${path}`, { cause });
42
+ }
43
+ try {
44
+ return parseData(JSON.parse(content));
45
+ } catch (cause) {
46
+ throw new ParseError(`Schema at ${path} is not valid JSON`, { cause });
47
+ }
48
+ }
49
+ var parse = Object.assign(parseData, {
50
+ file: parseFile,
51
+ url: parseUrl
52
+ });
53
+
54
+ export {
55
+ ParseError,
56
+ parse
57
+ };
@@ -0,0 +1,28 @@
1
+ // src/utils/camel-case.ts
2
+ function camelCase(name) {
3
+ const leading = /^_+/.exec(name)?.[0] ?? "";
4
+ return leading + name.slice(leading.length).replace(
5
+ /[-_]([a-z])/g,
6
+ (_match, character) => character.toUpperCase()
7
+ );
8
+ }
9
+
10
+ // src/utils/camel-case-keys.ts
11
+ function camelCaseKeys(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map(camelCaseKeys);
14
+ }
15
+ if (value !== null && typeof value === "object") {
16
+ const result = {};
17
+ for (const [key, entryValue] of Object.entries(value)) {
18
+ result[camelCase(key)] = camelCaseKeys(entryValue);
19
+ }
20
+ return result;
21
+ }
22
+ return value;
23
+ }
24
+
25
+ export {
26
+ camelCase,
27
+ camelCaseKeys
28
+ };
@@ -0,0 +1,37 @@
1
+ import {
2
+ buildEndpoints,
3
+ buildSchemas,
4
+ buildScopeIndex,
5
+ resolveGenerateOptions
6
+ } from "./chunk-QGKZLUZR.js";
7
+
8
+ // src/zod/generate.ts
9
+ function generate(schema, options = {}) {
10
+ const resolvedOptions = resolveGenerateOptions(options);
11
+ const scopeIndex = buildScopeIndex(schema);
12
+ const files = /* @__PURE__ */ new Map();
13
+ for (const [filename, content] of buildSchemas(
14
+ schema,
15
+ { zod: true },
16
+ resolvedOptions
17
+ )) {
18
+ files.set(filename, content);
19
+ }
20
+ for (const [filename, content] of buildEndpoints(
21
+ schema,
22
+ scopeIndex,
23
+ {
24
+ endpoints: false,
25
+ reexportChildren: false,
26
+ zod: true
27
+ },
28
+ resolvedOptions
29
+ )) {
30
+ files.set(filename, content);
31
+ }
32
+ return files;
33
+ }
34
+
35
+ export {
36
+ generate
37
+ };