diff --git a/.changeset/add-flatten.md b/.changeset/add-flatten.md deleted file mode 100644 index cdabd47..0000000 --- a/.changeset/add-flatten.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'flattify': minor ---- - -Add `flatten(input, options?)`, with the resulting keys and values inferred by TypeScript. - -- Walks plain objects and arrays only: dates, maps, sets, class instances and other objects are kept as values -- Options: `delimiter`, `maxDepth`, `safe` (keep arrays), `keepEmpty`, `escape`, `circular` (`'throw'` or `'skip'`), `transformKey` and `preserve` -- Iterative walk: deeply nested input cannot overflow the call stack -- Keys that contain the delimiter are escaped (`'a.b'` → `'a\.b'`), and a `__proto__` key never changes the prototype of the result diff --git a/.changeset/add-notations.md b/.changeset/add-notations.md deleted file mode 100644 index fe983bb..0000000 --- a/.changeset/add-notations.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'flattify': minor ---- - -Add the `notation` option to `flatten` and `unflatten`, with the keys inferred by TypeScript for each one: - -- `'dot'` (default): `items.0.id` -- `'bracket'`: `items[0].id`. `unflatten` also reads form-field names such as `user[name]` and `items[0][id]` -- `'pointer'`: `/items/0/id`, a JSON Pointer (RFC 6901) with `~0`/`~1` escaping, as used by JSON Patch and JSON Schema validators diff --git a/.changeset/add-path-helpers.md b/.changeset/add-path-helpers.md deleted file mode 100644 index 29c50b5..0000000 --- a/.changeset/add-path-helpers.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -'flattify': minor ---- - -Add the `flattify/path` entry to read and write nested values by path, with autocompleted paths and typed values: - -- `get(obj, 'items.0.sku')`: the result is typed from `obj` (`string | undefined`) -- `set(obj, 'db.port', 6543)`: the value is checked against the path, and missing objects and arrays are created -- `has`, `del`, `paths` (the paths `flatten` produces), `parsePath`, `stringifyPath` and `escapeKey` -- `Path`, `Get` and `SetValue` types -- Every helper takes the `delimiter`, `notation` and `escape` options, and is safe against prototype pollution -- The JSON Pointer `''` is the whole document, as JSON Schema validators report errors at the root: `get(obj, '', { notation: 'pointer' })` returns `obj` diff --git a/.changeset/add-unflatten.md b/.changeset/add-unflatten.md deleted file mode 100644 index af6313b..0000000 --- a/.changeset/add-unflatten.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'flattify': minor ---- - -Add `unflatten(input, options?)`, the reverse of `flatten`, with the nested result inferred by TypeScript (`Unflatten>` gives back `T` for plain data). - -- Safe on untrusted input: keys are only created as own properties, so no path reaches `Object.prototype`, and paths with a `__proto__` segment are skipped -- `arrayLimit` (default `1000`) keeps huge indices such as `'a.4294967294'` from allocating sparse arrays -- `asArray` returns the array given to `flatten`: `unflatten({ '0.id': 1, '1.id': 2 }, { asArray: true })` → `[{ id: 1 }, { id: 2 }]`, typed as an array. Without it the result is always an object -- Options: `delimiter`, `object`, `overwrite`, `escape`, `arrayLimit`, `asArray` and `transformKey` -- Objects and arrays in the input are copied before keys are added to them, never modified - -`flatten` now escapes keys only when they would be ambiguous, so multi-character delimiters round-trip: with `'__'`, `MAX_CONN` stays as it is while `MAX_` is escaped. diff --git a/.changeset/flattify-v2.md b/.changeset/flattify-v2.md index b9a78b0..c62e4ab 100644 --- a/.changeset/flattify-v2.md +++ b/.changeset/flattify-v2.md @@ -2,6 +2,48 @@ 'flattify': major --- -flattify 2.0 is a full rewrite in TypeScript: ESM and CommonJS builds, zero dependencies, published on npm and JSR (`@nandomb/flattify`), and tested on Node.js, Bun, Deno, browsers and Cloudflare Workers. +**Why a major version?** -**Breaking:** the default export `flattify(json, isFlattifyArray)` was removed. The migration guide ships with the new `flatten`/`unflatten` API. +Flattify 2.0 is a full rewrite in TypeScript. The single default export `flattify(json, isFlattifyArray)` is replaced by named exports, `flatten` and `unflatten`, with different defaults. Code written for 1.x has to be updated (see Migration below). + +**Breaking changes** + +- **Named exports.** `flattify(...)` is replaced by `flatten(...)`: `import { flatten } from 'flattify'` or `const { flatten } = require('flattify')`. +- **Arrays are flattened by default.** `flatten({ tags: ['a'] })` gives `{ 'tags.0': 'a' }`. The 1.x default, which turned arrays into JSON strings, is gone: pass `{ safe: true }` to keep arrays as arrays. +- **Top-level arrays are flattened as a whole.** `flatten([{ id: 1 }])` gives `{ '0.id': 1 }` instead of flattening each item: use `array.map((item) => flatten(item))` for the 1.x behavior. +- **JSON strings are no longer parsed.** Pass `JSON.parse(json)` instead of the string; anything but a plain object or an array throws a `TypeError`. +- **Keys that contain the delimiter are escaped** (`{ 'a.b': 1 }` gives `{ 'a\.b': 1 }`), so they come back whole. Pass `{ escape: false }` to leave them as they are. +- **TypeScript 5.0+** for the types. + +**Migration** + +```diff +- const flattify = require('flattify'); +- flattify(data, true); ++ const { flatten } = require('flattify'); ++ flatten(data); +``` + +| 1.x | 2.x | +| --- | --- | +| `flattify(obj, true)` | `flatten(obj)` | +| `flattify(obj)` | `flatten(obj, { safe: true })` | +| `flattify(array)` | `array.map((item) => flatten(item))` | +| `flattify(jsonString)` | `flatten(JSON.parse(jsonString))` | + +**Fixed** + +- Nested arrays kept their path: `{ a: { tags: [1] }, b: { tags: [2] } }` gave `{ tags: '[2]' }` in 1.x, losing `a.tags`. It now gives `{ 'a.tags.0': 1, 'b.tags.0': 2 }`. +- Calls no longer share state: 1.x stored the array option in a module-level variable. +- Inherited properties are no longer read, and circular references no longer overflow the stack. + +**New** + +- **`unflatten`**, the reverse of `flatten`: `unflatten(flatten(data))` gives back `data`, including dates, class instances, `null`, empty objects and arrays, and keys with dots in them. `asArray` gives back an array given to `flatten`. +- **Type inference:** every path and value of the result is typed (`Flatten`, `Unflatten`), for every option that changes them. +- **Options:** `delimiter`, `maxDepth`, `safe`, `keepEmpty`, `escape`, `circular`, `transformKey` and `preserve` for `flatten`; `delimiter`, `object`, `overwrite`, `arrayLimit`, `asArray`, `escape` and `transformKey` for `unflatten`. +- **Three notations:** `items.0.id` (dot), `items[0].id` (bracket, and `unflatten` reads form fields such as `items[0][id]`) and `/items/0/id` (JSON Pointer, RFC 6901). +- **`flattify/path`:** `get`, `set`, `has`, `del`, `paths`, `parsePath`, `stringifyPath` and `escapeKey`, with autocompleted paths and type-checked values. +- **Safe on untrusted input:** no prototype pollution through `__proto__`, `constructor` or `prototype`, no stack overflow on deep or circular input, and `arrayLimit` against huge sparse arrays. Covered by tests, including fuzzing. +- **Everywhere:** ESM and CommonJS builds with zero dependencies, published on npm and on JSR as [`@nandomb/flattify`](https://jsr.io/@nandomb/flattify), and tested on Node.js 22, 24 and 26, Bun, Deno, Chromium, Firefox, WebKit and Cloudflare Workers. +- **Trusted releases:** published from CI without long-lived tokens, with npm provenance, and only live after a maintainer approves them with 2FA (staged publishing). diff --git a/README.md b/README.md index 675a96b..f9bbd98 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [flattify](https://github.com/NandoMB/flattify) +# [Flattify](https://github.com/NandoMB/flattify) [![npm version](https://img.shields.io/npm/v/flattify.svg)](https://www.npmjs.com/package/flattify) [![JSR](https://jsr.io/badges/@nandomb/flattify)](https://jsr.io/@nandomb/flattify) @@ -17,14 +17,14 @@ Turn nested objects into flat `path → value` pairs **and back, without losing - **Checked, not claimed:** 100% test coverage, thousands of random round trips per run, and every [comparison](#why-flattify) with other libraries verified in CI - Zero dependencies, ESM and CommonJS, on [npm](https://www.npmjs.com/package/flattify) and [JSR](https://jsr.io/@nandomb/flattify). Runs on Node.js, Bun, Deno, browsers and Cloudflare Workers -> Upgrading from flattify 1.x? See [Migrating](#migrating). +> Upgrading from Flattify 1.x? See [Migrating](#migrating). ## Contents - [Installation](#installation) - [Quick start](#quick-start) - [Data integrity](#data-integrity) -- [Why flattify?](#why-flattify) +- [Why Flattify?](#why-flattify) - [Use cases](#use-cases) - [API](#api): [`flatten`](#flatteninput-options), [`unflatten`](#unflatteninput-options), [notations](#notations), [`flattify/path`](#flattifypath) - [Types](#types) @@ -88,7 +88,7 @@ back.items[0].sku; // string ## Data integrity -Most libraries lose something on the way: a `Date` becomes `{}`, `null` disappears, an empty list vanishes, `'utm.source'` is split in two. flattify keeps all of it: +Most libraries lose something on the way: a `Date` becomes `{}`, `null` disappears, an empty list vanishes, `'utm.source'` is split in two. Flattify keeps all of it: ```ts const input = { @@ -123,11 +123,11 @@ back['utm.source']; // 'newsletter', still one key Each guarantee is covered by the test suite, and the round trip is checked on thousands of random objects on every run ([fast-check](https://fast-check.dev)), for every notation. -## Why flattify? +## Why Flattify? Every row is checked by [`bench/claims.spec.ts`](./bench/claims.spec.ts) against flat 6.0.1, flattie 1.1.1, nestie 1.0.3, es-toolkit 1.52.0 and radashi 12.9.6, on every change in CI (`pnpm claims`). -| | flattify | [flat](https://www.npmjs.com/package/flat) | [flattie](https://www.npmjs.com/package/flattie) / [nestie](https://www.npmjs.com/package/nestie) | [es-toolkit](https://www.npmjs.com/package/es-toolkit) | [radashi](https://www.npmjs.com/package/radashi) | +| | Flattify | [flat](https://www.npmjs.com/package/flat) | [flattie](https://www.npmjs.com/package/flattie) / [nestie](https://www.npmjs.com/package/nestie) | [es-toolkit](https://www.npmjs.com/package/es-toolkit) | [radashi](https://www.npmjs.com/package/radashi) | | --- | --- | --- | --- | --- | --- | | Keys with the delimiter survive the round trip | ✅ | ❌ | ❌ | – | ❌ | | Keeps `Date` and `Map` as values | ✅ | ✅ | ❌ dropped | ✅ | ✅ | @@ -144,7 +144,7 @@ Every row is checked by [`bench/claims.spec.ts`](./bench/claims.spec.ts) against | ESM and CommonJS | ✅ | ❌ v6 is ESM only | ✅ | ✅ | ✅ | | Size (min + gzip) | 2.3 kB | 0.7 kB | 0.35 kB | 0.34 kB | 0.8 kB | -flattify is bigger because it does more: the checks above, three notations, escaping and clear error messages. `flattify/path` is a separate import, so it only counts when you use it. +Flattify is bigger because it does more: the checks above, three notations, escaping and clear error messages. `flattify/path` is a separate import, so it only counts when you use it. ## Use cases @@ -308,7 +308,7 @@ CI runs the tests on Node.js 22, 24 and 26, Bun, Deno, Chromium, Firefox, WebKit Same option names (`delimiter`, `maxDepth`, `safe`, `object`, `overwrite`, `transformKey`), with safer defaults: -| | `flat` | flattify | +| | `flat` | Flattify | | --- | --- | --- | | Result type | you pass it: `flatten()` | inferred | | Keys with the delimiter | split in two on the way back | escaped; `{ escape: false }` for the old behavior | @@ -318,7 +318,7 @@ Same option names (`delimiter`, `maxDepth`, `safe`, `object`, `overwrite`, `tran | Class instances | flattened | kept as values | | Circular references | stack overflow | `TypeError`, or `circular: 'skip'` | -### From flattify 1.x +### From Flattify 1.x 2.0 is a rewrite with named exports: