@keshavsoft/api-tree 1.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.
- package/README.md +77 -52
- package/docs/api.html +123 -0
- package/docs/examples.html +100 -0
- package/docs/how-it-works.html +69 -0
- package/docs/index.html +95 -26
- package/docs/style.css +272 -0
- package/docs/validation.html +88 -0
- package/package.json +7 -3
- package/src/index.d.ts +65 -19
- package/src/index.js +2 -2
- package/src/v2/blueprint/api.json +4 -0
- package/src/v2/blueprint/source.json +22 -0
- package/src/v2/index.js +33 -0
- package/src/v2/internal-working/route/attachPath.js +38 -0
- package/src/v2/internal-working/route/createLeafHandler.js +25 -0
- package/src/v2/internal-working/route/index.js +35 -0
- package/src/v2/internal-working/route/resolveLeafSpec.js +22 -0
- package/src/v2/internal-working/validate/index.js +42 -0
- package/src/v2/internal-working/validate/isPlainObject.js +12 -0
- package/src/v3/index.js +34 -0
- package/src/v3/internal-working/buildTree.js +35 -0
- package/src/v4/blueprint/api.json +13 -0
- package/src/v4/blueprint/source.json +228 -0
- package/src/v4/index.js +29 -0
- package/src/v4/internal-working/buildTree.js +35 -0
- package/src/v4/internal-working/guards/index.js +15 -0
- package/src/v4/internal-working/guards/isFunction.js +9 -0
- package/src/v4/internal-working/guards/isObject.js +9 -0
- package/src/v4/internal-working/guards/isStringArray.js +15 -0
- package/src/v5/blueprint/api.json +13 -0
- package/src/v5/blueprint/source.json +228 -0
- package/src/v5/engine/guards/index.js +15 -0
- package/src/v5/engine/guards/isFunction.js +9 -0
- package/src/v5/engine/guards/isObject.js +9 -0
- package/src/v5/engine/guards/isStringArray.js +15 -0
- package/src/v5/engine/run.js +35 -0
- package/src/v5/index.js +29 -0
- package/src/v6/blueprint/api.json +13 -0
- package/src/v6/blueprint/source.json +228 -0
- package/src/v6/engine/guards/index.js +15 -0
- package/src/v6/engine/guards/isFunction.js +9 -0
- package/src/v6/engine/guards/isObject.js +9 -0
- package/src/v6/engine/guards/isStringArray.js +15 -0
- package/src/v6/engine/run.js +35 -0
- package/src/v6/index.js +29 -0
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
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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 — Guarantees & Validation</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 • 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">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 active">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 →</a>
|
|
26
|
+
</div>
|
|
27
|
+
</aside>
|
|
28
|
+
|
|
29
|
+
<main class="content">
|
|
30
|
+
<h1>Guarantees & Validation</h1>
|
|
31
|
+
<div class="lead">Fail-fast contract validation and reliability guarantees.</div>
|
|
32
|
+
|
|
33
|
+
<h2>Fail-Fast Philosophy</h2>
|
|
34
|
+
<p><code>@keshavsoft/api-tree</code> strictly enforces contract integrity before building any runtime objects. If any input is malformed, it throws a descriptive <code>TypeError</code> immediately so errors never leak into runtime invocations.</p>
|
|
35
|
+
|
|
36
|
+
<h2>Validation Rules</h2>
|
|
37
|
+
|
|
38
|
+
<h3>1. Source Object</h3>
|
|
39
|
+
<ul>
|
|
40
|
+
<li>Must be a non-null plain JavaScript object.</li>
|
|
41
|
+
<li>Arrays, primitives, and null throw: <code>TypeError: source must be a JSON object.</code></li>
|
|
42
|
+
</ul>
|
|
43
|
+
|
|
44
|
+
<h3>2. API Paths Array</h3>
|
|
45
|
+
<ul>
|
|
46
|
+
<li>Must be a JavaScript Array. Non-arrays throw: <code>TypeError: apiPaths must be an array of strings.</code></li>
|
|
47
|
+
<li>Every element must be a string. Non-string elements throw: <code>TypeError: apiPaths must be an array of strings. apiPaths[i] must be a string.</code></li>
|
|
48
|
+
<li>Empty strings throw: <code>TypeError: apiPaths[i] must not be an empty string.</code></li>
|
|
49
|
+
<li>Paths with empty segments (such as <code>"users..fetch"</code> or <code>"users."</code>) throw: <code>TypeError: apiPaths[i] contains an invalid empty path segment.</code></li>
|
|
50
|
+
</ul>
|
|
51
|
+
|
|
52
|
+
<h3>3. Executor Function</h3>
|
|
53
|
+
<ul>
|
|
54
|
+
<li>Must be a callable function. Non-functions throw: <code>TypeError: executor must be a function.</code></li>
|
|
55
|
+
</ul>
|
|
56
|
+
|
|
57
|
+
<h3>4. Options</h3>
|
|
58
|
+
<ul>
|
|
59
|
+
<li>If provided, options must be a plain object. Passing non-objects throws: <code>TypeError: options must be an object if provided.</code></li>
|
|
60
|
+
</ul>
|
|
61
|
+
|
|
62
|
+
<h2>Test Suite Verification</h2>
|
|
63
|
+
<p>The package is verified using Node's native test runner with 100% passing tests:</p>
|
|
64
|
+
<pre><code>npm test
|
|
65
|
+
|
|
66
|
+
✔ v2: builds a callable API tree with single root unwrapped
|
|
67
|
+
✔ v2: automatically preserves multiple root namespaces
|
|
68
|
+
✔ v2: supports explicit inUnwrapRoot: false option
|
|
69
|
+
✔ v2: supports multi-argument leaves and inPathSegments
|
|
70
|
+
✔ v2: supports hybrid branches (callable node with children)
|
|
71
|
+
✔ v2: rejects empty paths or empty segments
|
|
72
|
+
✔ v2: rejects invalid source, executor, and options
|
|
73
|
+
✔ builds a callable API tree
|
|
74
|
+
✔ supports multi-argument leaves
|
|
75
|
+
✔ supports named-object signature
|
|
76
|
+
✔ rejects a non-object source
|
|
77
|
+
✔ rejects non-array api paths
|
|
78
|
+
✔ rejects non-string api paths
|
|
79
|
+
✔ rejects a non-function executor</code></pre>
|
|
80
|
+
|
|
81
|
+
<div class="page-nav">
|
|
82
|
+
<a href="examples.html">← Examples</a>
|
|
83
|
+
<a href="index.html">Back to The Story →</a>
|
|
84
|
+
</div>
|
|
85
|
+
</main>
|
|
86
|
+
</div>
|
|
87
|
+
</body>
|
|
88
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keshavsoft/api-tree",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "6.1.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
|
|
23
|
+
"test": "node --test test/*.test.js",
|
|
24
24
|
"prepublishOnly": "npm test"
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
@@ -40,5 +40,9 @@
|
|
|
40
40
|
"license": "MIT",
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=20.10"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"fast-xml-parser": "^5.11.2",
|
|
46
|
+
"tally-spec": "^1.0.0"
|
|
43
47
|
}
|
|
44
|
-
}
|
|
48
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,19 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Context passed to the executor function when a leaf method is invoked.
|
|
3
|
+
*/
|
|
4
|
+
export interface ApiTreeExecutionContext<TSource = Record<string, 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
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An executor function responsible for handling the execution of an invoked leaf.
|
|
17
|
+
*/
|
|
18
|
+
export type ApiTreeExecutor<TSource = Record<string, any>, TReturn = any> = (
|
|
19
|
+
context: ApiTreeExecutionContext<TSource>
|
|
20
|
+
) => Promise<TReturn> | TReturn;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for customizing api-tree building behavior.
|
|
24
|
+
*/
|
|
25
|
+
export interface ApiTreeOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Whether to unwrap a single shared root namespace.
|
|
28
|
+
* Default: true when all paths share one root namespace; false when paths have different roots.
|
|
29
|
+
*/
|
|
30
|
+
inUnwrapRoot?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Named argument object for apiTree.
|
|
35
|
+
*/
|
|
36
|
+
export interface ApiTreeParams<TSource = Record<string, any>, TReturn = any> {
|
|
37
|
+
inSource: TSource;
|
|
38
|
+
inApiPaths: string[];
|
|
39
|
+
inExecutor: ApiTreeExecutor<TSource, TReturn>;
|
|
40
|
+
inOptions?: ApiTreeOptions;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds and returns a callable runtime API tree from source JSON, API paths, and an executor.
|
|
45
|
+
*
|
|
46
|
+
* @param source Source JSON object containing schema definitions
|
|
47
|
+
* @param apiPaths Array of dot-separated route paths (e.g. ["app.users.fetch"])
|
|
48
|
+
* @param executor Execution function invoked when a leaf is called
|
|
49
|
+
* @param options Optional configuration
|
|
50
|
+
*/
|
|
51
|
+
export declare function apiTree<TSource = Record<string, any>, TReturn = any>(
|
|
52
|
+
source: TSource,
|
|
53
|
+
apiPaths: string[],
|
|
54
|
+
executor: ApiTreeExecutor<TSource, TReturn>,
|
|
55
|
+
options?: ApiTreeOptions
|
|
56
|
+
): Record<string, any>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Named argument signature for apiTree.
|
|
60
|
+
*/
|
|
61
|
+
export declare function apiTree<TSource = Record<string, any>, TReturn = any>(
|
|
62
|
+
params: ApiTreeParams<TSource, TReturn>
|
|
63
|
+
): Record<string, any>;
|
|
64
|
+
|
|
65
|
+
export default apiTree;
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { default } from "./
|
|
2
|
-
export * from "./
|
|
1
|
+
export { default } from "./v6/index.js";
|
|
2
|
+
export * from "./v6/index.js";
|
|
@@ -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
|
+
}
|
package/src/v2/index.js
ADDED
|
@@ -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;
|