@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 +115 -3
- package/dist/index.cjs +12 -6
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +12 -6
- package/package.json +3 -3
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 & Solid</b></a> ·
|
|
40
|
+
<a href="#claude-and-llms"><b>Claude & 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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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(
|
|
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
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
37
|
-
"@flattype/shared": "1.
|
|
36
|
+
"@flattype/core": "1.8.0",
|
|
37
|
+
"@flattype/shared": "1.8.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|