tally-simple 1.1.1 → 1.3.1
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/CHANGELOG.md +8 -8
- package/LICENSE +21 -21
- package/README.md +127 -70
- package/bin/tally-simple.js +175 -175
- package/docs/api.html +41 -0
- package/docs/api.md +20 -19
- package/docs/architecture.html +74 -0
- package/docs/architecture.md +65 -43
- package/docs/developer.html +57 -0
- package/docs/developer.md +41 -0
- package/docs/end-user.html +49 -0
- package/docs/end-user.md +34 -0
- package/docs/index.html +32 -0
- package/docs/repo-developer.html +91 -0
- package/docs/repo-developer.md +107 -0
- package/package.json +55 -47
- package/src/index.d.ts +20 -2
- package/src/index.js +8 -8
- package/src/v1/external-api/api.js +67 -67
- package/src/v1/external-api/api.json +4 -4
- package/src/v1/external-api/index.js +1 -1
- package/src/v1/index.js +1 -1
- package/src/v1/source.json +40 -40
- package/src/v1/traverse.js +103 -103
- package/src/v1/traverseObject/index.js +39 -39
- package/src/v2/external-api/api.js +67 -0
- package/src/v2/external-api/api.json +5 -0
- package/src/v2/external-api/index.js +1 -0
- package/src/v2/index.js +1 -0
- package/src/v2/source.json +40 -0
- package/src/v2/traverse.js +103 -0
- package/src/v2/traverseObject/index.js +39 -0
- package/src/v3/external-api/api.js +67 -0
- package/src/v3/external-api/api.json +6 -0
- package/src/v3/external-api/index.js +1 -0
- package/src/v3/index.js +1 -0
- package/src/v3/source.json +48 -0
- package/src/v3/traverse.js +103 -0
- package/src/v3/traverseObject/index.js +39 -0
- package/docs/cli.md +0 -32
- package/docs/development.md +0 -66
- package/docs/usage.md +0 -73
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 1.0.0
|
|
4
|
-
|
|
5
|
-
- Expose the Tally query API as an installable ESM package.
|
|
6
|
-
- Add `createTallyClient()` for endpoint, header, timeout, and fetch configuration.
|
|
7
|
-
- Add the `tally-simple` command for querying a published API path from a shell.
|
|
8
|
-
- Keep the default import as a ready-to-use client for backwards compatibility.
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
- Expose the Tally query API as an installable ESM package.
|
|
6
|
+
- Add `createTallyClient()` for endpoint, header, timeout, and fetch configuration.
|
|
7
|
+
- Add the `tally-simple` command for querying a published API path from a shell.
|
|
8
|
+
- Keep the default import as a ready-to-use client for backwards compatibility.
|
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -1,70 +1,127 @@
|
|
|
1
|
-
# Tally Simple
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
~~~
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
~~~
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
~~~
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
# Tally Simple
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/tally-simple)
|
|
4
|
+
[](https://github.com/keshavsoft/tally-simple/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Tally Simple is a small, typed JavaScript client and CLI for asking Tally for business data.
|
|
7
|
+
|
|
8
|
+
Choose a supported query, provide the company name, and receive Tally's response as XML. The same query paths work in an application and from a shell, so a quick experiment can grow into an automated job without changing the request model.
|
|
9
|
+
|
|
10
|
+
**[View the package on npm](https://www.npmjs.com/package/tally-simple)** · **[Open the visual guide](docs/index.html)**
|
|
11
|
+
|
|
12
|
+
## The story in one minute
|
|
13
|
+
|
|
14
|
+
Tally Simple sits between your code or shell and a Tally HTTP endpoint:
|
|
15
|
+
|
|
16
|
+
~~~text
|
|
17
|
+
your app / shell command
|
|
18
|
+
│ choose a public query + company
|
|
19
|
+
▼
|
|
20
|
+
Tally Simple
|
|
21
|
+
│ builds the XML request
|
|
22
|
+
▼
|
|
23
|
+
Tally HTTP endpoint
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
XML response text
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
It deliberately keeps the response raw. You can inspect it, save it, pipe it to another tool, or parse it with the XML library your application already uses.
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Node.js 20.10 or newer
|
|
34
|
+
- A Tally HTTP endpoint that accepts the request, usually http://localhost:9000
|
|
35
|
+
|
|
36
|
+
Tally Simple returns Tally's response body as XML text.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
~~~bash
|
|
41
|
+
npm install tally-simple
|
|
42
|
+
~~~
|
|
43
|
+
|
|
44
|
+
The package has no runtime dependencies and requires Node.js 20.10 or newer.
|
|
45
|
+
|
|
46
|
+
## Use it in JavaScript
|
|
47
|
+
|
|
48
|
+
~~~js
|
|
49
|
+
import tally from "tally-simple";
|
|
50
|
+
|
|
51
|
+
const xml = await tally.masters.units.fetch("Mani9");
|
|
52
|
+
console.log(xml);
|
|
53
|
+
~~~
|
|
54
|
+
|
|
55
|
+
The default import is a ready-to-use client. For a different endpoint, headers, timeout, or fetch implementation, use `createTallyClient()`:
|
|
56
|
+
|
|
57
|
+
~~~js
|
|
58
|
+
import { createTallyClient } from "tally-simple";
|
|
59
|
+
|
|
60
|
+
const tally = createTallyClient({
|
|
61
|
+
url: "http://localhost:9000",
|
|
62
|
+
timeout: 15_000
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const xml = await tally.masters.ledgers.withGstDetails("Mani9");
|
|
66
|
+
~~~
|
|
67
|
+
|
|
68
|
+
## Use it from the command line
|
|
69
|
+
|
|
70
|
+
Run a query with npm's command runner:
|
|
71
|
+
|
|
72
|
+
~~~bash
|
|
73
|
+
npx tally-simple masters.units.fetch --company Mani9
|
|
74
|
+
~~~
|
|
75
|
+
|
|
76
|
+
You can also include the root name:
|
|
77
|
+
|
|
78
|
+
~~~bash
|
|
79
|
+
npx tally-simple tally.masters.stockItems.withBatches \
|
|
80
|
+
--company Mani9 \
|
|
81
|
+
--url http://localhost:9000
|
|
82
|
+
~~~
|
|
83
|
+
|
|
84
|
+
The response is written to stdout, so it can be saved or piped:
|
|
85
|
+
|
|
86
|
+
~~~bash
|
|
87
|
+
npx tally-simple masters.ledgers.withGstDetails --company Mani9 > ledgers.xml
|
|
88
|
+
~~~
|
|
89
|
+
|
|
90
|
+
For environment-based use:
|
|
91
|
+
|
|
92
|
+
~~~bash
|
|
93
|
+
TALLY_COMPANY=Mani9 TALLY_URL=http://localhost:9000 \
|
|
94
|
+
npx tally-simple masters.units.fetch
|
|
95
|
+
~~~
|
|
96
|
+
|
|
97
|
+
Run npx tally-simple --help to see all CLI options.
|
|
98
|
+
|
|
99
|
+
In PowerShell, environment variables use this form:
|
|
100
|
+
|
|
101
|
+
~~~powershell
|
|
102
|
+
$env:TALLY_COMPANY = "Mani9"
|
|
103
|
+
$env:TALLY_URL = "http://localhost:9000"
|
|
104
|
+
npx tally-simple masters.units.fetch
|
|
105
|
+
~~~
|
|
106
|
+
|
|
107
|
+
## Available queries
|
|
108
|
+
|
|
109
|
+
| Query | Returns |
|
|
110
|
+
| --- | --- |
|
|
111
|
+
| masters.units.fetch | Units and their aliases |
|
|
112
|
+
| masters.stockItems.withBatches | Stock items, base units, and batch allocations |
|
|
113
|
+
| masters.ledgers.withGstDetails | Ledgers and GST registration details |
|
|
114
|
+
| masters.stockGroup.withParent | Stock groups and their parent groups |
|
|
115
|
+
|
|
116
|
+
Every query accepts one company name. Blank company names are rejected before a request is sent. HTTP errors include the status and response body.
|
|
117
|
+
|
|
118
|
+
## Choose your next step
|
|
119
|
+
|
|
120
|
+
- [JavaScript usage](docs/usage.md): configure the client, test without Tally, and use TypeScript.
|
|
121
|
+
- [CLI reference](docs/cli.md): see options, environment variables, piping, and errors.
|
|
122
|
+
- [Available query paths](docs/api.md): see the public API and the TDL each path requests.
|
|
123
|
+
- [Architecture story](docs/architecture.md): see how one definition drives the client and CLI.
|
|
124
|
+
- [Developer guide](docs/development.md): see versioning, declaration generation, and verification.
|
|
125
|
+
- [Visual guide](docs/index.html): a dependency-free HTML walkthrough of the same flow.
|
|
126
|
+
|
|
127
|
+
The source code is on [GitHub](https://github.com/keshavsoft/tally-simple), and the published package is on [npm](https://www.npmjs.com/package/tally-simple).
|
package/bin/tally-simple.js
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
import packageInfo from "../package.json" with { type: "json" };
|
|
6
|
-
import tally, { createTallyClient } from "../src/index.js";
|
|
7
|
-
|
|
8
|
-
const usage = [
|
|
9
|
-
"Usage:",
|
|
10
|
-
" tally-simple <api-path> --company <company> [options]",
|
|
11
|
-
"",
|
|
12
|
-
"Examples:",
|
|
13
|
-
" tally-simple masters.units.fetch --company Mani9",
|
|
14
|
-
" tally-simple tally.masters.ledgers.withGstDetails --company Mani9 --url http://localhost:9000",
|
|
15
|
-
"",
|
|
16
|
-
"Options:",
|
|
17
|
-
" -c, --company <name> Tally company name (or TALLY_COMPANY)",
|
|
18
|
-
" -u, --url <url> Tally HTTP endpoint (or TALLY_URL)",
|
|
19
|
-
" --header <k:v> Add a request header; may be repeated",
|
|
20
|
-
" --timeout <ms> Abort a request after the given number of milliseconds",
|
|
21
|
-
" -h, --help Show this help",
|
|
22
|
-
" -v, --version Show the package version",
|
|
23
|
-
"",
|
|
24
|
-
"The API path is one of the paths listed in docs/api.md. The response is
|
|
25
|
-
"to stdout
|
|
26
|
-
""
|
|
27
|
-
].join("\n");
|
|
28
|
-
|
|
29
|
-
const readValue = (args, index, option) => {
|
|
30
|
-
const value = args[index + 1];
|
|
31
|
-
|
|
32
|
-
if (!value || value.startsWith("-")) {
|
|
33
|
-
throw new Error(option + " requires a value.");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return value;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const parseHeader = (value) => {
|
|
40
|
-
const separator = value.indexOf(":");
|
|
41
|
-
|
|
42
|
-
if (separator < 1) {
|
|
43
|
-
throw new Error("Invalid header \"" + value + "\". Use the form name:value.");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return [
|
|
47
|
-
value.slice(0, separator).trim(),
|
|
48
|
-
value.slice(separator + 1).trim()
|
|
49
|
-
];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const parseArgs = (args) => {
|
|
53
|
-
const options = { headers: {} };
|
|
54
|
-
let apiPath;
|
|
55
|
-
|
|
56
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
57
|
-
const argument = args[index];
|
|
58
|
-
|
|
59
|
-
if (argument === "--help" || argument === "-h") {
|
|
60
|
-
return { help: true };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (argument === "--version" || argument === "-v") {
|
|
64
|
-
return { version: true };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (argument === "--company" || argument === "-c") {
|
|
68
|
-
options.company = readValue(args, index, argument);
|
|
69
|
-
index += 1;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (argument === "--url" || argument === "-u") {
|
|
74
|
-
options.url = readValue(args, index, argument);
|
|
75
|
-
index += 1;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (argument === "--timeout") {
|
|
80
|
-
const value = readValue(args, index, argument);
|
|
81
|
-
options.timeout = Number(value);
|
|
82
|
-
|
|
83
|
-
if (!Number.isFinite(options.timeout) || options.timeout <= 0) {
|
|
84
|
-
throw new Error("--timeout must be a positive number of milliseconds.");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
index += 1;
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (argument === "--header") {
|
|
92
|
-
const [name, value] = parseHeader(readValue(args, index, argument));
|
|
93
|
-
options.headers[name] = value;
|
|
94
|
-
index += 1;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (argument.startsWith("-")) {
|
|
99
|
-
throw new Error("Unknown option: " + argument);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (apiPath) {
|
|
103
|
-
throw new Error(
|
|
104
|
-
"Only one API path may be supplied; received \"" + argument + "\" too."
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
apiPath = argument;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { ...options, apiPath };
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const normalizePath = (apiPath) => apiPath.startsWith("tally.")
|
|
115
|
-
? apiPath.slice("tally.".length)
|
|
116
|
-
: apiPath;
|
|
117
|
-
|
|
118
|
-
const findEndpoint = (apiPath, client = tally) => {
|
|
119
|
-
const endpoint = normalizePath(apiPath).split(".").reduce(
|
|
120
|
-
(current, part) => current?.[part],
|
|
121
|
-
client
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
if (typeof endpoint !== "function") {
|
|
125
|
-
throw new Error("Unknown API path: " + apiPath);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return endpoint;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const run = async (args) => {
|
|
132
|
-
const parsed = parseArgs(args);
|
|
133
|
-
|
|
134
|
-
if (parsed.help) {
|
|
135
|
-
process.stdout.write(usage);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (parsed.version) {
|
|
140
|
-
process.stdout.write(packageInfo.version + "\n");
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!parsed.apiPath) {
|
|
145
|
-
throw new Error("An API path is required. Use --help to see examples.");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const company = parsed.company ?? process.env.TALLY_COMPANY;
|
|
149
|
-
|
|
150
|
-
if (!company) {
|
|
151
|
-
throw new Error("A company is required. Pass --company or set TALLY_COMPANY.");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const client = createTallyClient({
|
|
155
|
-
url: parsed.url ?? process.env.TALLY_URL,
|
|
156
|
-
headers: parsed.headers,
|
|
157
|
-
timeout: parsed.timeout
|
|
158
|
-
});
|
|
159
|
-
const endpoint = findEndpoint(parsed.apiPath, client);
|
|
160
|
-
const response = await endpoint(company);
|
|
161
|
-
|
|
162
|
-
process.stdout.write(response + "\n");
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const isMain = process.argv[1]
|
|
166
|
-
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
167
|
-
|
|
168
|
-
if (isMain) {
|
|
169
|
-
run(process.argv.slice(2)).catch((error) => {
|
|
170
|
-
process.stderr.write("Error: " + error.message + "\n\n" + usage);
|
|
171
|
-
process.exitCode = 1;
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export { findEndpoint, parseArgs, run };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import packageInfo from "../package.json" with { type: "json" };
|
|
6
|
+
import tally, { createTallyClient } from "../src/index.js";
|
|
7
|
+
|
|
8
|
+
const usage = [
|
|
9
|
+
"Usage:",
|
|
10
|
+
" tally-simple <api-path> --company <company> [options]",
|
|
11
|
+
"",
|
|
12
|
+
"Examples:",
|
|
13
|
+
" tally-simple masters.units.fetch --company Mani9",
|
|
14
|
+
" tally-simple tally.masters.ledgers.withGstDetails --company Mani9 --url http://localhost:9000",
|
|
15
|
+
"",
|
|
16
|
+
"Options:",
|
|
17
|
+
" -c, --company <name> Tally company name (or TALLY_COMPANY)",
|
|
18
|
+
" -u, --url <url> Tally HTTP endpoint (or TALLY_URL)",
|
|
19
|
+
" --header <k:v> Add a request header; may be repeated",
|
|
20
|
+
" --timeout <ms> Abort a request after the given number of milliseconds",
|
|
21
|
+
" -h, --help Show this help",
|
|
22
|
+
" -v, --version Show the package version",
|
|
23
|
+
"",
|
|
24
|
+
"The API path is one of the paths listed in docs/api.md. The response body is",
|
|
25
|
+
"written to stdout with a trailing newline, so it can be piped to another command.",
|
|
26
|
+
""
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
const readValue = (args, index, option) => {
|
|
30
|
+
const value = args[index + 1];
|
|
31
|
+
|
|
32
|
+
if (!value || value.startsWith("-")) {
|
|
33
|
+
throw new Error(option + " requires a value.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseHeader = (value) => {
|
|
40
|
+
const separator = value.indexOf(":");
|
|
41
|
+
|
|
42
|
+
if (separator < 1) {
|
|
43
|
+
throw new Error("Invalid header \"" + value + "\". Use the form name:value.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [
|
|
47
|
+
value.slice(0, separator).trim(),
|
|
48
|
+
value.slice(separator + 1).trim()
|
|
49
|
+
];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const parseArgs = (args) => {
|
|
53
|
+
const options = { headers: {} };
|
|
54
|
+
let apiPath;
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
57
|
+
const argument = args[index];
|
|
58
|
+
|
|
59
|
+
if (argument === "--help" || argument === "-h") {
|
|
60
|
+
return { help: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (argument === "--version" || argument === "-v") {
|
|
64
|
+
return { version: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (argument === "--company" || argument === "-c") {
|
|
68
|
+
options.company = readValue(args, index, argument);
|
|
69
|
+
index += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (argument === "--url" || argument === "-u") {
|
|
74
|
+
options.url = readValue(args, index, argument);
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (argument === "--timeout") {
|
|
80
|
+
const value = readValue(args, index, argument);
|
|
81
|
+
options.timeout = Number(value);
|
|
82
|
+
|
|
83
|
+
if (!Number.isFinite(options.timeout) || options.timeout <= 0) {
|
|
84
|
+
throw new Error("--timeout must be a positive number of milliseconds.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
index += 1;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (argument === "--header") {
|
|
92
|
+
const [name, value] = parseHeader(readValue(args, index, argument));
|
|
93
|
+
options.headers[name] = value;
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (argument.startsWith("-")) {
|
|
99
|
+
throw new Error("Unknown option: " + argument);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (apiPath) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Only one API path may be supplied; received \"" + argument + "\" too."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
apiPath = argument;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { ...options, apiPath };
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const normalizePath = (apiPath) => apiPath.startsWith("tally.")
|
|
115
|
+
? apiPath.slice("tally.".length)
|
|
116
|
+
: apiPath;
|
|
117
|
+
|
|
118
|
+
const findEndpoint = (apiPath, client = tally) => {
|
|
119
|
+
const endpoint = normalizePath(apiPath).split(".").reduce(
|
|
120
|
+
(current, part) => current?.[part],
|
|
121
|
+
client
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (typeof endpoint !== "function") {
|
|
125
|
+
throw new Error("Unknown API path: " + apiPath);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return endpoint;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const run = async (args) => {
|
|
132
|
+
const parsed = parseArgs(args);
|
|
133
|
+
|
|
134
|
+
if (parsed.help) {
|
|
135
|
+
process.stdout.write(usage);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (parsed.version) {
|
|
140
|
+
process.stdout.write(packageInfo.version + "\n");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!parsed.apiPath) {
|
|
145
|
+
throw new Error("An API path is required. Use --help to see examples.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const company = parsed.company ?? process.env.TALLY_COMPANY;
|
|
149
|
+
|
|
150
|
+
if (!company) {
|
|
151
|
+
throw new Error("A company is required. Pass --company or set TALLY_COMPANY.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const client = createTallyClient({
|
|
155
|
+
url: parsed.url ?? process.env.TALLY_URL,
|
|
156
|
+
headers: parsed.headers,
|
|
157
|
+
timeout: parsed.timeout
|
|
158
|
+
});
|
|
159
|
+
const endpoint = findEndpoint(parsed.apiPath, client);
|
|
160
|
+
const response = await endpoint(company);
|
|
161
|
+
|
|
162
|
+
process.stdout.write(response + "\n");
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const isMain = process.argv[1]
|
|
166
|
+
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
167
|
+
|
|
168
|
+
if (isMain) {
|
|
169
|
+
run(process.argv.slice(2)).catch((error) => {
|
|
170
|
+
process.stderr.write("Error: " + error.message + "\n\n" + usage);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { findEndpoint, parseArgs, run };
|
package/docs/api.html
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
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</title>
|
|
7
|
+
<style>
|
|
8
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Arial,sans-serif;max-width:900px;margin:40px auto;padding:0 24px;line-height:1.65;color:#202124}
|
|
9
|
+
nav{padding:12px 16px;margin-bottom:28px;background:#f5f5f5;border-radius:8px}
|
|
10
|
+
nav a{margin-right:16px;text-decoration:none}
|
|
11
|
+
h1{font-size:30px;margin-top:0}
|
|
12
|
+
h2{font-size:22px;margin-top:32px}
|
|
13
|
+
h3{font-size:18px;margin-top:24px}
|
|
14
|
+
pre{background:#f6f8fa;padding:16px;border-radius:8px;overflow:auto}
|
|
15
|
+
code{background:#f1f3f4;padding:2px 5px;border-radius:4px}
|
|
16
|
+
pre code{background:none;padding:0}
|
|
17
|
+
table{border-collapse:collapse;width:100%;margin:16px 0}
|
|
18
|
+
th,td{border:1px solid #ddd;padding:8px;text-align:left}
|
|
19
|
+
th{background:#f6f6f6}
|
|
20
|
+
a{color:#0969da}
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<nav>
|
|
25
|
+
<a href="../README.html">README</a>
|
|
26
|
+
<a href="end-user.html">End User</a>
|
|
27
|
+
<a href="developer.html">Application Developer</a>
|
|
28
|
+
<a href="repo-developer.html">Repository Developer</a>
|
|
29
|
+
<a href="api.html">API</a>
|
|
30
|
+
<a href="architecture.html">Architecture</a>
|
|
31
|
+
</nav>
|
|
32
|
+
<h1>API Reference</h1>
|
|
33
|
+
<p>The public API is generated from <code>src/v1/external-api/api.json</code>.</p>
|
|
34
|
+
<p>Current public paths:</p>
|
|
35
|
+
<pre><code>tally.masters.units.fetch
|
|
36
|
+
tally.masters.stockItems.withBatches</code></pre>
|
|
37
|
+
<p>Example:</p>
|
|
38
|
+
<pre><code>await tally.masters.units.fetch("Mani9");</code></pre>
|
|
39
|
+
<p>The exact public surface should be changed through <code>external-api/api.json</code>, followed by declaration generation.</p>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|