@flattype/models 1.7.0 → 1.8.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/README.md CHANGED
@@ -21,6 +21,7 @@
21
21
  <a href="https://www.npmjs.com/package/@flattype/angular"><img src="https://img.shields.io/npm/v/@flattype/angular?label=%40flattype%2Fangular&color=8b6cff" alt="npm @flattype/angular"></a>
22
22
  <a href="https://www.npmjs.com/package/@flattype/svelte"><img src="https://img.shields.io/npm/v/@flattype/svelte?label=%40flattype%2Fsvelte&color=8b6cff" alt="npm @flattype/svelte"></a>
23
23
  <a href="https://www.npmjs.com/package/@flattype/ai"><img src="https://img.shields.io/npm/v/@flattype/ai?label=%40flattype%2Fai&color=8b6cff" alt="npm @flattype/ai"></a>
24
+ <a href="https://www.npmjs.com/package/@flattype/cli"><img src="https://img.shields.io/npm/v/@flattype/cli?label=%40flattype%2Fcli&color=8b6cff" alt="npm @flattype/cli"></a>
24
25
  <a href="https://github.com/karammm/flattype/actions/workflows/ci.yml"><img src="https://github.com/karammm/flattype/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
25
26
  <img src="https://img.shields.io/badge/core-3.7_KB_min%2Bgz-2fd4b2" alt="3.7 KB min+gz">
26
27
  <img src="https://img.shields.io/badge/dependencies-0-2fd4b2" alt="No third-party dependencies">
@@ -37,6 +38,7 @@
37
38
  <a href="#angular"><b>Angular</b></a> ·
38
39
  <a href="#svelte-and-solid"><b>Svelte &amp; Solid</b></a> ·
39
40
  <a href="#claude-and-llms"><b>Claude &amp; LLMs</b></a> ·
41
+ <a href="#cli"><b>CLI</b></a> ·
40
42
  <a href="#benchmarks"><b>Benchmarks</b></a> ·
41
43
  <a href="https://github.com/karammm/flattype/blob/main/CHANGELOG.md"><b>Changelog</b></a> ·
42
44
  <a href="https://github.com/karammm/flattype/blob/main/ROADMAP.md"><b>Roadmap</b></a>
@@ -45,10 +47,10 @@
45
47
  <p align="center">
46
48
  <a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">
47
49
  <img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/flattype-intro.gif" width="820"
48
- alt="FlatType.js in 77 seconds: an API response encoded to Flat Notation, how tables and dot paths keep it small, typed models mapped from snake_case JSON, an Express server answering in Flat Notation or JSON, React, Vue, Angular and Svelte in real apps, and the size comparison with JSON and TOON">
50
+ alt="FlatType.js in 84 seconds: an API response encoded to Flat Notation, how tables and dot paths keep it small, typed models mapped from snake_case JSON, an Express server answering in Flat Notation or JSON, React, Vue, Angular and Svelte in real apps, the CLI generating models from OpenAPI, and the size comparison with JSON and TOON">
49
51
  </a>
50
52
  <br>
51
- <sub>▶ <a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">Watch the 77-second intro in full quality</a>. Every result shown is real output, generated by <a href="https://github.com/karammm/flattype/blob/main/scripts/video/record.mjs"><code>scripts/video/record.mjs</code></a>.</sub>
53
+ <sub>▶ <a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">Watch the 84-second intro in full quality</a>. Every result shown is real output, generated by <a href="https://github.com/karammm/flattype/blob/main/scripts/video/record.mjs"><code>scripts/video/record.mjs</code></a>.</sub>
52
54
  </p>
53
55
 
54
56
  ---
@@ -143,9 +145,10 @@ const [users] = createFlatResource(() => "/api/users", { model: User, list: true
143
145
  | [`@flattype/angular`](https://github.com/karammm/flattype/blob/main/packages/angular) | `flatInterceptor` for HttpClient, the signal-based `flatResource` / `flatStream`, the `flatModel` pipe | +1.8 KB |
144
146
  | [`@flattype/svelte`](https://github.com/karammm/flattype/blob/main/packages/svelte) | Svelte stores (`flatStore`, `flatStreamStore`, `modelStore`), and `/solid` with `createFlatResource` / `createFlatStream` | +1.1 KB / +0.9 KB |
145
147
  | [`@flattype/ai`](https://github.com/karammm/flattype/blob/main/packages/ai) | `toPrompt`, `fromModelOutput`, `countTokens`, and MCP / Claude tool results | +1.3 KB |
148
+ | [`@flattype/cli`](https://github.com/karammm/flattype/blob/main/packages/cli) | The `flattype` command: `encode`, `decode`, `stats`, and `gen` (models from samples or OpenAPI) | Node tool |
146
149
  | [`@flattype/shared`](https://github.com/karammm/flattype/blob/main/packages/shared) | Shared TypeScript types | types only |
147
150
 
148
- A CLI with code generation is planned. See the [roadmap](https://github.com/karammm/flattype/blob/main/ROADMAP.md).
151
+ Docs, a playground and published benchmarks are next on the [roadmap](https://github.com/karammm/flattype/blob/main/ROADMAP.md). See the [roadmap](https://github.com/karammm/flattype/blob/main/ROADMAP.md).
149
152
 
150
153
  ## The format
151
154
 
@@ -493,6 +496,58 @@ messages.push({ role: "user", content: [claudeToolResult(toolUse.id, await runTo
493
496
 
494
497
  See [`examples/ai-claude`](https://github.com/karammm/flattype/blob/main/examples/ai-claude) for a token report, a full Claude round trip, and a runnable MCP server (`claude mcp add flattype-orders -- node mcp-server.mjs`).
495
498
 
499
+ ## CLI
500
+
501
+ ```bash
502
+ npx @flattype/cli --help # or: npm install -D @flattype/cli, then use `flattype …`
503
+ ```
504
+
505
+ ```bash
506
+ # Convert, in pipes or files
507
+ curl -s https://api.example.com/users | npx @flattype/cli encode
508
+ npx @flattype/cli decode users.flat -o users.json
509
+
510
+ # Compare sizes
511
+ npx @flattype/cli stats users.json
512
+ # bytes gzip
513
+ # JSON 169545 12890
514
+ # Flat 93606 12568
515
+ # saved 45% 2%
516
+
517
+ # Generate typed models from an OpenAPI spec (3.0, 3.1 or Swagger 2; JSON or YAML)…
518
+ npx @flattype/cli gen --openapi openapi.yaml -o src/models.ts
519
+
520
+ # …or from a real response
521
+ curl -s https://api.example.com/users > users.json && npx @flattype/cli gen --sample users.json --name UsersPage
522
+ ```
523
+
524
+ For an OpenAPI schema like
525
+
526
+ ```yaml
527
+ Customer:
528
+ type: object
529
+ required: [id, full_name, created_at]
530
+ properties:
531
+ id: { type: integer }
532
+ full_name: { type: string }
533
+ created_at: { type: string, format: date-time }
534
+ address: { $ref: "#/components/schemas/Address" }
535
+ ```
536
+
537
+ `flattype gen --openapi` writes:
538
+
539
+ ```ts
540
+ export const Customer = defineModel({
541
+ id: "integer",
542
+ fullName: "string",
543
+ createdAt: "date",
544
+ address: { type: Address, optional: true },
545
+ }, { keys: "snake" });
546
+ export type Customer = Infer<typeof Customer>;
547
+ ```
548
+
549
+ It handles `$ref` between schemas, `allOf`, `required` (other fields become optional), `nullable` and `type: [..., "null"]`, `date` and `date-time` formats, enums and inline objects. A recursive reference becomes `"any"`, and the CLI warns you. Exit code 1 means bad input, 2 means a usage error. From code, use `fromOpenAPI(spec)` together with `toSource(models)`.
550
+
496
551
  ## Benchmarks
497
552
 
498
553
  `pnpm compare` on the fixtures in [`fixtures/`](https://github.com/karammm/flattype/blob/main/fixtures/index.mjs) (Node 24, Apple Silicon):
package/dist/index.cjs CHANGED
@@ -321,8 +321,10 @@ function singular(s) {
321
321
  if (/[^s]s$/.test(s)) return s.slice(0, -1);
322
322
  return s;
323
323
  }
324
- function toSource(model, name) {
324
+ function toSource(modelOrModels, name) {
325
+ const roots = isModel(modelOrModels) ? [[name ?? "Model", modelOrModels]] : Object.entries(modelOrModels);
325
326
  const names = /* @__PURE__ */ new Map();
327
+ const emitted = /* @__PURE__ */ new Set();
326
328
  const used = /* @__PURE__ */ new Set();
327
329
  const blocks = [];
328
330
  const unique = (base) => {
@@ -332,10 +334,13 @@ function toSource(model, name) {
332
334
  return n;
333
335
  };
334
336
  const emit = (m, want) => {
335
- const known = names.get(m);
336
- if (known) return known;
337
- const id = unique(want);
338
- names.set(m, id);
337
+ let id = names.get(m);
338
+ if (id && emitted.has(m)) return id;
339
+ if (!id) {
340
+ id = unique(want);
341
+ names.set(m, id);
342
+ }
343
+ emitted.add(m);
339
344
  const lines = [];
340
345
  for (const [field, spec] of Object.entries(m.shape)) {
341
346
  const key = IDENT.test(field) ? field : JSON.stringify(field);
@@ -363,7 +368,8 @@ export type ${id} = Infer<typeof ${id}>;`);
363
368
  if (o.from !== void 0) parts.push(`from: ${JSON.stringify(o.from)}`);
364
369
  return `{ ${parts.join(", ")} }`;
365
370
  };
366
- emit(model, pascal(name));
371
+ for (const [n, m] of roots) if (!names.has(m)) names.set(m, unique(pascal(n)));
372
+ for (const [, m] of roots) emit(m, names.get(m));
367
373
  return `import { defineModel, type Infer } from "@flattype/models";
368
374
 
369
375
  ${blocks.join("\n\n")}
package/dist/index.d.cts CHANGED
@@ -109,8 +109,12 @@ declare function defineModel<const S extends Shape>(shape: S, options?: ModelOpt
109
109
  declare function infer(sample: unknown, options?: {
110
110
  keys?: KeyStyle;
111
111
  }): Model<Record<string, unknown>>;
112
- /** Prints TypeScript source for a model and the models nested in it. */
112
+ /**
113
+ * Prints TypeScript source for a model and the models nested in it, or for several named models at once
114
+ * (`toSource({ User, Order })`), where a model used by another is referenced by its name, not repeated.
115
+ */
113
116
  declare function toSource(model: Model<unknown>, name: string): string;
117
+ declare function toSource(models: Record<string, Model<unknown>>): string;
114
118
 
115
119
  /** `createdAt` → `created_at`, `userID` → `user_id`. */
116
120
  declare function toSnake(s: string): string;
package/dist/index.d.ts CHANGED
@@ -109,8 +109,12 @@ declare function defineModel<const S extends Shape>(shape: S, options?: ModelOpt
109
109
  declare function infer(sample: unknown, options?: {
110
110
  keys?: KeyStyle;
111
111
  }): Model<Record<string, unknown>>;
112
- /** Prints TypeScript source for a model and the models nested in it. */
112
+ /**
113
+ * Prints TypeScript source for a model and the models nested in it, or for several named models at once
114
+ * (`toSource({ User, Order })`), where a model used by another is referenced by its name, not repeated.
115
+ */
113
116
  declare function toSource(model: Model<unknown>, name: string): string;
117
+ declare function toSource(models: Record<string, Model<unknown>>): string;
114
118
 
115
119
  /** `createdAt` → `created_at`, `userID` → `user_id`. */
116
120
  declare function toSnake(s: string): string;
package/dist/index.js CHANGED
@@ -289,8 +289,10 @@ function singular(s) {
289
289
  if (/[^s]s$/.test(s)) return s.slice(0, -1);
290
290
  return s;
291
291
  }
292
- function toSource(model, name) {
292
+ function toSource(modelOrModels, name) {
293
+ const roots = isModel(modelOrModels) ? [[name ?? "Model", modelOrModels]] : Object.entries(modelOrModels);
293
294
  const names = /* @__PURE__ */ new Map();
295
+ const emitted = /* @__PURE__ */ new Set();
294
296
  const used = /* @__PURE__ */ new Set();
295
297
  const blocks = [];
296
298
  const unique = (base) => {
@@ -300,10 +302,13 @@ function toSource(model, name) {
300
302
  return n;
301
303
  };
302
304
  const emit = (m, want) => {
303
- const known = names.get(m);
304
- if (known) return known;
305
- const id = unique(want);
306
- names.set(m, id);
305
+ let id = names.get(m);
306
+ if (id && emitted.has(m)) return id;
307
+ if (!id) {
308
+ id = unique(want);
309
+ names.set(m, id);
310
+ }
311
+ emitted.add(m);
307
312
  const lines = [];
308
313
  for (const [field, spec] of Object.entries(m.shape)) {
309
314
  const key = IDENT.test(field) ? field : JSON.stringify(field);
@@ -331,7 +336,8 @@ export type ${id} = Infer<typeof ${id}>;`);
331
336
  if (o.from !== void 0) parts.push(`from: ${JSON.stringify(o.from)}`);
332
337
  return `{ ${parts.join(", ")} }`;
333
338
  };
334
- emit(model, pascal(name));
339
+ for (const [n, m] of roots) if (!names.has(m)) names.set(m, unique(pascal(n)));
340
+ for (const [, m] of roots) emit(m, names.get(m));
335
341
  return `import { defineModel, type Infer } from "@flattype/models";
336
342
 
337
343
  ${blocks.join("\n\n")}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flattype/models",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Typed models for API data: key mapping, coercion and defaults, from JSON or Flat Notation.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,8 +33,8 @@
33
33
  "flattype"
34
34
  ],
35
35
  "dependencies": {
36
- "@flattype/core": "1.7.0",
37
- "@flattype/shared": "1.7.0"
36
+ "@flattype/core": "1.8.0",
37
+ "@flattype/shared": "1.8.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"