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.
Files changed (42) hide show
  1. package/CHANGELOG.md +8 -8
  2. package/LICENSE +21 -21
  3. package/README.md +127 -70
  4. package/bin/tally-simple.js +175 -175
  5. package/docs/api.html +41 -0
  6. package/docs/api.md +20 -19
  7. package/docs/architecture.html +74 -0
  8. package/docs/architecture.md +65 -43
  9. package/docs/developer.html +57 -0
  10. package/docs/developer.md +41 -0
  11. package/docs/end-user.html +49 -0
  12. package/docs/end-user.md +34 -0
  13. package/docs/index.html +32 -0
  14. package/docs/repo-developer.html +91 -0
  15. package/docs/repo-developer.md +107 -0
  16. package/package.json +55 -47
  17. package/src/index.d.ts +20 -2
  18. package/src/index.js +8 -8
  19. package/src/v1/external-api/api.js +67 -67
  20. package/src/v1/external-api/api.json +4 -4
  21. package/src/v1/external-api/index.js +1 -1
  22. package/src/v1/index.js +1 -1
  23. package/src/v1/source.json +40 -40
  24. package/src/v1/traverse.js +103 -103
  25. package/src/v1/traverseObject/index.js +39 -39
  26. package/src/v2/external-api/api.js +67 -0
  27. package/src/v2/external-api/api.json +5 -0
  28. package/src/v2/external-api/index.js +1 -0
  29. package/src/v2/index.js +1 -0
  30. package/src/v2/source.json +40 -0
  31. package/src/v2/traverse.js +103 -0
  32. package/src/v2/traverseObject/index.js +39 -0
  33. package/src/v3/external-api/api.js +67 -0
  34. package/src/v3/external-api/api.json +6 -0
  35. package/src/v3/external-api/index.js +1 -0
  36. package/src/v3/index.js +1 -0
  37. package/src/v3/source.json +48 -0
  38. package/src/v3/traverse.js +103 -0
  39. package/src/v3/traverseObject/index.js +39 -0
  40. package/docs/cli.md +0 -32
  41. package/docs/development.md +0 -66
  42. package/docs/usage.md +0 -73
@@ -1,39 +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;
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;
@@ -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,5 @@
1
+ [
2
+ "tally.masters.units.fetch",
3
+ "tally.masters.stockItems.withBatches",
4
+ "tally.masters.ledgers.withGstDetails"
5
+ ]
@@ -0,0 +1 @@
1
+ export { createApi, default } from "./api.js";
@@ -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
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;",
7
+ '"': "&quot;",
8
+ "'": "&apos;"
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;
@@ -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,6 @@
1
+ [
2
+ "tally.masters.units.fetch",
3
+ "tally.masters.stockItems.withBatches",
4
+ "tally.masters.ledgers.withGstDetails",
5
+ "tally.masters.stockGroup.withParent"
6
+ ]
@@ -0,0 +1 @@
1
+ export { createApi, default } from "./api.js";
@@ -0,0 +1 @@
1
+ export { createApi, default } from "./external-api/api.js";
@@ -0,0 +1,48 @@
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
+ "stockGroup": {
39
+ "withParent": {
40
+ "tdl": {
41
+ "collection": "<TYPE>StockGroup</TYPE><FETCH>$$Alias:Name</FETCH><FETCH>Parent</FETCH>"
42
+ },
43
+ "action": "fetch"
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,103 @@
1
+ import traverseObject from "./traverseObject/index.js";
2
+
3
+ const escapeXml = (value) => value.replace(/[&<>"']/g, (character) => ({
4
+ "&": "&amp;",
5
+ "<": "&lt;",
6
+ ">": "&gt;",
7
+ '"': "&quot;",
8
+ "'": "&apos;"
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;
package/docs/cli.md DELETED
@@ -1,32 +0,0 @@
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
- ~~~