Skip to content

Commit bfd7252

Browse files
committed
fix: readme fixes and add skills
1 parent c4fb04c commit bfd7252

5 files changed

Lines changed: 238 additions & 8 deletions

File tree

README.md

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A simple, modern, zero-dependency, and fully type-safe library for building, com
55
[![npm version](https://badge.fury.io/js/data-path.svg)](https://badge.fury.io/js/data-path)
66
[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
77

8-
## The Problem
8+
## ⚠️ The Problem
99

1010
When working with deep object structures, forms, or nested state, we often rely on string-based paths (e.g., `"users.0.name"` or `"company.departments[1].budget"`). This approach is fundamentally flawed in modern TypeScript development:
1111

@@ -14,7 +14,7 @@ When working with deep object structures, forms, or nested state, we often rely
1414
- **No Autocomplete:** Your IDE cannot guide you through the object structure.
1515
- **No Mathematics:** It is difficult to programmatically determine if path `A` is a child of path `B`, or if they overlap.
1616

17-
## The Solution
17+
## 🚀 The Solution
1818

1919
`data-path` solves this by using **Proxy-based lambda expressions** to capture paths. It gives you 100% type safety, IDE autocomplete, and a rich API for interacting with data and other paths.
2020

@@ -49,22 +49,67 @@ const updatedUser = firstNamePath.set(user, "Bob");
4949
console.log(updatedUser.profile.firstName); // "Bob"
5050
```
5151

52-
## Installation
52+
## 📦 Installation
5353

5454
```bash
5555
npm install data-path
5656
```
5757

5858
_(or use `yarn add data-path`, `pnpm add data-path`, `bun add data-path`)_
5959

60-
## Philosophy
60+
## 🤖 AI Ready
61+
62+
This package is available in [Context7](https://context7.com/) MCP, so AI assistants can load it directly into context when working with your object property paths.
63+
64+
It also ships an [Agent Skills](https://agentskills.io/) – compatible skill. Install it so your AI assistant loads data-path guidance:
65+
66+
```bash
67+
npx ctx7 skills install /sergeyshmakov/data-path data-path
68+
```
69+
70+
The skill lives in `skills/data-path/SKILL.md`.
71+
72+
## 💡 Philosophy
6173

6274
- **Stack Agnostic:** Pure data manipulation. Works perfectly with React, Vue, Node.js, or vanilla JavaScript.
6375
- **Zero Dependencies:** A tiny, efficient footprint that doesn't bloat your bundle.
6476
- **Fully Type-Safe:** Built strictly for TypeScript. If the structure changes, the compiler will instantly catch broken paths.
6577
- **Immutable:** All `.set()` operations return structurally cloned objects, making it the perfect companion for modern state managers (Redux, Zustand) and reactive frameworks.
6678

67-
## Core API
79+
## 📚 Core API
80+
81+
### 🏷️ API Cheatsheet
82+
83+
| API | Description |
84+
|-----|-------------|
85+
| **Creation** | |
86+
| `path<T>()` | Create root path |
87+
| `path<T>(p => p.a.b)` | Create path from lambda |
88+
| `path(base, p => p.c)` | Extend existing path |
89+
| `unsafePath<T>("a.b")` | Create path from raw string |
90+
| **Properties** | |
91+
| `path.$` | String representation (e.g. `"users.0.name"`) |
92+
| `path.segments` | Array of segments |
93+
| `path.length` | Number of segments |
94+
| `path.fn` | Accessor function for `.map()`, `.filter()` |
95+
| **Data Access** | |
96+
| `path.get(data)` | Read value at path (returns `undefined` if missing) |
97+
| `path.set(data, value)` | Immutable write, returns new object |
98+
| **Traversal** | |
99+
| `path.to(p => p.x)` | Extend path from current value |
100+
| `path.each(p => p.x)` | Template: match all items in collection |
101+
| `path.deep(node => node.id)` | Template: match property at any depth |
102+
| **Manipulation** | |
103+
| `path.merge(other)` | Append path (deduplicates overlap) |
104+
| `path.subtract(other)` | Remove prefix/suffix, or `null` |
105+
| `path.slice(start?, end?)` | Slice segments (like `Array.prototype.slice`) |
106+
| **Relational** | |
107+
| `path.startsWith(other)` | True if path is prefix |
108+
| `path.includes(other)` | True if path contains other |
109+
| `path.equals(other)` | True if paths are identical |
110+
| `path.match(other)` | Returns `{ relation, params }` or `null` |
111+
| **Template-only** | |
112+
| `templatePath.expand(data)` | Resolve template to concrete paths |
68113

69114
### Path Creation
70115

@@ -201,7 +246,7 @@ namePath.match(profilePath); // { relation: 'child', params: {} }
201246
profilePath.match(namePath); // { relation: 'parent', params: {} }
202247
```
203248

204-
## Real-World Examples
249+
## 💼 Real-World Examples
205250

206251
### 1. React Hook Form
207252

@@ -368,10 +413,10 @@ const columns = [
368413
];
369414
```
370415

371-
## Contributing
416+
## 🤝 Contributing
372417

373418
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
374419

375-
## License
420+
## 📄 License
376421

377422
This project is licensed under the [ISC License](LICENSE).

skills/data-path/SKILL.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
---
2+
name: data-path
3+
description: Creates and manipulates type-safe object property paths using TypeScript lambda expressions. Use when the user needs typed paths for nested forms (React Hook Form, TanStack Form), state updates (Zustand, useState), validation mapping (Zod), or column accessors (TanStack Table). Use when replacing string paths like "users.0.name" with type-safe alternatives. Don't use for lodash.get, JSONPath, or simple one-level property access.
4+
---
5+
6+
# `data-path` skill
7+
8+
Provides type-safe object property paths via Proxy-based lambda expressions. Paths are inferred from TypeScript types and support get/set, merge/subtract, templates (wildcards), and relational algebra.
9+
10+
## When to Apply
11+
12+
Trigger when the user:
13+
- Binds form fields to nested structures (e.g. `users[i].firstName`)
14+
- Updates deeply nested state immutably
15+
- Maps validation errors to specific fields
16+
- Defines table column accessors
17+
- Compares or composes paths programmatically
18+
19+
Do not trigger for:
20+
- Simple `obj.prop` access
21+
- lodash.get / lodash.set usage
22+
- JSONPath or XPath-style queries
23+
- Non-TypeScript projects
24+
25+
## Quick Start
26+
27+
1. Ensure `data-path` is installed: `npm install data-path`
28+
2. Import: `import { path, unsafePath } from "data-path"`
29+
3. Define a path with a lambda: `path<RootType>(p => p.nested.field)`
30+
4. Use `path.$` for string form (e.g. `register(path.$)`), `path.get(data)` to read, `path.set(data, value)` to write
31+
32+
## Step-by-Step Workflows
33+
34+
### Workflow 1: Form Field Binding
35+
36+
When binding a nested form field (React Hook Form, TanStack Form):
37+
38+
1. Define the form value type (e.g. `FormValues`).
39+
2. Create the path with the loop index in the lambda: `path<FormValues>(p => p.users[i].firstName)`.
40+
3. Pass `path.$` to `register()` (React Hook Form) or `name` prop (TanStack Form).
41+
4. Do not create the path outside the map callback—the index `i` must be captured inside the lambda.
42+
43+
### Workflow 2: Immutable State Update
44+
45+
When updating nested state (Zustand, useState):
46+
47+
1. Create the path once at module scope: `const themePath = path<State>(p => p.settings.profile.theme)`.
48+
2. In the setter: `set(state => themePath.set(state, newValue))`.
49+
3. `path.set()` returns a structural clone; no manual spreading required.
50+
51+
### Workflow 3: Validation Error Mapping (Zod)
52+
53+
When mapping Zod errors to UI fields:
54+
55+
1. Create the expected path: `path<FormData>(p => p.user.age)`.
56+
2. For each `issue` in `result.error.issues`, build a path from `issue.path`: `unsafePath<FormData>(issue.path.join("."))`.
57+
3. Compare: `if (errorPath.equals(agePath)) { /* show error */ }`.
58+
59+
### Workflow 4: Bulk Operations (Templates)
60+
61+
When operating on all items in a collection:
62+
63+
1. Create a template: `path<Data>(p => p.users).each(u => u.name)`.
64+
2. `path.$` becomes `"users.*.name"`.
65+
3. Use `templatePath.get(data)` → array of values; `templatePath.set(data, value)` → updates all matches.
66+
4. Use `templatePath.expand(data)` to get concrete paths for each match.
67+
68+
### Workflow 5: Composing Paths (Reusable Components)
69+
70+
When a component receives a base path and needs to extend it:
71+
72+
1. Base path: `employeePath = path<Company>(p => p.departments[0].employees[5])`.
73+
2. Sub-path: `nameSubPath = path<Employee>(p => p.profile.firstName)`.
74+
3. Merge: `absolutePath = employeePath.merge(nameSubPath)`.
75+
4. Subtract for relative: `relative = absolutePath.subtract(employeePath)`.
76+
77+
## Key Rules
78+
79+
- **Lambda captures at creation time**: Use `path<T>(p => p.users[i].name)` with `i` from the enclosing scope. The path is built once when the lambda runs.
80+
- **Use `unsafePath` only for dynamic strings**: e.g. from `issue.path.join(".")` or API responses. Prefer `path()` for static structure.
81+
- **`.get()` returns `undefined`** if any intermediate segment is missing; it does not throw.
82+
- **`.set()` is immutable**: Returns a new object. Use with functional updaters.
83+
- **`.each()` and `.deep()`** require a non-primitive value at the path; they are not available on paths ending in string/number/etc.
84+
85+
## API Reference
86+
87+
For the full API cheatsheet (creation, properties, data access, traversal, manipulation, relational), see [references/api.md](references/api.md).
88+
89+
## Common Integrations
90+
91+
| Integration | Use `path.$` for | Use `path.get`/`path.set` for |
92+
|------------------|------------------|-------------------------------|
93+
| React Hook Form | `register(name)` ||
94+
| TanStack Form | `Field name` ||
95+
| Zustand || `set(state => path.set(state, v))` |
96+
| useState || `setState(prev => path.set(prev, v))` |
97+
| TanStack Table | `id` in accessor | `accessor: path.fn` |
98+
| Zod || `unsafePath(issue.path.join(".")).equals(path)` |

skills/data-path/references/api.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# `data-path` API cheatsheet
2+
3+
## Creation
4+
5+
| API | Description |
6+
|-----|-------------|
7+
| `path<T>()` | Create root path |
8+
| `path<T>(p => p.a.b)` | Create path from lambda, generic type |
9+
| `path((p: T) => p.a.b)` | Create path from lambda, infer type |
10+
| `path(base, p => p.c)` | Extend existing path, `p` must have the output type of `base` |
11+
| `unsafePath<T>("a.b")` | Create path from raw string, not recommended in usual cases |
12+
13+
## Properties
14+
15+
| API | Description |
16+
|-----|-------------|
17+
| `path.$` | String representation (e.g. `"users.0.name"`) |
18+
| `path.segments` | Array of segments |
19+
| `path.length` | Number of segments |
20+
| `path.fn` | Accessor function for `.map()`, `.filter()` |
21+
22+
## Data Access
23+
24+
| API | Description |
25+
|-----|-------------|
26+
| `path.get(data)` | Read value at path (returns `undefined` if missing) |
27+
| `path.set(data, value)` | Immutable write, returns new object |
28+
29+
## Traversal
30+
31+
| API | Description |
32+
|-----|-------------|
33+
| `path.to(p => p.x)` | Extend path from current value |
34+
| `path.each(p => p.x)` | Template: match all items in collection |
35+
| `path.each().to(p => p.x)` | Same as above |
36+
| `path.deep(node => node.id)` | Template: match property at any depth |
37+
38+
## Manipulation
39+
40+
| API | Description |
41+
|-----|-------------|
42+
| `path.merge(other)` | Append path (deduplicates overlap) |
43+
| `path.subtract(other)` | Remove prefix/suffix, or `null` |
44+
| `path.slice(start?, end?)` | Slice segments (like `Array.prototype.slice`) |
45+
46+
## Relational
47+
48+
| API | Description |
49+
|-----|-------------|
50+
| `path.startsWith(other)` | True if path is prefix |
51+
| `path.includes(other)` | True if path contains other |
52+
| `path.equals(other)` | True if paths are identical |
53+
| `path.match(other)` | Returns `{ relation, params }` or `null` |
54+
55+
## Template-only
56+
57+
| API | Description |
58+
|-----|-------------|
59+
| `templatePath.expand(data)` | Resolve template to concrete paths |
60+
61+
## Match Relations
62+
63+
`path.match(other)` returns `MatchResult` with `relation` one of:
64+
- `'includes'` — this path contains other
65+
- `'included-by'` — other contains this path
66+
- `'equals'` — paths are identical
67+
- `'parent'` — this path is parent of other
68+
- `'child'` — this path is child of other
69+
- `null` — no relation

src/tests/templates.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ describe("Template paths", () => {
3131
expect(tmpl.segments).toEqual(["items", "*", "name"]);
3232
});
3333

34+
it("path.each().to(p => p.x) produces same path string as path.each(p => p.x)", () => {
35+
const viaTo = path((p: User) => p.items).each().to((i) => i.name);
36+
const viaEach = path((p: User) => p.items).each((i) => i.name);
37+
expect(viaTo.$).toBe("items.*.name");
38+
expect(viaTo.segments).toEqual(["items", "*", "name"]);
39+
expect(viaTo.$).toBe(viaEach.$);
40+
expect(viaTo.segments).toEqual(viaEach.segments);
41+
});
42+
3443
it("immutability: does not mutate original path", () => {
3544
const base = path((p: User) => p.items);
3645
base.each((i: { name: string }) => i.name);

src/tests/types.test-d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ describe("Type checks", () => {
6262
>();
6363
});
6464

65+
it("path.each().to(p => p.x) returns Path with same structure as path.each(p => p.x)", () => {
66+
const viaTo = path((p: { items: Array<{ name: string }> }) => p.items)
67+
.each()
68+
.to((i) => i.name);
69+
expectTypeOf(viaTo).toEqualTypeOf<
70+
Path<{ items: Array<{ name: string }> }, string, string>
71+
>();
72+
});
73+
6574
it(".deep() returns TemplatePath", () => {
6675
const deep = path((p: { tree: { label: string } }) => p.tree).deep(
6776
(n: { label: string }) => n.label,

0 commit comments

Comments
 (0)