@flattype/models 1.6.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
@@ -20,6 +20,8 @@
20
20
  <a href="https://www.npmjs.com/package/@flattype/vue"><img src="https://img.shields.io/npm/v/@flattype/vue?label=%40flattype%2Fvue&color=8b6cff" alt="npm @flattype/vue"></a>
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
+ <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>
23
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>
24
26
  <img src="https://img.shields.io/badge/core-3.7_KB_min%2Bgz-2fd4b2" alt="3.7 KB min+gz">
25
27
  <img src="https://img.shields.io/badge/dependencies-0-2fd4b2" alt="No third-party dependencies">
@@ -35,6 +37,8 @@
35
37
  <a href="#vue"><b>Vue</b></a> ·
36
38
  <a href="#angular"><b>Angular</b></a> ·
37
39
  <a href="#svelte-and-solid"><b>Svelte &amp; Solid</b></a> ·
40
+ <a href="#claude-and-llms"><b>Claude &amp; LLMs</b></a> ·
41
+ <a href="#cli"><b>CLI</b></a> ·
38
42
  <a href="#benchmarks"><b>Benchmarks</b></a> ·
39
43
  <a href="https://github.com/karammm/flattype/blob/main/CHANGELOG.md"><b>Changelog</b></a> ·
40
44
  <a href="https://github.com/karammm/flattype/blob/main/ROADMAP.md"><b>Roadmap</b></a>
@@ -43,10 +47,10 @@
43
47
  <p align="center">
44
48
  <a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">
45
49
  <img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/flattype-intro.gif" width="820"
46
- 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">
47
51
  </a>
48
52
  <br>
49
- <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>
50
54
  </p>
51
55
 
52
56
  ---
@@ -140,9 +144,11 @@ const [users] = createFlatResource(() => "/api/users", { model: User, list: true
140
144
  | [`@flattype/vue`](https://github.com/karammm/flattype/blob/main/packages/vue) | The same composables for Vue 3, with reactive URLs, plus the `createFlat()` plugin | +1.2 KB |
141
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 |
142
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 |
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 |
143
149
  | [`@flattype/shared`](https://github.com/karammm/flattype/blob/main/packages/shared) | Shared TypeScript types | types only |
144
150
 
145
- Claude helpers and a CLI are 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).
146
152
 
147
153
  ## The format
148
154
 
@@ -436,6 +442,112 @@ const events = createFlatStream(() => "/api/export", { model: Row, table: "rows"
436
442
 
437
443
  The stores also work in Solid, or anywhere else, through the store contract: `from(flatStore(...))`. Runnable apps with Playwright tests in three browsers are in [`examples/svelte-app`](https://github.com/karammm/flattype/blob/main/examples/svelte-app) and [`examples/solid-app`](https://github.com/karammm/flattype/blob/main/examples/solid-app).
438
444
 
445
+ ## Claude and LLMs
446
+
447
+ ```bash
448
+ npm install @flattype/ai
449
+ ```
450
+
451
+ ```ts
452
+ import Anthropic from "@anthropic-ai/sdk";
453
+ import { countTokens, fromModelOutput, outputInstructions, toPrompt } from "@flattype/ai";
454
+
455
+ const client = new Anthropic();
456
+
457
+ // 1. Data in: Flat Notation inside <data> tags, with a one-line legend the model reads once.
458
+ const response = await client.beta.messages.create({
459
+ model: "claude-opus-5-5",
460
+ max_tokens: 16000,
461
+ betas: ["server-side-fallback-2026-07-01"],
462
+ fallbacks: "default", // if a safety classifier declines, retry on the recommended fallback model
463
+ messages: [{
464
+ role: "user",
465
+ content: `${toPrompt(orders, { name: "orders" })}\n\nTotal spent per customer?\n\n${outputInstructions([{ customer: "Alice", spent: 120 }])}`,
466
+ }],
467
+ });
468
+
469
+ // 2. Data out: tolerant of tags, fences, prose and JSON, then validated into typed models.
470
+ const reply = response.content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
471
+ const totals = fromModelOutput(reply, { model: Total, list: true }); // Total[]
472
+
473
+ // 3. Measure it with Claude's own tokenizer (count_tokens is free).
474
+ const { json, flat, saved } = await countTokens(client, orders);
475
+ ```
476
+
477
+ **Tool results** shrink too, which matters most for agents that call tools over and over:
478
+
479
+ ```ts
480
+ import { claudeToolResult, toolResult } from "@flattype/ai";
481
+
482
+ // MCP server (@modelcontextprotocol/sdk): returns a CallToolResult with Flat Notation text
483
+ server.registerTool("list_orders", { description: "…" }, async () => toolResult(await db.orders()));
484
+
485
+ // Claude API tool loop: a tool_result block for the next user message
486
+ messages.push({ role: "user", content: [claudeToolResult(toolUse.id, await runTool(toolUse))] });
487
+ ```
488
+
489
+ | API | What it does |
490
+ | --- | --- |
491
+ | `toPrompt(data, { name, legend })` | `<data format="flat-notation">` plus a one-line legend. The block is still valid Flat Notation; put the legend in your system prompt once with `legend: false` |
492
+ | `outputInstructions(example)` | Asks the model to answer in Flat Notation and shows the exact shape you expect |
493
+ | `fromModelOutput(reply, { model, list })` | Finds the data (last `<data>` block, code fence, or the text itself), skips prose lines, and accepts JSON too |
494
+ | `countTokens(client, data, { model })` | `{ json, flat, saved }` in real Claude tokens, minus per-message overhead. Takes any client with `messages.countTokens` |
495
+ | `toolResult(data, { structured })` / `claudeToolResult(id, data)` | MCP `CallToolResult` and Claude API `tool_result` blocks |
496
+
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`).
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
+
439
551
  ## Benchmarks
440
552
 
441
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.6.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.6.0",
37
- "@flattype/shared": "1.6.0"
36
+ "@flattype/core": "1.8.0",
37
+ "@flattype/shared": "1.8.0"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"