@apiquest/fracture 1.0.2 → 1.0.4

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 (168) hide show
  1. package/README.md +119 -0
  2. package/bin/cli.js +2 -2
  3. package/dist/CollectionRunner.js +3 -3
  4. package/dist/ScriptEngine.js +4 -4
  5. package/dist/cli/plugin-commands.d.ts.map +1 -1
  6. package/dist/cli/plugin-commands.js +2 -1
  7. package/dist/cli/plugin-commands.js.map +1 -1
  8. package/package.json +55 -50
  9. package/src/CollectionAnalyzer.ts +102 -102
  10. package/src/CollectionRunner.ts +1423 -1423
  11. package/src/CollectionRunner.types.ts +9 -9
  12. package/src/CollectionValidator.ts +289 -289
  13. package/src/ConsoleReporter.ts +143 -143
  14. package/src/CookieJar.ts +258 -258
  15. package/src/DagScheduler.ts +439 -439
  16. package/src/Logger.ts +85 -85
  17. package/src/PluginLoader.ts +126 -126
  18. package/src/PluginManager.ts +208 -208
  19. package/src/PluginResolver.ts +154 -154
  20. package/src/QuestAPI.ts +764 -764
  21. package/src/QuestAPI.types.ts +33 -33
  22. package/src/QuestTestAPI.ts +164 -164
  23. package/src/RequestFilter.ts +224 -224
  24. package/src/ScriptEngine.ts +219 -219
  25. package/src/ScriptValidator.ts +428 -428
  26. package/src/TaskGraph.ts +598 -598
  27. package/src/TestCounter.ts +109 -109
  28. package/src/VariableResolver.ts +114 -114
  29. package/src/cli/index.ts +480 -480
  30. package/src/cli/plugin-commands.ts +342 -341
  31. package/src/cli/plugin-discovery.ts +44 -44
  32. package/src/index.ts +24 -24
  33. package/src/utils.ts +52 -52
  34. package/tsconfig.json +20 -20
  35. package/tsconfig.test.json +5 -5
  36. package/vitest.config.ts +22 -22
  37. package/dist/ExecutionTree.d.ts +0 -77
  38. package/dist/ExecutionTree.d.ts.map +0 -1
  39. package/dist/ExecutionTree.js +0 -265
  40. package/dist/ExecutionTree.js.map +0 -1
  41. package/dist/fracture/src/CollectionAnalyzer.d.ts +0 -17
  42. package/dist/fracture/src/CollectionAnalyzer.d.ts.map +0 -1
  43. package/dist/fracture/src/CollectionAnalyzer.js +0 -70
  44. package/dist/fracture/src/CollectionAnalyzer.js.map +0 -1
  45. package/dist/fracture/src/CollectionRunner.d.ts +0 -39
  46. package/dist/fracture/src/CollectionRunner.d.ts.map +0 -1
  47. package/dist/fracture/src/CollectionRunner.js +0 -802
  48. package/dist/fracture/src/CollectionRunner.js.map +0 -1
  49. package/dist/fracture/src/CollectionRunner.types.d.ts +0 -8
  50. package/dist/fracture/src/CollectionRunner.types.d.ts.map +0 -1
  51. package/dist/fracture/src/CollectionRunner.types.js +0 -2
  52. package/dist/fracture/src/CollectionRunner.types.js.map +0 -1
  53. package/dist/fracture/src/CollectionValidator.d.ts +0 -14
  54. package/dist/fracture/src/CollectionValidator.d.ts.map +0 -1
  55. package/dist/fracture/src/CollectionValidator.js +0 -145
  56. package/dist/fracture/src/CollectionValidator.js.map +0 -1
  57. package/dist/fracture/src/ConsoleReporter.d.ts +0 -24
  58. package/dist/fracture/src/ConsoleReporter.d.ts.map +0 -1
  59. package/dist/fracture/src/ConsoleReporter.js +0 -123
  60. package/dist/fracture/src/ConsoleReporter.js.map +0 -1
  61. package/dist/fracture/src/CookieJar.d.ts +0 -70
  62. package/dist/fracture/src/CookieJar.d.ts.map +0 -1
  63. package/dist/fracture/src/CookieJar.js +0 -233
  64. package/dist/fracture/src/CookieJar.js.map +0 -1
  65. package/dist/fracture/src/ExecutionTree.d.ts +0 -77
  66. package/dist/fracture/src/ExecutionTree.d.ts.map +0 -1
  67. package/dist/fracture/src/ExecutionTree.js +0 -258
  68. package/dist/fracture/src/ExecutionTree.js.map +0 -1
  69. package/dist/fracture/src/Logger.d.ts +0 -25
  70. package/dist/fracture/src/Logger.d.ts.map +0 -1
  71. package/dist/fracture/src/Logger.js +0 -78
  72. package/dist/fracture/src/Logger.js.map +0 -1
  73. package/dist/fracture/src/PluginLoader.d.ts +0 -23
  74. package/dist/fracture/src/PluginLoader.d.ts.map +0 -1
  75. package/dist/fracture/src/PluginLoader.js +0 -102
  76. package/dist/fracture/src/PluginLoader.js.map +0 -1
  77. package/dist/fracture/src/PluginManager.d.ts +0 -64
  78. package/dist/fracture/src/PluginManager.d.ts.map +0 -1
  79. package/dist/fracture/src/PluginManager.js +0 -162
  80. package/dist/fracture/src/PluginManager.js.map +0 -1
  81. package/dist/fracture/src/PluginResolver.d.ts +0 -35
  82. package/dist/fracture/src/PluginResolver.d.ts.map +0 -1
  83. package/dist/fracture/src/PluginResolver.js +0 -128
  84. package/dist/fracture/src/PluginResolver.js.map +0 -1
  85. package/dist/fracture/src/QuestAPI.d.ts +0 -9
  86. package/dist/fracture/src/QuestAPI.d.ts.map +0 -1
  87. package/dist/fracture/src/QuestAPI.js +0 -679
  88. package/dist/fracture/src/QuestAPI.js.map +0 -1
  89. package/dist/fracture/src/QuestAPI.types.d.ts +0 -35
  90. package/dist/fracture/src/QuestAPI.types.d.ts.map +0 -1
  91. package/dist/fracture/src/QuestAPI.types.js +0 -3
  92. package/dist/fracture/src/QuestAPI.types.js.map +0 -1
  93. package/dist/fracture/src/QuestTestAPI.d.ts +0 -12
  94. package/dist/fracture/src/QuestTestAPI.d.ts.map +0 -1
  95. package/dist/fracture/src/QuestTestAPI.js +0 -133
  96. package/dist/fracture/src/QuestTestAPI.js.map +0 -1
  97. package/dist/fracture/src/ScriptEngine.d.ts +0 -21
  98. package/dist/fracture/src/ScriptEngine.d.ts.map +0 -1
  99. package/dist/fracture/src/ScriptEngine.js +0 -183
  100. package/dist/fracture/src/ScriptEngine.js.map +0 -1
  101. package/dist/fracture/src/ScriptValidator.d.ts +0 -68
  102. package/dist/fracture/src/ScriptValidator.d.ts.map +0 -1
  103. package/dist/fracture/src/ScriptValidator.js +0 -351
  104. package/dist/fracture/src/ScriptValidator.js.map +0 -1
  105. package/dist/fracture/src/TestCounter.d.ts +0 -18
  106. package/dist/fracture/src/TestCounter.d.ts.map +0 -1
  107. package/dist/fracture/src/TestCounter.js +0 -82
  108. package/dist/fracture/src/TestCounter.js.map +0 -1
  109. package/dist/fracture/src/VariableResolver.d.ts +0 -20
  110. package/dist/fracture/src/VariableResolver.d.ts.map +0 -1
  111. package/dist/fracture/src/VariableResolver.js +0 -100
  112. package/dist/fracture/src/VariableResolver.js.map +0 -1
  113. package/dist/fracture/src/cli/index.d.ts +0 -3
  114. package/dist/fracture/src/cli/index.d.ts.map +0 -1
  115. package/dist/fracture/src/cli/index.js +0 -347
  116. package/dist/fracture/src/cli/index.js.map +0 -1
  117. package/dist/fracture/src/cli/plugin-commands.d.ts +0 -6
  118. package/dist/fracture/src/cli/plugin-commands.d.ts.map +0 -1
  119. package/dist/fracture/src/cli/plugin-commands.js +0 -263
  120. package/dist/fracture/src/cli/plugin-commands.js.map +0 -1
  121. package/dist/fracture/src/cli/plugin-discovery.d.ts +0 -11
  122. package/dist/fracture/src/cli/plugin-discovery.d.ts.map +0 -1
  123. package/dist/fracture/src/cli/plugin-discovery.js +0 -64
  124. package/dist/fracture/src/cli/plugin-discovery.js.map +0 -1
  125. package/dist/fracture/src/index.d.ts +0 -13
  126. package/dist/fracture/src/index.d.ts.map +0 -1
  127. package/dist/fracture/src/index.js +0 -17
  128. package/dist/fracture/src/index.js.map +0 -1
  129. package/dist/fracture/src/utils.d.ts +0 -28
  130. package/dist/fracture/src/utils.d.ts.map +0 -1
  131. package/dist/fracture/src/utils.js +0 -48
  132. package/dist/fracture/src/utils.js.map +0 -1
  133. package/dist/plugin-auth/src/apikey-auth.d.ts +0 -3
  134. package/dist/plugin-auth/src/apikey-auth.d.ts.map +0 -1
  135. package/dist/plugin-auth/src/apikey-auth.js +0 -73
  136. package/dist/plugin-auth/src/apikey-auth.js.map +0 -1
  137. package/dist/plugin-auth/src/basic-auth.d.ts +0 -3
  138. package/dist/plugin-auth/src/basic-auth.d.ts.map +0 -1
  139. package/dist/plugin-auth/src/basic-auth.js +0 -61
  140. package/dist/plugin-auth/src/basic-auth.js.map +0 -1
  141. package/dist/plugin-auth/src/bearer-auth.d.ts +0 -3
  142. package/dist/plugin-auth/src/bearer-auth.d.ts.map +0 -1
  143. package/dist/plugin-auth/src/bearer-auth.js +0 -49
  144. package/dist/plugin-auth/src/bearer-auth.js.map +0 -1
  145. package/dist/plugin-auth/src/helpers.d.ts +0 -3
  146. package/dist/plugin-auth/src/helpers.d.ts.map +0 -1
  147. package/dist/plugin-auth/src/helpers.js +0 -8
  148. package/dist/plugin-auth/src/helpers.js.map +0 -1
  149. package/dist/plugin-auth/src/index.d.ts +0 -10
  150. package/dist/plugin-auth/src/index.d.ts.map +0 -1
  151. package/dist/plugin-auth/src/index.js +0 -25
  152. package/dist/plugin-auth/src/index.js.map +0 -1
  153. package/dist/plugin-auth/src/oauth2-auth.d.ts +0 -35
  154. package/dist/plugin-auth/src/oauth2-auth.d.ts.map +0 -1
  155. package/dist/plugin-auth/src/oauth2-auth.js +0 -266
  156. package/dist/plugin-auth/src/oauth2-auth.js.map +0 -1
  157. package/dist/plugin-http/src/index.d.ts +0 -4
  158. package/dist/plugin-http/src/index.d.ts.map +0 -1
  159. package/dist/plugin-http/src/index.js +0 -266
  160. package/dist/plugin-http/src/index.js.map +0 -1
  161. package/dist/plugin-vault-file/src/index.d.ts +0 -67
  162. package/dist/plugin-vault-file/src/index.d.ts.map +0 -1
  163. package/dist/plugin-vault-file/src/index.js +0 -171
  164. package/dist/plugin-vault-file/src/index.js.map +0 -1
  165. package/dist/types.d.ts +0 -374
  166. package/dist/types.d.ts.map +0 -1
  167. package/dist/types.js +0 -13
  168. package/dist/types.js.map +0 -1
