adfinem 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CODE_OF_CONDUCT.md +21 -0
  3. package/CONTRIBUTING.md +29 -0
  4. package/LICENSE +21 -0
  5. package/README.md +86 -2
  6. package/SECURITY.md +13 -0
  7. package/catalogs/.gitkeep +0 -0
  8. package/catalogs/api-operations.yaml +21 -0
  9. package/catalogs/batches.yaml +74 -0
  10. package/catalogs/queries.yaml +75 -0
  11. package/config/environments.yaml +13 -0
  12. package/dist/actions/assert-db.js +3 -0
  13. package/dist/actions/run-eod.js +3 -0
  14. package/dist/adapters/api/api-collections.js +296 -0
  15. package/dist/adapters/api/body-utils.js +9 -0
  16. package/dist/adapters/api/rest-client.js +557 -0
  17. package/dist/adapters/api/soap-client.js +5 -0
  18. package/dist/adapters/db/assertions.js +87 -0
  19. package/dist/adapters/db/oracle-client.js +115 -0
  20. package/dist/adapters/db/query-catalog.js +75 -0
  21. package/dist/adapters/unix/batch-catalog.js +71 -0
  22. package/dist/adapters/unix/batch-input-files.js +36 -0
  23. package/dist/adapters/unix/batch-runner.js +382 -0
  24. package/dist/adapters/unix/ssh-client.js +228 -0
  25. package/dist/app/server.js +826 -0
  26. package/dist/cli.js +465 -0
  27. package/dist/config/environments.js +138 -0
  28. package/dist/config/registry.js +18 -0
  29. package/dist/config/secrets.js +123 -0
  30. package/dist/dsl/parser.js +20 -0
  31. package/dist/dsl/schema.js +182 -0
  32. package/dist/dsl/types.js +1 -0
  33. package/dist/dsl/validator.js +264 -0
  34. package/dist/engine/captures.js +68 -0
  35. package/dist/engine/context.js +69 -0
  36. package/dist/engine/evidence.js +33 -0
  37. package/dist/engine/known-errors.js +129 -0
  38. package/dist/engine/retry.js +13 -0
  39. package/dist/engine/runner.js +710 -0
  40. package/dist/engine/step-result.js +58 -0
  41. package/dist/flows/catalog-normalizer.js +72 -0
  42. package/dist/flows/compiler.js +237 -0
  43. package/dist/flows/concat.js +130 -0
  44. package/dist/flows/parser.js +21 -0
  45. package/dist/flows/schema.js +142 -0
  46. package/dist/flows/types.js +1 -0
  47. package/dist/flows/validator.js +470 -0
  48. package/dist/reports/html-report.js +112 -0
  49. package/dist/reports/junit-report.js +48 -0
  50. package/docs/.gitkeep +0 -0
  51. package/docs/DB_UNIX_OPERATIONS.md +118 -0
  52. package/docs/FLOW_BUILDER.md +87 -0
  53. package/flows/account_processing_cycle.flow.yaml +88 -0
  54. package/flows/new_flow.flow.yaml +22 -0
  55. package/package.json +92 -7
  56. package/scenarios/smoke/account-processing-smoke.yaml +44 -0
  57. package/scenarios/smoke/api-db-batch-check.yaml +40 -0
  58. package/src/actions/assert-db.ts +6 -0
  59. package/src/actions/run-eod.ts +6 -0
  60. package/src/adapters/api/api-collections.ts +375 -0
  61. package/src/adapters/api/body-utils.ts +10 -0
  62. package/src/adapters/api/rest-client.ts +587 -0
  63. package/src/adapters/api/soap-client.ts +7 -0
  64. package/src/adapters/db/assertions.ts +83 -0
  65. package/src/adapters/db/oracle-client.ts +133 -0
  66. package/src/adapters/db/query-catalog.ts +80 -0
  67. package/src/adapters/unix/batch-catalog.ts +81 -0
  68. package/src/adapters/unix/batch-input-files.ts +39 -0
  69. package/src/adapters/unix/batch-runner.ts +456 -0
  70. package/src/adapters/unix/ssh-client.ts +248 -0
  71. package/src/app/server.ts +913 -0
  72. package/src/cli.ts +466 -0
  73. package/src/config/environments.ts +193 -0
  74. package/src/config/registry.ts +23 -0
  75. package/src/config/secrets.ts +128 -0
  76. package/src/dsl/parser.ts +24 -0
  77. package/src/dsl/schema.ts +189 -0
  78. package/src/dsl/types.ts +371 -0
  79. package/src/dsl/validator.ts +282 -0
  80. package/src/engine/captures.ts +66 -0
  81. package/src/engine/context.ts +76 -0
  82. package/src/engine/evidence.ts +35 -0
  83. package/src/engine/known-errors.ts +145 -0
  84. package/src/engine/retry.ts +11 -0
  85. package/src/engine/runner.ts +746 -0
  86. package/src/engine/step-result.ts +64 -0
  87. package/src/flows/catalog-normalizer.ts +86 -0
  88. package/src/flows/compiler.ts +247 -0
  89. package/src/flows/concat.ts +149 -0
  90. package/src/flows/parser.ts +27 -0
  91. package/src/flows/schema.ts +154 -0
  92. package/src/flows/types.ts +130 -0
  93. package/src/flows/validator.ts +468 -0
  94. package/src/llm/system-prompt.md +9 -0
  95. package/src/reports/html-report.ts +113 -0
  96. package/src/reports/junit-report.ts +55 -0
  97. package/src/types/oracledb.d.ts +1 -0
  98. package/templates/.gitkeep +0 -0
  99. package/templates/api/create-test-case.json +5 -0
  100. package/templates/api/record-test-activity.json +6 -0
  101. package/tsconfig.json +15 -0
  102. package/vite.config.ts +17 -0
  103. package/web/index.html +12 -0
  104. package/web/src/App.tsx +6588 -0
  105. package/web/src/main.tsx +10 -0
  106. package/web/src/styles.css +3147 -0
  107. package/index.js +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ All notable changes to Adfinem are documented here.
