@keshavsoft/api-tree 2.0.0 → 6.1.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.
Files changed (36) hide show
  1. package/README.md +52 -134
  2. package/docs/api.html +123 -0
  3. package/docs/examples.html +100 -0
  4. package/docs/how-it-works.html +69 -0
  5. package/docs/index.html +63 -266
  6. package/docs/style.css +272 -0
  7. package/docs/validation.html +88 -0
  8. package/package.json +7 -3
  9. package/src/index.d.ts +1 -5
  10. package/src/index.js +2 -2
  11. package/src/v3/index.js +34 -0
  12. package/src/v3/internal-working/buildTree.js +35 -0
  13. package/src/v4/blueprint/api.json +13 -0
  14. package/src/v4/blueprint/source.json +228 -0
  15. package/src/v4/index.js +29 -0
  16. package/src/v4/internal-working/buildTree.js +35 -0
  17. package/src/v4/internal-working/guards/index.js +15 -0
  18. package/src/v4/internal-working/guards/isFunction.js +9 -0
  19. package/src/v4/internal-working/guards/isObject.js +9 -0
  20. package/src/v4/internal-working/guards/isStringArray.js +15 -0
  21. package/src/v5/blueprint/api.json +13 -0
  22. package/src/v5/blueprint/source.json +228 -0
  23. package/src/v5/engine/guards/index.js +15 -0
  24. package/src/v5/engine/guards/isFunction.js +9 -0
  25. package/src/v5/engine/guards/isObject.js +9 -0
  26. package/src/v5/engine/guards/isStringArray.js +15 -0
  27. package/src/v5/engine/run.js +35 -0
  28. package/src/v5/index.js +29 -0
  29. package/src/v6/blueprint/api.json +13 -0
  30. package/src/v6/blueprint/source.json +228 -0
  31. package/src/v6/engine/guards/index.js +15 -0
  32. package/src/v6/engine/guards/isFunction.js +9 -0
  33. package/src/v6/engine/guards/isObject.js +9 -0
  34. package/src/v6/engine/guards/isStringArray.js +15 -0
  35. package/src/v6/engine/run.js +35 -0
  36. package/src/v6/index.js +29 -0
package/docs/index.html CHANGED
@@ -3,296 +3,93 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
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
- }
6
+ <title>api-tree — The Situation &amp; Solution</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="layout">
11
+ <aside class="sidebar">
12
+ <div class="sidebar-brand">
13
+ <a href="index.html" class="brand-title">api-tree</a>
14
+ <div class="brand-subtitle">v2.0.0 &bull; KeshavSoft</div>
15
+ </div>
16
+ <div class="nav-label">Documentation</div>
17
+ <ul class="nav-list">
18
+ <li><a href="index.html" class="nav-link active">The Story</a></li>
19
+ <li><a href="how-it-works.html" class="nav-link">How It Works</a></li>
20
+ <li><a href="api.html" class="nav-link">API Reference</a></li>
21
+ <li><a href="examples.html" class="nav-link">Examples</a></li>
22
+ <li><a href="validation.html" class="nav-link">Guarantees</a></li>
23
+ </ul>
24
+ <div class="sidebar-footer">
25
+ <a href="https://github.com/keshavsoft/api-tree" target="_blank" rel="noopener">GitHub Repository &rarr;</a>
26
+ </div>
27
+ </aside>
132
28
 
133
- pre code {
134
- color: #e2e8f0;
135
- }
29
+ <main class="content">
30
+ <h1>api-tree</h1>
31
+ <div class="lead">A small, zero-dependency runtime API-tree builder.</div>
136
32
 
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
- }
33
+ <p>We had a situation in our codebase: we were writing function after function just to expose different endpoints.</p>
143
34
 
144
- .card {
145
- background: var(--card-bg);
146
- border: 1px solid var(--card-border);
147
- border-radius: 10px;
148
- padding: 1.25rem;
149
- }
35
+ <p>Yet under the hood, every single one of those functions was doing the exact same thing: calling one core execution function. The only difference from endpoint to endpoint was a few variables—like a resource name, an action, or a query string.</p>
150
36
 