@@ -1,109 +1,109 @@
1
- import type { Collection, CollectionItem, Request, Folder } from '@apiquest/types';
2
- import { ScriptValidator } from './ScriptValidator.js';
3
- import type { PluginManager } from './PluginManager.js';
4
- import { Logger } from './Logger.js';
5
-
6
- /**
7
- * Counts total expected tests in a collection for deterministic test reporting
8
- */
9
- export class TestCounter {
10
- private logger: Logger;
11
-
12
- constructor(
13
- private readonly pluginManager: PluginManager,
14
- baseLogger?: Logger
15
- ) {
16
- this.logger = baseLogger?.createLogger('TestCounter') ?? new Logger('TestCounter');
17
- }
18
-
19
- /**
20
- * Count total expected tests in collection
21
- * - Counts quest.test() calls in all scripts
22
- * - Multiplies by iteration count
23
- * - Returns -1 if collection has dynamic plugin events (can't determine count)
24
- * @returns Total expected test count, or -1 if dynamic
25
- */
26
- countTests(collection: Collection): number {
27
- let totalTests = 0;
28
- let hasDynamicTests = false;
29
-
30
- this.logger.debug(`Counting tests for collection: ${collection.info.name}`);
31
-
32
- // Note: collectionPre/Post and folderPre/Post scripts CANNOT have tests (validation catches this)
33
- // Only preRequestScript and postRequestScript can have tests
34
-
35
- // Helper to walk tree and count tests for each REQUEST with inherited scripts
36
- const countRequestTests = (item: CollectionItem, inheritedPreRequest: string[], inheritedPostRequest: string[]): void => {
37
- if (item.type === 'folder') {
38
- const folder = item;
39
-
40
- // Build inherited script chain (scripts STACK, not override)
41
- const newInheritedPre = (folder.preRequestScript !== null && folder.preRequestScript !== undefined && folder.preRequestScript.length > 0)
42
- ? [...inheritedPreRequest, folder.preRequestScript]
43
- : inheritedPreRequest;
44
- const newInheritedPost = (folder.postRequestScript !== null && folder.postRequestScript !== undefined && folder.postRequestScript.length > 0)
45
- ? [...inheritedPostRequest, folder.postRequestScript]
46
- : inheritedPostRequest;
47
-
48
- // Recursively process folder contents
49
- for (const child of folder.items) {
50
- countRequestTests(child, newInheritedPre, newInheritedPost);
51
- }
52
- } else {
53
- // Request - count ALL scripts in execution chain for THIS request
54
- const request = item;
55
-
56
- // Inherited postRequestScripts (collection and all ancestor folders) - they STACK
57
- for (const script of inheritedPostRequest) {
58
- totalTests += ScriptValidator.countTests(script);
59
- }
60
-
61
- // Request-level postRequestScript (this is where tests are!)
62
- if (request.postRequestScript !== null && request.postRequestScript !== undefined && request.postRequestScript.length > 0) {
63
- totalTests += ScriptValidator.countTests(request.postRequestScript);
64
- }
65
-
66
- // Plugin event scripts
67
- if (request.data.scripts !== null && request.data.scripts !== undefined && Array.isArray(request.data.scripts)) {
68
- const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
69
- if (protocolPlugin?.events !== null && protocolPlugin?.events !== undefined) {
70
- for (const script of request.data.scripts) {
71
- const eventDef = protocolPlugin.events.find(e => e.name === script.event);
72
- if (eventDef?.canHaveTests === true) {
73
- const expectedMessages = ScriptValidator.extractExpectedMessages(
74
- (request.preRequestScript !== null && request.preRequestScript !== undefined && request.preRequestScript.length > 0) ? request.preRequestScript : ''
75
- );
76
-
77
- if (expectedMessages !== null) {
78
- const testsPerEvent = ScriptValidator.countTests(script.script);
79
- totalTests += testsPerEvent * expectedMessages;
80
- } else {
81
- hasDynamicTests = true;
82
- }
83
- }
84
- }
85
- }
86
- }
87
- }
88
- };
89
-
90
- // Process all items with collection-level inherited scripts
91
- const collectionPre = (collection.preRequestScript !== null && collection.preRequestScript !== undefined && collection.preRequestScript.length > 0) ? [collection.preRequestScript] : [];
92
- const collectionPost = (collection.postRequestScript !== null && collection.postRequestScript !== undefined && collection.postRequestScript.length > 0) ? [collection.postRequestScript] : [];
93
-
94
- for (const item of collection.items) {
95
- countRequestTests(item, collectionPre, collectionPost);
96
- }
97
-
98
- // Multiply by iteration count
99
- const iterationCount = (collection.testData?.length !== null && collection.testData?.length !== undefined && collection.testData.length > 0) ? collection.testData.length : 1;
100
- totalTests *= iterationCount;
101
-
102
- const result = hasDynamicTests ? -1 : totalTests;
103
- if (hasDynamicTests) {
104
- this.logger.debug('Dynamic test count detected; returning -1');
105
- }
106
- this.logger.debug(`Expected test count resolved: ${result}`);
107
- return result;
108
- }
109
- }
1
+ import type { Collection, CollectionItem, Request, Folder } from '@apiquest/types';
2
+ import { ScriptValidator } from './ScriptValidator.js';
3
+ import type { PluginManager } from './PluginManager.js';
4
+ import { Logger } from './Logger.js';
5
+
6
+ /**
7
+ * Counts total expected tests in a collection for deterministic test reporting
8
+ */
9
+ export class TestCounter {
10
+ private logger: Logger;
11
+
12
+ constructor(
13
+ private readonly pluginManager: PluginManager,
14
+ baseLogger?: Logger
15
+ ) {
16
+ this.logger = baseLogger?.createLogger('TestCounter') ?? new Logger('TestCounter');
17
+ }
18
+
19
+ /**
20
+ * Count total expected tests in collection
21
+ * - Counts quest.test() calls in all scripts
22
+ * - Multiplies by iteration count
23
+ * - Returns -1 if collection has dynamic plugin events (can't determine count)
24
+ * @returns Total expected test count, or -1 if dynamic
25
+ */
26
+ countTests(collection: Collection): number {
27
+ let totalTests = 0;
28
+ let hasDynamicTests = false;
29
+
30
+ this.logger.debug(`Counting tests for collection: ${collection.info.name}`);
31
+
32
+ // Note: collectionPre/Post and folderPre/Post scripts CANNOT have tests (validation catches this)
33
+ // Only preRequestScript and postRequestScript can have tests
34
+
35
+ // Helper to walk tree and count tests for each REQUEST with inherited scripts
36
+ const countRequestTests = (item: CollectionItem, inheritedPreRequest: string[], inheritedPostRequest: string[]): void => {
37
+ if (item.type === 'folder') {
38
+ const folder = item;
39
+
40
+ // Build inherited script chain (scripts STACK, not override)
41
+ const newInheritedPre = (folder.preRequestScript !== null && folder.preRequestScript !== undefined && folder.preRequestScript.length > 0)
42
+ ? [...inheritedPreRequest, folder.preRequestScript]
43
+ : inheritedPreRequest;
44
+ const newInheritedPost = (folder.postRequestScript !== null && folder.postRequestScript !== undefined && folder.postRequestScript.length > 0)
45
+ ? [...inheritedPostRequest, folder.postRequestScript]
46
+ : inheritedPostRequest;
47
+
48
+ // Recursively process folder contents
49
+ for (const child of folder.items) {
50
+ countRequestTests(child, newInheritedPre, newInheritedPost);
51
+ }
52
+ } else {
53
+ // Request - count ALL scripts in execution chain for THIS request
54
+ const request = item;
55
+
56
+ // Inherited postRequestScripts (collection and all ancestor folders) - they STACK
57
+ for (const script of inheritedPostRequest) {
58
+ totalTests += ScriptValidator.countTests(script);
59
+ }
60
+
61
+ // Request-level postRequestScript (this is where tests are!)
62
+ if (request.postRequestScript !== null && request.postRequestScript !== undefined && request.postRequestScript.length > 0) {
63
+ totalTests += ScriptValidator.countTests(request.postRequestScript);
64
+ }
65
+
66
+ // Plugin event scripts
67
+ if (request.data.scripts !== null && request.data.scripts !== undefined && Array.isArray(request.data.scripts)) {
68
+ const protocolPlugin = this.pluginManager.getPlugin(collection.protocol);
69
+ if (protocolPlugin?.events !== null && protocolPlugin?.events !== undefined) {
70
+ for (const script of request.data.scripts) {
71
+ const eventDef = protocolPlugin.events.find(e => e.name === script.event);
72
+ if (eventDef?.canHaveTests === true) {
73
+ const expectedMessages = ScriptValidator.extractExpectedMessages(
74
+ (request.preRequestScript !== null && request.preRequestScript !== undefined && request.preRequestScript.length > 0) ? request.preRequestScript : ''
75
+ );
76
+
77
+ if (expectedMessages !== null) {
78
+ const testsPerEvent = ScriptValidator.countTests(script.script);
79
+ totalTests += testsPerEvent * expectedMessages;
80
+ } else {
81
+ hasDynamicTests = true;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ };
89
+
90
+ // Process all items with collection-level inherited scripts
91
+ const collectionPre = (collection.preRequestScript !== null && collection.preRequestScript !== undefined && collection.preRequestScript.length > 0) ? [collection.preRequestScript] : [];
92
+ const collectionPost = (collection.postRequestScript !== null && collection.postRequestScript !== undefined && collection.postRequestScript.length > 0) ? [collection.postRequestScript] : [];
93
+
94
+ for (const item of collection.items) {
95
+ countRequestTests(item, collectionPre, collectionPost);
96
+ }
97
+
98
+ // Multiply by iteration count
99
+ const iterationCount = (collection.testData?.length !== null && collection.testData?.length !== undefined && collection.testData.length > 0) ? collection.testData.length : 1;
100
+ totalTests *= iterationCount;
101
+
102
+ const result = hasDynamicTests ? -1 : totalTests;
103
+ if (hasDynamicTests) {
104
+ this.logger.debug('Dynamic test count detected; returning -1');
105
+ }
106
+ this.logger.debug(`Expected test count resolved: ${result}`);
107
+ return result;
108
+ }
109
+ }
@@ -1,114 +1,114 @@
1
- import type { ExecutionContext, IterationData } from '@apiquest/types';
2
- import { extractValue, isNullOrEmpty } from './utils.js';
3
- import { Logger } from './Logger.js';
4
-
5
- export class VariableResolver {
6
- private logger: Logger;
7
-
8
- constructor(baseLogger?: Logger) {
9
- this.logger = baseLogger?.createLogger('VariableResolver') ?? new Logger('VariableResolver');
10
- }
11
-
12
- /**
13
- * Resolve {{variables}} in a template string
14
- * Priority: iteration data > local > collection > environment > global
15
- */
16
- resolve(template: string, context: ExecutionContext): string {
17
- if (isNullOrEmpty(template) || typeof template !== 'string') {
18
- return template;
19
- }
20
-
21
- // Find all {{variable}} patterns
22
- const variableCount = (template.match(/\{\{([^}]+)\}\}/g) ?? []).length;
23
- if (variableCount > 0) {
24
- this.logger.trace(`Resolving ${variableCount} variable(s) in template`);
25
- }
26
-
27
- return template.replace(/\{\{([^}]+)\}\}/g, (match: string, varName: string) => {
28
- const trimmedName = varName.trim();
29
- const value = this.getVariable(trimmedName, context);
30
-
31
- if (value !== null && value !== undefined) {
32
- this.logger.trace(`Resolved {{${trimmedName}}} -> ${typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value}`);
33
- return String(value);
34
- } else {
35
- this.logger.trace(`Variable {{${trimmedName}}} not found, keeping placeholder`);
36
- return match;
37
- }
38
- });
39
- }
40
-
41
- /**
42
- * Resolve all values in an object (recursively)
43
- */
44
- resolveAll(obj: unknown, context: ExecutionContext): unknown {
45
- if (obj === null || obj === undefined) {
46
- return obj;
47
- }
48
-
49
- if (typeof obj === 'string') {
50
- return this.resolve(obj, context);
51
- }
52
-
53
- if (Array.isArray(obj)) {
54
- return obj.map(item => this.resolveAll(item, context));
55
- }
56
-
57
- if (typeof obj === 'object') {
58
- const resolved: Record<string, unknown> = {};
59
- for (const [key, value] of Object.entries(obj)) {
60
- resolved[key] = this.resolveAll(value, context);
61
- }
62
- return resolved;
63
- }
64
-
65
- return obj;
66
- }
67
-
68
- /**
69
- * Get variable value with cascading priority
70
- * Priority: iteration data > scope stack (innermost to outermost) > collection > environment > global
71
- */
72
- private getVariable(name: string, context: ExecutionContext): unknown {
73
- // 1. Iteration data (highest priority)
74
- const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
75
- if (currentIterationData !== null && currentIterationData !== undefined && name in currentIterationData) {
76
- this.logger.trace(`Variable '${name}' found in iteration data`);
77
- return currentIterationData[name];
78
- }
79
-
80
- // 2. Scope stack (hierarchical scope variables - search from innermost to outermost)
81
- // This represents quest.scope.variables which flows through the script inheritance chain
82
- if (context.scopeStack !== null && context.scopeStack !== undefined && context.scopeStack.length > 0) {
83
- // Search from top of stack (most specific) to bottom (least specific)
84
- for (let i = context.scopeStack.length - 1; i >= 0; i--) {
85
- const frame = context.scopeStack[i];
86
- if (name in frame.vars) {
87
- this.logger.trace(`Variable '${name}' found in scope stack (frame ${context.scopeStack.length - 1 - i})`);
88
- return frame.vars[name];
89
- }
90
- }
91
- }
92
-
93
- // 3. Collection variables
94
- if (name in context.collectionVariables) {
95
- this.logger.trace(`Variable '${name}' found in collection variables`);
96
- return extractValue(context.collectionVariables[name]);
97
- }
98
-
99
- // 4. Environment variables
100
- if (context.environment !== null && context.environment !== undefined && name in context.environment.variables) {
101
- this.logger.trace(`Variable '${name}' found in environment variables`);
102
- return extractValue(context.environment.variables[name]);
103
- }
104
-
105
- // 5. Global variables (lowest priority)
106
- if (name in context.globalVariables) {
107
- this.logger.trace(`Variable '${name}' found in global variables`);
108
- return extractValue(context.globalVariables[name]);
109
- }
110
-
111
- this.logger.trace(`Variable '${name}' not found in any scope`);
112
- return null;
113
- }
114
- }
1
+ import type { ExecutionContext, IterationData } from '@apiquest/types';
2
+ import { extractValue, isNullOrEmpty } from './utils.js';
3
+ import { Logger } from './Logger.js';
4
+
5
+ export class VariableResolver {
6
+ private logger: Logger;
7
+
8
+ constructor(baseLogger?: Logger) {
9
+ this.logger = baseLogger?.createLogger('VariableResolver') ?? new Logger('VariableResolver');
10
+ }
11
+
12
+ /**
13
+ * Resolve {{variables}} in a template string
14
+ * Priority: iteration data > local > collection > environment > global
15
+ */
16
+ resolve(template: string, context: ExecutionContext): string {
17
+ if (isNullOrEmpty(template) || typeof template !== 'string') {
18
+ return template;
19
+ }
20
+
21
+ // Find all {{variable}} patterns
22
+ const variableCount = (template.match(/\{\{([^}]+)\}\}/g) ?? []).length;
23
+ if (variableCount > 0) {
24
+ this.logger.trace(`Resolving ${variableCount} variable(s) in template`);
25
+ }
26
+
27
+ return template.replace(/\{\{([^}]+)\}\}/g, (match: string, varName: string) => {
28
+ const trimmedName = varName.trim();
29
+ const value = this.getVariable(trimmedName, context);
30
+
31
+ if (value !== null && value !== undefined) {
32
+ this.logger.trace(`Resolved {{${trimmedName}}} -> ${typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value}`);
33
+ return String(value);
34
+ } else {
35
+ this.logger.trace(`Variable {{${trimmedName}}} not found, keeping placeholder`);
36
+ return match;
37
+ }
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Resolve all values in an object (recursively)
43
+ */
44
+ resolveAll(obj: unknown, context: ExecutionContext): unknown {
45
+ if (obj === null || obj === undefined) {
46
+ return obj;
47
+ }
48
+
49
+ if (typeof obj === 'string') {
50
+ return this.resolve(obj, context);
51
+ }
52
+
53
+ if (Array.isArray(obj)) {
54
+ return obj.map(item => this.resolveAll(item, context));
55
+ }
56
+
57
+ if (typeof obj === 'object') {
58
+ const resolved: Record<string, unknown> = {};
59
+ for (const [key, value] of Object.entries(obj)) {
60
+ resolved[key] = this.resolveAll(value, context);
61
+ }
62
+ return resolved;
63
+ }
64
+
65
+ return obj;
66
+ }
67
+
68
+ /**
69
+ * Get variable value with cascading priority
70
+ * Priority: iteration data > scope stack (innermost to outermost) > collection > environment > global
71
+ */
72
+ private getVariable(name: string, context: ExecutionContext): unknown {
73
+ // 1. Iteration data (highest priority)
74
+ const currentIterationData = context.iterationData?.[context.iterationCurrent - 1];
75
+ if (currentIterationData !== null && currentIterationData !== undefined && name in currentIterationData) {
76
+ this.logger.trace(`Variable '${name}' found in iteration data`);
77
+ return currentIterationData[name];
78
+ }
79
+
80
+ // 2. Scope stack (hierarchical scope variables - search from innermost to outermost)
81
+ // This represents quest.scope.variables which flows through the script inheritance chain
82
+ if (context.scopeStack !== null && context.scopeStack !== undefined && context.scopeStack.length > 0) {
83
+ // Search from top of stack (most specific) to bottom (least specific)
84
+ for (let i = context.scopeStack.length - 1; i >= 0; i--) {
85
+ const frame = context.scopeStack[i];
86
+ if (name in frame.vars) {
87
+ this.logger.trace(`Variable '${name}' found in scope stack (frame ${context.scopeStack.length - 1 - i})`);
88
+ return frame.vars[name];
89
+ }
90
+ }
91
+ }
92
+
93
+ // 3. Collection variables
94
+ if (name in context.collectionVariables) {
95
+ this.logger.trace(`Variable '${name}' found in collection variables`);
96
+ return extractValue(context.collectionVariables[name]);
97
+ }
98
+
99
+ // 4. Environment variables
100
+ if (context.environment !== null && context.environment !== undefined && name in context.environment.variables) {
101
+ this.logger.trace(`Variable '${name}' found in environment variables`);
102
+ return extractValue(context.environment.variables[name]);
103
+ }
104
+
105
+ // 5. Global variables (lowest priority)
106
+ if (name in context.globalVariables) {
107
+ this.logger.trace(`Variable '${name}' found in global variables`);
108
+ return extractValue(context.globalVariables[name]);
109
+ }
110
+
111
+ this.logger.trace(`Variable '${name}' not found in any scope`);
112
+ return null;
113
+ }
114
+ }