tally-simple 0.0.0-stage → 1.1.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 -0
- package/LICENSE +21 -0
- package/README.md +69 -2
- package/bin/tally-simple.js +175 -0
- package/docs/api.md +19 -0
- package/docs/architecture.md +43 -0
- package/docs/cli.md +32 -0
- package/docs/development.md +66 -0
- package/docs/usage.md +73 -0
- package/package.json +46 -5
- package/src/index.d.ts +36 -0
- package/src/index.js +8 -0
- package/src/v1/external-api/api.js +67 -0
- package/src/v1/external-api/api.json +5 -0
- package/src/v1/external-api/index.js +1 -0
- package/src/v1/index.js +1 -0
- package/src/v1/source.json +40 -0
- package/src/v1/traverse.js +103 -0
- package/src/v1/traverseObject/index.js +39 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +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.
|
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
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tally Simple
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Tally Simple gives JavaScript applications and shell scripts a straightforward way to ask Tally for business data.
|
|
4
|
+
|
|
5
|
+
Choose a supported query, provide the company name, and receive the response directly from Tally. The same queries work from an application or from the command line.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 20.10 or newer
|
|
10
|
+
- A Tally HTTP endpoint that accepts the request, usually http://localhost:9000
|
|
11
|
+
|
|
12
|
+
Tally Simple returns Tally's response body as XML text.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
~~~bash
|
|
17
|
+
npm install tally-simple
|
|
18
|
+
~~~
|
|
19
|
+
|
|
20
|
+
## Use it in JavaScript
|
|
21
|
+
|
|
22
|
+
~~~js
|
|
23
|
+
import tally from "tally-simple";
|
|
24
|
+
|
|
25
|
+
const xml = await tally.masters.units.fetch("Mani9");
|
|
26
|
+
console.log(xml);
|
|
27
|
+
~~~
|
|
28
|
+
|
|
29
|
+
## Use it from the command line
|
|
30
|
+
|
|
31
|
+
Run a query with npm's command runner:
|
|
32
|
+
|
|
33
|
+
~~~bash
|
|
34
|
+
npx tally-simple masters.units.fetch --company Mani9
|
|
35
|
+
~~~
|
|
36
|
+
|
|
37
|
+
You can also include the root name:
|
|
38
|
+
|
|
39
|
+
~~~bash
|
|
40
|
+
npx tally-simple tally.masters.stockItems.withBatches \
|
|
41
|
+
--company Mani9 \
|
|
42
|
+
--url http://localhost:9000
|
|
43
|
+
~~~
|
|
44
|
+
|
|
45
|
+
The response is written to stdout, so it can be saved or piped:
|
|
46
|
+
|
|
47
|
+
~~~bash
|
|
48
|
+
npx tally-simple masters.ledgers.withGstDetails --company Mani9 > ledgers.xml
|
|
49
|
+
~~~
|
|
50
|
+
|
|
51
|
+
For environment-based use:
|
|
52
|
+
|
|
53
|
+
~~~bash
|
|
54
|
+
TALLY_COMPANY=Mani9 TALLY_URL=http://localhost:9000 \
|
|
55
|
+
npx tally-simple masters.units.fetch
|
|
56
|
+
~~~
|
|
57
|
+
|
|
58
|
+
Run npx tally-simple --help to see all CLI options.
|
|
59
|
+
|
|
60
|
+
## Available queries
|
|
61
|
+
|
|
62
|
+
| Query | Returns |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| masters.units.fetch | Units and their aliases |
|
|
65
|
+
| masters.stockItems.withBatches | Stock items, base units, and batch allocations |
|
|
66
|
+
| masters.ledgers.withGstDetails | Ledgers and GST registration details |
|
|
67
|
+
|
|
68
|
+
Every query accepts one company name. Blank company names are rejected before a request is sent. HTTP errors include the status and response body.
|
|
69
|
+
|
|
70
|
+
More user-facing details are available in [usage](docs/usage.md), the [CLI reference](docs/cli.md), and the [available query list](docs/api.md).
|
|
@@ -0,0 +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 written",
|
|
25
|
+
"to stdout exactly as Tally returns it, 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.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Published API paths
|
|
2
|
+
|
|
3
|
+
These are the paths intentionally exposed by the package. The root tally is present on the default JavaScript import, while the CLI accepts paths with or without that root.
|
|
4
|
+
|
|
5
|
+
| JavaScript call | CLI path | TDL request |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| tally.masters.units.fetch(company) | masters.units.fetch | Unit with $$Alias:Name |
|
|
8
|
+
| tally.masters.stockItems.withBatches(company) | masters.stockItems.withBatches | StockItem with base units and batch allocations |
|
|
9
|
+
| tally.masters.ledgers.withGstDetails(company) | masters.ledgers.withGstDetails | Ledger with GST registration details |
|
|
10
|
+
|
|
11
|
+
All current calls:
|
|
12
|
+
|
|
13
|
+
- accept one company name;
|
|
14
|
+
- make a POST request with XML;
|
|
15
|
+
- return Tally's response body as a string;
|
|
16
|
+
- use the configured endpoint and headers;
|
|
17
|
+
- reject blank company names.
|
|
18
|
+
|
|
19
|
+
The source of truth for the request definitions is src/v1/source.json. The allowlist for this published surface is src/v1/external-api/api.json.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Architecture and the package story
|
|
2
|
+
|
|
3
|
+
## One definition, two ways to use it
|
|
4
|
+
|
|
5
|
+
The package has one query model and two entrypoints:
|
|
6
|
+
|
|
7
|
+
~~~text
|
|
8
|
+
src/v1/source.json
|
|
9
|
+
│
|
|
10
|
+
├── api.json ──> generated JavaScript client ──> application imports
|
|
11
|
+
│
|
|
12
|
+
└── request traversal ──> XML over HTTP ──> Tally
|
|
13
|
+
|
|
14
|
+
api.json ──> tally-simple CLI ──> stdout
|
|
15
|
+
~~~
|
|
16
|
+
|
|
17
|
+
source.json owns the TDL collection definitions and the default request envelope. api.json is the product boundary: only paths listed there become public methods or CLI commands.
|
|
18
|
+
|
|
19
|
+
## Request lifecycle
|
|
20
|
+
|
|
21
|
+
1. An application or CLI selects a public path.
|
|
22
|
+
2. The client validates and trims the company name.
|
|
23
|
+
3. The path is traversed into the source definition.
|
|
24
|
+
4. The company is XML-escaped and inserted into the request envelope.
|
|
25
|
+
5. The collection body is inserted into the envelope.
|
|
26
|
+
6. The configured fetch implementation sends the request to Tally.
|
|
27
|
+
7. The response body is returned as text; non-2xx responses become errors.
|
|
28
|
+
|
|
29
|
+
The default client is useful for a quick start. createTallyClient() provides the boundary for applications that need a different URL, headers, timeout, or fetch implementation.
|
|
30
|
+
|
|
31
|
+
## Adding a public endpoint
|
|
32
|
+
|
|
33
|
+
1. Add the TDL definition under src/v1/source.json.
|
|
34
|
+
2. Add its complete path to src/v1/external-api/api.json.
|
|
35
|
+
3. Run npm run generate:dts.
|
|
36
|
+
4. Add or update an offline test.
|
|
37
|
+
5. Run npm run verify.
|
|
38
|
+
|
|
39
|
+
Do not edit src/index.d.ts by hand; it is generated from the public path list.
|
|
40
|
+
|
|
41
|
+
## Publish boundary
|
|
42
|
+
|
|
43
|
+
package.json exposes the ESM entrypoint and CLI explicitly. Its files allowlist publishes src, bin, docs, and the package-facing metadata while excluding tests and development scripts. prepublishOnly runs declaration generation and the offline test suite before npm publish.
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
The package publishes the tally-simple executable. With npm, use it through npx:
|
|
4
|
+
|
|
5
|
+
~~~bash
|
|
6
|
+
npx tally-simple <api-path> --company <company>
|
|
7
|
+
~~~
|
|
8
|
+
|
|
9
|
+
## Options
|
|
10
|
+
|
|
11
|
+
| Option | Meaning |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| -c, --company <name> | Company to query. Also reads TALLY_COMPANY. |
|
|
14
|
+
| -u, --url <url> | Tally endpoint. Also reads TALLY_URL. |
|
|
15
|
+
| --header <name:value> | Adds a request header. Repeat it for multiple headers. |
|
|
16
|
+
| --timeout <ms> | Aborts a request after the given number of milliseconds. |
|
|
17
|
+
| -h, --help | Prints command help. |
|
|
18
|
+
| -v, --version | Prints the installed package version. |
|
|
19
|
+
|
|
20
|
+
The command accepts masters.units.fetch and tally.masters.units.fetch equivalently. The response is written unchanged to stdout; errors and usage help are written to stderr.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
|
|
24
|
+
~~~bash
|
|
25
|
+
npx tally-simple masters.units.fetch --company Mani9
|
|
26
|
+
|
|
27
|
+
npx tally-simple masters.stockItems.withBatches \
|
|
28
|
+
--company Mani9 \
|
|
29
|
+
--header X-Request-Source:nightly
|
|
30
|
+
|
|
31
|
+
TALLY_COMPANY=Mani9 npx tally-simple masters.units.fetch
|
|
32
|
+
~~~
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Developer guide
|
|
2
|
+
|
|
3
|
+
This document explains how Tally Simple is built and how to change it. End-user installation and query examples are kept in the root README.
|
|
4
|
+
|
|
5
|
+
## The developer story
|
|
6
|
+
|
|
7
|
+
Tally Simple keeps the TDL request definitions in one internal source and exposes only the queries deliberately selected for users.
|
|
8
|
+
|
|
9
|
+
~~~text
|
|
10
|
+
source.json
|
|
11
|
+
│
|
|
12
|
+
├── public API list ──> JavaScript client
|
|
13
|
+
│
|
|
14
|
+
└── request traversal ──> XML over HTTP ──> Tally
|
|
15
|
+
│
|
|
16
|
+
└──> CLI stdout
|
|
17
|
+
~~~
|
|
18
|
+
|
|
19
|
+
The source definitions describe how Tally should answer a query. The public API list is the product boundary: only listed paths become JavaScript methods and CLI commands.
|
|
20
|
+
|
|
21
|
+
## Repository map
|
|
22
|
+
|
|
23
|
+
- src/v1/source.json: Tally connection defaults, request envelope, and TDL collections.
|
|
24
|
+
- src/v1/external-api/api.json: public query allowlist.
|
|
25
|
+
- src/v1/external-api/api.js: builds the nested runtime API.
|
|
26
|
+
- src/v1/traverse.js: validates, builds, and sends a request.
|
|
27
|
+
- src/index.d.ts: generated TypeScript declarations.
|
|
28
|
+
- bin/tally-simple.js: command-line entrypoint.
|
|
29
|
+
- test/test.js and test/units.js: offline behavior and CLI tests.
|
|
30
|
+
- test/v1/units.js: manual integration smoke check against a reachable Tally instance; it is not part of npm test.
|
|
31
|
+
|
|
32
|
+
## Request lifecycle
|
|
33
|
+
|
|
34
|
+
1. The application or CLI selects a public query path.
|
|
35
|
+
2. The client validates and trims the company name.
|
|
36
|
+
3. The path is resolved in source.json.
|
|
37
|
+
4. The company name is XML-escaped and inserted into the request envelope.
|
|
38
|
+
5. The selected collection body is inserted into the envelope.
|
|
39
|
+
6. The configured fetch implementation sends the request to Tally.
|
|
40
|
+
7. The response body is returned as text; non-2xx responses become errors.
|
|
41
|
+
|
|
42
|
+
## Adding a query
|
|
43
|
+
|
|
44
|
+
1. Add or update the TDL definition in src/v1/source.json.
|
|
45
|
+
2. Add its complete path to src/v1/external-api/api.json.
|
|
46
|
+
3. Run npm run generate:dts.
|
|
47
|
+
4. Add or update an offline test.
|
|
48
|
+
5. Run npm run verify.
|
|
49
|
+
|
|
50
|
+
Do not edit src/index.d.ts by hand. It is generated from the source definition and public path list.
|
|
51
|
+
|
|
52
|
+
## Client configuration
|
|
53
|
+
|
|
54
|
+
createTallyClient() is the application boundary for a different URL, method, headers, timeout, or fetch implementation. The default import uses the default connection from source.json.
|
|
55
|
+
|
|
56
|
+
The fetch option is intentionally injectable so tests can run without a Tally installation or network connection.
|
|
57
|
+
|
|
58
|
+
## Verification and publishing
|
|
59
|
+
|
|
60
|
+
~~~bash
|
|
61
|
+
npm install
|
|
62
|
+
npm run verify
|
|
63
|
+
npm pack --dry-run
|
|
64
|
+
~~~
|
|
65
|
+
|
|
66
|
+
The prepack and prepublishOnly hooks run declaration generation and the offline test suite. The package files allowlist publishes the runtime, CLI, user documentation, and package metadata while excluding tests and development-only scripts.
|
package/docs/usage.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Usage
|
|
2
|
+
|
|
3
|
+
## The shortest library path
|
|
4
|
+
|
|
5
|
+
~~~js
|
|
6
|
+
import tally from "tally-simple";
|
|
7
|
+
|
|
8
|
+
const response = await tally.masters.units.fetch("Mani9");
|
|
9
|
+
~~~
|
|
10
|
+
|
|
11
|
+
Every endpoint takes a company name. Blank names are rejected before a request is sent.
|
|
12
|
+
|
|
13
|
+
## Configure a client
|
|
14
|
+
|
|
15
|
+
~~~js
|
|
16
|
+
import { createTallyClient } from "tally-simple";
|
|
17
|
+
|
|
18
|
+
const tally = createTallyClient({
|
|
19
|
+
url: "http://localhost:9000",
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: "Bearer example"
|
|
22
|
+
},
|
|
23
|
+
timeout: 15_000
|
|
24
|
+
});
|
|
25
|
+
~~~
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
|
|
29
|
+
- url: Tally HTTP endpoint. The default is http://localhost:9000.
|
|
30
|
+
- method: request method. The default is POST.
|
|
31
|
+
- headers: headers merged with the default Content-Type: text/xml.
|
|
32
|
+
- timeout: optional request timeout in milliseconds.
|
|
33
|
+
- fetch: optional fetch-compatible function, useful for tests or an application adapter.
|
|
34
|
+
|
|
35
|
+
The endpoint returns the response text. HTTP responses outside the 2xx range throw an error containing the status and response body.
|
|
36
|
+
|
|
37
|
+
## Test without Tally
|
|
38
|
+
|
|
39
|
+
~~~js
|
|
40
|
+
import { createTallyClient } from "tally-simple";
|
|
41
|
+
|
|
42
|
+
const tally = createTallyClient({
|
|
43
|
+
fetch: async (url, options) => {
|
|
44
|
+
console.log(url, options.body);
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
status: 200,
|
|
48
|
+
text: async () => "<ENVELOPE><STATUS>1</STATUS></ENVELOPE>"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const response = await tally.masters.units.fetch("Mani9");
|
|
54
|
+
~~~
|
|
55
|
+
|
|
56
|
+
The package escapes XML-sensitive characters in the company name before inserting it into the request.
|
|
57
|
+
|
|
58
|
+
## TypeScript
|
|
59
|
+
|
|
60
|
+
The package ships declarations generated from the public API definition:
|
|
61
|
+
|
|
62
|
+
~~~ts
|
|
63
|
+
import { createTallyClient } from "tally-simple";
|
|
64
|
+
|
|
65
|
+
const tally = createTallyClient();
|
|
66
|
+
const response: Promise<string> = tally.masters.units.fetch("Mani9");
|
|
67
|
+
~~~
|
|
68
|
+
|
|
69
|
+
Regenerate the declarations after changing an API path:
|
|
70
|
+
|
|
71
|
+
~~~bash
|
|
72
|
+
npm run generate:dts
|
|
73
|
+
~~~
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "tally-simple",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "A small, typed JavaScript client and CLI for querying Tally through TDL definitions.",
|
|
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
|
+
"bin": {
|
|
15
|
+
"tally-simple": "bin/tally-simple.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "./src/index.js",
|
|
18
|
+
"types": "./src/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"src",
|
|
22
|
+
"docs",
|
|
23
|
+
"README.md",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"generate:dts": "node generate-dts.js",
|
|
29
|
+
"test": "node --test test/test.js test/units.js",
|
|
30
|
+
"verify": "npm run generate:dts && npm test",
|
|
31
|
+
"prepack": "npm run verify",
|
|
32
|
+
"prepublishOnly": "npm run verify"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"tally",
|
|
36
|
+
"tdl",
|
|
37
|
+
"api",
|
|
38
|
+
"accounting"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20.10"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface TallyClientOptions {
|
|
2
|
+
url?: string;
|
|
3
|
+
method?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
fetch?: (url: string, options: {
|
|
7
|
+
method: string;
|
|
8
|
+
headers: Record<string, string>;
|
|
9
|
+
body: string;
|
|
10
|
+
signal?: unknown;
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
ok: boolean;
|
|
13
|
+
status: number;
|
|
14
|
+
text: () => Promise<string>;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type TallyApi = {
|
|
19
|
+
masters: {
|
|
20
|
+
units: {
|
|
21
|
+
fetch: (company: string) => Promise<string>;
|
|
22
|
+
};
|
|
23
|
+
stockItems: {
|
|
24
|
+
withBatches: (company: string) => Promise<string>;
|
|
25
|
+
};
|
|
26
|
+
ledgers: {
|
|
27
|
+
withGstDetails: (company: string) => Promise<string>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
declare const tally: TallyApi;
|
|
33
|
+
|
|
34
|
+
export declare const createTallyClient: (options?: TallyClientOptions) => TallyApi;
|
|
35
|
+
export declare const tally: TallyApi;
|
|
36
|
+
export default tally;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import source from "../source.json" with { type: "json" };
|
|
2
|
+
import traverse from "../traverse.js";
|
|
3
|
+
import apiPaths from "./api.json" with { type: "json" };
|
|
4
|
+
|
|
5
|
+
const createApi = (options = {}) => {
|
|
6
|
+
const connection = {
|
|
7
|
+
...source.tally.connection,
|
|
8
|
+
...options.connection,
|
|
9
|
+
...(options.url ? { url: options.url } : {}),
|
|
10
|
+
...(options.method ? { method: options.method } : {}),
|
|
11
|
+
headers: {
|
|
12
|
+
...source.tally.connection.headers,
|
|
13
|
+
...options.connection?.headers,
|
|
14
|
+
...options.headers
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const request = options.request ?? source.tally.request;
|
|
18
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
19
|
+
const clientSource = {
|
|
20
|
+
...source,
|
|
21
|
+
tally: {
|
|
22
|
+
...source.tally,
|
|
23
|
+
connection,
|
|
24
|
+
request
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const createFunction = (path) => async (company) => {
|
|
29
|
+
if (typeof company !== "string" || !company.trim()) {
|
|
30
|
+
throw new TypeError("Company name is required.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await traverse(clientSource, "", path, {
|
|
34
|
+
company: company.trim(),
|
|
35
|
+
connection,
|
|
36
|
+
request,
|
|
37
|
+
fetch: fetchImpl,
|
|
38
|
+
timeout: options.timeout
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const root = {};
|
|
43
|
+
|
|
44
|
+
for (const path of apiPaths) {
|
|
45
|
+
const parts = path.split(".");
|
|
46
|
+
let current = root;
|
|
47
|
+
|
|
48
|
+
parts.forEach((part, index) => {
|
|
49
|
+
const isLast = index === parts.length - 1;
|
|
50
|
+
|
|
51
|
+
if (isLast) {
|
|
52
|
+
current[part] = createFunction(path);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
current[part] ??= {};
|
|
57
|
+
current = current[part];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rootName = apiPaths[0]?.split(".")[0];
|
|
62
|
+
|
|
63
|
+
return rootName ? root[rootName] : root;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export { createApi };
|
|
67
|
+
export default createApi();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createApi, default } from "./api.js";
|
package/src/v1/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createApi, default } from "./external-api/api.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tally": {
|
|
3
|
+
"connection": {
|
|
4
|
+
"url": "http://localhost:9000",
|
|
5
|
+
"method": "POST",
|
|
6
|
+
"headers": {
|
|
7
|
+
"Content-Type": "text/xml"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"request": {
|
|
11
|
+
"body": "<ENVELOPE>\n<HEADER>\n<VERSION>1</VERSION>\n<TALLYREQUEST>Export</TALLYREQUEST>\n<TYPE>Collection</TYPE>\n<ID>TDLID</ID>\n</HEADER>\n<BODY>\n<DESC>\n<STATICVARIABLES>\n<SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT>\n<SVCURRENTCOMPANY>{company}</SVCURRENTCOMPANY>\n</STATICVARIABLES>\n<TDL>\n<TDLMESSAGE>\n<COLLECTION NAME=\"TDLID\">\n{collectionBody}</COLLECTION>\n</TDLMESSAGE>\n</TDL>\n</DESC>\n</BODY>\n</ENVELOPE>"
|
|
12
|
+
},
|
|
13
|
+
"masters": {
|
|
14
|
+
"units": {
|
|
15
|
+
"fetch": {
|
|
16
|
+
"tdl": {
|
|
17
|
+
"collection": "<TYPE>Unit</TYPE>\n<FETCH>$$Alias:Name</FETCH>\n"
|
|
18
|
+
},
|
|
19
|
+
"action": "fetch"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"stockItems": {
|
|
23
|
+
"withBatches": {
|
|
24
|
+
"tdl": {
|
|
25
|
+
"collection": "<TYPE>StockItem</TYPE><FETCH>$$Alias:Name</FETCH><FETCH>BaseUnits</FETCH><FETCH>BatchAllocations.*</FETCH>"
|
|
26
|
+
},
|
|
27
|
+
"action": "fetch"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"ledgers": {
|
|
31
|
+
"withGstDetails": {
|
|
32
|
+
"tdl": {
|
|
33
|
+
"collection": "<TYPE>Ledger</TYPE><FETCH>$$Alias:Name</FETCH><FETCH>LEDGSTREGDETAILS.LIST</FETCH>"
|
|
34
|
+
},
|
|
35
|
+
"action": "fetch"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import traverseObject from "./traverseObject/index.js";
|
|
2
|
+
|
|
3
|
+
const escapeXml = (value) => value.replace(/[&<>"']/g, (character) => ({
|
|
4
|
+
"&": "&",
|
|
5
|
+
"<": "<",
|
|
6
|
+
">": ">",
|
|
7
|
+
'"': """,
|
|
8
|
+
"'": "'"
|
|
9
|
+
}[character]));
|
|
10
|
+
|
|
11
|
+
const buildRequestBody = (requestBody, { company, collection }) => {
|
|
12
|
+
return requestBody
|
|
13
|
+
.replace("{company}", escapeXml(company))
|
|
14
|
+
.replace("{collectionBody}", collection);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const execute = async (connection, xml, fetchImpl, timeout) => {
|
|
18
|
+
if (typeof fetchImpl !== "function") {
|
|
19
|
+
throw new Error(
|
|
20
|
+
"No fetch implementation is available. Use Node.js 20+ or provide fetch to createTallyClient()."
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const controller = typeof AbortController === "function"
|
|
25
|
+
? new AbortController()
|
|
26
|
+
: undefined;
|
|
27
|
+
const timeoutId = Number.isFinite(timeout) && timeout > 0
|
|
28
|
+
? setTimeout(() => controller?.abort(), timeout)
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
31
|
+
let response;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
response = await fetchImpl(connection.url, {
|
|
35
|
+
method: connection.method,
|
|
36
|
+
headers: connection.headers,
|
|
37
|
+
body: xml,
|
|
38
|
+
...(controller ? { signal: controller.signal } : {})
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (controller?.signal.aborted) {
|
|
42
|
+
throw new Error(`Tally request timed out after ${timeout} ms.`, {
|
|
43
|
+
cause: error
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw error;
|
|
48
|
+
} finally {
|
|
49
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const responseText = await response.text();
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Tally request failed with HTTP ${response.status}: ${responseText}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return responseText;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const traverse = async (
|
|
64
|
+
raka,
|
|
65
|
+
relativePath,
|
|
66
|
+
pathToFind,
|
|
67
|
+
context = {}
|
|
68
|
+
) => {
|
|
69
|
+
if (relativePath === pathToFind) {
|
|
70
|
+
if (raka?.action === "fetch") {
|
|
71
|
+
const body = buildRequestBody(
|
|
72
|
+
context.request.body,
|
|
73
|
+
{
|
|
74
|
+
company: context.company,
|
|
75
|
+
collection: raka.tdl.collection
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return await execute(
|
|
80
|
+
context.connection,
|
|
81
|
+
body,
|
|
82
|
+
context.fetch,
|
|
83
|
+
context.timeout
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return raka;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof raka === "object" && raka !== null) {
|
|
91
|
+
return await traverseObject(
|
|
92
|
+
raka,
|
|
93
|
+
relativePath,
|
|
94
|
+
pathToFind,
|
|
95
|
+
context
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export { traverse };
|
|
103
|
+
export default traverse;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { traverse } from "../traverse.js";
|
|
2
|
+
|
|
3
|
+
const traverseObject = async (
|
|
4
|
+
specJson,
|
|
5
|
+
relativePath,
|
|
6
|
+
pathToFind,
|
|
7
|
+
context = {}
|
|
8
|
+
) => {
|
|
9
|
+
const nextContext = {
|
|
10
|
+
...context,
|
|
11
|
+
connection: specJson.connection ?? context.connection,
|
|
12
|
+
request: specJson.request ?? context.request
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
for (const [key, value] of Object.entries(specJson)) {
|
|
16
|
+
if (key === "connection" || key === "request") continue;
|
|
17
|
+
|
|
18
|
+
if (typeof value === "object" && value !== null) {
|
|
19
|
+
const nextPath = relativePath
|
|
20
|
+
? `${relativePath}.${key}`
|
|
21
|
+
: key;
|
|
22
|
+
|
|
23
|
+
const result = await traverse(
|
|
24
|
+
value,
|
|
25
|
+
nextPath,
|
|
26
|
+
pathToFind,
|
|
27
|
+
nextContext
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (result !== undefined) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default traverseObject;
|