151
- .card h4 {
152
- color: #60a5fa;
153
- margin-bottom: 0.5rem;
154
- font-size: 1.05rem;
155
- }
37
+ <p>Writing dozens of nearly identical wrapper functions created boilerplate, duplicated code, and made maintenance tedious.</p>
156
38
 
157
- .card p {
158
- color: var(--text-muted);
159
- font-size: 0.9rem;
160
- margin-bottom: 0;
161
- }
39
+ <h2>What We Did</h2>
162
40
 
163
- table {
164
- width: 100%;
165
- border-collapse: collapse;
166
- margin: 1.5rem 0;
167
- }
41
+ <p>We separated the parts that never change from the parts that do:</p>
168
42
 
169
- th, td {
170
- border: 1px solid var(--border);
171
- padding: 0.75rem 1rem;
172
- text-align: left;
173
- font-size: 0.9rem;
174
- }
43
+ <ul>
44
+ <li><strong>The Single Function (Executor)</strong>: The core execution engine is written exactly once. It handles the network requests, headers, and response parsing.</li>
45
+ <li><strong>The Variables (source.json)</strong>: The parameters that vary per endpoint are moved into a clean JSON contract.</li>
46
+ <li><strong>The Routes (api.json)</strong>: The allowed endpoints are defined in a flat array of dot-notation paths.</li>
47
+ </ul>
175
48
 
176
- th {
177
- background: var(--card-bg);
178
- color: #ffffff;
179
- }
49
+ <p><strong>api-tree</strong> marries the single executor function with the variable JSON, dynamically building the callable dot-notation tree at runtime.</p>
180
50
 
181
- td {
182
- background: rgba(17, 24, 39, 0.4);
183
- }
51
+ <h2>The Code</h2>
184
52
 
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>
53
+ <p>Instead of writing endless wrapper functions like this:</p>
207
54
 
208
- <div class="formula-box">
209
- Runtime Tree = Source Schema (JSON) + API Paths (Routes) + Custom Executor
210
- </div>
55
+ <pre><code>// The repetitive pattern
56
+ export const getUnits = () => execute({ resource: "Unit" });
57
+ export const getLedgers = () => execute({ resource: "Ledger" });
58
+ export const getCompany = () => execute({ resource: "Company" });</code></pre>
211
59
 
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>
60
+ <p>You write your executor once, put your variables in JSON, and let <code>api-tree</code> generate the surface:</p>
228
61
 
229
- <h2>Quick Start</h2>
230
- <pre><code>import apiTree from "@keshavsoft/api-tree";
62
+ <pre><code>import apiTree from "@keshavsoft/api-tree";
231
63
 
