adfinem 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +17 -0
  3. package/CODE_OF_CONDUCT.md +21 -0
  4. package/CONTRIBUTING.md +29 -0
  5. package/LICENSE +21 -0
  6. package/README.md +97 -3
  7. package/SECURITY.md +13 -0
  8. package/catalogs/.gitkeep +0 -0
  9. package/catalogs/api-operations.yaml +21 -0
  10. package/catalogs/batches.yaml +74 -0
  11. package/catalogs/queries.yaml +75 -0
  12. package/config/environments.yaml +13 -0
  13. package/dist/actions/assert-db.js +3 -0
  14. package/dist/actions/run-eod.js +3 -0
  15. package/dist/adapters/api/api-collections.js +296 -0
  16. package/dist/adapters/api/body-utils.js +9 -0
  17. package/dist/adapters/api/rest-client.js +557 -0
  18. package/dist/adapters/api/soap-client.js +5 -0
  19. package/dist/adapters/db/assertions.js +87 -0
  20. package/dist/adapters/db/oracle-client.js +115 -0
  21. package/dist/adapters/db/query-catalog.js +75 -0
  22. package/dist/adapters/unix/batch-catalog.js +71 -0
  23. package/dist/adapters/unix/batch-input-files.js +36 -0
  24. package/dist/adapters/unix/batch-runner.js +382 -0
  25. package/dist/adapters/unix/ssh-client.js +228 -0
  26. package/dist/app/server.js +827 -0
  27. package/dist/cli.js +516 -0
  28. package/dist/config/environments.js +138 -0
  29. package/dist/config/registry.js +18 -0
  30. package/dist/config/secrets.js +123 -0
  31. package/dist/dsl/parser.js +20 -0
  32. package/dist/dsl/schema.js +182 -0
  33. package/dist/dsl/types.js +1 -0
  34. package/dist/dsl/validator.js +264 -0
  35. package/dist/engine/captures.js +68 -0
  36. package/dist/engine/context.js +69 -0
  37. package/dist/engine/evidence.js +33 -0
  38. package/dist/engine/known-errors.js +129 -0
  39. package/dist/engine/retry.js +13 -0
  40. package/dist/engine/runner.js +710 -0
  41. package/dist/engine/step-result.js +58 -0
  42. package/dist/flows/catalog-normalizer.js +72 -0
  43. package/dist/flows/compiler.js +237 -0
  44. package/dist/flows/concat.js +130 -0
  45. package/dist/flows/parser.js +21 -0
  46. package/dist/flows/schema.js +142 -0
  47. package/dist/flows/types.js +1 -0
  48. package/dist/flows/validator.js +470 -0
  49. package/dist/reports/html-report.js +112 -0
  50. package/dist/reports/junit-report.js +48 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/DB_UNIX_OPERATIONS.md +118 -0
  53. package/docs/FLOW_BUILDER.md +87 -0
  54. package/flows/account_processing_cycle.flow.yaml +88 -0
  55. package/flows/new_flow.flow.yaml +22 -0
  56. package/package.json +98 -11
  57. package/scenarios/smoke/account-processing-smoke.yaml +44 -0
  58. package/scenarios/smoke/api-db-batch-check.yaml +40 -0
  59. package/src/actions/assert-db.ts +6 -0
  60. package/src/actions/run-eod.ts +6 -0
  61. package/src/adapters/api/api-collections.ts +375 -0
  62. package/src/adapters/api/body-utils.ts +10 -0
  63. package/src/adapters/api/rest-client.ts +587 -0
  64. package/src/adapters/api/soap-client.ts +7 -0
  65. package/src/adapters/db/assertions.ts +83 -0
  66. package/src/adapters/db/oracle-client.ts +133 -0
  67. package/src/adapters/db/query-catalog.ts +80 -0
  68. package/src/adapters/unix/batch-catalog.ts +81 -0
  69. package/src/adapters/unix/batch-input-files.ts +39 -0
  70. package/src/adapters/unix/batch-runner.ts +456 -0
  71. package/src/adapters/unix/ssh-client.ts +248 -0
  72. package/src/app/server.ts +914 -0
  73. package/src/cli.ts +517 -0
  74. package/src/config/environments.ts +193 -0
  75. package/src/config/registry.ts +23 -0
  76. package/src/config/secrets.ts +128 -0
  77. package/src/dsl/parser.ts +24 -0
  78. package/src/dsl/schema.ts +189 -0
  79. package/src/dsl/types.ts +371 -0
  80. package/src/dsl/validator.ts +282 -0
  81. package/src/engine/captures.ts +66 -0
  82. package/src/engine/context.ts +76 -0
  83. package/src/engine/evidence.ts +35 -0
  84. package/src/engine/known-errors.ts +145 -0
  85. package/src/engine/retry.ts +11 -0
  86. package/src/engine/runner.ts +746 -0
  87. package/src/engine/step-result.ts +64 -0
  88. package/src/flows/catalog-normalizer.ts +86 -0
  89. package/src/flows/compiler.ts +247 -0
  90. package/src/flows/concat.ts +149 -0
  91. package/src/flows/parser.ts +27 -0
  92. package/src/flows/schema.ts +154 -0
  93. package/src/flows/types.ts +130 -0
  94. package/src/flows/validator.ts +468 -0
  95. package/src/llm/system-prompt.md +9 -0
  96. package/src/reports/html-report.ts +113 -0
  97. package/src/reports/junit-report.ts +55 -0
  98. package/src/types/oracledb.d.ts +1 -0
  99. package/templates/.gitkeep +0 -0
  100. package/templates/api/create-test-case.json +5 -0
  101. package/templates/api/record-test-activity.json +6 -0
  102. package/tsconfig.json +15 -0
  103. package/vite.config.ts +17 -0
  104. package/web/index.html +12 -0
  105. package/web/src/App.tsx +6588 -0
  106. package/web/src/main.tsx +10 -0
  107. package/web/src/styles.css +3147 -0
  108. package/web-dist/assets/elk.bundled-ChwRCIWJ.js +24 -0
  109. package/web-dist/assets/index-CArbX4zm.css +1 -0
  110. package/web-dist/assets/index-vDCbj8xB.js +28 -0
  111. package/web-dist/index.html +13 -0
  112. package/index.js +0 -1
