@flattype/models 1.7.0 → 1.9.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 +70 -5
- package/dist/index.cjs +13 -7
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +13 -7
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<a href="https://github.
|
|
2
|
+
<a href="https://karammm.github.io/flattype/docs/">
|
|
3
3
|
<picture>
|
|
4
4
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/logo-dark.svg">
|
|
5
5
|
<img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/logo-light.svg" alt="FlatType.js" width="300">
|
|
@@ -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">
|
|
@@ -28,6 +29,8 @@
|
|
|
28
29
|
</p>
|
|
29
30
|
|
|
30
31
|
<p align="center">
|
|
32
|
+
<a href="https://karammm.github.io/flattype/docs/"><b>Documentation</b></a> ·
|
|
33
|
+
<a href="https://karammm.github.io/flattype/"><b>Playground</b></a> ·
|
|
31
34
|
<a href="#quick-start"><b>Quick start</b></a> ·
|
|
32
35
|
<a href="#the-format"><b>Format</b></a> ·
|
|
33
36
|
<a href="#typed-models"><b>Models</b></a> ·
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
<a href="#angular"><b>Angular</b></a> ·
|
|
38
41
|
<a href="#svelte-and-solid"><b>Svelte & Solid</b></a> ·
|
|
39
42
|
<a href="#claude-and-llms"><b>Claude & LLMs</b></a> ·
|
|
43
|
+
<a href="#cli"><b>CLI</b></a> ·
|
|
40
44
|
<a href="#benchmarks"><b>Benchmarks</b></a> ·
|
|
41
45
|
<a href="https://github.com/karammm/flattype/blob/main/CHANGELOG.md"><b>Changelog</b></a> ·
|
|
42
46
|
<a href="https://github.com/karammm/flattype/blob/main/ROADMAP.md"><b>Roadmap</b></a>
|
|
@@ -45,10 +49,10 @@
|
|
|
45
49
|
<p align="center">
|
|
46
50
|
<a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">
|
|
47
51
|
<img src="https://raw.githubusercontent.com/karammm/flattype/main/docs/assets/flattype-intro.gif" width="820"
|
|
48
|
-
alt="FlatType.js in
|
|
52
|
+
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
53
|
</a>
|
|
50
54
|
<br>
|
|
51
|
-
<sub>▶ <a href="https://github.com/karammm/flattype/blob/main/docs/assets/flattype-intro.mp4">Watch the
|
|
55
|
+
<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
56
|
</p>
|
|
53
57
|
|
|
54
58
|
---
|
|
@@ -143,9 +147,10 @@ const [users] = createFlatResource(() => "/api/users", { model: User, list: true
|
|
|
143
147
|
| [`@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
148
|
| [`@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
149
|
| [`@flattype/ai`](https://github.com/karammm/flattype/blob/main/packages/ai) | `toPrompt`, `fromModelOutput`, `countTokens`, and MCP / Claude tool results | +1.3 KB |
|
|
150
|
+
| [`@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
151
|
| [`@flattype/shared`](https://github.com/karammm/flattype/blob/main/packages/shared) | Shared TypeScript types | types only |
|
|
147
152
|
|
|
148
|
-
|
|
153
|
+
Everything is documented at [karammm.github.io/flattype/docs](https://karammm.github.io/flattype/docs/), and you can try the format on your own data in the [playground](https://karammm.github.io/flattype/). See the [roadmap](https://github.com/karammm/flattype/blob/main/ROADMAP.md).
|
|
149
154
|
|
|
150
155
|
## The format
|
|
151
156
|
|
|
@@ -493,6 +498,58 @@ messages.push({ role: "user", content: [claudeToolResult(toolUse.id, await runTo
|
|
|
493
498
|
|
|
494
499
|
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
500
|
|
|
501
|
+
## CLI
|
|
502
|
+
|
|
503
|
+
```bash
|
|
504
|
+
npx @flattype/cli --help # or: npm install -D @flattype/cli, then use `flattype …`
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
# Convert, in pipes or files
|
|
509
|
+
curl -s https://api.example.com/users | npx @flattype/cli encode
|
|
510
|
+
npx @flattype/cli decode users.flat -o users.json
|
|
511
|
+
|
|
512
|
+
# Compare sizes
|
|
513
|
+
npx @flattype/cli stats users.json
|
|
514
|
+
# bytes gzip
|
|
515
|
+
# JSON 169545 12890
|
|
516
|
+
# Flat 93606 12568
|
|
517
|
+
# saved 45% 2%
|
|
518
|
+
|
|
519
|
+
# Generate typed models from an OpenAPI spec (3.0, 3.1 or Swagger 2; JSON or YAML)…
|
|
520
|
+
npx @flattype/cli gen --openapi openapi.yaml -o src/models.ts
|
|
521
|
+
|
|
522
|
+
# …or from a real response
|
|
523
|
+
curl -s https://api.example.com/users > users.json && npx @flattype/cli gen --sample users.json --name UsersPage
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
For an OpenAPI schema like
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
Customer:
|
|
530
|
+
type: object
|
|
531
|
+
required: [id, full_name, created_at]
|
|
532
|
+
properties:
|
|
533
|
+
id: { type: integer }
|
|
534
|
+
full_name: { type: string }
|
|
535
|
+
created_at: { type: string, format: date-time }
|
|
536
|
+
address: { $ref: "#/components/schemas/Address" }
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
`flattype gen --openapi` writes:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
export const Customer = defineModel({
|
|
543
|
+
id: "integer",
|
|
544
|
+
fullName: "string",
|
|
545
|
+
createdAt: "date",
|
|
546
|
+
address: { type: Address, optional: true },
|
|
547
|
+
}, { keys: "snake" });
|
|
548
|
+
export type Customer = Infer<typeof Customer>;
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
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)`.
|
|
552
|
+
|
|
496
553
|
## Benchmarks
|
|
497
554
|
|
|
498
555
|
`pnpm compare` on the fixtures in [`fixtures/`](https://github.com/karammm/flattype/blob/main/fixtures/index.mjs) (Node 24, Apple Silicon):
|
|
@@ -510,7 +567,7 @@ See [`examples/ai-claude`](https://github.com/karammm/flattype/blob/main/example
|
|
|
510
567
|
| **FlatType `decode`** | **0.73 ms** |
|
|
511
568
|
| TOON `decode` | 5.9 ms |
|
|
512
569
|
|
|
513
|
-
Some honest notes on
|
|
570
|
+
Numbers measured on every docs build are on the [benchmarks page](https://karammm.github.io/flattype/docs/guide/benchmarks). Some honest notes on them:
|
|
514
571
|
|
|
515
572
|
- **The savings come from lists.** A single small object is about the same size as JSON.
|
|
516
573
|
- **Gzip already removes most of JSON's repeated keys,** so gzipped network sizes come out close. FlatType makes the biggest difference where text isn't compressed: LLM prompts and tool results, logs, caches and local storage.
|
|
@@ -524,11 +581,19 @@ pnpm build && pnpm typecheck && pnpm test
|
|
|
524
581
|
pnpm bench # encode/decode vs JSON
|
|
525
582
|
pnpm compare # size and parse time vs JSON and TOON
|
|
526
583
|
pnpm check:size # bundle budgets
|
|
584
|
+
pnpm --filter @flattype/core test:perf # linear-time checks on hostile input
|
|
585
|
+
FUZZ_RUNS=50000 pnpm --filter @flattype/core exec vitest run # deep fuzzing, as in CI
|
|
586
|
+
pnpm --filter @flattype/docs dev # docs site (VitePress)
|
|
587
|
+
pnpm --filter @flattype/playground dev # playground
|
|
527
588
|
pnpm video # re-render docs/assets/flattype-intro.{mp4,gif} (needs ffmpeg and the Angular example built)
|
|
528
589
|
```
|
|
529
590
|
|
|
530
591
|
This is a pnpm + Turborepo monorepo built with tsup and tested with Vitest and fast-check. Releases use Changesets, and all `@flattype/*` packages share one version number. Every release follows the checklist in [ROADMAP.md](https://github.com/karammm/flattype/blob/main/ROADMAP.md).
|
|
531
592
|
|
|
593
|
+
## Security
|
|
594
|
+
|
|
595
|
+
The decoder is prototype-safe, runs in linear time on hostile input, and only ever throws `FlatDecodeError`. Deep fuzzing and timing tests check this on every push, as described on the [security page](https://karammm.github.io/flattype/docs/guide/security). Please report vulnerabilities privately, following [SECURITY.md](https://github.com/karammm/flattype/blob/main/SECURITY.md).
|
|
596
|
+
|
|
532
597
|
## Contributing
|
|
533
598
|
|
|
534
599
|
Issues and pull requests are welcome. If you have a payload that FlatType handles badly, please [open an issue](https://github.com/karammm/flattype/issues) with a sample; it becomes a test fixture.
|
package/dist/index.cjs
CHANGED
|
@@ -55,7 +55,7 @@ var ModelError = class extends Error {
|
|
|
55
55
|
// src/model.ts
|
|
56
56
|
var PRIMITIVES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "date", "any"]);
|
|
57
57
|
var FAIL = /* @__PURE__ */ Symbol("fail");
|
|
58
|
-
var NUMERIC = /^\s*[-+]?(?:\d
|
|
58
|
+
var NUMERIC = /^\s*[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?\s*$/;
|
|
59
59
|
var ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
|
|
60
60
|
function isModel(v) {
|
|
61
61
|
return typeof v === "object" && v !== null && v.__model === true;
|
|
@@ -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
|
@@ -23,7 +23,7 @@ var ModelError = class extends Error {
|
|
|
23
23
|
// src/model.ts
|
|
24
24
|
var PRIMITIVES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "date", "any"]);
|
|
25
25
|
var FAIL = /* @__PURE__ */ Symbol("fail");
|
|
26
|
-
var NUMERIC = /^\s*[-+]?(?:\d
|
|
26
|
+
var NUMERIC = /^\s*[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?\s*$/;
|
|
27
27
|
var ISO_DATE = /^\d{4}-\d{2}-\d{2}/;
|
|
28
28
|
function isModel(v) {
|
|
29
29
|
return typeof v === "object" && v !== null && v.__model === true;
|
|
@@ -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.9.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.9.0",
|
|
37
|
+
"@flattype/shared": "1.9.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"build": "tsup",
|
|
56
56
|
"typecheck": "tsc --noEmit",
|
|
57
57
|
"clean": "rm -rf dist .turbo",
|
|
58
|
-
"test": "vitest run"
|
|
58
|
+
"test": "vitest run",
|
|
59
|
+
"test:perf": "vitest run --config vitest.perf.config.ts"
|
|
59
60
|
}
|
|
60
61
|
}
|