@keshavsoft/api-tree 1.0.0 → 2.0.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,93 +1,200 @@
1
1
  # @keshavsoft/api-tree
2
2
 
3
- A small runtime API-tree builder.
3
+ [![npm version](https://img.shields.io/npm/v/@keshavsoft/api-tree.svg)](https://www.npmjs.com/package/@keshavsoft/api-tree)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js CI](https://img.shields.io/badge/node-%3E%3D20.10-brightgreen.svg)]()
4
6
 
5
- `@keshavsoft/api-tree` does one job: it takes **source JSON**, **API paths**, and an **executor**, then returns a callable runtime tree.
7
+ > A declarative, ultra-lean routing engine that transforms **Source Schemas**, **API Paths**, and an **Executor** into a callable runtime API tree.
8
+
9
+ ---
10
+
11
+ ## 💡 The Problem & Philosophy
12
+
13
+ In modern modular architectures, APIs often end up bloated because **schema contracts**, **route definitions**, and **runtime execution** are tangled together.
14
+
15
+ `@keshavsoft/api-tree` decouples these concerns completely:
16
+
17
+ $$\mathbf{Runtime\ Tree} = \underbrace{\mathbf{Source\ Schema}}_{\text{JSON Contract}} \;+\; \underbrace{\mathbf{API\ Paths}}_{\text{Dotted Routes}} \;+\; \underbrace{\mathbf{Executor}}_{\text{Execution Flavor}}$$
18
+
19
+ - **Domain-Agnostic**: Does not know about HTTP, XML, TDL, databases, or specific business logic.
20
+ - **Single Responsibility**: Generates the navigable runtime object tree from contracts and leaves execution to your handler.
21
+ - **Zero Dependencies**: Pure, modern ES module running at native speeds.
6
22
 
7
23
  ```text
8
- source JSON
9
- +
10
- API paths
11
- +
12
- executor
13
- ↓
14
- api-tree
15
- ↓
16
- callable API
24
+ ┌─────────────────────────┐ ┌────────────────────────┐ ┌───────────────────────┐
25
+ │ Source JSON │ + │ API Paths │ + │ Executor │
26
+ │ (Schema Specifications) │ │ (Dotted Route Strings) │ │ (Custom Handler Func) │
27
+ └───────────┬─────────────┘ └───────────┬────────────┘ └──────────┬────────────┘
28
+ │ │ │
29
+ └───────────────────────┬───────┴─────────────────────────────┘
30
+ │
31
+ ▼
32
+ @keshavsoft/api-tree
33
+ │
34
+ ▼
35
+ Callable Runtime API Tree
36
+ app.users.profile.fetch("123")
17
37
  ```
18
38
 
19
- ## Usage
39
+ ---
40
+
41
+ ## 📦 Installation
42
+
43
+ ```bash
44
+ npm install @keshavsoft/api-tree
45
+ ```
46
+
47
+ ---
48
+
49
+ ## 🚀 Quick Start
20
50
 
21
- ```js
51
+ ```javascript
22
52
  import apiTree from "@keshavsoft/api-tree";
23
53
 
54
+ // 1. Source JSON (the schema/metadata contract)
24
55
  const source = {
25
56
  app: {
26
57
  users: {
27
58
  profile: {
28
- fetch: { action: "fetch", resource: "users" }
59
+ fetch: { action: "fetch", resource: "User", timeout: 5000 }
29
60
  }
30
61
  }
31
62
  }
32
63
  };
33
64
 
65
+ // 2. Allowable API Paths
34
66
  const apiPaths = [
35
67
  "app.users.profile.fetch"
36
68
  ];
37
69
 
38
- const executor = async ({ inRoutePath, inParam, inSource }) => {
39
- return {
40
- path: inRoutePath,
41
- param: inParam,
42
- spec: inSource.app.users.profile.fetch
43
- };
70
+ // 3. Executor Function
71
+ const executor = async ({ inRoutePath, inParam, inLeafSpec }) => {
72
+ console.log(`Executing ${inRoutePath} for ID: ${inParam}`);
73
+ console.log("Leaf schema definition:", inLeafSpec);
74
+ return { id: inParam, name: "Alice", action: inLeafSpec.action };
44
75
  };
45
76
 
77
+ // 4. Build the callable tree
46
78
  const api = apiTree(source, apiPaths, executor);
47
79
 
48
- const result = await api.users.profile.fetch("123");
80
+ // 5. Call your generated tree!
81
+ const user = await api.users.profile.fetch("123");
82
+ console.log(user);
83
+ // => { id: "123", name: "Alice", action: "fetch" }
49
84
  ```
50
85
 
51
- ## Contract
86
+ ---
52
87
 
53
- `apiTree(source, apiPaths, executor)` accepts exactly three responsibilities:
88
+ ## ⚡ Key Features (v2)
54
89
 
55
- 1. **source** — the domain/source JSON object.
56
- 2. **apiPaths** — a flat array of API paths such as `app.users.profile.fetch`.
57
- 3. **executor** — the function that decides what the selected operation actually does.
90
+ ### 1. Pre-Resolved Leaf Specification (`inLeafSpec`)
91
+ Your executor automatically receives the pre-resolved definition object directly from `source.json` under `inLeafSpec`. No need to write repetitive nested property access code!
58
92
 
59
- `@keshavsoft/api-tree` does not know Tally, XML, HTTP, databases, or business rules.
93
+ ### 2. Intelligent Root Handling
94
+ - **Single-Root Unwrapping**: When all paths share a common root namespace (e.g., `app.users.list`, `app.orders.create`), `api-tree` automatically unwraps the root so you call `api.users.list()` directly.
95
+ - **Multi-Root Preservation**: When paths span multiple top-level domains (e.g., `users.list` and `orders.create`), `api-tree` automatically preserves all top-level roots (`api.users.list()` and `api.orders.create()`).
96
+ - **Explicit Override**: You can pass `{ inUnwrapRoot: false }` to keep the root prefix intact.
60
97
 
61
- ## Validation
98
+ ### 3. Dual Signature Support
99
+ Supports both **positional** arguments and the **in-local named object** convention:
62
100
 
63
- The public entry point checks:
101
+ ```javascript
102
+ // Positional
103
+ const api = apiTree(source, apiPaths, executor, options);
64
104
 
65
- - `source` is a JSON-compatible object.
66
- - `apiPaths` is an array.
67
- - every API path is a string.
68
- - `executor` is a function.
105
+ // Named Object
106
+ const api = apiTree({
107
+ inSource: source,
108
+ inApiPaths: apiPaths,
109
+ inExecutor: executor,
110
+ inOptions: { inUnwrapRoot: false }
111
+ });
112
+ ```
69
113
 
70
- Invalid input produces a clear `TypeError` before the tree is built.
114
+ ### 4. Callable Hybrid Branches
115
+ If a path is both a callable node and has child branches (e.g. `api.users` and `api.users.profile`), `api-tree` attaches child branches directly onto the function:
116
+ ```javascript
117
+ await api.users(); // Callable root!
118
+ await api.users.profile(); // Child leaf also callable!
119
+ ```
71
120
 
72
- ## Blueprint examples
121
+ ---
73
122
 
74
- The package includes `src/v1/blueprint/source.json` and `src/v1/blueprint/api.json` as small reference examples. They are documentation/blueprint material, not hidden runtime configuration.
123
+ ## 📖 Execution Context Reference
75
124
 
76
- ## Architecture
125
+ Whenever an attached leaf function is invoked:
126
+ ```javascript
127
+ await api.users.profile.fetch("param1", "extraArg1", "extraArg2");
128
+ ```
77
129
 
78
- ```text
79
- Domain repository
80
- │
81
- ├── source.json
82
- ├── api.json
83
- └── execution code
84
- │
85
- │ source + paths + executor
86
- ▼
87
- @keshavsoft/api-tree
88
- │
89
- ▼
90
- callable runtime API
130
+ Your `executor` receives a single, standardized context object:
131
+
132
+ | Property | Type | Description |
133
+ | :--- | :--- | :--- |
134
+ | `inRoutePath` | `string` | The full dot-notation route path (e.g. `"app.users.profile.fetch"`). |
135
+ | `inParam` | `any` | The primary argument passed to the leaf method. |
136
+ | `inArgs` | `any[]` | Array of all additional arguments passed beyond `inParam`. |
137
+ | `inLeafSpec` | `object \| undefined` | The resolved leaf definition object found in `source.json`. |
138
+ | `inSource` | `object` | The complete raw `source` schema object. |
139
+ | `inPathSegments` | `string[]` | Array of path segments (e.g. `["app", "users", "profile", "fetch"]`). |
140
+
141
+ ---
142
+
143
+ ## 🛡️ Input Validation & Error Handling
144
+
145
+ `@keshavsoft/api-tree` performs strict pre-flight validation to catch contract misconfigurations early:
146
+
147
+ - **`source`**: Must be a non-null plain JSON object (throws `TypeError: source must be a JSON object.`).
148
+ - **`apiPaths`**: Must be an array of non-empty strings without empty segments (e.g., `"users..fetch"` or `"users."` throws `TypeError`).
149
+ - **`executor`**: Must be a valid callable function (throws `TypeError: executor must be a function.`).
150
+
151
+ ---
152
+
153
+ ## 🌍 Real-World Architecture Examples
154
+
155
+ ### Example A: Decoupling TallyPrime Runtimes
156
+ ```javascript
157
+ import apiTree from "@keshavsoft/api-tree";
158
+ import { source, apiPaths } from "tally-spec";
159
+ import tallyXmlExecutor from "./tallyXmlExecutor.js";
160
+
161
+ // Generates app.masters.unit.all(), app.company.fetch(), etc.
162
+ const tally = apiTree(source, apiPaths, tallyXmlExecutor);
163
+ const units = await tally.masters.unit.all();
164
+ ```
165
+
166
+ ### Example B: Dynamic HTTP / REST Client
167
+ ```javascript
168
+ import apiTree from "@keshavsoft/api-tree";
169
+
170
+ const endpoints = {
171
+ api: {
172
+ v1: {
173
+ users: { get: { method: "GET", url: "/api/v1/users" } }
174
+ }
175
+ }
176
+ };
177
+
178
+ const client = apiTree(endpoints, ["api.v1.users.get"], async ({ inLeafSpec, inParam }) => {
179
+ const res = await fetch(`${inLeafSpec.url}/${inParam}`, { method: inLeafSpec.method });
180
+ return res.json();
181
+ });
182
+
183
+ const user = await client.v1.users.get(42);
184
+ ```
185
+
186
+ ---
187
+
188
+ ## 🧪 Testing
189
+
190
+ The package includes a comprehensive test suite using Node's native test runner:
191
+
192
+ ```bash
193
+ npm test
91
194
  ```
92
195
 
93
- The domain repository owns the meaning and execution. `@keshavsoft/api-tree` only creates the navigable runtime surface.
196
+ ---
197
+
198
+ ## 📄 License
199
+
200
+ MIT © [KeshavSoft](https://github.com/keshavsoft)
package/docs/index.html CHANGED
@@ -1,26 +1,298 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <title>api-tree</title>
7
- <style>body{max-width:850px;margin:50px auto;padding:0 20px;font:16px/1.6 system-ui,sans-serif}code,pre{background:#f3f3f3;padding:3px 6px;border-radius:4px}pre{padding:16px;overflow:auto}h1{margin-bottom:4px}.muted{color:#666}</style>
8
- </head>
9
- <body>
10
- <h1>api-tree</h1>
11
- <p class="muted">A small runtime API-tree builder.</p>
12
- <h2>The story</h2>
13
- <p>A domain repository already knows its structure and its execution logic. api-tree does not replace that logic. It only turns a flat list of API paths into a callable runtime tree.</p>
14
- <pre>source JSON + API paths + executor
15
- ↓
16
- api-tree
17
- ↓
18
- app.users.profile.fetch()</pre>
19
- <h2>Three inputs</h2>
20
- <ul><li><b>source</b> — source/domain JSON</li><li><b>apiPaths</b> — flat array of dotted paths</li><li><b>executor</b> — external function that performs the operation</li></ul>
21
- <h2>Why separate it?</h2>
22
- <p>The route mechanism is reusable. Tally XML, Tally JSON, Tally Simple, or another domain can keep its own JSON and execution code while sharing the same runtime API-tree mechanism.</p>
23
- <h2>Validation</h2>
24
- <p>The public function rejects invalid source, API-path arrays, and executors with clear TypeErrors.</p>
25
- </body>
26
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>@keshavsoft/api-tree — Declarative Runtime API Tree Builder</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg: #0b0f19;
13
+ --card-bg: #111827;
14
+ --card-border: #1f2937;
15
+ --text: #f3f4f6;
16
+ --text-muted: #9ca3af;
17
+ --primary: #3b82f6;
18
+ --primary-light: #60a5fa;
19
+ --accent: #10b981;
20
+ --code-bg: #030712;
21
+ --border: #374151;
22
+ }
23
+
24
+ * { box-sizing: border-box; margin: 0; padding: 0; }
25
+ body {
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ line-height: 1.6;
30
+ padding: 0 1.5rem 4rem;
31
+ }
32
+
33
+ .container {
34
+ max-width: 900px;
35
+ margin: 0 auto;
36
+ }
37
+
38
+ header {
39
+ padding: 4rem 0 2.5rem;
40
+ border-bottom: 1px solid var(--border);
41
+ margin-bottom: 3rem;
42
+ }
43
+
44
+ .badge-bar {
45
+ display: flex;
46
+ gap: 0.5rem;
47
+ margin-bottom: 1rem;
48
+ flex-wrap: wrap;
49
+ }
50
+
51
+ .badge {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ padding: 0.25rem 0.65rem;
55
+ border-radius: 9999px;
56
+ font-size: 0.75rem;
57
+ font-weight: 600;
58
+ background: #1e293b;
59
+ color: #94a3b8;
60
+ border: 1px solid #334155;
61
+ }
62
+ .badge.green { background: #064e3b; color: #34d399; border-color: #059669; }
63
+ .badge.blue { background: #1e3a8a; color: #93c5fd; border-color: #2563eb; }
64
+
65
+ h1 {
66
+ font-size: 2.75rem;
67
+ font-weight: 800;
68
+ letter-spacing: -0.03em;
69
+ margin-bottom: 0.75rem;
70
+ background: linear-gradient(135deg, #ffffff 0%, #93c5fd 100%);
71
+ -webkit-background-clip: text;
72
+ -webkit-text-fill-color: transparent;
73
+ }
74
+
75
+ .tagline {
76
+ font-size: 1.25rem;
77
+ color: var(--text-muted);
78
+ max-width: 700px;
79
+ }
80
+
81
+ h2 {
82
+ font-size: 1.75rem;
83
+ font-weight: 700;
84
+ margin: 2.5rem 0 1rem;
85
+ color: #ffffff;
86
+ border-bottom: 1px solid var(--border);
87
+ padding-bottom: 0.5rem;
88
+ }
89
+
90
+ h3 {
91
+ font-size: 1.25rem;
92
+ font-weight: 600;
93
+ margin: 1.5rem 0 0.75rem;
94
+ color: var(--primary-light);
95
+ }
96
+
97
+ p, ul {
98
+ color: #d1d5db;
99
+ margin-bottom: 1.25rem;
100
+ }
101
+
102
+ ul { padding-left: 1.5rem; }
103
+ li { margin-bottom: 0.5rem; }
104
+
105
+ .formula-box {
106
+ background: linear-gradient(180deg, rgba(30, 58, 138, 0.2) 0%, rgba(17, 24, 39, 0.6) 100%);
107
+ border: 1px solid #2563eb;
108
+ border-radius: 12px;
109
+ padding: 1.5rem;
110
+ margin: 2rem 0;
111
+ text-align: center;
112
+ font-size: 1.15rem;
113
+ font-weight: 600;
114
+ color: #bfdbfe;
115
+ }
116
+
117
+ pre {
118
+ background: var(--code-bg);
119
+ border: 1px solid var(--card-border);
120
+ border-radius: 8px;
121
+ padding: 1.25rem;
122
+ overflow-x: auto;
123
+ font-family: 'Fira Code', monospace;
124
+ font-size: 0.9rem;
125
+ margin-bottom: 1.5rem;
126
+ }
127
+
128
+ code {
129
+ font-family: 'Fira Code', monospace;
130
+ color: #93c5fd;
131
+ }
132
+
133
+ pre code {
134
+ color: #e2e8f0;
135
+ }
136
+
137
+ .grid {
138
+ display: grid;
139
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
140
+ gap: 1.25rem;
141
+ margin: 1.5rem 0;
142
+ }
143
+
144
+ .card {
145
+ background: var(--card-bg);
146
+ border: 1px solid var(--card-border);
147
+ border-radius: 10px;
148
+ padding: 1.25rem;
149
+ }
150
+
151
+ .card h4 {
152
+ color: #60a5fa;
153
+ margin-bottom: 0.5rem;
154
+ font-size: 1.05rem;
155
+ }
156
+
157
+ .card p {
158
+ color: var(--text-muted);
159
+ font-size: 0.9rem;
160
+ margin-bottom: 0;
161
+ }
162
+
163
+ table {
164
+ width: 100%;
165
+ border-collapse: collapse;
166
+ margin: 1.5rem 0;
167
+ }
168
+
169
+ th, td {
170
+ border: 1px solid var(--border);
171
+ padding: 0.75rem 1rem;
172
+ text-align: left;
173
+ font-size: 0.9rem;
174
+ }
175
+
176
+ th {
177
+ background: var(--card-bg);
178
+ color: #ffffff;
179
+ }
180
+
181
+ td {
182
+ background: rgba(17, 24, 39, 0.4);
183
+ }
184
+
185
+ footer {
186
+ margin-top: 4rem;
187
+ padding-top: 2rem;
188
+ border-top: 1px solid var(--border);
189
+ color: var(--text-muted);
190
+ font-size: 0.85rem;
191
+ text-align: center;
192
+ }
193
+ </style>
194
+ </head>
195
+ <body>
196
+ <div class="container">
197
+ <header>
198
+ <div class="badge-bar">
199
+ <span class="badge blue">@keshavsoft/api-tree</span>
200
+ <span class="badge green">v2.0.0</span>
201
+ <span class="badge">Zero Dependencies</span>
202
+ <span class="badge">Node &gt;= 20.10</span>
203
+ </div>
204
+ <h1>@keshavsoft/api-tree</h1>
205
+ <p class="tagline">A declarative, ultra-lean routing engine that turns source schemas, API paths, and an external executor into a callable runtime API tree.</p>
206
+ </header>
207
+
208
+ <div class="formula-box">
209
+ Runtime Tree = Source Schema (JSON) + API Paths (Routes) + Custom Executor
210
+ </div>
211
+
212
+ <h2>Core Architecture</h2>
213
+ <p><code>@keshavsoft/api-tree</code> decouples specification contracts from runtime execution:</p>
214
+ <div class="grid">
215
+ <div class="card">
216
+ <h4>1. Source Schema</h4>
217
+ <p>The single source of truth containing leaf specifications, metadata, and domain contracts.</p>
218
+ </div>
219
+ <div class="card">
220
+ <h4>2. API Paths</h4>
221
+ <p>A flat, declarative array of dotted routes (e.g. <code>app.users.profile.fetch</code>).</p>
222
+ </div>
223
+ <div class="card">
224
+ <h4>3. Custom Executor</h4>
225
+ <p>A pure execution handler that receives standardized context and executes the operation.</p>
226
+ </div>
227
+ </div>
228
+
229
+ <h2>Quick Start</h2>
230
+ <pre><code>import apiTree from "@keshavsoft/api-tree";
231
+
232
+ const source = {
233
+ app: {
234
+ users: {
235
+ profile: {
236
+ fetch: { action: "fetch", resource: "User" }
237
+ }
238
+ }
239
+ }
240
+ };
241
+
242
+ const apiPaths = ["app.users.profile.fetch"];
243
+
244
+ const executor = async ({ inRoutePath, inLeafSpec, inParam }) => {
245
+ return { id: inParam, action: inLeafSpec.action };
246
+ };
247
+
248
+ const api = apiTree(source, apiPaths, executor);
249
+
250
+ // Call the navigable tree!
251
+ const result = await api.users.profile.fetch("123");
252
+ console.log(result); // { id: "123", action: "fetch" }</code></pre>
253
+
254
+ <h2>Execution Context</h2>
255
+ <p>When an attached leaf function is invoked, your executor receives a rich context object:</p>
256
+ <table>
257
+ <thead>
258
+ <tr>
259
+ <th>Property</th>
260
+ <th>Type</th>
261
+ <th>Description</th>
262
+ </tr>
263
+ </thead>
264
+ <tbody>
265
+ <tr>
266
+ <td><code>inRoutePath</code></td>
267
+ <td>string</td>
268
+ <td>The full dot-notation route path.</td>
269
+ </tr>
270
+ <tr>
271
+ <td><code>inParam</code></td>
272
+ <td>any</td>
273
+ <td>First argument passed to the leaf.</td>
274
+ </tr>
275
+ <tr>
276
+ <td><code>inArgs</code></td>
277
+ <td>any[]</td>
278
+ <td>Array of all additional arguments.</td>
279
+ </tr>
280
+ <tr>
281
+ <td><code>inLeafSpec</code></td>
282
+ <td>object</td>
283
+ <td>Pre-resolved leaf definition from source.json.</td>
284
+ </tr>
285
+ <tr>
286
+ <td><code>inSource</code></td>
287
+ <td>object</td>
288
+ <td>The raw source schema object.</td>
289
+ </tr>
290
+ </tbody>
291
+ </table>
292
+
293
+ <footer>
294
+ <p>MIT License &copy; KeshavSoft. Distributed via npm.</p>
295
+ </footer>
296
+ </div>
297
+ </body>
298
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keshavsoft/api-tree",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Build a callable runtime API tree from API paths, source JSON, and an external executor.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node --test test/test.js",
23
+ "test": "node --test test/test.js test/test-v2.js",
24
24
  "prepublishOnly": "npm test"
25
25
  },
26
26
  "repository": {
package/src/index.d.ts CHANGED
@@ -1,19 +1,69 @@
1
- export type ApiExecutor<TSource = Record<string, unknown>, TResult = unknown, TParam = unknown> = (input: {
2
- inRoutePath: string;
3
- inParam: TParam;
4
- inSource: TSource;
5
- }) => TResult | Promise<TResult>;
6
-
7
- export type ApiPaths = string[];
8
-
9
- export default function start<TSource = Record<string, unknown>, TResult = unknown, TParam = unknown>(
10
- source: TSource,
11
- apiPaths: ApiPaths,
12
- executor: ApiExecutor<TSource, TResult, TParam>
13
- ): Record<string, unknown>;
14
-
15
- export function start<TSource = Record<string, unknown>, TResult = unknown, TParam = unknown>(
16
- source: TSource,
17
- apiPaths: ApiPaths,
18
- executor: ApiExecutor<TSource, TResult, TParam>
19
- ): Record<string, unknown>;
1
+ /**
2
+ * Context passed to the executor function when a leaf method is invoked.
3
+ */
4
+ export interface ApiTreeExecutionContext<TSource = Record<string, any>, TLeafSpec = any> {
5
+ /** The full dot-notation path of the invoked leaf (e.g. "app.users.profile.fetch") */
6
+ inRoutePath: string;
7
+ /** The first argument passed to the leaf method */
8
+ inParam?: any;
9
+ /** Array of all remaining arguments passed to the leaf method */
10
+ inArgs: any[];
11
+ /** The complete source JSON object */
12
+ inSource: TSource;
13
+ /** The resolved leaf specification object from source.json, if found */
14
+ inLeafSpec?: TLeafSpec;
15
+ /** The segments of the route path split by "." */
16
+ inPathSegments: string[];
17
+ }
18
+
19
+ /**
20
+ * An executor function responsible for handling the execution of an invoked leaf.
21
+ */
22
+ export type ApiTreeExecutor<TSource = Record<string, any>, TReturn = any> = (
23
+ context: ApiTreeExecutionContext<TSource>
24
+ ) => Promise<TReturn> | TReturn;
25
+
26
+ /**
27
+ * Options for customizing api-tree building behavior.
28
+ */
29
+ export interface ApiTreeOptions {
30
+ /**
31
+ * Whether to unwrap a single shared root namespace.
32
+ * Default: true when all paths share one root namespace; false when paths have different roots.
33
+ */
34
+ inUnwrapRoot?: boolean;
35
+ }
36
+
37
+ /**
38
+ * Named argument object for apiTree.
39
+ */
40
+ export interface ApiTreeParams<TSource = Record<string, any>, TReturn = any> {
41
+ inSource: TSource;
42
+ inApiPaths: string[];
43
+ inExecutor: ApiTreeExecutor<TSource, TReturn>;
44
+ inOptions?: ApiTreeOptions;
45
+ }
46
+
47
+ /**
48
+ * Builds and returns a callable runtime API tree from source JSON, API paths, and an executor.
49
+ *
50
+ * @param source Source JSON object containing schema definitions
51
+ * @param apiPaths Array of dot-separated route paths (e.g. ["app.users.fetch"])
52
+ * @param executor Execution function invoked when a leaf is called
53
+ * @param options Optional configuration
54
+ */
55
+ export declare function apiTree<TSource = Record<string, any>, TReturn = any>(
56
+ source: TSource,
57
+ apiPaths: string[],
58
+ executor: ApiTreeExecutor<TSource, TReturn>,
59
+ options?: ApiTreeOptions
60
+ ): Record<string, any>;
61
+
62
+ /**
63
+ * Named argument signature for apiTree.
64
+ */
65
+ export declare function apiTree<TSource = Record<string, any>, TReturn = any>(
66
+ params: ApiTreeParams<TSource, TReturn>
67
+ ): Record<string, any>;
68
+
69
+ export default apiTree;
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { default } from "./v1/index.js";
2
- export * from "./v1/index.js";
1
+ export { default } from "./v2/index.js";
2
+ export * from "./v2/index.js";
@@ -0,0 +1,4 @@
1
+ [
2
+ "app.users.profile.fetch",
3
+ "app.reports.summary.fetch"
4
+ ]
@@ -0,0 +1,22 @@
1
+ {
2
+ "app": {
3
+ "users": {
4
+ "profile": {
5
+ "fetch": {
6
+ "action": "fetch",
7
+ "resource": "User",
8
+ "description": "Fetch user profile details"
9
+ }
10
+ }
11
+ },
12
+ "reports": {
13
+ "summary": {
14
+ "fetch": {
15
+ "action": "fetch",
16
+ "resource": "Report",
17
+ "description": "Fetch summary report"
18
+ }
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,33 @@
1
+ import validate from "./internal-working/validate/index.js";
2
+ import routeStart from "./internal-working/route/index.js";
3
+
4
+ const start = (inSource, inApiPaths, inExecutor, inOptions) => {
5
+ let source = inSource;
6
+ let paths = inApiPaths;
7
+ let executor = inExecutor;
8
+ let options = inOptions;
9
+
10
+ if (inSource && typeof inSource === "object" && "inSource" in inSource) {
11
+ source = inSource.inSource;
12
+ paths = inSource.inApiPaths;
13
+ executor = inSource.inExecutor;
14
+ options = inSource.inOptions;
15
+ }
16
+
17
+ validate({
18
+ inSource: source,
19
+ inApiPaths: paths,
20
+ inExecutor: executor,
21
+ inOptions: options
22
+ });
23
+
24
+ return routeStart({
25
+ inApiPaths: paths,
26
+ inSource: source,
27
+ inExecutor: executor,
28
+ inOptions: options
29
+ });
30
+ };
31
+
32
+ export default start;
33
+ export { start, start as apiTree };
@@ -0,0 +1,38 @@
1
+ import createLeafHandler from "./createLeafHandler.js";
2
+ import resolveLeafSpec from "./resolveLeafSpec.js";
3
+
4
+ const startFunc = ({ inTree, inPath, inSource, inExecutor }) => {
5
+ const localTree = inTree;
6
+ const localPath = inPath;
7
+ const localSource = inSource;
8
+ const localExecutor = inExecutor;
9
+
10
+ const parts = localPath.split(".");
11
+ const leafName = parts.pop();
12
+
13
+ let branch = localTree;
14
+ for (const segment of parts) {
15
+ branch[segment] ??= {};
16
+ branch = branch[segment];
17
+ }
18
+
19
+ const leafSpec = resolveLeafSpec({
20
+ inSource: localSource,
21
+ inPath: localPath
22
+ });
23
+
24
+ const handler = createLeafHandler({
25
+ inPath: localPath,
26
+ inSource: localSource,
27
+ inExecutor: localExecutor,
28
+ inLeafSpec: leafSpec
29
+ });
30
+
31
+ if (branch[leafName] && typeof branch[leafName] === "object") {
32
+ Object.assign(handler, branch[leafName]);
33
+ }
34
+
35
+ branch[leafName] = handler;
36
+ };
37
+
38
+ export default startFunc;
@@ -0,0 +1,25 @@
1
+ const startFunc = ({ inPath, inSource, inExecutor, inLeafSpec }) => {
2
+ const localPath = inPath;
3
+ const localSource = inSource;
4
+ const localExecutor = inExecutor;
5
+ const localLeafSpec = inLeafSpec;
6
+ const localPathSegments = localPath.split(".");
7
+
8
+ const handler = async (inParam, ...inArgs) => {
9
+ const localParam = inParam;
10
+ const localArgs = inArgs;
11
+
12
+ return await localExecutor({
13
+ inRoutePath: localPath,
14
+ inParam: localParam,
15
+ inArgs: localArgs,
16
+ inSource: localSource,
17
+ inLeafSpec: localLeafSpec,
18
+ inPathSegments: localPathSegments
19
+ });
20
+ };
21
+
22
+ return handler;
23
+ };
24
+
25
+ export default startFunc;
@@ -0,0 +1,35 @@
1
+ import attachPath from "./attachPath.js";
2
+
3
+ const startFunc = ({ inApiPaths, inSource, inExecutor, inOptions }) => {
4
+ const localApiPaths = inApiPaths;
5
+ const localSource = inSource;
6
+ const localExecutor = inExecutor;
7
+ const localOptions = inOptions ?? {};
8
+
9
+ const tree = {};
10
+
11
+ for (const path of localApiPaths) {
12
+ attachPath({
13
+ inTree: tree,
14
+ inPath: path,
15
+ inSource: localSource,
16
+ inExecutor: localExecutor
17
+ });
18
+ }
19
+
20
+ if (localApiPaths.length === 0) {
21
+ return tree;
22
+ }
23
+
24
+ const rootSegments = new Set(localApiPaths.map((path) => path.split(".")[0]));
25
+
26
+ if (rootSegments.size === 1) {
27
+ const sharedRoot = rootSegments.values().next().value;
28
+ const unwrap = localOptions.inUnwrapRoot !== false;
29
+ return unwrap && tree[sharedRoot] ? tree[sharedRoot] : tree;
30
+ }
31
+
32
+ return tree;
33
+ };
34
+
35
+ export default startFunc;
@@ -0,0 +1,22 @@
1
+ const startFunc = ({ inSource, inPath }) => {
2
+ const localSource = inSource;
3
+ const localPath = inPath;
4
+
5
+ if (!localSource || typeof localSource !== "object" || !localPath) {
6
+ return undefined;
7
+ }
8
+
9
+ const segments = localPath.split(".");
10
+ let current = localSource;
11
+
12
+ for (const segment of segments) {
13
+ if (current === null || typeof current !== "object" || !(segment in current)) {
14
+ return undefined;
15
+ }
16
+ current = current[segment];
17
+ }
18
+
19
+ return current;
20
+ };
21
+
22
+ export default startFunc;
@@ -0,0 +1,42 @@
1
+ import isPlainObject from "./isPlainObject.js";
2
+
3
+ const startFunc = ({ inSource, inApiPaths, inExecutor, inOptions }) => {
4
+ const localSource = inSource;
5
+ const localApiPaths = inApiPaths;
6
+ const localExecutor = inExecutor;
7
+ const localOptions = inOptions;
8
+
9
+ if (!isPlainObject({ inValue: localSource })) {
10
+ throw new TypeError("source must be a JSON object.");
11
+ }
12
+
13
+ if (!Array.isArray(localApiPaths)) {
14
+ throw new TypeError("apiPaths must be an array of strings.");
15
+ }
16
+
17
+ localApiPaths.forEach((path, index) => {
18
+ if (typeof path !== "string") {
19
+ throw new TypeError(`apiPaths must be an array of strings. apiPaths[${index}] must be a string.`);
20
+ }
21
+
22
+ const trimmed = path.trim();
23
+ if (trimmed.length === 0) {
24
+ throw new TypeError(`apiPaths[${index}] must not be an empty string.`);
25
+ }
26
+
27
+ const segments = trimmed.split(".");
28
+ if (segments.some((segment) => segment.length === 0)) {
29
+ throw new TypeError(`apiPaths[${index}] '${path}' contains an invalid empty path segment.`);
30
+ }
31
+ });
32
+
33
+ if (typeof localExecutor !== "function") {
34
+ throw new TypeError("executor must be a function.");
35
+ }
36
+
37
+ if (localOptions !== undefined && !isPlainObject({ inValue: localOptions })) {
38
+ throw new TypeError("options must be an object if provided.");
39
+ }
40
+ };
41
+
42
+ export default startFunc;
@@ -0,0 +1,12 @@
1
+ const startFunc = ({ inValue }) => {
2
+ const localValue = inValue;
3
+
4
+ if (localValue === null || typeof localValue !== "object") {
5
+ return false;
6
+ }
7
+
8
+ const prototype = Object.getPrototypeOf(localValue);
9
+ return prototype === Object.prototype || prototype === null;
10
+ };
11
+
12
+ export default startFunc;