@@ -0,0 +1,133 @@
1
+ import type { EnvironmentConfig } from "../../config/environments.js";
2
+ import type { DbExecuteResult, QueryCatalogEntry } from "../../dsl/types.js";
3
+ import { assertQueryResult } from "./assertions.js";
4
+ import { normalizeBindParamRecord, validateQueryParams } from "./query-catalog.js";
5
+
6
+ type OracleDbModule = {
7
+ OUT_FORMAT_OBJECT: unknown;
8
+ getConnection(config: { user?: string; password?: string; connectString?: string }): Promise<{
9
+ execute(sql: string, binds: Record<string, unknown>, options: Record<string, unknown>): Promise<{ rows?: Record<string, unknown>[] }>;
10
+ close(): Promise<void>;
11
+ }>;
12
+ };
13
+
14
+ export class OracleClient {
15
+ constructor(private readonly env: EnvironmentConfig) {}
16
+
17
+ async query(entry: QueryCatalogEntry, params: Record<string, unknown>): Promise<Record<string, unknown>[]> {
18
+ const bindParams = normalizeBindParamRecord(params);
19
+ validateQueryParams(entry, bindParams);
20
+ if (!this.env.oracle.user || !this.env.oracle.password || !this.env.oracle.connectString) {
21
+ throw new Error("ADFINEM_DB_USER, ADFINEM_DB_PASSWORD, and ADFINEM_DB_CONNECT_STRING are required for DB execution.");
22
+ }
23
+
24
+ const oracledb = await loadOracleDbModule();
25
+ const connection = await oracledb.getConnection(this.env.oracle);
26
+ try {
27
+ const prepared = expandArrayBinds(entry.sql, bindParams);
28
+ const result = await connection.execute(prepared.sql, prepared.binds, { outFormat: oracledb.OUT_FORMAT_OBJECT });
29
+ return limitRows(result.rows ?? [], entry.maxRows);
30
+ } finally {
31
+ await closeQuietly(connection);
32
+ }
33
+ }
34
+
35
+ async assert(entry: QueryCatalogEntry, params: Record<string, unknown>): Promise<Record<string, unknown>[]> {
36
+ const rows = await this.query(entry, params);
37
+ assertQueryResult(entry, rows);
38
+ return rows;
39
+ }
40
+
41
+ async execute(entry: QueryCatalogEntry, params: Record<string, unknown>): Promise<DbExecuteResult> {
42
+ const bindParams = normalizeBindParamRecord(params);
43
+ validateQueryParams(entry, bindParams);
44
+ if (!this.env.oracle.user || !this.env.oracle.password || !this.env.oracle.connectString) {
45
+ throw new Error("ADFINEM_DB_USER, ADFINEM_DB_PASSWORD, and ADFINEM_DB_CONNECT_STRING are required for DB execution.");
46
+ }
47
+
48
+ const oracledb = await loadOracleDbModule();
49
+ const connection = await oracledb.getConnection(this.env.oracle);
50
+ try {
51
+ const prepared = expandArrayBinds(entry.sql, bindParams);
52
+ const result = await connection.execute(prepared.sql, prepared.binds, {
53
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
54
+ autoCommit: true
55
+ });
56
+ const execution = result as { rowsAffected?: number; outBinds?: Record<string, unknown> };
57
+ return {
58
+ status: "passed",
59
+ rowsAffected: execution.rowsAffected,
60
+ outBinds: execution.outBinds
61
+ };
62
+ } finally {
63
+ await closeQuietly(connection);
64
+ }
65
+ }
66
+ }
67
+
68
+ async function closeQuietly(connection: { close(): Promise<void> }): Promise<void> {
69
+ try {
70
+ await connection.close();
71
+ } catch (error) {
72
+ const message = error instanceof Error ? error.message : String(error);
73
+ // Connection close failures must not mask the operation error from the surrounding try.
74
+ console.warn(`Oracle connection close failed: ${message}`);
75
+ }
76
+ }
77
+
78
+ let oracleDbModule: OracleDbModule | undefined;
79
+
80
+ async function loadOracleDbModule(): Promise<OracleDbModule> {
81
+ if (oracleDbModule) return oracleDbModule;
82
+ try {
83
+ const imported = await import("oracledb") as unknown;
84
+ oracleDbModule = normalizeOracleDbModule(imported);
85
+ return oracleDbModule;
86
+ } catch (error) {
87
+ const err = error instanceof Error ? error : new Error(String(error));
88
+ throw new Error(`Could not load optional dependency 'oracledb'. Run 'npm install' and ensure Oracle client libraries are available. Original error: ${err.message}`);
89
+ }
90
+ }
91
+
92
+ function normalizeOracleDbModule(imported: unknown): OracleDbModule {
93
+ const moduleValue = imported as OracleDbModule & { default?: OracleDbModule };
94
+ const candidate = typeof moduleValue.getConnection === "function" ? moduleValue : moduleValue.default;
95
+ if (!candidate || typeof candidate.getConnection !== "function") {
96
+ throw new Error("Loaded 'oracledb', but it did not expose getConnection().");
97
+ }
98
+ return candidate;
99
+ }
100
+
101
+ function limitRows(rows: Record<string, unknown>[], maxRows: number | undefined): Record<string, unknown>[] {
102
+ if (maxRows === undefined) return rows;
103
+ return rows.slice(0, maxRows);
104
+ }
105
+
106
+ function expandArrayBinds(sql: string, params: Record<string, unknown>): { sql: string; binds: Record<string, unknown> } {
107
+ const binds: Record<string, unknown> = {};
108
+ let expandedSql = sql;
109
+
110
+ for (const [name, value] of Object.entries(params)) {
111
+ if (!Array.isArray(value)) {
112
+ binds[name] = value;
113
+ continue;
114
+ }
115
+
116
+ if (value.length === 0) {
117
+ throw new Error(`Query param '${name}' must contain at least one value.`);
118
+ }
119
+
120
+ const placeholders = value.map((entry, index) => {
121
+ const bindName = `${name}_${index}`;
122
+ binds[bindName] = entry;
123
+ return `:${bindName}`;
124
+ });
125
+ expandedSql = expandedSql.replace(new RegExp(`:${escapeRegExp(name)}\\b`, "g"), placeholders.join(", "));
126
+ }
127
+
128
+ return { sql: expandedSql, binds };
129
+ }
130
+
131
+ function escapeRegExp(value: string): string {
132
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ }
@@ -0,0 +1,80 @@
1
+ import { isLuhnValid } from "../../config/secrets.js";
2
+ import type { CatalogParam, QueryCatalogEntry } from "../../dsl/types.js";
3
+
4
+ export function validateQueryParams(entry: QueryCatalogEntry, params: Record<string, unknown>): void {
5
+ const specs = normalizeBindParamRecord(entry.params ?? {});
6
+ const normalizedParams = normalizeBindParamRecord(params);
7
+ for (const [name, spec] of Object.entries(specs)) {
8
+ const value = normalizedParams[name];
9
+ if (spec.required && (value === undefined || value === null || value === "")) {
10
+ throw new Error(`Missing required query param '${name}'.`);
11
+ }
12
+ validateParamValue(name, value, spec);
13
+ }
14
+ }
15
+
16
+ export function normalizeQueryCatalog(queries: Record<string, QueryCatalogEntry>): Record<string, QueryCatalogEntry> {
17
+ return Object.fromEntries(Object.entries(queries).map(([id, entry]) => [id, normalizeQueryCatalogEntry(entry)]));
18
+ }
19
+
20
+ export function normalizeQueryCatalogEntry(entry: QueryCatalogEntry): QueryCatalogEntry {
21
+ return {
22
+ ...entry,
23
+ params: entry.params ? normalizeBindParamRecord(entry.params) : undefined
24
+ };
25
+ }
26
+
27
+ export function normalizeBindParamRecord<T>(value: Record<string, T>): Record<string, T> {
28
+ const normalized: Record<string, T> = {};
29
+ for (const [name, entry] of Object.entries(value)) {
30
+ const bindName = normalizeBindParamName(name);
31
+ if (bindName) normalized[bindName] = entry;
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ export function normalizeBindParamName(name: string): string {
37
+ return String(name ?? "").trim().replace(/^:+/, "");
38
+ }
39
+
40
+ function validateParamValue(name: string, value: unknown, spec: CatalogParam): void {
41
+ if (value === undefined || value === null) return;
42
+ if (spec.type?.endsWith("[]")) {
43
+ const expectedItemType = spec.type.slice(0, -"[]".length);
44
+ const values = Array.isArray(value) ? value : [value];
45
+ if (values.length === 0) throw new Error(`Query param '${name}' must contain at least one value.`);
46
+ for (const item of values) {
47
+ if (typeof item !== expectedItemType) {
48
+ throw new Error(`Query param '${name}' must be ${spec.type}; got ${typeof item} item.`);
49
+ }
50
+ validatePattern(name, item, spec);
51
+ validateLuhn(name, item, spec);
52
+ }
53
+ return;
54
+ }
55
+ if (spec.type && typeof value !== spec.type) {
56
+ throw new Error(`Query param '${name}' must be ${spec.type}; got ${typeof value}.`);
57
+ }
58
+ validatePattern(name, value, spec);
59
+ validateLuhn(name, value, spec);
60
+ }
61
+
62
+ function validatePattern(name: string, value: unknown, spec: CatalogParam): void {
63
+ if (spec.pattern && typeof value === "string" && !new RegExp(spec.pattern).test(value)) {
64
+ throw new Error(`Query param '${name}' does not match ${spec.pattern}.`);
65
+ }
66
+ }
67
+
68
+ function validateLuhn(name: string, value: unknown, spec: CatalogParam): void {
69
+ if (!spec.luhn) return;
70
+ if (typeof value !== "string" && typeof value !== "number") {
71
+ throw new Error(`Query param '${name}' must be a string or number for Luhn validation.`);
72
+ }
73
+ const digits = String(value).replace(/\D+/g, "");
74
+ if (digits.length < 12 || digits.length > 19) {
75
+ throw new Error(`Query param '${name}' must be 12-19 digits for Luhn validation.`);
76
+ }
77
+ if (!isLuhnValid(digits)) {
78
+ throw new Error(`Query param '${name}' fails Luhn check.`);
79
+ }
80
+ }
@@ -0,0 +1,81 @@
1
+ import type { BatchCatalogEntry } from "../../dsl/types.js";
2
+
3
+ export function buildBatchCommand(entry: BatchCatalogEntry, params: Record<string, unknown>): string {
4
+ return buildBatchCommandDetails(entry, params).command;
5
+ }
6
+
7
+ export function buildBatchDisplayCommand(entry: BatchCatalogEntry, params: Record<string, unknown>): string {
8
+ return buildBatchCommandDetails(entry, params).displayCommand;
9
+ }
10
+
11
+ export interface BuiltBatchCommand {
12
+ command: string;
13
+ displayCommand: string;
14
+ }
15
+
16
+ export function buildBatchCommandDetails(entry: BatchCatalogEntry, params: Record<string, unknown>, extraArgs: Array<string | number | boolean> = []): BuiltBatchCommand {
17
+ const fixedArgs = (entry.fixedArgs ?? []).map((value) => resolvePlaceholders(String(value), params));
18
+ const args = (entry.args ?? []).map((arg) => {
19
+ const hasValue = Object.prototype.hasOwnProperty.call(params, arg.name);
20
+ const value = params[arg.name];
21
+ if (!hasValue || value === undefined || value === null) {
22
+ return undefined;
23
+ }
24
+ if (value === "") {
25
+ if (arg.required === false) return undefined;
26
+ throw new Error(`Missing required batch arg '${arg.name}'.`);
27
+ }
28
+ if (arg.pattern && !new RegExp(arg.pattern).test(String(value))) {
29
+ throw new Error(`Batch arg '${arg.name}' does not match ${arg.pattern}.`);
30
+ }
31
+ return resolvePlaceholders(String(value), params);
32
+ }).filter((value): value is string => Boolean(value));
33
+ const appendedArgs = extraArgs.map((value) => resolvePlaceholders(String(value), params));
34
+
35
+ const commandTokens = [resolvePlaceholders(entry.command, params), ...fixedArgs, ...args, ...appendedArgs];
36
+ const command = commandTokens.map(shellQuote).join(" ");
37
+ const displayCommand = commandTokens.map(displayToken).join(" ");
38
+ const envEntries = Object.entries(entry.environment ?? {})
39
+ .map(([name, value]) => [name, resolvePlaceholders(String(value), params)] as const);
40
+ const envPrefix = envEntries
41
+ .map(([name, value]) => `${name}=${shellQuote(value)}`)
42
+ .join(" ");
43
+ const displayEnvPrefix = envEntries
44
+ .map(([name, value]) => `${name}=${displayToken(value)}`)
45
+ .join(" ");
46
+ const commandWithEnv = envPrefix ? `${envPrefix} ${command}` : command;
47
+ const displayCommandWithEnv = displayEnvPrefix ? `${displayEnvPrefix} ${displayCommand}` : displayCommand;
48
+ const workingDirectory = entry.useWorkingDirectory === true
49
+ ? blankToUndefined(entry.workingDirectory ? resolvePlaceholders(entry.workingDirectory, params) : undefined)
50
+ : undefined;
51
+ return {
52
+ command: workingDirectory
53
+ ? `cd ${shellQuote(workingDirectory)} && ${commandWithEnv}`
54
+ : commandWithEnv,
55
+ displayCommand: workingDirectory
56
+ ? `cd ${displayToken(workingDirectory)} && ${displayCommandWithEnv}`
57
+ : displayCommandWithEnv
58
+ };
59
+ }
60
+
61
+ function shellQuote(value: string): string {
62
+ return `'${value.replace(/'/g, "'\\''")}'`;
63
+ }
64
+
65
+ function resolvePlaceholders(value: string, params: Record<string, unknown>): string {
66
+ return value.replace(/\$\{([A-Za-z0-9_.-]+)\}/g, (_match, name: string) => {
67
+ const paramValue = params[name];
68
+ if (paramValue !== undefined && paramValue !== null) return String(paramValue);
69
+ return process.env[name] ?? "";
70
+ });
71
+ }
72
+
73
+ function displayToken(value: string): string {
74
+ if (value === "") return "\"\"";
75
+ if (/^[A-Za-z0-9_./:=@%+-]+$/.test(value)) return value;
76
+ return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
77
+ }
78
+
79
+ function blankToUndefined(value: string | undefined): string | undefined {
80
+ return value && value.trim() ? value : undefined;
81
+ }
@@ -0,0 +1,39 @@
1
+ import type { BatchCatalogEntry, BatchInputFileSpec, BatchInputFileValue } from "../../dsl/types.js";
2
+
3
+ export function batchInputFiles(entry: BatchCatalogEntry | undefined): BatchInputFileSpec[] {
4
+ return entry?.inputFiles ?? [];
5
+ }
6
+
7
+ export function batchInputFileParamNames(entry: BatchCatalogEntry | undefined): string[] {
8
+ return batchInputFiles(entry).flatMap((file) => {
9
+ const names = [file.name];
10
+ if (file.paramName && file.paramName !== file.name) names.push(file.paramName);
11
+ return names;
12
+ });
13
+ }
14
+
15
+ export function batchFileBackedArgNames(entry: BatchCatalogEntry | undefined): Set<string> {
16
+ return new Set(batchInputFiles(entry).map((file) => file.paramName || file.name));
17
+ }
18
+
19
+ export function isBatchInputFileValue(value: unknown): value is BatchInputFileValue {
20
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
21
+ }
22
+
23
+ export function hasBatchInputFilePayload(value: unknown): boolean {
24
+ if (typeof value === "string") return value.trim().length > 0;
25
+ if (!isBatchInputFileValue(value)) return false;
26
+ return Boolean(value.localPath || value.contentBase64);
27
+ }
28
+
29
+ export function batchArgParamsForValidation(params: Record<string, unknown>, entry: BatchCatalogEntry | undefined): Record<string, unknown> {
30
+ const next = { ...params };
31
+ for (const file of batchInputFiles(entry)) {
32
+ const value = params[file.name];
33
+ if (!hasBatchInputFilePayload(value)) continue;
34
+ next[file.paramName || file.name] = isBatchInputFileValue(value)
35
+ ? value.remotePath || file.remotePath || "__uploaded_input_file__"
36
+ : file.remotePath || "__uploaded_input_file__";
37
+ }
38
+ return next;
39
+ }