tally-simple 1.1.1 → 1.8.2
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 +176 -70
- package/bin/tally-simple.js +170 -175
- package/docs/.nojekyll +1 -0
- package/docs/architecture.html +73 -0
- package/docs/architecture.md +82 -43
- package/docs/execution-pipeline.html +71 -0
- package/docs/execution-pipeline.md +158 -0
- package/docs/index.html +294 -0
- package/docs/queries.html +107 -0
- package/docs/queries.md +122 -0
- package/docs/route-engine.html +57 -0
- package/docs/route-engine.md +141 -0
- package/docs/style.css +137 -0
- package/docs/usage.html +82 -0
- package/docs/usage.md +117 -73
- package/package.json +61 -47
- package/src/index.js +2 -8
- package/src/v5/external-api/api.js +21 -0
- package/src/v5/external-api/api.json +6 -0
- package/src/v5/index.js +1 -0
- package/src/v5/internal-working/execution/buildXmlBody.js +41 -0
- package/src/v5/internal-working/execution/getTdl.js +12 -0
- package/src/v5/internal-working/execution/index.js +30 -0
- package/src/v5/internal-working/execution/postHttp.js +26 -0
- package/src/v5/internal-working/execution/validateCompany.js +11 -0
- package/src/v5/internal-working/route/attachPath.js +25 -0
- package/src/v5/internal-working/route/createLeafHandler.js +17 -0
- package/src/v5/internal-working/route/index.js +24 -0
- package/src/{v1 → v5}/source.json +38 -40
- package/src/v6/external-api/api.js +21 -0
- package/src/v6/external-api/api.json +6 -0
- package/src/v6/index.js +1 -0
- package/src/v6/internal-working/execution/buildXmlBody.js +41 -0
- package/src/v6/internal-working/execution/getTdl.js +12 -0
- package/src/v6/internal-working/execution/index.js +30 -0
- package/src/v6/internal-working/execution/postHttp.js +26 -0
- package/src/v6/internal-working/execution/validateCompany.js +11 -0
- package/src/v6/internal-working/route/attachPath.js +25 -0
- package/src/v6/internal-working/route/createLeafHandler.js +17 -0
- package/src/v6/internal-working/route/index.js +24 -0
- package/src/v6/source.json +38 -0
- package/src/v7/api.json +6 -0
- package/src/v7/index.js +14 -0
- package/src/v7/internal-working/execution/buildXmlBody.js +41 -0
- package/src/v7/internal-working/execution/getTdl.js +12 -0
- package/src/v7/internal-working/execution/index.js +30 -0
- package/src/v7/internal-working/execution/postHttp.js +26 -0
- package/src/v7/internal-working/execution/validateCompany.js +11 -0
- package/src/v7/internal-working/route/attachPath.js +25 -0
- package/src/v7/internal-working/route/createLeafHandler.js +17 -0
- package/src/v7/internal-working/route/index.js +24 -0
- package/src/v7/source.json +38 -0
- package/src/v8/engine/index.js +18 -0
- package/src/v8/engine/parseXml.js +11 -0
- package/src/v8/index.js +21 -0
- package/docs/api.md +0 -19
- package/docs/cli.md +0 -32
- package/docs/development.md +0 -66
- package/src/index.d.ts +0 -36
- package/src/v1/external-api/api.js +0 -67
- package/src/v1/external-api/api.json +0 -5
- package/src/v1/external-api/index.js +0 -1
- package/src/v1/index.js +0 -1
- package/src/v1/traverse.js +0 -103
- package/src/v1/traverseObject/index.js +0 -39
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,176 @@
|
|
|
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
|
+
[](LICENSE)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](package.json)
|
|
7
|
+
|
|
8
|
+
**A typed JavaScript client and CLI for querying business data from Tally via TDL definitions.**
|
|
9
|
+
|
|
10
|
+
Query Tally using declarative endpoint paths, supply a company name, and receive Tally's exact, raw XML response.
|
|
11
|
+
|
|
12
|
+
**[Documentation Hub](https://keshavsoft.github.io/tally-simple/)** · **[View on npm](https://www.npmjs.com/package/tally-simple)**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 📖 The Story of Tally Simple
|
|
17
|
+
|
|
18
|
+
Tally ERP and Tally Prime expose an HTTP XML/ODBC interface (defaulting to `http://localhost:9000`), but speaking to it directly is notoriously complex:
|
|
19
|
+
- Requests require heavy, boilerplate `<ENVELOPE>` XML wrappers.
|
|
20
|
+
- Field extraction depends on specific, case-sensitive TDL (Tally Definition Language) collection queries.
|
|
21
|
+
- Developers often end up copy-pasting raw XML string templates throughout their applications.
|
|
22
|
+
|
|
23
|
+
### The Purpose of `tally-simple`
|
|
24
|
+
`tally-simple` solves this by serving as the **pure transport and query execution engine** for Tally:
|
|
25
|
+
1. It encapsulates complex TDL collection definitions declaratively inside the library.
|
|
26
|
+
2. It provides an intuitive, autocomplete-friendly JavaScript API and matching CLI commands.
|
|
27
|
+
3. It deliberately returns **pure raw XML strings** from Tally.
|
|
28
|
+
|
|
29
|
+
### Why Raw XML?
|
|
30
|
+
Keeping the response as raw XML follows the **UNIX philosophy**:
|
|
31
|
+
- **Zero Loss**: No information or attributes are lost or warped by premature JSON conversion.
|
|
32
|
+
- **Pipeable & Inspectable**: Output can be redirected directly to files (`> units.xml`) or formatted with standard XML tools (`| xmllint`).
|
|
33
|
+
- **Downstream Decoupling**: Downstream layers (such as `tally-simple-json` or custom parsers) can parse and shape the XML as needed, without coupling transport to data formatting.
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
flowchart LR
|
|
37
|
+
A[Your App or CLI] -->|Path + Company Name| B[tally-simple<br/>Transport & TDL Engine]
|
|
38
|
+
B -->|TDL XML Request Envelope| C[Tally ERP / Prime<br/>http://localhost:9000]
|
|
39
|
+
C -->|Raw XML Response| B
|
|
40
|
+
B -->|Raw XML String| A
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## ⚡ Quick Start
|
|
46
|
+
|
|
47
|
+
### Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install tally-simple
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
*Requires Node.js 20.10+ and a running Tally instance with XML server enabled on `http://localhost:9000`.*
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 💻 JavaScript Usage (ESM)
|
|
58
|
+
|
|
59
|
+
Import the default `tally` client and call any query path by passing the company name:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
import tally from "tally-simple";
|
|
63
|
+
|
|
64
|
+
// Fetch units of measurement
|
|
65
|
+
const unitsXml = await tally.masters.units.fetch("Mani9");
|
|
66
|
+
console.log(unitsXml);
|
|
67
|
+
|
|
68
|
+
// Fetch stock items with batch allocations
|
|
69
|
+
const stockXml = await tally.masters.stockItems.withBatches("Mani9");
|
|
70
|
+
|
|
71
|
+
// Fetch ledgers with GST registration details
|
|
72
|
+
const ledgersXml = await tally.masters.ledgers.withGstDetails("Mani9");
|
|
73
|
+
|
|
74
|
+
// Fetch stock groups with parent hierarchies
|
|
75
|
+
const groupsXml = await tally.masters.stockGroup.withParent("Mani9");
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each query returns a `Promise<string>` resolving to Tally's complete raw XML response body.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## ⌨️ Command Line Interface (CLI)
|
|
83
|
+
|
|
84
|
+
Run any query directly from your terminal using `npx`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Query units
|
|
88
|
+
npx tally-simple masters.units.fetch --company Mani9
|
|
89
|
+
|
|
90
|
+
# Or include the 'tally.' root prefix
|
|
91
|
+
npx tally-simple tally.masters.stockItems.withBatches --company Mani9
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Piping and Saving Output
|
|
95
|
+
|
|
96
|
+
Because the response is written directly to `stdout`, you can seamlessly pipe or save the raw XML:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Save to file
|
|
100
|
+
npx tally-simple masters.units.fetch --company Mani9 > units.xml
|
|
101
|
+
|
|
102
|
+
# Format output using xmllint
|
|
103
|
+
npx tally-simple masters.ledgers.withGstDetails --company Mani9 | xmllint --format -
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 📋 Available Queries
|
|
109
|
+
|
|
110
|
+
All queries target master records and return Tally's native export collection XML:
|
|
111
|
+
|
|
112
|
+
| Query Path | Target Domain | TDL Collection Query | What it returns |
|
|
113
|
+
| :--- | :--- | :--- | :--- |
|
|
114
|
+
| `masters.units.fetch` | Unit | `<TYPE>Unit</TYPE><FETCH>$$Alias:Name</FETCH>` | Units of measurement and aliases |
|
|
115
|
+
| `masters.stockItems.withBatches` | StockItem | `<TYPE>StockItem</TYPE><FETCH>BaseUnits</FETCH><FETCH>BatchAllocations.*</FETCH>` | Stock items, base units, and batch allocations |
|
|
116
|
+
| `masters.ledgers.withGstDetails` | Ledger | `<TYPE>Ledger</TYPE><FETCH>LEDGSTREGDETAILS.LIST</FETCH>` | Accounting ledgers and GST details |
|
|
117
|
+
| `masters.stockGroup.withParent` | StockGroup | `<TYPE>StockGroup</TYPE><FETCH>Parent</FETCH>` | Stock groups and parent hierarchy |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 🧠 System Architecture
|
|
122
|
+
|
|
123
|
+
`tally-simple` is built on a clean, declarative 3-layer architecture:
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
127
|
+
│ 1. Declarative Specifications │
|
|
128
|
+
│ │
|
|
129
|
+
│ src/v7/api.json src/v7/source.json │
|
|
130
|
+
│ (Public Route Allowlist) (TDL Definition Tree) │
|
|
131
|
+
└──────────────────────────────┬──────────────────────────────┘
|
|
132
|
+
│
|
|
133
|
+
▼
|
|
134
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
135
|
+
│ 2. Public Composition Root │
|
|
136
|
+
│ │
|
|
137
|
+
│ src/v7/index.js │
|
|
138
|
+
│ (Binds Specifications to Engines via Dependency Injection)│
|
|
139
|
+
└──────────────────────────────┬──────────────────────────────┘
|
|
140
|
+
│
|
|
141
|
+
▼
|
|
142
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
143
|
+
│ 3. Internal Working Engines │
|
|
144
|
+
│ │
|
|
145
|
+
│ internal-working/route/ internal-working/execution│
|
|
146
|
+
│ (Builds Callable Object Tree) (4-Step Request Pipeline) │
|
|
147
|
+
└─────────────────────────────────────────────────────────────┘
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### The 4-Step Execution Pipeline
|
|
151
|
+
|
|
152
|
+
When any query method is called (e.g. `tally.masters.units.fetch("Mani9")`), execution flows through 4 focused steps:
|
|
153
|
+
|
|
154
|
+
1. **`validateCompany`**: Ensures the company name is a non-empty string and trims whitespace.
|
|
155
|
+
2. **`getTdl`**: Fast $O(1)$ reduction lookup into `source.json` to extract the corresponding TDL definition.
|
|
156
|
+
3. **`buildXmlBody`**: Safely escapes XML special characters and injects the company and TDL into the `<ENVELOPE>` request template.
|
|
157
|
+
4. **`postHttp`**: Sends an HTTP POST request to `http://localhost:9000` with `Content-Type: text/xml` and returns the raw response body.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📚 Documentation Guides
|
|
162
|
+
|
|
163
|
+
Explore the detailed architecture and usage guides:
|
|
164
|
+
|
|
165
|
+
- **[Documentation Hub](https://keshavsoft.github.io/tally-simple/)**: The interactive documentation portal.
|
|
166
|
+
- **[System Architecture](docs/architecture.md)**: Deep dive into the declarative design and layers.
|
|
167
|
+
- **[Execution Pipeline](docs/execution-pipeline.md)**: Step-by-step walkthrough of the 4-stage request flow.
|
|
168
|
+
- **[Route Assembly Engine](docs/route-engine.md)**: How the callable JavaScript tree is synthesized from `api.json`.
|
|
169
|
+
- **[Available Queries Reference](docs/queries.md)**: Complete TDL definitions and XML structures.
|
|
170
|
+
- **[Usage Guide](docs/usage.md)**: Advanced options, CLI piping, and TypeScript integration.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 📄 License
|
|
175
|
+
|
|
176
|
+
MIT © [KeshavSoft](https://github.com/keshavsoft)
|
package/bin/tally-simple.js
CHANGED
|
@@ -1,175 +1,170 @@
|
|
|
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
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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 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 endpoint = findEndpoint(parsed.apiPath, tally);
|
|
155
|
+
const response = await endpoint(company);
|
|
156
|
+
|
|
157
|
+
process.stdout.write(response + "\n");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const isMain = process.argv[1]
|
|
161
|
+
&& path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
162
|
+
|
|
163
|
+
if (isMain) {
|
|
164
|
+
run(process.argv.slice(2)).catch((error) => {
|
|
165
|
+
process.stderr.write("Error: " + error.message + "\n\n" + usage);
|
|
166
|
+
process.exitCode = 1;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export { findEndpoint, parseArgs, run };
|
package/docs/.nojekyll
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# disable jekyll
|