4
+
5
+ ## [0.1.0] - 2026-06-06
6
+
7
+ - Publish the initial open-source Adfinem snapshot.
8
+ - Include catalog-gated API, database, Unix batch, and flow automation.
9
+ - Include smoke scenarios, sample flows, a React workbench, and compiled CLI output.
10
+ - Add npm package metadata and publication safeguards.
@@ -0,0 +1,21 @@
1
+ # Code of Conduct
2
+
3
+ Adfinem follows the Contributor Covenant Code of Conduct.
4
+
5
+ ## Our Pledge
6
+
7
+ We pledge to make participation in this project a harassment-free experience for everyone, regardless of background, identity, experience level, or viewpoint.
8
+
9
+ ## Expected Behavior
10
+
11
+ - Be respectful and constructive.
12
+ - Prefer clear technical discussion over personal criticism.
13
+ - Assume good intent, while taking reported harm seriously.
14
+
15
+ ## Unacceptable Behavior
16
+
17
+ Harassment, threats, discriminatory language, sustained disruption, or publishing private information without permission are not acceptable.
18
+
19
+ ## Enforcement
20
+
21
+ Project maintainers may remove, edit, or reject comments, commits, issues, and pull requests that do not align with this Code of Conduct.
@@ -0,0 +1,29 @@
1
+ # Contributing
2
+
3
+ Thanks for helping improve Adfinem.
4
+
5
+ ## Local Setup
6
+
7
+ ```bash
8
+ npm install
9
+ npm run check
10
+ npm run validate
11
+ npm run smoke:dry
12
+ ```
13
+
14
+ Use dry runs while changing catalog or environment configuration.
15
+
16
+ ## Development Notes
17
+
18
+ - Keep scenarios catalog-gated; do not add arbitrary SQL, shell, or request bodies directly to scenario files when a catalog entry is the safer fit.
19
+ - Do not commit `.env`, evidence folders, uploaded batch input files, or local logs.
20
+ - Add or update tests when changing compiler, runner, adapter, or validation behavior.
21
+ - Run `npm run package:dry-run` before proposing publication-related changes.
22
+
23
+ ## Pull Requests
24
+
25
+ Please include:
26
+
27
+ - A short summary of the change.
28
+ - The commands you ran.
29
+ - Any migration notes for catalogs, scenarios, or flows.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adfinem contributors
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,87 @@
1
- # Adfinem Open Source Project
1
+ # Adfinem
2
2
 