64
+ // 1. Only the variables
232
65
  const source = {
233
- app: {
234
- users: {
235
- profile: {
236
- fetch: { action: "fetch", resource: "User" }
237
- }
66
+ app: {
67
+ masters: {
68
+ unit: { all: { resource: "Unit", action: "fetch" } }
69
+ }
238
70
  }
239
- }
240
71
  };
241
72
 
242
- const apiPaths = ["app.users.profile.fetch"];
73
+ const apiPaths = ["app.masters.unit.all"];
243
74
 
244
- const executor = async ({ inRoutePath, inLeafSpec, inParam }) => {
245
- return { id: inParam, action: inLeafSpec.action };
75
+ // 2. The single executor function
76
+ const execute = async ({ inLeafSpec, inParam }) => {
77
+ return await dispatch(inLeafSpec.resource, inParam);
246
78
  };
247
79
 
248
- const api = apiTree(source, apiPaths, executor);
80
+ // 3. Build the callable tree
81
+ const app = apiTree(source, apiPaths, execute);
249
82
 
250
- // Call the navigable tree!
251
- const result = await api.users.profile.fetch("123");
252
- console.log(result); // { id: "123", action: "fetch" }</code></pre>
83
+ // 4. Call naturally
84
+ const units = await app.masters.unit.all();</code></pre>
253
85
 
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>
86
+ <p>When you need a new endpoint tomorrow, you do not write a new JavaScript function. You simply add a new entry to your JSON file, and it is immediately available on the tree.</p>
292
87
 
293
- <footer>
294
- <p>MIT License &copy; KeshavSoft. Distributed via npm.</p>
295
- </footer>
88
+ <div class="page-nav">
89
+ <span></span>
90
+ <a href="how-it-works.html">Next: How It Works &rarr;</a>
91
+ </div>
92
+ </main>
296
93
  </div>
297
94
  </body>
298
95
  </html>
package/docs/style.css ADDED
@@ -0,0 +1,272 @@
1
+ :root {
2
+ --bg: #0d1117;
3
+ --sidebar-bg: #090d13;
4
+ --card-bg: #161b22;
5
+ --border: #30363d;
6
+ --text: #e6edf3;
7
+ --text-muted: #8b949e;
8
+ --accent: #58a6ff;
9
+ --accent-hover: #79c0ff;
10
+ --active-bg: rgba(56, 139, 253, 0.15);
11
+ --code-bg: #161b22;
12
+ --callout-border: #388bfd;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ body {
22
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ line-height: 1.7;
26
+ min-height: 100vh;
27
+ }
28
+
29
+ .layout {
30
+ display: flex;
31
+ min-height: 100vh;
32
+ }
33
+
34
+ /* Sidebar Navigation (Left Side) */
35
+ .sidebar {
36
+ width: 250px;
37
+ background: var(--sidebar-bg);
38
+ border-right: 1px solid var(--border);
39
+ padding: 36px 20px 40px;
40
+ flex-shrink: 0;
41
+ position: sticky;
42
+ top: 0;
43
+ height: 100vh;
44
+ overflow-y: auto;
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .sidebar-brand {
50
+ margin-bottom: 32px;
51
+ }
52
+
53
+ .brand-title {
54
+ font-size: 1.25rem;
55
+ font-weight: 700;
56
+ color: #ffffff;
57
+ text-decoration: none;
58
+ display: block;
59
+ letter-spacing: -0.01em;
60
+ }
61
+
62
+ .brand-subtitle {
63
+ font-size: 0.8rem;
64
+ color: var(--text-muted);
65
+ margin-top: 4px;
66
+ }
67
+
68
+ .nav-label {
69
+ font-size: 0.72rem;
70
+ text-transform: uppercase;
71
+ letter-spacing: 0.08em;
72
+ color: var(--text-muted);
73
+ font-weight: 600;
74
+ margin-bottom: 12px;
75
+ }
76
+
77
+ .nav-list {
78
+ list-style: none;
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 4px;
82
+ margin-bottom: auto;
83
+ }
84
+
85
+ .nav-link {
86
+ display: flex;
87
+ align-items: center;
88
+ padding: 8px 12px;
89
+ border-radius: 6px;
90
+ font-size: 0.92rem;
91
+ color: var(--text-muted);
92
+ text-decoration: none;
93
+ transition: all 0.15s ease;
94
+ }
95
+
96
+ .nav-link:hover {
97
+ color: var(--text);
98
+ background: rgba(255, 255, 255, 0.04);
99
+ }
100
+
101
+ .nav-link.active {
102
+ color: var(--accent);
103
+ background: var(--active-bg);
104
+ font-weight: 600;
105
+ }
106
+
107
+ .sidebar-footer {
108
+ padding-top: 24px;
109
+ border-top: 1px solid var(--border);
110
+ font-size: 0.8rem;
111
+ color: var(--text-muted);
112
+ }
113
+
114
+ .sidebar-footer a {
115
+ color: var(--text-muted);
116
+ text-decoration: none;
117
+ }
118
+
119
+ .sidebar-footer a:hover {
120
+ color: var(--accent);
121
+ }
122
+
123
+ /* Main Content Area (Right Side) */
124
+ .content {
125
+ flex: 1;
126
+ max-width: 800px;
127
+ padding: 56px 64px 96px;
128
+ }
129
+
130
+ h1 {
131
+ font-size: 2.2rem;
132
+ font-weight: 700;
133
+ color: #ffffff;
134
+ margin-bottom: 8px;
135
+ letter-spacing: -0.025em;
136
+ }
137
+
138
+ .lead {
139
+ font-size: 1.15rem;
140
+ color: var(--text-muted);
141
+ margin-bottom: 40px;
142
+ }
143
+
144
+ h2 {
145
+ font-size: 1.45rem;
146
+ font-weight: 600;
147
+ color: #ffffff;
148
+ margin: 44px 0 16px;
149
+ letter-spacing: -0.015em;
150
+ }
151
+
152
+ h3 {
153
+ font-size: 1.15rem;
154
+ font-weight: 600;
155
+ color: #ffffff;
156
+ margin: 28px 0 12px;
157
+ }
158
+
159
+ p {
160
+ margin-bottom: 18px;
161
+ color: #c9d1d9;
162
+ font-size: 1.02rem;
163
+ }
164
+
165
+ ul, ol {
166
+ margin: 0 0 24px 24px;
167
+ color: #c9d1d9;
168
+ }
169
+
170
+ li {
171
+ margin-bottom: 10px;
172
+ font-size: 1.02rem;
173
+ }
174
+
175
+ strong {
176
+ color: #ffffff;
177
+ }
178
+
179
+ pre {
180
+ background: var(--card-bg);
181
+ border: 1px solid var(--border);
182
+ border-radius: 6px;
183
+ padding: 18px 22px;
184
+ overflow-x: auto;
185
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
186
+ font-size: 0.9rem;
187
+ line-height: 1.6;
188
+ margin: 20px 0 28px;
189
+ color: #e6edf3;
190
+ }
191
+
192
+ code {
193
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
194
+ font-size: 0.88em;
195
+ background: rgba(110, 118, 129, 0.2);
196
+ padding: 2px 6px;
197
+ border-radius: 4px;
198
+ color: #79c0ff;
199
+ }
200
+
201
+ pre code {
202
+ background: none;
203
+ padding: 0;
204
+ color: inherit;
205
+ }
206
+
207
+
208
+ table {
209
+ width: 100%;
210
+ border-collapse: collapse;
211
+ margin: 24px 0;
212
+ }
213
+
214
+ th, td {
215
+ border: 1px solid var(--border);
216
+ padding: 12px 16px;
217
+ text-align: left;
218
+ font-size: 0.92rem;
219
+ }
220
+
221
+ th {
222
+ background: var(--card-bg);
223
+ color: #ffffff;
224
+ font-weight: 600;
225
+ }
226
+
227
+ td {
228
+ background: rgba(22, 27, 34, 0.4);
229
+ }
230
+
231
+ .page-nav {
232
+ margin-top: 60px;
233
+ padding-top: 24px;
234
+ border-top: 1px solid var(--border);
235
+ display: flex;
236
+ justify-content: space-between;
237
+ }
238
+
239
+ .page-nav a {
240
+ color: var(--accent);
241
+ text-decoration: none;
242
+ font-size: 0.95rem;
243
+ font-weight: 500;
244
+ }
245
+
246
+ .page-nav a:hover {
247
+ color: var(--accent-hover);
248
+ text-decoration: underline;
249
+ }
250
+
251
+ /* Mobile Responsive */
252
+ @media (max-width: 820px) {
253
+ .layout {
254
+ flex-direction: column;
255
+ }
256
+ .sidebar {
257
+ width: 100%;
258
+ height: auto;
259
+ position: static;
260
+ border-right: none;
261
+ border-bottom: 1px solid var(--border);
262
+ padding: 24px 20px;
263
+ }
264
+ .nav-list {
265
+ flex-direction: row;
266
+ flex-wrap: wrap;
267
+ gap: 8px;
268
+ }
269
+ .content {
270
+ padding: 36px 20px 64px;
271
+ }
272
+ }