@keshavsoft/api-tree 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KeshavSoft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,93 @@
1
+ # @keshavsoft/api-tree
2
+
3
+ A small runtime API-tree builder.
4
+
5
+ `@keshavsoft/api-tree` does one job: it takes **source JSON**, **API paths**, and an **executor**, then returns a callable runtime tree.
6
+
7
+ ```text
8
+ source JSON
9
+ +
10
+ API paths
11
+ +
12
+ executor
13
+ ↓
14
+ api-tree
15
+ ↓
16
+ callable API
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```js
22
+ import apiTree from "@keshavsoft/api-tree";
23
+
24
+ const source = {
25
+ app: {
26
+ users: {
27
+ profile: {
28
+ fetch: { action: "fetch", resource: "users" }
29
+ }
30
+ }
31
+ }
32
+ };
33
+
34
+ const apiPaths = [
35
+ "app.users.profile.fetch"
36
+ ];
37
+
38
+ const executor = async ({ inRoutePath, inParam, inSource }) => {
39
+ return {
40
+ path: inRoutePath,
41
+ param: inParam,
42
+ spec: inSource.app.users.profile.fetch
43
+ };
44
+ };
45
+
46
+ const api = apiTree(source, apiPaths, executor);
47
+
48
+ const result = await api.users.profile.fetch("123");
49
+ ```
50
+
51
+ ## Contract
52
+
53
+ `apiTree(source, apiPaths, executor)` accepts exactly three responsibilities:
54
+
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.
58
+
59
+ `@keshavsoft/api-tree` does not know Tally, XML, HTTP, databases, or business rules.
60
+
61
+ ## Validation
62
+
63
+ The public entry point checks:
64
+
65
+ - `source` is a JSON-compatible object.
66
+ - `apiPaths` is an array.
67
+ - every API path is a string.
68
+ - `executor` is a function.
69
+
70
+ Invalid input produces a clear `TypeError` before the tree is built.
71
+
72
+ ## Blueprint examples
73
+
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.
75
+
76
+ ## Architecture
77
+
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
91
+ ```
92
+
93
+ The domain repository owns the meaning and execution. `@keshavsoft/api-tree` only creates the navigable runtime surface.
package/docs/api.md ADDED
@@ -0,0 +1,40 @@
1
+ # API Reference
2
+
3
+ ## `apiTree(source, apiPaths, executor)`
4
+
5
+ Builds and returns a callable runtime API tree.
6
+
7
+ ### `source`
8
+
9
+ A JSON-compatible object containing the domain definitions.
10
+
11
+ ### `apiPaths`
12
+
13
+ A flat array of dotted API paths.
14
+
15
+ ```js
16
+ [
17
+ "app.users.profile.fetch",
18
+ "app.reports.summary.fetch"
19
+ ]
20
+ ```
21
+
22
+ Nested arrays or non-string path entries are rejected.
23
+
24
+ ### `executor`
25
+
26
+ A function called when a generated leaf is invoked.
27
+
28
+ It receives:
29
+
30
+ ```js
31
+ {
32
+ inRoutePath,
33
+ inParam,
34
+ inSource
35
+ }
36
+ ```
37
+
38
+ ### Return value
39
+
40
+ A callable object representing the API paths.
@@ -0,0 +1,26 @@
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>
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@keshavsoft/api-tree",
3
+ "version": "1.0.0",
4
+ "description": "Build a callable runtime API tree from API paths, source JSON, and an external executor.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./src/index.d.ts",
9
+ "import": "./src/index.js",
10
+ "default": "./src/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "main": "./src/index.js",
15
+ "types": "./src/index.d.ts",
16
+ "files": [
17
+ "src",
18
+ "docs",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "test": "node --test test/test.js",
24
+ "prepublishOnly": "npm test"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/keshavsoft/api-tree.git"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "keywords": [
34
+ "api-tree",
35
+ "json-driven",
36
+ "runtime-api",
37
+ "api-paths",
38
+ "declarative-api"
39
+ ],
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=20.10"
43
+ }
44
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,19 @@
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>;
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default } from "./v1/index.js";
2
+ export * from "./v1/index.js";
@@ -0,0 +1,6 @@
1
+ [
2
+ "app.users.profile.fetch",
3
+ "app.users.settings.fetch",
4
+ "app.reports.summary.fetch",
5
+ "app.reports.metrics.fetch"
6
+ ]
@@ -0,0 +1,36 @@
1
+ {
2
+ "app": {
3
+ "users": {
4
+ "profile": {
5
+ "fetch": {
6
+ "action": "fetch",
7
+ "resource": "users",
8
+ "description": "Fetches user profile details by ID."
9
+ }
10
+ },
11
+ "settings": {
12
+ "fetch": {
13
+ "action": "fetch",
14
+ "resource": "user_settings",
15
+ "description": "Fetches user configuration settings by ID."
16
+ }
17
+ }
18
+ },
19
+ "reports": {
20
+ "summary": {
21
+ "fetch": {
22
+ "action": "fetch",
23
+ "resource": "daily_summary",
24
+ "description": "Fetches aggregate business summary by date."
25
+ }
26
+ },
27
+ "metrics": {
28
+ "fetch": {
29
+ "action": "fetch",
30
+ "resource": "system_metrics",
31
+ "description": "Fetches performance metrics by date."
32
+ }
33
+ }
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,48 @@
1
+ import routeStart from "./internal-working/route/index.js";
2
+
3
+ const isPlainObject = (value) => {
4
+ if (value === null || typeof value !== "object") return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ };
8
+
9
+ const validate = (inSource, inApiPaths, inExecutor) => {
10
+ if (!isPlainObject(inSource)) {
11
+ throw new TypeError("source must be a JSON object.");
12
+ }
13
+
14
+ if (!Array.isArray(inApiPaths)) {
15
+ throw new TypeError("apiPaths must be an array of strings.");
16
+ }
17
+
18
+ if (!inApiPaths.every((path) => typeof path === "string")) {
19
+ throw new TypeError("apiPaths must be an array of strings.");
20
+ }
21
+
22
+ if (typeof inExecutor !== "function") {
23
+ throw new TypeError("executor must be a function.");
24
+ }
25
+ };
26
+
27
+ const start = (inSource, inApiPaths, inExecutor) => {
28
+ let source = inSource;
29
+ let paths = inApiPaths;
30
+ let executor = inExecutor;
31
+
32
+ if (inSource && typeof inSource === "object" && "inSource" in inSource) {
33
+ source = inSource.inSource;
34
+ paths = inSource.inApiPaths;
35
+ executor = inSource.inExecutor;
36
+ }
37
+
38
+ validate(source, paths, executor);
39
+
40
+ return routeStart({
41
+ inApiPaths: paths,
42
+ inSource: source,
43
+ inExecutor: executor
44
+ });
45
+ };
46
+
47
+ export default start;
48
+ export { start };
@@ -0,0 +1,25 @@
1
+ import createLeafHandler from "./createLeafHandler.js";
2
+
3
+ const startFunc = ({ inTree, inPath, inSource, inExecutor }) => {
4
+ const localTree = inTree;
5
+ const localPath = inPath;
6
+ const localSource = inSource;
7
+ const localExecutor = inExecutor;
8
+
9
+ const parts = localPath.split(".");
10
+ const leafName = parts.pop();
11
+
12
+ let branch = localTree;
13
+ for (const segment of parts) {
14
+ branch[segment] ??= {};
15
+ branch = branch[segment];
16
+ }
17
+
18
+ branch[leafName] = createLeafHandler({
19
+ inPath: localPath,
20
+ inSource: localSource,
21
+ inExecutor: localExecutor
22
+ });
23
+ };
24
+
25
+ export default startFunc;
@@ -0,0 +1,19 @@
1
+ const startFunc = ({ inPath, inSource, inExecutor }) => {
2
+ const localPath = inPath;
3
+ const localSource = inSource;
4
+ const localExecutor = inExecutor;
5
+
6
+ return async (inParam, ...inArgs) => {
7
+ const localParam = inParam;
8
+ const localArgs = inArgs;
9
+
10
+ return await localExecutor({
11
+ inRoutePath: localPath,
12
+ inParam: localParam,
13
+ inArgs: localArgs,
14
+ inSource: localSource
15
+ });
16
+ };
17
+ };
18
+
19
+ export default startFunc;
@@ -0,0 +1,24 @@
1
+ import attachPath from "./attachPath.js";
2
+
3
+ const startFunc = ({ inApiPaths, inSource, inExecutor }) => {
4
+ const localApiPaths = inApiPaths;
5
+ const localSource = inSource;
6
+ const localExecutor = inExecutor;
7
+
8
+ const tree = {};
9
+
10
+ for (const path of localApiPaths) {
11
+ attachPath({
12
+ inTree: tree,
13
+ inPath: path,
14
+ inSource: localSource,
15
+ inExecutor: localExecutor
16
+ });
17
+ }
18
+
19
+ const rootNamespace = localApiPaths[0]?.split(".")[0];
20
+
21
+ return rootNamespace ? tree[rootNamespace] : tree;
22
+ };
23
+
24
+ export default startFunc;