@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="https://github.com/karammm/flattype">
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 &amp; Solid</b></a> ·
39
42
  <a href="#claude-and-llms"><b>Claude &amp; 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 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">
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 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>
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
- A CLI with code generation is planned. See the [roadmap](https://github.com/karammm/flattype/blob/main/ROADMAP.md).
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 these numbers:
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+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*$/;
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(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
@@ -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+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\s*$/;
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(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.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.7.0",
37
- "@flattype/shared": "1.7.0"
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
  }