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.
- package/CHANGELOG.md +10 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +29 -0
- package/LICENSE +21 -0
- package/README.md +86 -2
- package/SECURITY.md +13 -0
- package/catalogs/.gitkeep +0 -0
- package/catalogs/api-operations.yaml +21 -0
- package/catalogs/batches.yaml +74 -0
- package/catalogs/queries.yaml +75 -0
- package/config/environments.yaml +13 -0
- package/dist/actions/assert-db.js +3 -0
- package/dist/actions/run-eod.js +3 -0
- package/dist/adapters/api/api-collections.js +296 -0
- package/dist/adapters/api/body-utils.js +9 -0
- package/dist/adapters/api/rest-client.js +557 -0
- package/dist/adapters/api/soap-client.js +5 -0
- package/dist/adapters/db/assertions.js +87 -0
- package/dist/adapters/db/oracle-client.js +115 -0
- package/dist/adapters/db/query-catalog.js +75 -0
- package/dist/adapters/unix/batch-catalog.js +71 -0
- package/dist/adapters/unix/batch-input-files.js +36 -0
- package/dist/adapters/unix/batch-runner.js +382 -0
- package/dist/adapters/unix/ssh-client.js +228 -0
- package/dist/app/server.js +826 -0
- package/dist/cli.js +465 -0
- package/dist/config/environments.js +138 -0
- package/dist/config/registry.js +18 -0
- package/dist/config/secrets.js +123 -0
- package/dist/dsl/parser.js +20 -0
- package/dist/dsl/schema.js +182 -0
- package/dist/dsl/types.js +1 -0
- package/dist/dsl/validator.js +264 -0
- package/dist/engine/captures.js +68 -0
- package/dist/engine/context.js +69 -0
- package/dist/engine/evidence.js +33 -0
- package/dist/engine/known-errors.js +129 -0
- package/dist/engine/retry.js +13 -0
- package/dist/engine/runner.js +710 -0
- package/dist/engine/step-result.js +58 -0
- package/dist/flows/catalog-normalizer.js +72 -0
- package/dist/flows/compiler.js +237 -0
- package/dist/flows/concat.js +130 -0
- package/dist/flows/parser.js +21 -0
- package/dist/flows/schema.js +142 -0
- package/dist/flows/types.js +1 -0
- package/dist/flows/validator.js +470 -0
- package/dist/reports/html-report.js +112 -0
- package/dist/reports/junit-report.js +48 -0
- package/docs/.gitkeep +0 -0
- package/docs/DB_UNIX_OPERATIONS.md +118 -0
- package/docs/FLOW_BUILDER.md +87 -0
- package/flows/account_processing_cycle.flow.yaml +88 -0
- package/flows/new_flow.flow.yaml +22 -0
- package/package.json +92 -7
- package/scenarios/smoke/account-processing-smoke.yaml +44 -0
- package/scenarios/smoke/api-db-batch-check.yaml +40 -0
- package/src/actions/assert-db.ts +6 -0
- package/src/actions/run-eod.ts +6 -0
- package/src/adapters/api/api-collections.ts +375 -0
- package/src/adapters/api/body-utils.ts +10 -0
- package/src/adapters/api/rest-client.ts +587 -0
- package/src/adapters/api/soap-client.ts +7 -0
- package/src/adapters/db/assertions.ts +83 -0
- package/src/adapters/db/oracle-client.ts +133 -0
- package/src/adapters/db/query-catalog.ts +80 -0
- package/src/adapters/unix/batch-catalog.ts +81 -0
- package/src/adapters/unix/batch-input-files.ts +39 -0
- package/src/adapters/unix/batch-runner.ts +456 -0
- package/src/adapters/unix/ssh-client.ts +248 -0
- package/src/app/server.ts +913 -0
- package/src/cli.ts +466 -0
- package/src/config/environments.ts +193 -0
- package/src/config/registry.ts +23 -0
- package/src/config/secrets.ts +128 -0
- package/src/dsl/parser.ts +24 -0
- package/src/dsl/schema.ts +189 -0
- package/src/dsl/types.ts +371 -0
- package/src/dsl/validator.ts +282 -0
- package/src/engine/captures.ts +66 -0
- package/src/engine/context.ts +76 -0
- package/src/engine/evidence.ts +35 -0
- package/src/engine/known-errors.ts +145 -0
- package/src/engine/retry.ts +11 -0
- package/src/engine/runner.ts +746 -0
- package/src/engine/step-result.ts +64 -0
- package/src/flows/catalog-normalizer.ts +86 -0
- package/src/flows/compiler.ts +247 -0
- package/src/flows/concat.ts +149 -0
- package/src/flows/parser.ts +27 -0
- package/src/flows/schema.ts +154 -0
- package/src/flows/types.ts +130 -0
- package/src/flows/validator.ts +468 -0
- package/src/llm/system-prompt.md +9 -0
- package/src/reports/html-report.ts +113 -0
- package/src/reports/junit-report.ts +55 -0
- package/src/types/oracledb.d.ts +1 -0
- package/templates/.gitkeep +0 -0
- package/templates/api/create-test-case.json +5 -0
- package/templates/api/record-test-activity.json +6 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +17 -0
- package/web/index.html +12 -0
- package/web/src/App.tsx +6588 -0
- package/web/src/main.tsx +10 -0
- package/web/src/styles.css +3147 -0
- 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.
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
|
1
|
+
# Adfinem
|
|
2
2
|
|
|
3
|
-
|
|
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,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
|
+
}
|