3
- Adfinem Open Source Project.
3
+ Open-source end-to-end QA test helper for deterministic API, database, Unix batch, and workflow automation.
4
+
5
+ Adfinem is built for testers working on enterprise-grade business solutions where repeatable, catalog-gated checks matter.
6
+
7
+ Adfinem executes catalog-gated workflows only. Scenario YAML can reference:
8
+
9
+ - `catalogs/api-operations.yaml`
10
+ - `catalogs/queries.yaml`
11
+ - `catalogs/batches.yaml`
12
+
13
+ ## What It Does
14
+
15
+ Adfinem lets QA teams define repeatable end-to-end checks that combine:
16
+
17
+ - REST API calls from an allowlisted operation catalog
18
+ - database queries, assertions, and execution steps
19
+ - Unix commands over SSH, including batch operations and file-backed processing
20
+ - SFTP file placement before Unix jobs and output retrieval after they finish
21
+ - workflow files that chain API, DB, Unix, loop, parallel, and reusable flow blocks
22
+ - dry-run validation before touching external systems
23
+ - evidence output for executed runs
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install
29
+ npm run check
30
+ ```
31
+
32
+ When published to npm, the CLI can also be installed globally:
33
+
34
+ ```bash
35
+ npm install -g adfinem
36
+ adfinem validate scenarios/smoke/account-processing-smoke.yaml
37
+ ```
38
+
39
+ ## Commands
40
+
41
+ ```bash
42
+ npm install
43
+ npm test
44
+ npm run build
45
+ npm run validate
46
+ npm run smoke:dry
47
+ ```
48
+
49
+ ## CLI-Only Usage
50
+
51
+ Adfinem can be used fully from the terminal when a GUI is not wanted.
52
+
53
+ ```bash
54
+ # Validate a scenario before running it
55
+ npm run adfinem -- validate scenarios/smoke/account-processing-smoke.yaml
56
+
57
+ # Run a scenario without external side effects
58
+ npm run adfinem -- run scenarios/smoke/account-processing-smoke.yaml --env local --dry-run
59
+
60
+ # Execute a cataloged API operation
61
+ npm run adfinem -- api-call create_test_case --env local --param tenant=demo --param external_id=CASE-1001 --param case_type=account-processing
62
+
63
+ # Execute a cataloged DB query
64
+ npm run adfinem -- db-query test_activity_exists --env local --param case_id=CASE-1001 --param amount=json:111
65
+
66
+ # Run a cataloged Unix batch
67
+ npm run adfinem -- run-batch daily_processing --env local --param processing_date=2026-04-27
68
+ ```
69
+
70
+ Use `--dry-run` while catalogs and environment credentials are still being completed.
71
+
72
+ See `docs/FLOW_BUILDER.md` for flow files and `docs/DB_UNIX_OPERATIONS.md` for database and Unix scenario steps.
73
+
74
+ ## Package Safety
75
+
76
+ The npm package is allowlisted through `package.json#files` so local state such as `.env`, evidence, logs, dependencies, and uploaded batch input files are not included in published artifacts.
77
+
78
+ Before publishing or opening a release PR, run:
79
+
80
+ ```bash
81
+ npm run check
82
+ npm run package:dry-run
83
+ ```
84
+
85
+ ## License
86
+
87
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,13 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report suspected vulnerabilities privately through GitHub security advisories when available, or by contacting the repository owner directly.
6
+
7
+ Do not include secrets, credentials, private endpoints, raw production payloads, or proprietary SQL in public issues.
8
+
9
+ ## Sensitive Data
10
+
11
+ Adfinem is designed around catalog-gated actions, but users are still responsible for keeping local environment files, evidence, logs, and batch input files out of published artifacts.
12
+
13
+ The repository `.gitignore` excludes common local-state paths such as `.env`, `evidence/`, `web-dist/`, and uploaded batch input files.
File without changes
@@ -0,0 +1,21 @@
1
+ create_test_case:
2
+ description: Creates a generic test case through an allowlisted API operation.
3
+ type: rest
4
+ method: POST
5
+ path: "/api/test-cases"
6
+ requestTemplate: "templates/api/create-test-case.json"
7
+ idempotent: false
8
+ captures:
9
+ case_id: "$.case.id"
10
+ case_status: "$.case.status"
11
+
12
+ record_test_activity:
13
+ description: Records a generic activity against an existing test case.
14
+ type: rest
15
+ method: POST
16
+ path: "/api/test-activities"
17
+ requestTemplate: "templates/api/record-test-activity.json"
18
+ idempotent: false
19
+ captures:
20
+ activity_id: "$.activity.id"
21
+ activity_status: "$.activity.status"
@@ -0,0 +1,74 @@
1
+ daily_processing:
2
+ description: Runs a daily processing batch for a processing date.
3
+ hostRef: qa_worker
4
+ command: run_daily_processing.sh
5
+ args:
6
+ - name: processing_date
7
+ required: true
8
+ pattern: ^\d{4}-\d{2}-\d{2}$
9
+ timeoutSeconds: 3600
10
+ success:
11
+ exitCodes:
12
+ - 0
13
+ requiredOutput:
14
+ - SUCCESS
15
+ captures:
16
+ daily_processing_exit_code: $.exitCode
17
+
18
+ nightly_reconciliation:
19
+ description: Runs a generic nightly reconciliation batch.
20
+ hostRef: qa_worker
21
+ command: sh
22
+ fixedArgs:
23
+ - -x
24
+ - reconcile_nightly.sh
25
+ - reconcile_nightly.log
26
+ workingDirectory: ${ADFINEM_BATCH_WORKDIR}
27
+ timeoutSeconds: 7200
28
+ success:
29
+ exitCodes:
30
+ - 0
31
+ - 1
32
+ captures:
33
+ nightly_reconciliation_exit_code: $.exitCode
34
+
35
+ report_generation:
36
+ description: Generates a generic QA report.
37
+ hostRef: qa_worker
38
+ command: sh
39
+ fixedArgs:
40
+ - -x
41
+ - generate_report.sh
42
+ - generate_report.log
43
+ workingDirectory: ${ADFINEM_BATCH_WORKDIR}
44
+ timeoutSeconds: 3600
45
+ success:
46
+ exitCodes:
47
+ - 0
48
+ outputFiles:
49
+ - name: report
50
+ source: stderr
51
+ pathPattern: "Generated file:\\s*(\\S+)"
52
+ download: true
53
+ captures:
54
+ report_generation_exit_code: $.exitCode
55
+
56
+ import_transactions:
57
+ description: Imports a generic transaction file for downstream checks.
58
+ hostRef: qa_worker
59
+ command: sh
60
+ fixedArgs:
61
+ - -x
62
+ - import_transactions.sh
63
+ - ${transactions_file}
64
+ inputFiles:
65
+ - name: transactions_file
66
+ required: true
67
+ remotePath: /tmp/adfinem/inbox/
68
+ paramName: transactions_file
69
+ timeoutSeconds: 3600
70
+ success:
71
+ exitCodes:
72
+ - 0
73
+ captures:
74
+ import_transactions_exit_code: $.exitCode
@@ -0,0 +1,75 @@
1
+ set_processing_date:
2
+ description: Updates a generic processing date before running batch automation.
3
+ mode: execute
4
+ sql: |
5
+ update test_run_control
6
+ set processing_date = to_date(:processing_date, 'YYYY-MM-DD')
7
+ where control_name = 'default';
8
+ commit;
9
+ params:
10
+ processing_date:
11
+ required: true
12
+ type: string
13
+ pattern: ^\d{4}-\d{2}-\d{2}$
14
+
15
+ test_activity_exists:
16
+ description: Verifies an activity was recorded for a test case and amount.
17
+ sql: |
18
+ select count(*) as CNT
19
+ from test_activity
20
+ where case_id = :case_id
21
+ and amount = :amount
22
+ params:
23
+ case_id:
24
+ required: true
25
+ pattern: ^[0-9A-Za-z_-]+$
26
+ amount:
27
+ required: true
28
+ type: number
29
+ expect:
30
+ type: number
31
+ column: CNT
32
+ operator: ">"
33
+ value: 0
34
+ captures:
35
+ activity_count: $.rows[0].CNT
36
+
37
+ processing_balanced:
38
+ description: Verifies debit and credit totals are balanced for a test case and processing date.
39
+ sql: |
40
+ select abs(nvl(sum(debit_amount), 0) - nvl(sum(credit_amount), 0)) as DIFF
41
+ from test_ledger_entry
42
+ where case_id = :case_id
43
+ and processing_date = to_date(:processing_date, 'YYYY-MM-DD')
44
+ params:
45
+ case_id:
46
+ required: true
47
+ pattern: ^[0-9A-Za-z_-]+$
48
+ processing_date:
49
+ required: true
50
+ pattern: ^\d{4}-\d{2}-\d{2}$
51
+ expect:
52
+ type: number
53
+ column: DIFF
54
+ operator: =
55
+ value: 0
56
+ captures:
57
+ balance_diff: $.rows[0].DIFF
58
+
59
+ case_by_external_id:
60
+ description: Retrieves a generic test case by external identifier.
61
+ sql: |
62
+ select case_id as CASE_ID, status as STATUS
63
+ from test_case
64
+ where external_id = :external_id
65
+ params:
66
+ external_id:
67
+ required: true
68
+ type: string
69
+ expect:
70
+ type: rowCount
71
+ operator: ">"
72
+ value: 0
73
+ captures:
74
+ case_id: $.rows[0].CASE_ID
75
+ case_status: $.rows[0].STATUS
@@ -0,0 +1,13 @@
1
+ local:
2
+ apiBaseUrl: "${ADFINEM_API_BASE_URL}"
3
+ apiTlsInsecure: "${ADFINEM_API_TLS_INSECURE}"
4
+ oracle:
5
+ user: "${ADFINEM_DB_USER}"
6
+ password: "${ADFINEM_DB_PASSWORD}"
7
+ connectString: "${ADFINEM_DB_CONNECT_STRING}"
8
+ sshHosts:
9
+ qa_worker:
10
+ host: "${ADFINEM_SSH_QA_WORKER_HOST}"
11
+ username: "${ADFINEM_SSH_USER}"
12
+ password: "${ADFINEM_SSH_PASSWORD}"
13
+ privateKeyPath: "${ADFINEM_SSH_PRIVATE_KEY_PATH}"
@@ -0,0 +1,3 @@
1
+ export async function assertDb(oracle, entry, params) {
2
+ return await oracle.assert(entry, params);
3
+ }
@@ -0,0 +1,3 @@
1
+ export async function runBatch(batchRunner, entry, params, options) {
2
+ return await batchRunner.run(entry, params, options);
3
+ }
@@ -0,0 +1,296 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, join } from "node:path";
4
+ export function apiCollectionsPath(rootDir) {
5
+ return join(rootDir, "catalogs", "api-collections.json");
6
+ }
7
+ export async function loadApiCollections(rootDir) {
8
+ try {
9
+ const parsed = JSON.parse(await readFile(apiCollectionsPath(rootDir), "utf8"));
10
+ return normalizeCollectionFile(parsed);
11
+ }
12
+ catch (error) {
13
+ const err = error;
14
+ if (err.code === "ENOENT")
15
+ return { version: 1, collections: [] };
16
+ throw error;
17
+ }
18
+ }
19
+ export async function writeApiCollections(rootDir, file) {
20
+ const outputPath = apiCollectionsPath(rootDir);
21
+ await mkdir(dirname(outputPath), { recursive: true });
22
+ await writeFile(outputPath, `${JSON.stringify(normalizeCollectionFile(file), null, 2)}\n`, "utf8");
23
+ }
24
+ export async function importPostmanCollection(rootDir, payload) {
25
+ const collection = parsePostmanCollection(payload);
26
+ const file = await loadApiCollections(rootDir);
27
+ const remaining = file.collections.filter((item) => item.id !== collection.id);
28
+ await writeApiCollections(rootDir, { version: 1, collections: [...remaining, collection] });
29
+ return collection;
30
+ }
31
+ export function parsePostmanCollection(payload) {
32
+ const source = asObject(payload);
33
+ const name = String(source.info?.name || "Imported API Collection").trim();
34
+ const collectionId = sanitizeId(source.info?._postman_id || `${name}_${shortHash(JSON.stringify(payload))}`);
35
+ const variables = postmanVariables(source.variable);
36
+ const requests = [];
37
+ const rootAuth = source.auth;
38
+ function visit(items, folderPath, inheritedAuth) {
39
+ for (const item of items ?? []) {
40
+ if (Array.isArray(item.item)) {
41
+ visit(item.item, [...folderPath, String(item.name || "Folder")], inheritedAuth);
42
+ continue;
43
+ }
44
+ const request = typeof item.request === "string" ? undefined : item.request;
45
+ if (!request)
46
+ continue;
47
+ const requestId = uniqueRequestId(collectionId, folderPath, item.name || request.method || "request", requests.length);
48
+ const spec = postmanRequestSpec(request, inheritedAuth);
49
+ const variableNames = [...new Set([
50
+ ...findTemplateVariables(spec),
51
+ ...Object.keys(variables)
52
+ ])].sort((a, b) => a.localeCompare(b));
53
+ requests.push({
54
+ id: requestId,
55
+ name: String(item.name || requestId),
56
+ folderPath,
57
+ operationKey: `pm_${collectionId}_${requestId}`,
58
+ description: descriptionText(request.description),
59
+ request: spec,
60
+ variables,
61
+ variableNames
62
+ });
63
+ }
64
+ }
65
+ visit(source.item, [], rootAuth);
66
+ return {
67
+ id: collectionId,
68
+ name,
69
+ importedAt: new Date().toISOString(),
70
+ variables,
71
+ requestCount: requests.length,
72
+ requests
73
+ };
74
+ }
75
+ export function previewPostmanCollection(payload) {
76
+ const collection = parsePostmanCollection(payload);
77
+ return {
78
+ name: collection.name,
79
+ id: collection.id,
80
+ requestCount: collection.requestCount,
81
+ variables: collection.variables ?? {},
82
+ folders: [...new Set(collection.requests.map((request) => request.folderPath.join(" / ")).filter(Boolean))].sort(),
83
+ requests: collection.requests.map((request) => ({
84
+ id: request.id,
85
+ name: request.name,
86
+ folderPath: request.folderPath,
87
+ operationKey: request.operationKey,
88
+ variableNames: request.variableNames,
89
+ ...request.request
90
+ }))
91
+ };
92
+ }
93
+ export function importedOperationsFromCollections(file) {
94
+ const operations = {};
95
+ for (const collection of file.collections) {
96
+ for (const request of collection.requests) {
97
+ operations[request.operationKey] = {
98
+ type: "rest",
99
+ description: request.description || `${collection.name}${request.folderPath.length ? ` / ${request.folderPath.join(" / ")}` : ""} / ${request.name}`,
100
+ ...request.request,
101
+ params: paramsFromVariables(request.variableNames),
102
+ source: {
103
+ collectionId: collection.id,
104
+ collectionName: collection.name,
105
+ requestId: request.id,
106
+ folderPath: request.folderPath
107
+ }
108
+ };
109
+ }
110
+ }
111
+ return operations;
112
+ }
113
+ export function operationToRequestSpec(operation) {
114
+ if (!operation)
115
+ return {};
116
+ return {
117
+ method: operation.method,
118
+ path: operation.path,
119
+ headers: operation.headers,
120
+ query: operation.query,
121
+ body: operation.body,
122
+ rawBody: operation.rawBody,
123
+ bodyMode: operation.bodyMode,
124
+ auth: operation.auth,
125
+ acceptStatuses: operation.acceptStatuses
126
+ };
127
+ }
128
+ export function mergeApiRequest(base, override) {
129
+ const source = operationToRequestSpec(base);
130
+ return cleanRequest({
131
+ ...source,
132
+ ...(override ?? {}),
133
+ headers: mergeRecord(source.headers, override?.headers),
134
+ query: mergeRecord(source.query, override?.query),
135
+ acceptStatuses: override?.acceptStatuses ?? source.acceptStatuses
136
+ });
137
+ }
138
+ function postmanRequestSpec(request, inheritedAuth) {
139
+ const url = parsePostmanUrl(request.url);
140
+ const body = parsePostmanBody(request.body);
141
+ return cleanRequest({
142
+ method: normalizeMethod(request.method),
143
+ path: url.path,
144
+ query: url.query,
145
+ headers: mergeRecord(generatedPostmanHeaders(body), keyValueRecord(request.header)),
146
+ auth: request.auth ?? inheritedAuth,
147
+ ...body
148
+ });
149
+ }
150
+ function parsePostmanUrl(value) {
151
+ if (typeof value === "string")
152
+ return pathAndQueryFromRaw(value);
153
+ const url = asObject(value);
154
+ const raw = optionalString(url.raw);
155
+ const query = keyValueRecord(url.query);
156
+ if (raw) {
157
+ const parsed = pathAndQueryFromRaw(raw);
158
+ return { path: parsed.path, query: mergeRecord(parsed.query, query) };
159
+ }
160
+ const pathParts = Array.isArray(url.path) ? url.path.map(String) : [];
161
+ return {
162
+ path: pathParts.length ? `/${pathParts.join("/")}` : undefined,
163
+ query
164
+ };
165
+ }
166
+ function pathAndQueryFromRaw(raw) {
167
+ const trimmed = raw.trim();
168
+ const withoutBase = trimmed.replace(/^\{\{[^}]+\}\}/, "");
169
+ try {
170
+ const url = new URL(trimmed.replace(/^\{\{[^}]+\}\}/, "http://postman.local"));
171
+ return { path: url.pathname, query: Object.fromEntries(url.searchParams.entries()) };
172
+ }
173
+ catch {
174
+ const [path, qs] = withoutBase.split("?");
175
+ return {
176
+ path: path.startsWith("/") ? path : `/${path}`,
177
+ query: qs ? Object.fromEntries(new URLSearchParams(qs).entries()) : undefined
178
+ };
179
+ }
180
+ }
181
+ function parsePostmanBody(body) {
182
+ if (!body?.mode)
183
+ return { bodyMode: "none" };
184
+ if (body.mode === "raw") {
185
+ const rawBody = body.raw ?? "";
186
+ const trimmed = rawBody.trim();
187
+ if (trimmed && looksLikeJson(trimmed)) {
188
+ return { bodyMode: "json", rawBody };
189
+ }
190
+ return { bodyMode: "raw", rawBody };
191
+ }
192
+ if (body.mode === "urlencoded") {
193
+ return { bodyMode: "urlencoded", body: keyValueRecord(body.urlencoded) ?? {} };
194
+ }
195
+ if (body.mode === "formdata") {
196
+ return { bodyMode: "formdata", body: keyValueRecord(body.formdata) ?? {} };
197
+ }
198
+ return { bodyMode: "raw", rawBody: body.raw ?? "" };
199
+ }
200
+ function normalizeMethod(value) {
201
+ const method = String(value || "GET").toUpperCase();
202
+ if (["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(method)) {
203
+ return method;
204
+ }
205
+ return "GET";
206
+ }
207
+ function keyValueRecord(items) {
208
+ const entries = (items ?? [])
209
+ .filter((item) => !item.disabled && item.key)
210
+ .map((item) => [String(item.key), String(item.value ?? "")]);
211
+ return entries.length ? Object.fromEntries(entries) : undefined;
212
+ }
213
+ function generatedPostmanHeaders(body) {
214
+ const headers = {
215
+ Accept: "*/*",
216
+ "Cache-Control": "no-cache"
217
+ };
218
+ if (body.bodyMode === "json")
219
+ headers["Content-Type"] = "application/json";
220
+ if (body.bodyMode === "urlencoded")
221
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
222
+ return Object.keys(headers).length ? headers : undefined;
223
+ }
224
+ function postmanVariables(items) {
225
+ return Object.fromEntries((items ?? [])
226
+ .filter((item) => !item.disabled && item.key)
227
+ .map((item) => [String(item.key), item.value ?? ""]));
228
+ }
229
+ function findTemplateVariables(value) {
230
+ const names = [];
231
+ const visit = (current) => {
232
+ if (typeof current === "string") {
233
+ for (const match of current.matchAll(/\{\{([A-Za-z0-9_.-]+)\}\}/g))
234
+ names.push(match[1]);
235
+ for (const match of current.matchAll(/\$\{([A-Za-z0-9_.-]+)\}/g))
236
+ names.push(match[1]);
237
+ return;
238
+ }
239
+ if (Array.isArray(current))
240
+ current.forEach(visit);
241
+ if (current && typeof current === "object")
242
+ Object.values(current).forEach(visit);
243
+ };
244
+ visit(value);
245
+ return names;
246
+ }
247
+ function paramsFromVariables(variableNames) {
248
+ if (!variableNames.length)
249
+ return undefined;
250
+ return Object.fromEntries(variableNames.map((name) => [name, { required: false, type: "string" }]));
251
+ }
252
+ function normalizeCollectionFile(file) {
253
+ return {
254
+ version: 1,
255
+ collections: Array.isArray(file.collections) ? file.collections.map((collection) => ({
256
+ ...collection,
257
+ requestCount: collection.requests?.length ?? collection.requestCount ?? 0,
258
+ requests: collection.requests ?? []
259
+ })) : []
260
+ };
261
+ }
262
+ function cleanRequest(request) {
263
+ return Object.fromEntries(Object.entries(request).filter(([, value]) => value !== undefined && value !== null && !(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)));
264
+ }
265
+ function mergeRecord(base, override) {
266
+ const merged = { ...(base ?? {}), ...(override ?? {}) };
267
+ return Object.keys(merged).length ? merged : undefined;
268
+ }
269
+ function asObject(value) {
270
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
271
+ }
272
+ function optionalString(value) {
273
+ return value === undefined || value === null ? undefined : String(value);
274
+ }
275
+ function looksLikeJson(value) {
276
+ return value.startsWith("{") || value.startsWith("[");
277
+ }
278
+ function descriptionText(value) {
279
+ if (typeof value === "string")
280
+ return value;
281
+ const description = asObject(value);
282
+ return optionalString(description.content);
283
+ }
284
+ function uniqueRequestId(collectionId, folderPath, name, index) {
285
+ return sanitizeId(`${folderPath.join("_")}_${name}_${index || ""}`) || `${collectionId}_request_${index + 1}`;
286
+ }
287
+ function sanitizeId(value) {
288
+ return String(value ?? "")
289
+ .toLowerCase()
290
+ .replace(/[^a-z0-9_-]+/g, "_")
291
+ .replace(/^_+|_+$/g, "")
292
+ .slice(0, 80) || "collection";
293
+ }
294
+ function shortHash(value) {
295
+ return createHash("sha1").update(value).digest("hex").slice(0, 8);
296
+ }