@ductape/sdk 0.1.25 → 0.1.27

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 (152) hide show
  1. package/README.md +83 -52
  2. package/dist/agents/agent-context.d.ts +1 -1
  3. package/dist/agents/agent-context.js +1 -1
  4. package/dist/agents/agent-context.js.map +1 -1
  5. package/dist/agents/agents.service.js +4 -4
  6. package/dist/agents/agents.service.js.map +1 -1
  7. package/dist/agents/types/agents.types.d.ts +6 -6
  8. package/dist/api/services/featureApi.service.d.ts +199 -0
  9. package/dist/api/services/featureApi.service.js +201 -0
  10. package/dist/api/services/featureApi.service.js.map +1 -0
  11. package/dist/api/services/processorApi.service.d.ts +2 -2
  12. package/dist/api/services/processorApi.service.js +1 -1
  13. package/dist/api/services/productsApi.service.d.ts +6 -6
  14. package/dist/api/services/productsApi.service.js +3 -3
  15. package/dist/api/services/productsApi.service.js.map +1 -1
  16. package/dist/api/urls.d.ts +12 -12
  17. package/dist/api/urls.js +22 -22
  18. package/dist/api/urls.js.map +1 -1
  19. package/dist/apps/validators/index.d.ts +5 -1
  20. package/dist/apps/validators/index.js +9 -1
  21. package/dist/apps/validators/index.js.map +1 -1
  22. package/dist/apps/validators/joi-validators/create.appWebhook.validator.d.ts +1 -0
  23. package/dist/apps/validators/joi-validators/create.appWebhook.validator.js +1 -0
  24. package/dist/apps/validators/joi-validators/create.appWebhook.validator.js.map +1 -1
  25. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.d.ts +1 -0
  26. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.js +1 -0
  27. package/dist/apps/validators/joi-validators/create.appWebhookEvent.validator.js.map +1 -1
  28. package/dist/apps/validators/joi-validators/update.appWebhook.validator.d.ts +1 -0
  29. package/dist/apps/validators/joi-validators/update.appWebhook.validator.js +1 -0
  30. package/dist/apps/validators/joi-validators/update.appWebhook.validator.js.map +1 -1
  31. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.d.ts +1 -0
  32. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.js +1 -0
  33. package/dist/apps/validators/joi-validators/update.appWebhookEvent.validator.js.map +1 -1
  34. package/dist/brokers/brokers.service.d.ts +2 -2
  35. package/dist/brokers/brokers.service.js +2 -2
  36. package/dist/brokers/brokers.service.js.map +1 -1
  37. package/dist/brokers/types/index.d.ts +1 -1
  38. package/dist/cloud/cloud-database-link.util.js +22 -3
  39. package/dist/cloud/cloud-database-link.util.js.map +1 -1
  40. package/dist/cloud/cloud-service-map.util.js +6 -5
  41. package/dist/cloud/cloud-service-map.util.js.map +1 -1
  42. package/dist/cloud/index.d.ts +1 -1
  43. package/dist/cloud/index.js +3 -1
  44. package/dist/cloud/index.js.map +1 -1
  45. package/dist/cloud/types/cloud.types.d.ts +1 -1
  46. package/dist/cloud/vpc-connector-connect.util.d.ts +11 -2
  47. package/dist/cloud/vpc-connector-connect.util.js +7 -0
  48. package/dist/cloud/vpc-connector-connect.util.js.map +1 -1
  49. package/dist/cloud/vpc-connector-tunnel.client.d.ts +26 -0
  50. package/dist/cloud/vpc-connector-tunnel.client.js +70 -0
  51. package/dist/cloud/vpc-connector-tunnel.client.js.map +1 -1
  52. package/dist/database/adapters/mysql.adapter.js +15 -0
  53. package/dist/database/adapters/mysql.adapter.js.map +1 -1
  54. package/dist/database/adapters/postgresql.adapter.js +11 -55
  55. package/dist/database/adapters/postgresql.adapter.js.map +1 -1
  56. package/dist/database/databases.service.d.ts +3 -3
  57. package/dist/database/databases.service.js +18 -18
  58. package/dist/database/databases.service.js.map +1 -1
  59. package/dist/database/index.d.ts +1 -1
  60. package/dist/database/triggers/trigger-processor.d.ts +7 -7
  61. package/dist/database/triggers/trigger-processor.js +17 -17
  62. package/dist/database/triggers/trigger-processor.js.map +1 -1
  63. package/dist/database/types/connection.interface.d.ts +0 -2
  64. package/dist/database/types/index.d.ts +1 -1
  65. package/dist/database/types/trigger.interface.d.ts +9 -9
  66. package/dist/database/types/trigger.interface.js +3 -3
  67. package/dist/database/types/trigger.interface.js.map +1 -1
  68. package/dist/features/feature-builder.d.ts +70 -0
  69. package/dist/features/feature-builder.js +348 -0
  70. package/dist/features/feature-builder.js.map +1 -0
  71. package/dist/features/feature-executor.d.ts +289 -0
  72. package/dist/features/feature-executor.js +2444 -0
  73. package/dist/features/feature-executor.js.map +1 -0
  74. package/dist/features/features.service.d.ts +412 -0
  75. package/dist/features/features.service.js +2222 -0
  76. package/dist/features/features.service.js.map +1 -0
  77. package/dist/features/index.d.ts +30 -0
  78. package/dist/features/index.js +64 -0
  79. package/dist/features/index.js.map +1 -0
  80. package/dist/features/types/features.types.d.ts +1081 -0
  81. package/dist/features/types/features.types.js +13 -0
  82. package/dist/features/types/features.types.js.map +1 -0
  83. package/dist/features/types/index.d.ts +6 -0
  84. package/dist/features/types/index.js +23 -0
  85. package/dist/features/types/index.js.map +1 -0
  86. package/dist/index.d.ts +35 -48
  87. package/dist/index.js +48 -63
  88. package/dist/index.js.map +1 -1
  89. package/dist/init.interface.d.ts +1 -11
  90. package/dist/inputs/inputs.service.d.ts +4 -4
  91. package/dist/inputs/inputs.service.js.map +1 -1
  92. package/dist/introspect/index.d.ts +31 -0
  93. package/dist/introspect/index.js +168 -0
  94. package/dist/introspect/index.js.map +1 -0
  95. package/dist/jobs/index.d.ts +1 -1
  96. package/dist/jobs/types.d.ts +3 -3
  97. package/dist/logs/logs.service.d.ts +0 -3
  98. package/dist/logs/logs.service.js +1 -4
  99. package/dist/logs/logs.service.js.map +1 -1
  100. package/dist/logs/logs.types.d.ts +4 -7
  101. package/dist/logs/logs.types.js +2 -3
  102. package/dist/logs/logs.types.js.map +1 -1
  103. package/dist/processor/services/processor.service.d.ts +27 -26
  104. package/dist/processor/services/processor.service.js +71 -72
  105. package/dist/processor/services/processor.service.js.map +1 -1
  106. package/dist/processor/utils/processor.utils.d.ts +2 -2
  107. package/dist/processor/utils/processor.utils.js.map +1 -1
  108. package/dist/products/bootstrap-cache.d.ts +2 -2
  109. package/dist/products/bootstrap-cache.js +3 -3
  110. package/dist/products/bootstrap-cache.js.map +1 -1
  111. package/dist/products/services/products.service.d.ts +32 -57
  112. package/dist/products/services/products.service.js +44 -397
  113. package/dist/products/services/products.service.js.map +1 -1
  114. package/dist/products/validators/index.d.ts +1 -3
  115. package/dist/products/validators/index.js +1 -5
  116. package/dist/products/validators/index.js.map +1 -1
  117. package/dist/products/validators/joi-validators/create.productDatabase.validator.js +1 -1
  118. package/dist/products/validators/joi-validators/create.productDatabase.validator.js.map +1 -1
  119. package/dist/products/validators/joi-validators/create.productFeature.validator.d.ts +1 -7
  120. package/dist/products/validators/joi-validators/create.productFeature.validator.js +6 -61
  121. package/dist/products/validators/joi-validators/create.productFeature.validator.js.map +1 -1
  122. package/dist/products/validators/joi-validators/update.productDatabase.validator.js +23 -27
  123. package/dist/products/validators/joi-validators/update.productDatabase.validator.js.map +1 -1
  124. package/dist/resilience/fallback.service.js +19 -41
  125. package/dist/resilience/fallback.service.js.map +1 -1
  126. package/dist/resilience/healthcheck.service.d.ts +5 -5
  127. package/dist/resilience/healthcheck.service.js +26 -26
  128. package/dist/resilience/healthcheck.service.js.map +1 -1
  129. package/dist/resilience/quota.service.js +17 -27
  130. package/dist/resilience/quota.service.js.map +1 -1
  131. package/dist/resilience/resilience-execution.utils.d.ts +2 -2
  132. package/dist/resilience/resilience-execution.utils.js +7 -7
  133. package/dist/resilience/resilience-execution.utils.js.map +1 -1
  134. package/dist/resilience/resilience.service.js +14 -30
  135. package/dist/resilience/resilience.service.js.map +1 -1
  136. package/dist/resilience/types/index.d.ts +20 -23
  137. package/dist/resilience/types/index.js +2 -3
  138. package/dist/resilience/types/index.js.map +1 -1
  139. package/dist/types/enums.d.ts +1 -2
  140. package/dist/types/enums.js +1 -2
  141. package/dist/types/enums.js.map +1 -1
  142. package/dist/types/index.types.d.ts +3 -4
  143. package/dist/types/index.types.js +2 -2
  144. package/dist/types/index.types.js.map +1 -1
  145. package/dist/types/logs.types.d.ts +0 -1
  146. package/dist/types/logs.types.js.map +1 -1
  147. package/dist/types/processor.types.d.ts +11 -13
  148. package/dist/types/processor.types.js.map +1 -1
  149. package/dist/types/productsBuilder.types.d.ts +52 -58
  150. package/dist/types/productsBuilder.types.js +57 -55
  151. package/dist/types/productsBuilder.types.js.map +1 -1
  152. package/package.json +5 -2
@@ -0,0 +1,2222 @@
1
+ "use strict";
2
+ /**
3
+ * Feature Service
4
+ *
5
+ * Main service class for feature operations.
6
+ * Provides CRUD operations and execution for Ductape Features.
7
+ *
8
+ * Based on Ductape Features Code-First API documentation.
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.featureService = exports.FeatureService = exports.FeatureError = void 0;
48
+ const crypto_1 = require("crypto");
49
+ const products_service_1 = __importDefault(require("../products/services/products.service"));
50
+ const processor_service_1 = __importDefault(require("../processor/services/processor.service"));
51
+ const featureApi_service_1 = require("../api/services/featureApi.service");
52
+ const logs_service_1 = __importDefault(require("../logs/logs.service"));
53
+ const logs_types_1 = require("../logs/logs.types");
54
+ const processor_utils_1 = require("../processor/utils/processor.utils");
55
+ const cache_manager_1 = require("../cache/cache.manager");
56
+ const productsBuilder_types_1 = require("../types/productsBuilder.types");
57
+ const feature_executor_1 = require("./feature-executor");
58
+ /**
59
+ * Error class for feature-related errors
60
+ */
61
+ class FeatureError extends Error {
62
+ constructor(message, code, details) {
63
+ if (typeof window !== 'undefined') {
64
+ throw new Error('Ductape SDK can only be initialized in a server environment.');
65
+ }
66
+ super(message);
67
+ this.code = code;
68
+ this.details = details;
69
+ this.name = 'FeatureError';
70
+ }
71
+ static configurationError(message) {
72
+ return new FeatureError(message, 'CONFIGURATION_ERROR');
73
+ }
74
+ static validationError(message, details) {
75
+ return new FeatureError(message, 'VALIDATION_ERROR', details);
76
+ }
77
+ static notFoundError(message) {
78
+ return new FeatureError(message, 'NOT_FOUND');
79
+ }
80
+ static executionError(message, details) {
81
+ return new FeatureError(message, 'EXECUTION_ERROR', details);
82
+ }
83
+ }
84
+ exports.FeatureError = FeatureError;
85
+ /**
86
+ * Main Feature Service class
87
+ * Provides unified interface for feature management and execution
88
+ */
89
+ class FeatureService {
90
+ /**
91
+ * Create a new FeatureService instance
92
+ * @param config - Optional configuration for authentication and workspace context
93
+ */
94
+ constructor(config) {
95
+ /** Service configuration */
96
+ this.config = null;
97
+ /** ProductBuilder instances cache (keyed by product tag) */
98
+ this.productBuilders = new Map();
99
+ /** Local feature configurations */
100
+ this.localConfigs = new Map();
101
+ /** Feature API service for backend communication */
102
+ this.featureApiService = null;
103
+ /** LogService instance for logging operations */
104
+ this.logService = null;
105
+ /** Current product ID for logging */
106
+ this.productId = '';
107
+ /** Cache manager for 3-tier caching */
108
+ this.cacheManager = null;
109
+ /** Local cache for cache configurations to avoid repeated API calls */
110
+ this.cacheConfigCache = new Map();
111
+ //console.log('[FeatureService] constructor', { hasConfig: !!config, env_type: config?.env_type, hasRedis: !!config?.redis_client });
112
+ this.config = config || null;
113
+ this._privateKey = (config === null || config === void 0 ? void 0 : config.private_key) || '';
114
+ if (config) {
115
+ this.featureApiService = new featureApi_service_1.FeatureApiService(config.env_type);
116
+ console.log('[FeatureService] FeatureApiService initialized', { env_type: config.env_type });
117
+ // Initialize CacheManager if Redis client is provided
118
+ if (config.redis_client) {
119
+ this.cacheManager = new cache_manager_1.CacheManager({
120
+ workspace_id: config.workspace_id,
121
+ public_key: config.public_key,
122
+ user_id: config.user_id,
123
+ token: config.token,
124
+ env_type: config.env_type,
125
+ redis_client: config.redis_client,
126
+ });
127
+ console.log('[FeatureService] CacheManager initialized');
128
+ }
129
+ else {
130
+ console.log('[FeatureService] No redis_client provided, cache disabled');
131
+ }
132
+ }
133
+ else {
134
+ //console.log('[FeatureService] No config provided, service unconfigured');
135
+ }
136
+ }
137
+ // ==================== CONFIGURATION ====================
138
+ /**
139
+ * Update service configuration
140
+ */
141
+ updateConfig(config) {
142
+ console.log('[FeatureService] updateConfig', { env_type: config.env_type, workspace_id: config.workspace_id });
143
+ this.config = config;
144
+ this.featureApiService = new featureApi_service_1.FeatureApiService(config.env_type);
145
+ }
146
+ /**
147
+ * Get auth payload for API calls
148
+ */
149
+ getAuthPayload() {
150
+ if (!this.config) {
151
+ console.log('[FeatureService] getAuthPayload failed: no config');
152
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
153
+ }
154
+ const hasAccessKey = !!this.config.access_key;
155
+ console.log('[FeatureService] getAuthPayload', { user_id: this.config.user_id, workspace_id: this.config.workspace_id, hasAccessKey });
156
+ return Object.assign({ user_id: this.config.user_id, workspace_id: this.config.workspace_id, public_key: this.config.public_key, token: this.config.token }, (this.config.access_key && { access_key: this.config.access_key }));
157
+ }
158
+ /**
159
+ * Ensure FeatureApiService is initialized
160
+ */
161
+ ensureApiService() {
162
+ if (!this.featureApiService) {
163
+ console.log('[FeatureService] ensureApiService failed: FeatureApiService not initialized');
164
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
165
+ }
166
+ return this.featureApiService;
167
+ }
168
+ /**
169
+ * Get service configuration
170
+ */
171
+ getConfig() {
172
+ var _a;
173
+ console.log('[FeatureService] getConfig', { hasConfig: !!this.config, env_type: (_a = this.config) === null || _a === void 0 ? void 0 : _a.env_type });
174
+ return this.config;
175
+ }
176
+ // ==================== PRODUCT BUILDER MANAGEMENT ====================
177
+ /**
178
+ * Create a new ProductBuilder instance
179
+ */
180
+ createNewProductBuilder() {
181
+ if (!this.config) {
182
+ console.log('[FeatureService] createNewProductBuilder failed: no config');
183
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
184
+ }
185
+ console.log('[FeatureService] createNewProductBuilder', { env_type: this.config.env_type, hasAccessKey: !!this.config.access_key });
186
+ return new products_service_1.default({
187
+ workspace_id: this.config.workspace_id,
188
+ public_key: this.config.public_key,
189
+ user_id: this.config.user_id,
190
+ token: this.config.token,
191
+ env_type: this.config.env_type,
192
+ access_key: this.config.access_key,
193
+ workspace_private_key: this._privateKey || undefined,
194
+ });
195
+ }
196
+ /**
197
+ * Get or create a ProductBuilder instance for the given product tag
198
+ */
199
+ async getProductBuilder(productTag) {
200
+ let builder = this.productBuilders.get(productTag);
201
+ if (!builder) {
202
+ console.log('[FeatureService] getProductBuilder creating new builder', { productTag });
203
+ builder = this.createNewProductBuilder();
204
+ await builder.initializeProductByTag(productTag);
205
+ this.productBuilders.set(productTag, builder);
206
+ // Update productId for logging
207
+ this.productId = builder.fetchProductId() || '';
208
+ console.log('[FeatureService] getProductBuilder initialized', { productTag, productId: this.productId });
209
+ }
210
+ else {
211
+ console.log('[FeatureService] getProductBuilder cache hit', { productTag });
212
+ }
213
+ return builder;
214
+ }
215
+ /**
216
+ * Initialize logging service
217
+ */
218
+ initializeLogService() {
219
+ var _a;
220
+ if (!this.logService && this.config) {
221
+ console.log('[FeatureService] initializeLogService', { productId: this.productId });
222
+ this.logService = new logs_service_1.default({
223
+ product_id: this.productId,
224
+ workspace_id: this.config.workspace_id,
225
+ public_key: this.config.public_key,
226
+ user_id: this.config.user_id,
227
+ token: this.config.token,
228
+ env_type: this.config.env_type,
229
+ workspace_private_key: (_a = this._privateKey) !== null && _a !== void 0 ? _a : undefined,
230
+ });
231
+ }
232
+ }
233
+ /**
234
+ * Validate cache tag exists in product and return cache configuration
235
+ */
236
+ async validateCache(productTag, cacheTag) {
237
+ const cacheKey = `${productTag}:${cacheTag}`;
238
+ const cached = this.cacheConfigCache.get(cacheKey);
239
+ const now = Date.now();
240
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
241
+ console.log('[FeatureService] validateCache', { productTag, cacheTag, cacheKey, hasCached: !!cached });
242
+ // Return cached config if still valid
243
+ if (cached && (now - cached.fetchedAt) < CACHE_TTL) {
244
+ console.log('[FeatureService] validateCache using cached config', { cacheKey, expiry: cached.expiry });
245
+ return { expiry: cached.expiry };
246
+ }
247
+ // Fetch from API and cache the result
248
+ const builder = await this.getProductBuilder(productTag);
249
+ const cache = await builder.fetchCache(cacheTag);
250
+ if (!cache) {
251
+ console.log('[FeatureService] validateCache cache config not found', { cacheTag });
252
+ throw FeatureError.validationError(`Cache configuration '${cacheTag}' does not exist`);
253
+ }
254
+ this.cacheConfigCache.set(cacheKey, { expiry: cache.expiry, fetchedAt: now });
255
+ console.log('[FeatureService] validateCache fetched and cached', { cacheKey, expiry: cache.expiry });
256
+ return { expiry: cache.expiry };
257
+ }
258
+ /**
259
+ * Create a new ProcessorService instance for job scheduling
260
+ */
261
+ createNewProcessor() {
262
+ if (!this.config) {
263
+ console.log('[FeatureService] createNewProcessor failed: no config');
264
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
265
+ }
266
+ console.log('[FeatureService] createNewProcessor', { env_type: this.config.env_type, hasAccessKey: !!this.config.access_key });
267
+ return new processor_service_1.default({
268
+ workspace_id: this.config.workspace_id,
269
+ public_key: this.config.public_key,
270
+ user_id: this.config.user_id,
271
+ token: this.config.token,
272
+ env_type: this.config.env_type,
273
+ private_key: this._privateKey,
274
+ access_key: this.config.access_key,
275
+ });
276
+ }
277
+ // ==================== WORKFLOW CRUD OPERATIONS ====================
278
+ /**
279
+ * Create a new feature from JSON schema
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * await ductape.feature.create('my-product', {
284
+ * tag: 'order-fulfillment',
285
+ * name: 'Order Fulfillment',
286
+ * steps: [
287
+ * { tag: 'validate', type: 'action', app: 'orders', event: 'validate', input: {} },
288
+ * { tag: 'process', type: 'database_action', database: 'orders-db', event: 'create', input: {} },
289
+ * ],
290
+ * envs: [{ slug: 'prd' }],
291
+ * });
292
+ * ```
293
+ */
294
+ async create(productTag, featureData) {
295
+ var _a, _b, _c;
296
+ console.log('[FeatureService] create ENTRY', { productTag, tag: featureData.tag, name: featureData.name, stepsCount: (_b = (_a = featureData.steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0 });
297
+ // Validate required fields
298
+ if (!featureData.name || !featureData.tag || !((_c = featureData.steps) === null || _c === void 0 ? void 0 : _c.length)) {
299
+ console.log('[FeatureService] create validation failed: missing name, tag, or steps');
300
+ throw FeatureError.validationError('Feature requires name, tag, and at least one step');
301
+ }
302
+ // Persist to backend using ProductBuilder
303
+ const builder = await this.getProductBuilder(productTag);
304
+ console.log('[FeatureService] create calling builder.createFeature', { tag: featureData.tag });
305
+ await builder.createFeature(featureData);
306
+ // Store locally
307
+ const localConfig = {
308
+ name: featureData.name,
309
+ tag: featureData.tag,
310
+ description: featureData.description,
311
+ input: featureData.input,
312
+ output: featureData.output,
313
+ steps: featureData.steps,
314
+ signals: featureData.signals,
315
+ queries: featureData.queries,
316
+ options: featureData.options,
317
+ envs: featureData.envs,
318
+ };
319
+ this.localConfigs.set(featureData.tag, localConfig);
320
+ console.log('[FeatureService] create SUCCESS', { productTag, tag: featureData.tag });
321
+ }
322
+ /**
323
+ * Fetch all features for a product
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const features = await ductape.feature.fetchAll('my-product');
328
+ * features.forEach(f => console.log(f.name, f.tag));
329
+ * ```
330
+ */
331
+ async fetchAll(productTag) {
332
+ var _a;
333
+ console.log('[FeatureService] fetchAll ENTRY', { productTag });
334
+ // Return local configs if no product tag
335
+ if (!productTag) {
336
+ const localCount = this.localConfigs.size;
337
+ console.log('[FeatureService] fetchAll no productTag, returning local configs', { count: localCount });
338
+ return Array.from(this.localConfigs.values());
339
+ }
340
+ // Fetch from API using ProductBuilder
341
+ try {
342
+ const builder = await this.getProductBuilder(productTag);
343
+ console.log('[FeatureService] fetchAll calling builder.fetchWorkflows', { productTag });
344
+ const features = await builder.fetchFeatures();
345
+ // Convert IProductFeature[] to IFeatureConfig[]
346
+ const result = features.map((f) => this.productFeatureToConfig(f));
347
+ console.log('[FeatureService] fetchAll SUCCESS', { productTag, count: result.length });
348
+ return result;
349
+ }
350
+ catch (error) {
351
+ // Fall back to local configs if API fails
352
+ console.log('[FeatureService] fetchAll API failed, falling back to local', { productTag, error: (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error });
353
+ return Array.from(this.localConfigs.values());
354
+ }
355
+ }
356
+ /**
357
+ * Fetch a specific feature by tag
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * const feature = await ductape.feature.fetch('order-fulfillment', 'my-product');
362
+ * console.log(feature?.name, feature?.steps.length);
363
+ * ```
364
+ */
365
+ async fetch(featureTag, productTag) {
366
+ var _a, _b;
367
+ console.log('[FeatureService] fetch ENTRY', { featureTag, productTag });
368
+ // If productTag is provided, fetch from API
369
+ if (productTag) {
370
+ try {
371
+ const builder = await this.getProductBuilder(productTag);
372
+ console.log('[FeatureService] fetch calling builder.fetchWorkflow', { featureTag, productTag });
373
+ const feature = await builder.fetchFeature(featureTag);
374
+ if (feature) {
375
+ const config = this.productFeatureToConfig(feature);
376
+ // Update local cache
377
+ this.localConfigs.set(featureTag, config);
378
+ console.log('[FeatureService] fetch SUCCESS from API', { featureTag, productTag, stepsCount: (_a = config.steps) === null || _a === void 0 ? void 0 : _a.length });
379
+ return config;
380
+ }
381
+ console.log('[FeatureService] fetch not found on API', { featureTag, productTag });
382
+ return null;
383
+ }
384
+ catch (error) {
385
+ // Fall back to local config
386
+ console.log('[FeatureService] fetch API failed, trying local', { featureTag, productTag, error: (_b = error === null || error === void 0 ? void 0 : error.message) !== null && _b !== void 0 ? _b : error });
387
+ return this.localConfigs.get(featureTag) || null;
388
+ }
389
+ }
390
+ const local = this.localConfigs.get(featureTag) || null;
391
+ console.log('[FeatureService] fetch from local only', { featureTag, found: !!local });
392
+ return local;
393
+ }
394
+ /**
395
+ * Update a feature
396
+ *
397
+ * @example
398
+ * ```ts
399
+ * await ductape.feature.update('order-fulfillment', 'my-product', {
400
+ * description: 'Updated description',
401
+ * options: { timeout: 3600000 },
402
+ * });
403
+ * ```
404
+ */
405
+ async update(featureTag, productTag, updates) {
406
+ var _a, _b, _c, _d, _e, _f, _g;
407
+ console.log('[FeatureService] update ENTRY', { featureTag, productTag, updateKeys: Object.keys(updates || {}) });
408
+ // Update in backend
409
+ const builder = await this.getProductBuilder(productTag);
410
+ await builder.updateFeature(featureTag, updates);
411
+ // Update local cache
412
+ const existing = this.localConfigs.get(featureTag);
413
+ if (existing) {
414
+ const updated = Object.assign(Object.assign({}, existing), { name: updates.name || existing.name, description: (_a = updates.description) !== null && _a !== void 0 ? _a : existing.description, input: (_b = updates.input) !== null && _b !== void 0 ? _b : existing.input, output: (_c = updates.output) !== null && _c !== void 0 ? _c : existing.output, steps: updates.steps || existing.steps, signals: (_d = updates.signals) !== null && _d !== void 0 ? _d : existing.signals, queries: (_e = updates.queries) !== null && _e !== void 0 ? _e : existing.queries, options: (_f = updates.options) !== null && _f !== void 0 ? _f : existing.options, envs: (_g = updates.envs) !== null && _g !== void 0 ? _g : existing.envs });
415
+ this.localConfigs.set(featureTag, updated);
416
+ }
417
+ console.log('[FeatureService] update SUCCESS', { featureTag, productTag });
418
+ }
419
+ /**
420
+ * Delete a feature
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * await ductape.feature.delete('order-fulfillment', 'my-product');
425
+ * ```
426
+ */
427
+ async delete(featureTag, productTag) {
428
+ console.log('[FeatureService] delete ENTRY', { featureTag, productTag });
429
+ // Delete from backend
430
+ const builder = await this.getProductBuilder(productTag);
431
+ await builder.deleteFeature(featureTag);
432
+ // Remove from local cache
433
+ this.localConfigs.delete(featureTag);
434
+ console.log('[FeatureService] delete SUCCESS', { featureTag, productTag });
435
+ }
436
+ // ==================== CODE-FIRST API ====================
437
+ /**
438
+ * Define a feature using the code-first API
439
+ * Compiles the handler to JSON schema and creates the feature in one step
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const orderFeature = await ductape.feature.define({
444
+ * product: 'my-product',
445
+ * tag: 'order-fulfillment',
446
+ * name: 'Order Fulfillment',
447
+ * handler: async (ctx) => {
448
+ * const validation = await ctx.step('validate', async () => {
449
+ * return ctx.action.run({
450
+ * app: 'inventory-service',
451
+ * event: 'validate-order',
452
+ * input: { body: ctx.input },
453
+ * });
454
+ * });
455
+ *
456
+ * if (!validation.valid) {
457
+ * return { success: false, error: validation.reason };
458
+ * }
459
+ *
460
+ * return { success: true };
461
+ * },
462
+ * });
463
+ * ```
464
+ */
465
+ async define(options) {
466
+ var _a, _b;
467
+ console.log('[FeatureService] define ENTRY', { tag: options.tag, name: options.name, product: options.product });
468
+ // Validate required fields
469
+ if (!options.tag || !options.name || !options.handler) {
470
+ console.log('[FeatureService] define validation failed: missing tag, name, or handler');
471
+ throw FeatureError.validationError('Feature definition requires tag, name, and handler');
472
+ }
473
+ // Create a feature compiler instance
474
+ const compiler = new FeatureCompiler(options);
475
+ // Compile the handler to JSON schema (await so async handler runs and steps are captured)
476
+ console.log('[FeatureService] define compiling handler (compileAsync)', { tag: options.tag });
477
+ const schema = await compiler.compileAsync();
478
+ console.log('[FeatureService] define compiled schema', { tag: schema.tag, stepsCount: (_b = (_a = schema.steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0 });
479
+ // If product is specified, create the feature
480
+ if (options.product) {
481
+ console.log('[FeatureService] define creating feature on backend', { product: options.product, tag: schema.tag });
482
+ await this.create(options.product, schema);
483
+ }
484
+ // Store locally
485
+ const localConfig = {
486
+ name: schema.name,
487
+ tag: schema.tag,
488
+ description: schema.description,
489
+ input: schema.input,
490
+ output: schema.output,
491
+ steps: schema.steps,
492
+ signals: schema.signals,
493
+ queries: schema.queries,
494
+ options: schema.options,
495
+ envs: schema.envs,
496
+ };
497
+ this.localConfigs.set(schema.tag, localConfig);
498
+ console.log('[FeatureService] define SUCCESS', { tag: options.tag, product: options.product });
499
+ // Return the defined feature with compile method
500
+ return {
501
+ tag: options.tag,
502
+ name: options.name,
503
+ handler: options.handler,
504
+ compile: () => schema,
505
+ schema,
506
+ };
507
+ }
508
+ // ==================== WORKFLOW EXECUTION ====================
509
+ /**
510
+ * Execute a feature
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * const result = await ductape.feature.execute({
515
+ * product: 'my-product',
516
+ * env: 'production',
517
+ * tag: 'order-fulfillment',
518
+ * input: {
519
+ * order_id: 'ORD-12345',
520
+ * items: [{ product_id: 'PROD-1', quantity: 2 }],
521
+ * },
522
+ * });
523
+ *
524
+ * console.log(result.status); // 'completed' | 'failed' | 'rolled_back'
525
+ * console.log(result.output);
526
+ * ```
527
+ */
528
+ async execute(options) {
529
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
530
+ console.log('[FeatureService] execute ENTRY', { product: options.product, env: options.env, tag: options.tag, hasSession: !!options.session, cache: options.cache });
531
+ if (!this.config) {
532
+ console.log('[FeatureService] execute failed: no config');
533
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
534
+ }
535
+ const process_id = (0, processor_utils_1.generateObjectId)();
536
+ console.log('[FeatureService] execute process_id', { process_id });
537
+ // Session log fields (will be populated if session is provided)
538
+ let sessionLogFields = {};
539
+ let resolvedOptions = options;
540
+ // Prefer locally cached feature (from a recent define()) so step conditions and full schema are preserved
541
+ const builder = await this.getProductBuilder(options.product);
542
+ console.log('[FeatureService] execute fetching feature', { tag: options.tag, product: options.product });
543
+ const localConfig = this.localConfigs.get(options.tag);
544
+ let feature = localConfig
545
+ ? Object.assign(Object.assign({}, localConfig), { tag: localConfig.tag, name: localConfig.name, steps: localConfig.steps }) : null;
546
+ if (feature) {
547
+ console.log('[FeatureService] execute using local feature config', { tag: options.tag });
548
+ }
549
+ if (!feature) {
550
+ feature = await builder.fetchFeature(options.tag);
551
+ }
552
+ if (!feature) {
553
+ console.log('[FeatureService] execute feature not found', { tag: options.tag });
554
+ throw FeatureError.notFoundError(`Feature ${options.tag} not found`);
555
+ }
556
+ console.log('[FeatureService] execute feature loaded', { tag: feature.tag, stepsCount: (_b = (_a = feature.steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0 });
557
+ // Process session if provided - verify and resolve $Session{} references
558
+ if (options.session) {
559
+ const privateKey = builder.fetchPrivateKey();
560
+ if (privateKey) {
561
+ const { processSessionForExecution } = await Promise.resolve().then(() => __importStar(require('../sessions')));
562
+ const sessionResult = await processSessionForExecution(options.session, privateKey, options, options.env, {
563
+ fetchSessionSelector: async (sessionTag) => {
564
+ const sessionConfig = await builder.fetchSession(sessionTag);
565
+ return sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.selector;
566
+ },
567
+ });
568
+ if (sessionResult.error) {
569
+ console.log('[FeatureService] execute session validation failed', { error: sessionResult.error });
570
+ throw FeatureError.executionError(`Session validation failed: ${sessionResult.error}`);
571
+ }
572
+ resolvedOptions = sessionResult.input;
573
+ sessionLogFields = sessionResult.logFields;
574
+ console.log('[FeatureService] execute session resolved', { hasLogFields: Object.keys(sessionLogFields).length > 0 });
575
+ }
576
+ }
577
+ // Initialize logging
578
+ this.initializeLogService();
579
+ const baseLogs = Object.assign({ product_tag: options.product, workspace_id: this.config.workspace_id, env: options.env, process_id, type: logs_types_1.LogEventTypes.FEATURE, parent_tag: options.tag, data: { feature: options.tag, operation: 'execute' } }, sessionLogFields);
580
+ (_c = this.logService) === null || _c === void 0 ? void 0 : _c.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature execute - initiated', status: logs_types_1.LogEventStatus.PROCESSING }));
581
+ const { cache } = options;
582
+ try {
583
+ // Check cache for idempotency (prevent duplicate executions with same input)
584
+ if (cache && this.cacheManager && this._privateKey) {
585
+ console.log('[FeatureService] execute checking cache', { cache, tag: options.tag });
586
+ const cacheConfig = await this.validateCache(options.product, cache);
587
+ const cacheOptions = {
588
+ cache_tag: cache,
589
+ product_tag: options.product,
590
+ component_tag: options.tag,
591
+ component_type: 'feature',
592
+ operation: 'execute',
593
+ input: { tag: options.tag, input: resolvedOptions.input },
594
+ privateKey: this._privateKey,
595
+ expiry: cacheConfig.expiry,
596
+ };
597
+ const cached = await this.cacheManager.fetch(cacheOptions);
598
+ if (cached.hit && cached.data) {
599
+ console.log('[FeatureService] execute cache HIT', { cache, source: cached.source });
600
+ (_d = this.logService) === null || _d === void 0 ? void 0 : _d.add(Object.assign(Object.assign({}, baseLogs), { message: `Feature execute - cache hit (${cached.source})`, successful_execution: true, status: logs_types_1.LogEventStatus.SUCCESS, data: { feature: options.tag, operation: 'execute', cache_source: cached.source }, cache_tag: cache, cache_key: cached.key, cache_status: true }));
601
+ (_e = this.logService) === null || _e === void 0 ? void 0 : _e.publish();
602
+ return cached.data;
603
+ }
604
+ }
605
+ // Create and run the feature executor with resolved options and session context (pass builder to avoid duplicate init)
606
+ console.log('[FeatureService] execute creating FeatureExecutor and running', { tag: options.tag });
607
+ const executor = new feature_executor_1.FeatureExecutor(this.config, feature, resolvedOptions, this._privateKey, sessionLogFields, builder);
608
+ const result = await executor.execute();
609
+ console.log('[FeatureService] execute executor completed', { tag: options.tag, status: result === null || result === void 0 ? void 0 : result.status });
610
+ // Store in cache for idempotency (fire-and-forget)
611
+ if (cache && this.cacheManager && this._privateKey) {
612
+ const cacheConfig = await this.validateCache(options.product, cache);
613
+ this.cacheManager.store({
614
+ cache_tag: cache,
615
+ product_tag: options.product,
616
+ component_tag: options.tag,
617
+ component_type: 'feature',
618
+ operation: 'execute',
619
+ input: { tag: options.tag, input: resolvedOptions.input },
620
+ privateKey: this._privateKey,
621
+ expiry: cacheConfig.expiry,
622
+ }, result);
623
+ }
624
+ (_f = this.logService) === null || _f === void 0 ? void 0 : _f.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature execute - success', successful_execution: true, status: logs_types_1.LogEventStatus.SUCCESS }));
625
+ (_g = this.logService) === null || _g === void 0 ? void 0 : _g.publish();
626
+ console.log('[FeatureService] execute SUCCESS', { tag: options.tag, process_id });
627
+ return result;
628
+ }
629
+ catch (error) {
630
+ console.log('[FeatureService] execute FAILED', { tag: options.tag, process_id, error: (_h = error === null || error === void 0 ? void 0 : error.message) !== null && _h !== void 0 ? _h : error });
631
+ (_j = this.logService) === null || _j === void 0 ? void 0 : _j.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature execute - failed', failed_execution: true, data: { feature: options.tag, operation: 'execute', error: error.message || String(error) }, status: logs_types_1.LogEventStatus.FAIL }));
632
+ await ((_k = this.logService) === null || _k === void 0 ? void 0 : _k.publish());
633
+ throw error;
634
+ }
635
+ }
636
+ // ==================== WORKFLOW DISPATCH ====================
637
+ /**
638
+ * Dispatch a feature to run as a scheduled job
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * // Schedule a feature to run in 1 hour
643
+ * const result = await ductape.feature.dispatch({
644
+ * product: 'my-product',
645
+ * env: 'production',
646
+ * feature: 'order-fulfillment',
647
+ * input: { order_id: 'ORD-123' },
648
+ * schedule: { start_at: Date.now() + 3600000 },
649
+ * });
650
+ *
651
+ * // Run on a cron schedule
652
+ * const result = await ductape.feature.dispatch({
653
+ * product: 'my-product',
654
+ * env: 'production',
655
+ * feature: 'daily-report',
656
+ * input: {},
657
+ * schedule: { cron: '0 0 * * *' }, // Daily at midnight
658
+ * });
659
+ * ```
660
+ */
661
+ async dispatch(data) {
662
+ var _a, _b, _c, _d, _e, _f, _g, _h;
663
+ console.log('[FeatureService] dispatch ENTRY', { product: data.product, env: data.env, feature: data.feature, schedule: !!data.schedule, session: !!data.session, cache: data.cache });
664
+ const process_id = (0, processor_utils_1.generateObjectId)();
665
+ const processorService = this.createNewProcessor();
666
+ const productBuilder = await this.getProductBuilder(data.product);
667
+ const schedule = data.schedule || {};
668
+ const startAt = typeof schedule.start_at === 'string' ? new Date(schedule.start_at).getTime() : schedule.start_at || Date.now();
669
+ // Generate job tag based on feature
670
+ const jobTag = `feature:${data.feature}`;
671
+ console.log('[FeatureService] dispatch jobTag', { jobTag, startAt, cron: schedule.cron, every: schedule.every });
672
+ // Session log fields (will be populated if session is provided)
673
+ let sessionLogFields = {};
674
+ let resolvedData = data;
675
+ // Process session if provided - verify and resolve $Session{} references
676
+ if (data.session) {
677
+ const privateKey = productBuilder.fetchPrivateKey();
678
+ if (privateKey) {
679
+ const { processSessionForExecution } = await Promise.resolve().then(() => __importStar(require('../sessions')));
680
+ const sessionResult = await processSessionForExecution(data.session, privateKey, data, data.env, {
681
+ fetchSessionSelector: async (sessionTag) => {
682
+ const sessionConfig = await productBuilder.fetchSession(sessionTag);
683
+ return sessionConfig === null || sessionConfig === void 0 ? void 0 : sessionConfig.selector;
684
+ },
685
+ });
686
+ if (sessionResult.error) {
687
+ console.log('[FeatureService] dispatch session validation failed', { error: sessionResult.error });
688
+ throw FeatureError.executionError(`Session validation failed: ${sessionResult.error}`);
689
+ }
690
+ resolvedData = sessionResult.input;
691
+ sessionLogFields = sessionResult.logFields;
692
+ console.log('[FeatureService] dispatch session resolved');
693
+ }
694
+ }
695
+ // Initialize logging
696
+ this.initializeLogService();
697
+ const baseLogs = Object.assign({ product_tag: data.product, workspace_id: ((_a = this.config) === null || _a === void 0 ? void 0 : _a.workspace_id) || '', env: data.env, process_id, type: logs_types_1.LogEventTypes.FEATURE, parent_tag: data.feature, data: { feature: data.feature, operation: 'dispatch' } }, sessionLogFields);
698
+ (_b = this.logService) === null || _b === void 0 ? void 0 : _b.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature dispatch - initiated', status: logs_types_1.LogEventStatus.PROCESSING }));
699
+ const { cache } = data;
700
+ try {
701
+ // Check cache for idempotency (prevent duplicate dispatches with same input)
702
+ if (cache && this.cacheManager && this._privateKey) {
703
+ console.log('[FeatureService] dispatch checking cache', { cache, feature: data.feature });
704
+ const cacheConfig = await this.validateCache(data.product, cache);
705
+ const cacheOptions = {
706
+ cache_tag: cache,
707
+ product_tag: data.product,
708
+ component_tag: data.feature,
709
+ component_type: 'feature',
710
+ operation: 'dispatch',
711
+ input: { feature: data.feature, input: data.input, schedule: data.schedule },
712
+ privateKey: this._privateKey,
713
+ expiry: cacheConfig.expiry,
714
+ };
715
+ const cached = await this.cacheManager.fetch(cacheOptions);
716
+ if (cached.hit && cached.data) {
717
+ console.log('[FeatureService] dispatch cache HIT', { cache, source: cached.source });
718
+ (_c = this.logService) === null || _c === void 0 ? void 0 : _c.add(Object.assign(Object.assign({}, baseLogs), { message: `Feature dispatch - cache hit (${cached.source})`, successful_execution: true, status: logs_types_1.LogEventStatus.SUCCESS, data: { feature: data.feature, operation: 'dispatch', cache_source: cached.source }, cache_tag: cache, cache_key: cached.key, cache_status: true }));
719
+ (_d = this.logService) === null || _d === void 0 ? void 0 : _d.publish();
720
+ return cached.data;
721
+ }
722
+ }
723
+ // Check if job exists, if not create it
724
+ console.log('[FeatureService] dispatch fetching existing job', { jobTag });
725
+ const existingJob = await productBuilder.fetchJob(jobTag);
726
+ if (!existingJob) {
727
+ console.log('[FeatureService] dispatch job not found, creating', { jobTag });
728
+ await productBuilder.createJob({
729
+ tag: jobTag,
730
+ name: `Feature: ${data.feature}`,
731
+ description: `Auto-created job for feature ${data.feature}`,
732
+ type: productsBuilder_types_1.JobEventTypes.FEATURE, // Use feature (formerly feature) job type
733
+ app: data.feature,
734
+ event: 'execute',
735
+ executions: 0,
736
+ intervals: schedule.every || 0,
737
+ start_at: startAt,
738
+ });
739
+ }
740
+ const jobInput = {
741
+ product: data.product,
742
+ env: data.env,
743
+ event: jobTag,
744
+ input: resolvedData.input,
745
+ retries: data.retries || 0,
746
+ start_at: startAt,
747
+ session: data.session,
748
+ cache: data.cache,
749
+ repeat: schedule.cron || schedule.every
750
+ ? {
751
+ cron: schedule.cron,
752
+ every: schedule.every,
753
+ limit: schedule.limit,
754
+ endDate: schedule.endDate,
755
+ tz: schedule.tz,
756
+ }
757
+ : undefined,
758
+ };
759
+ console.log('[FeatureService] dispatch calling processJob', { jobTag, hasRepeat: !!(schedule.cron || schedule.every) });
760
+ await processorService.processJob(jobInput);
761
+ const isRecurring = !!(schedule.cron || schedule.every);
762
+ console.log('[FeatureService] dispatch processJob completed', { isRecurring });
763
+ let nextRunAt;
764
+ if (isRecurring && schedule.every) {
765
+ nextRunAt = startAt + schedule.every;
766
+ }
767
+ const job_id = (0, crypto_1.randomUUID)();
768
+ const result = {
769
+ job_id,
770
+ status: startAt > Date.now() ? 'scheduled' : 'queued',
771
+ scheduled_at: startAt,
772
+ recurring: isRecurring,
773
+ next_run_at: nextRunAt,
774
+ };
775
+ // Store in cache for idempotency (fire-and-forget)
776
+ if (cache && this.cacheManager && this._privateKey) {
777
+ const cacheConfig = await this.validateCache(data.product, cache);
778
+ this.cacheManager.store({
779
+ cache_tag: cache,
780
+ product_tag: data.product,
781
+ component_tag: data.feature,
782
+ component_type: 'feature',
783
+ operation: 'dispatch',
784
+ input: { feature: data.feature, input: data.input, schedule: data.schedule },
785
+ privateKey: this._privateKey,
786
+ expiry: cacheConfig.expiry,
787
+ }, result);
788
+ }
789
+ (_e = this.logService) === null || _e === void 0 ? void 0 : _e.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature dispatch - success', successful_execution: true, data: { feature: data.feature, operation: 'dispatch', job_id }, status: logs_types_1.LogEventStatus.SUCCESS }));
790
+ (_f = this.logService) === null || _f === void 0 ? void 0 : _f.publish();
791
+ console.log('[FeatureService] dispatch SUCCESS', { feature: data.feature, job_id: result.job_id, status: result.status });
792
+ return result;
793
+ }
794
+ catch (error) {
795
+ console.log('[FeatureService] dispatch FAILED', { feature: data.feature, error: String(error) });
796
+ (_g = this.logService) === null || _g === void 0 ? void 0 : _g.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature dispatch - failed', failed_execution: true, data: { feature: data.feature, operation: 'dispatch', error: String(error) }, status: logs_types_1.LogEventStatus.FAIL }));
797
+ await ((_h = this.logService) === null || _h === void 0 ? void 0 : _h.publish());
798
+ throw error;
799
+ }
800
+ }
801
+ // ==================== SIGNAL & QUERY ====================
802
+ /**
803
+ * Send a signal to a running feature
804
+ *
805
+ * @example
806
+ * ```ts
807
+ * await ductape.feature.signal({
808
+ * product: 'my-product',
809
+ * env: 'production',
810
+ * feature_id: 'wf-123',
811
+ * signal: 'approve',
812
+ * payload: { approver_id: 'user-456', comments: 'Approved' },
813
+ * });
814
+ * ```
815
+ */
816
+ async signal(options) {
817
+ var _a, _b, _c, _d, _e, _f;
818
+ console.log('[FeatureService] signal ENTRY', { feature_id: options.feature_id, signal: options.signal, product: options.product, env: options.env });
819
+ const process_id = (0, processor_utils_1.generateObjectId)();
820
+ // Initialize logging
821
+ if (options.product) {
822
+ await this.getProductBuilder(options.product);
823
+ }
824
+ this.initializeLogService();
825
+ const baseLogs = {
826
+ product_tag: options.product,
827
+ workspace_id: ((_a = this.config) === null || _a === void 0 ? void 0 : _a.workspace_id) || '',
828
+ env: options.env,
829
+ process_id,
830
+ type: logs_types_1.LogEventTypes.FEATURE,
831
+ parent_tag: options.feature_id,
832
+ child_tag: options.signal,
833
+ data: { feature_id: options.feature_id, signal: options.signal, operation: 'signal' },
834
+ };
835
+ (_b = this.logService) === null || _b === void 0 ? void 0 : _b.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature signal - initiated', status: logs_types_1.LogEventStatus.PROCESSING }));
836
+ try {
837
+ const apiService = this.ensureApiService();
838
+ const auth = this.getAuthPayload();
839
+ console.log('[FeatureService] signal calling apiService.sendSignal', { feature_id: options.feature_id, signal: options.signal });
840
+ await apiService.sendSignal(options.feature_id, {
841
+ product: options.product,
842
+ env: options.env,
843
+ signal: options.signal,
844
+ payload: options.payload,
845
+ }, auth);
846
+ console.log('[FeatureService] signal SUCCESS', { feature_id: options.feature_id, signal: options.signal });
847
+ (_c = this.logService) === null || _c === void 0 ? void 0 : _c.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature signal - success', successful_execution: true, status: logs_types_1.LogEventStatus.SUCCESS }));
848
+ (_d = this.logService) === null || _d === void 0 ? void 0 : _d.publish();
849
+ }
850
+ catch (error) {
851
+ console.log('[FeatureService] signal FAILED', { feature_id: options.feature_id, signal: options.signal, error: String(error) });
852
+ (_e = this.logService) === null || _e === void 0 ? void 0 : _e.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature signal - failed', failed_execution: true, data: { feature_id: options.feature_id, signal: options.signal, operation: 'signal', error: String(error) }, status: logs_types_1.LogEventStatus.FAIL }));
853
+ await ((_f = this.logService) === null || _f === void 0 ? void 0 : _f.publish());
854
+ throw error;
855
+ }
856
+ }
857
+ /**
858
+ * Query a running feature
859
+ *
860
+ * @example
861
+ * ```ts
862
+ * const status = await ductape.feature.query({
863
+ * product: 'my-product',
864
+ * env: 'production',
865
+ * feature_id: 'wf-123',
866
+ * query: 'getStatus',
867
+ * });
868
+ *
869
+ * console.log(status.current_step, status.progress);
870
+ * ```
871
+ */
872
+ async query(options) {
873
+ console.log('[FeatureService] query ENTRY', { feature_id: options.feature_id, query: options.query, product: options.product, env: options.env });
874
+ // Query is handled by getting feature status and extracting query result
875
+ const status = await this.status({
876
+ product: options.product,
877
+ env: options.env,
878
+ feature_id: options.feature_id,
879
+ });
880
+ if (!status) {
881
+ console.log('[FeatureService] query feature not found', { feature_id: options.feature_id });
882
+ throw FeatureError.notFoundError(`Feature ${options.feature_id} not found`);
883
+ }
884
+ const result = (status.state[options.query] || status);
885
+ console.log('[FeatureService] query SUCCESS', { feature_id: options.feature_id, query: options.query, hasResult: result !== undefined });
886
+ // Return the state which contains query results
887
+ return result;
888
+ }
889
+ // ==================== REPLAY, RESTART & RESUME ====================
890
+ /**
891
+ * Replay a feature with the same input
892
+ *
893
+ * @example
894
+ * ```ts
895
+ * const result = await ductape.feature.replay({
896
+ * product: 'my-product',
897
+ * env: 'production',
898
+ * feature_id: 'wf-123',
899
+ * reason: 'Debugging payment failure',
900
+ * });
901
+ * ```
902
+ */
903
+ async replay(options) {
904
+ var _a, _b, _c, _d, _e, _f;
905
+ console.log('[FeatureService] replay ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env });
906
+ const process_id = (0, processor_utils_1.generateObjectId)();
907
+ if (!this.config) {
908
+ console.log('[FeatureService] replay failed: no config');
909
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
910
+ }
911
+ // Initialize logging
912
+ await this.getProductBuilder(options.product);
913
+ this.initializeLogService();
914
+ const baseLogs = {
915
+ product_tag: options.product,
916
+ workspace_id: ((_a = this.config) === null || _a === void 0 ? void 0 : _a.workspace_id) || '',
917
+ env: options.env,
918
+ process_id,
919
+ type: logs_types_1.LogEventTypes.FEATURE,
920
+ parent_tag: options.feature_id,
921
+ data: { feature_id: options.feature_id, operation: 'replay' },
922
+ };
923
+ (_b = this.logService) === null || _b === void 0 ? void 0 : _b.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature replay - initiated', status: logs_types_1.LogEventStatus.PROCESSING }));
924
+ try {
925
+ // Get original feature execution state from backend
926
+ console.log('[FeatureService] replay fetching original status', { feature_id: options.feature_id });
927
+ const originalStatus = await this.status({
928
+ product: options.product,
929
+ env: options.env,
930
+ feature_id: options.feature_id,
931
+ });
932
+ if (!originalStatus) {
933
+ console.log('[FeatureService] replay original feature not found', { feature_id: options.feature_id });
934
+ throw FeatureError.notFoundError(`Feature ${options.feature_id} not found`);
935
+ }
936
+ console.log('[FeatureService] replay original status', { workflow_tag: originalStatus.feature_tag, status: originalStatus.status });
937
+ // Execute with the same input
938
+ console.log('[FeatureService] replay executing with same input', { tag: originalStatus.feature_tag });
939
+ const result = await this.execute({
940
+ product: options.product,
941
+ env: options.env,
942
+ tag: originalStatus.feature_tag,
943
+ input: originalStatus.input || {},
944
+ idempotency_key: options.idempotency_key,
945
+ });
946
+ (_c = this.logService) === null || _c === void 0 ? void 0 : _c.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature replay - success', successful_execution: true, status: logs_types_1.LogEventStatus.SUCCESS }));
947
+ (_d = this.logService) === null || _d === void 0 ? void 0 : _d.publish();
948
+ console.log('[FeatureService] replay SUCCESS', { feature_id: options.feature_id, replayed_from: options.feature_id });
949
+ return Object.assign(Object.assign({}, result), { replayed_from: options.feature_id });
950
+ }
951
+ catch (error) {
952
+ console.log('[FeatureService] replay FAILED', { feature_id: options.feature_id, error: String(error) });
953
+ (_e = this.logService) === null || _e === void 0 ? void 0 : _e.add(Object.assign(Object.assign({}, baseLogs), { message: 'Feature replay - failed', failed_execution: true, data: { feature_id: options.feature_id, operation: 'replay', error: String(error) }, status: logs_types_1.LogEventStatus.FAIL }));
954
+ await ((_f = this.logService) === null || _f === void 0 ? void 0 : _f.publish());
955
+ throw error;
956
+ }
957
+ }
958
+ /**
959
+ * Restart a feature with new or modified input
960
+ *
961
+ * @example
962
+ * ```ts
963
+ * const result = await ductape.feature.restart({
964
+ * product: 'my-product',
965
+ * env: 'production',
966
+ * feature_id: 'wf-123',
967
+ * input: { email: 'corrected@email.com' },
968
+ * reason: 'Customer email was incorrect',
969
+ * });
970
+ * ```
971
+ */
972
+ async restart(options) {
973
+ console.log('[FeatureService] restart ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env, hasInput: !!options.input });
974
+ if (!this.config) {
975
+ console.log('[FeatureService] restart failed: no config');
976
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
977
+ }
978
+ // Get original feature execution state from backend
979
+ const originalStatus = await this.status({
980
+ product: options.product,
981
+ env: options.env,
982
+ feature_id: options.feature_id,
983
+ });
984
+ if (!originalStatus) {
985
+ console.log('[FeatureService] restart feature not found', { feature_id: options.feature_id });
986
+ throw FeatureError.notFoundError(`Feature ${options.feature_id} not found`);
987
+ }
988
+ console.log('[FeatureService] restart original status', { workflow_tag: originalStatus.feature_tag });
989
+ // Determine the new input
990
+ let newInput;
991
+ if (options.input) {
992
+ newInput = options.input;
993
+ }
994
+ else if (options.input_override && options.merge_input) {
995
+ newInput = Object.assign(Object.assign({}, (originalStatus.input || {})), options.input_override);
996
+ }
997
+ else if (options.input_override) {
998
+ newInput = options.input_override;
999
+ }
1000
+ else {
1001
+ newInput = originalStatus.input || {};
1002
+ }
1003
+ // Execute with the new input
1004
+ console.log('[FeatureService] restart executing with new input', { tag: originalStatus.feature_tag, inputKeys: Object.keys(newInput || {}) });
1005
+ const result = await this.execute({
1006
+ product: options.product,
1007
+ env: options.env,
1008
+ tag: originalStatus.feature_tag,
1009
+ input: newInput,
1010
+ });
1011
+ console.log('[FeatureService] restart SUCCESS', { feature_id: options.feature_id, restarted_from: options.feature_id });
1012
+ return Object.assign(Object.assign({}, result), { restarted_from: options.feature_id });
1013
+ }
1014
+ /**
1015
+ * Resume a paused or failed feature from where it stopped
1016
+ *
1017
+ * @example
1018
+ * ```ts
1019
+ * const result = await ductape.feature.resume({
1020
+ * product: 'my-product',
1021
+ * env: 'production',
1022
+ * feature_id: 'wf-123',
1023
+ * from_checkpoint: 'payment-complete',
1024
+ * });
1025
+ * ```
1026
+ */
1027
+ async resume(options) {
1028
+ var _a, _b;
1029
+ console.log('[FeatureService] resume ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env, from_checkpoint: options.from_checkpoint, from_step: options.from_step });
1030
+ if (!this.config) {
1031
+ console.log('[FeatureService] resume failed: no config');
1032
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
1033
+ }
1034
+ // Get original feature execution state from backend
1035
+ const originalStatus = await this.status({
1036
+ product: options.product,
1037
+ env: options.env,
1038
+ feature_id: options.feature_id,
1039
+ });
1040
+ if (!originalStatus) {
1041
+ console.log('[FeatureService] resume feature not found', { feature_id: options.feature_id });
1042
+ throw FeatureError.notFoundError(`Feature ${options.feature_id} not found`);
1043
+ }
1044
+ console.log('[FeatureService] resume original status', { workflow_tag: originalStatus.feature_tag, completed_steps: (_b = (_a = originalStatus.completed_steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0 });
1045
+ // Fetch the feature configuration
1046
+ const builder = await this.getProductBuilder(options.product);
1047
+ const feature = await builder.fetchFeature(originalStatus.feature_tag);
1048
+ if (!feature) {
1049
+ console.log('[FeatureService] resume feature config not found', { tag: originalStatus.feature_tag });
1050
+ throw FeatureError.notFoundError(`Feature not found`);
1051
+ }
1052
+ // Create executor with resume options
1053
+ console.log('[FeatureService] resume creating executor and resuming', { tag: feature.tag });
1054
+ const executor = new feature_executor_1.FeatureExecutor(this.config, feature, {
1055
+ product: options.product,
1056
+ env: options.env,
1057
+ tag: originalStatus.feature_tag,
1058
+ input: Object.assign(Object.assign({}, (originalStatus.input || {})), (options.input || {})),
1059
+ }, this._privateKey);
1060
+ // Resume execution (executor handles skipping completed steps)
1061
+ const result = await executor.resume({
1062
+ completed_steps: originalStatus.completed_steps,
1063
+ state: originalStatus.state,
1064
+ from_checkpoint: options.from_checkpoint,
1065
+ from_step: options.from_step,
1066
+ skip_steps: options.skip_steps,
1067
+ });
1068
+ console.log('[FeatureService] resume SUCCESS', { feature_id: options.feature_id, resumed_from: options.feature_id });
1069
+ return Object.assign(Object.assign({}, result), { resumed_from: options.feature_id, resumed_checkpoint: options.from_checkpoint });
1070
+ }
1071
+ /**
1072
+ * Replay a feature starting from a specific step
1073
+ *
1074
+ * @example
1075
+ * ```ts
1076
+ * const result = await ductape.feature.replayFromStep({
1077
+ * product: 'my-product',
1078
+ * env: 'production',
1079
+ * feature_id: 'wf-123',
1080
+ * from_step: 'process-payment',
1081
+ * });
1082
+ * ```
1083
+ */
1084
+ async replayFromStep(options) {
1085
+ console.log('[FeatureService] replayFromStep ENTRY', { feature_id: options.feature_id, from_step: options.from_step, product: options.product, env: options.env });
1086
+ if (!this.config) {
1087
+ console.log('[FeatureService] replayFromStep failed: no config');
1088
+ throw FeatureError.configurationError('FeatureService not configured. Please provide config when initializing.');
1089
+ }
1090
+ // Get original feature execution state
1091
+ const originalStatus = await this.status({
1092
+ product: options.product,
1093
+ env: options.env,
1094
+ feature_id: options.feature_id,
1095
+ });
1096
+ if (!originalStatus) {
1097
+ console.log('[FeatureService] replayFromStep feature not found', { feature_id: options.feature_id });
1098
+ throw FeatureError.notFoundError(`Feature ${options.feature_id} not found`);
1099
+ }
1100
+ // Fetch the feature configuration
1101
+ const builder = await this.getProductBuilder(options.product);
1102
+ const feature = await builder.fetchFeature(originalStatus.feature_tag);
1103
+ if (!feature) {
1104
+ console.log('[FeatureService] replayFromStep feature config not found', { tag: originalStatus.feature_tag });
1105
+ throw FeatureError.notFoundError(`Feature not found`);
1106
+ }
1107
+ // Find steps before from_step to mark as completed
1108
+ const stepIndex = feature.steps.findIndex((s) => s.tag === options.from_step);
1109
+ if (stepIndex === -1) {
1110
+ console.log('[FeatureService] replayFromStep step not found', { from_step: options.from_step });
1111
+ throw FeatureError.validationError(`Step ${options.from_step} not found in feature`);
1112
+ }
1113
+ const completedSteps = feature.steps.slice(0, stepIndex).map((s) => s.tag);
1114
+ console.log('[FeatureService] replayFromStep stepIndex and completedSteps', { from_step: options.from_step, stepIndex, completedStepsCount: completedSteps.length });
1115
+ // Create executor with partial state
1116
+ const executor = new feature_executor_1.FeatureExecutor(this.config, feature, {
1117
+ product: options.product,
1118
+ env: options.env,
1119
+ tag: originalStatus.feature_tag,
1120
+ input: originalStatus.input || {},
1121
+ }, this._privateKey);
1122
+ // Resume from the specified step with overridden outputs
1123
+ const result = await executor.resume({
1124
+ completed_steps: completedSteps,
1125
+ state: originalStatus.state,
1126
+ step_outputs: options.step_outputs,
1127
+ });
1128
+ console.log('[FeatureService] replayFromStep SUCCESS', { feature_id: options.feature_id, from_step: options.from_step });
1129
+ return Object.assign(Object.assign({}, result), { replayed_from: options.feature_id });
1130
+ }
1131
+ // ==================== STATUS & MANAGEMENT ====================
1132
+ /**
1133
+ * Get the status of a feature execution
1134
+ *
1135
+ * @example
1136
+ * ```ts
1137
+ * const status = await ductape.feature.status({
1138
+ * product: 'my-product',
1139
+ * env: 'production',
1140
+ * feature_id: 'wf-123',
1141
+ * });
1142
+ * ```
1143
+ */
1144
+ async status(options) {
1145
+ console.log('[FeatureService] status ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env });
1146
+ const apiService = this.ensureApiService();
1147
+ const auth = this.getAuthPayload();
1148
+ const response = await apiService.getStatus(options.feature_id, options.product, options.env, auth);
1149
+ if (!response) {
1150
+ console.log('[FeatureService] status not found', { feature_id: options.feature_id });
1151
+ return null;
1152
+ }
1153
+ console.log('[FeatureService] status SUCCESS', { feature_id: response.feature_id, feature_tag: response.feature_tag, status: response.status });
1154
+ return {
1155
+ feature_id: response.feature_id,
1156
+ feature_tag: response.feature_tag,
1157
+ status: response.status,
1158
+ current_step: response.current_step,
1159
+ completed_steps: response.completed_steps,
1160
+ state: response.state,
1161
+ started_at: response.started_at,
1162
+ updated_at: response.updated_at,
1163
+ input: response.input,
1164
+ output: response.output,
1165
+ error: response.error,
1166
+ };
1167
+ }
1168
+ /**
1169
+ * Cancel a running feature
1170
+ *
1171
+ * @example
1172
+ * ```ts
1173
+ * const result = await ductape.feature.cancel({
1174
+ * product: 'my-product',
1175
+ * env: 'production',
1176
+ * feature_id: 'wf-123',
1177
+ * reason: 'User requested cancellation',
1178
+ * });
1179
+ * ```
1180
+ */
1181
+ async cancel(options) {
1182
+ var _a, _b, _c, _d;
1183
+ console.log('[FeatureService] cancel ENTRY', {
1184
+ feature_id: options.feature_id,
1185
+ product: options.product,
1186
+ env: options.env,
1187
+ reason: options.reason,
1188
+ });
1189
+ const apiService = this.ensureApiService();
1190
+ const auth = this.getAuthPayload();
1191
+ const response = await apiService.cancel(options.feature_id, options.product, options.env, options.reason || 'Cancelled by user', auth);
1192
+ console.log('[FeatureService] cancel SUCCESS', {
1193
+ feature_id: options.feature_id,
1194
+ cancelled: response.cancelled,
1195
+ rolled_back_count: (_b = (_a = response.rolled_back_steps) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
1196
+ failed_rollbacks_count: (_d = (_c = response.failed_rollbacks) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0,
1197
+ });
1198
+ return {
1199
+ cancelled: response.cancelled,
1200
+ rolled_back_steps: response.rolled_back_steps,
1201
+ failed_rollbacks: response.failed_rollbacks,
1202
+ };
1203
+ }
1204
+ /**
1205
+ * Get the execution history of a feature
1206
+ *
1207
+ * @example
1208
+ * ```ts
1209
+ * const history = await ductape.feature.history({
1210
+ * product: 'my-product',
1211
+ * env: 'production',
1212
+ * feature_id: 'wf-123',
1213
+ * });
1214
+ * ```
1215
+ */
1216
+ async history(options) {
1217
+ var _a, _b, _c, _d;
1218
+ console.log('[FeatureService] history ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env });
1219
+ const apiService = this.ensureApiService();
1220
+ const auth = this.getAuthPayload();
1221
+ const response = await apiService.getHistory(options.feature_id, options.product, options.env, auth);
1222
+ console.log('[FeatureService] history SUCCESS', { feature_id: response.feature_id, eventsCount: (_b = (_a = response.events) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0, checkpointsCount: (_d = (_c = response.checkpoints) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0 });
1223
+ return {
1224
+ feature_id: response.feature_id,
1225
+ feature_tag: response.feature_tag,
1226
+ status: response.status,
1227
+ events: response.events.map((e) => ({
1228
+ type: e.type,
1229
+ timestamp: e.timestamp,
1230
+ data: e.data,
1231
+ })),
1232
+ checkpoints: response.checkpoints,
1233
+ replays: response.replays,
1234
+ restarts: response.restarts,
1235
+ };
1236
+ }
1237
+ /**
1238
+ * Get detailed information about a specific step
1239
+ *
1240
+ * @example
1241
+ * ```ts
1242
+ * const detail = await ductape.feature.stepDetail({
1243
+ * product: 'my-product',
1244
+ * env: 'production',
1245
+ * feature_id: 'wf-123',
1246
+ * step_tag: 'process-payment',
1247
+ * });
1248
+ * ```
1249
+ */
1250
+ async stepDetail(options) {
1251
+ console.log('[FeatureService] stepDetail ENTRY', { feature_id: options.feature_id, step_tag: options.step_tag, product: options.product, env: options.env });
1252
+ const apiService = this.ensureApiService();
1253
+ const auth = this.getAuthPayload();
1254
+ const response = await apiService.getStepDetail(options.feature_id, options.step_tag, options.product, options.env, auth);
1255
+ if (!response) {
1256
+ console.log('[FeatureService] stepDetail not found', { feature_id: options.feature_id, step_tag: options.step_tag });
1257
+ return null;
1258
+ }
1259
+ console.log('[FeatureService] stepDetail SUCCESS', { feature_id: options.feature_id, step_tag: response.tag, status: response.status });
1260
+ return {
1261
+ tag: response.tag,
1262
+ name: response.name,
1263
+ status: response.status,
1264
+ input: response.input,
1265
+ output: response.output,
1266
+ error: response.error,
1267
+ attempts: response.attempts,
1268
+ start_time: response.start_time,
1269
+ end_time: response.end_time,
1270
+ duration: response.duration,
1271
+ rollback_status: response.rollback_status,
1272
+ rollback_error: response.rollback_error,
1273
+ };
1274
+ }
1275
+ /**
1276
+ * List all related executions (replays, restarts, resumes)
1277
+ *
1278
+ * @example
1279
+ * ```ts
1280
+ * const related = await ductape.feature.relatedExecutions({
1281
+ * product: 'my-product',
1282
+ * env: 'production',
1283
+ * feature_id: 'wf-123',
1284
+ * });
1285
+ * ```
1286
+ */
1287
+ async relatedExecutions(options) {
1288
+ var _a, _b;
1289
+ console.log('[FeatureService] relatedExecutions ENTRY', { feature_id: options.feature_id, product: options.product, env: options.env });
1290
+ const apiService = this.ensureApiService();
1291
+ const auth = this.getAuthPayload();
1292
+ const response = await apiService.getRelatedExecutions(options.feature_id, options.product, options.env, auth);
1293
+ console.log('[FeatureService] relatedExecutions SUCCESS', { feature_id: options.feature_id, executionsCount: (_b = (_a = response.executions) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0 });
1294
+ return {
1295
+ original: response.original,
1296
+ executions: response.executions.map((e) => ({
1297
+ feature_id: e.feature_id,
1298
+ type: e.type,
1299
+ status: e.status,
1300
+ created_at: e.created_at,
1301
+ replayed_from: e.replayed_from,
1302
+ restarted_from: e.restarted_from,
1303
+ resumed_from: e.resumed_from,
1304
+ })),
1305
+ };
1306
+ }
1307
+ /**
1308
+ * Compare two feature executions
1309
+ *
1310
+ * @example
1311
+ * ```ts
1312
+ * const comparison = await ductape.feature.compare({
1313
+ * product: 'my-product',
1314
+ * env: 'production',
1315
+ * feature_ids: ['wf-123', 'wf-456'],
1316
+ * });
1317
+ * ```
1318
+ */
1319
+ async compare(options) {
1320
+ console.log('[FeatureService] compare ENTRY', { feature_ids: options.feature_ids, product: options.product, env: options.env });
1321
+ const apiService = this.ensureApiService();
1322
+ const auth = this.getAuthPayload();
1323
+ const response = await apiService.compareExecutions(options.feature_ids, options.product, options.env, auth);
1324
+ console.log('[FeatureService] compare SUCCESS', { feature_ids: response.features });
1325
+ return {
1326
+ features: response.features,
1327
+ input_diff: response.input_diff,
1328
+ step_diffs: response.step_diffs,
1329
+ outcome_diff: response.outcome_diff,
1330
+ };
1331
+ }
1332
+ // ==================== HELPER METHODS ====================
1333
+ /**
1334
+ * Convert IProductFeature to IFeatureConfig
1335
+ */
1336
+ productFeatureToConfig(feature) {
1337
+ return {
1338
+ name: feature.name,
1339
+ tag: feature.tag,
1340
+ description: feature.description,
1341
+ input: feature.input,
1342
+ output: feature.output,
1343
+ steps: feature.steps,
1344
+ signals: feature.signals,
1345
+ queries: feature.queries,
1346
+ options: feature.options,
1347
+ envs: feature.envs,
1348
+ };
1349
+ }
1350
+ }
1351
+ exports.FeatureService = FeatureService;
1352
+ // ==================== WORKFLOW COMPILER ====================
1353
+ /** Symbol to detect feature input proxy when serializing step input to operator strings */
1354
+ const INPUT_PROXY_SYMBOL = Symbol.for('RecordingContext.inputProxy');
1355
+ /** Symbol to detect step result proxy (values are already operator strings) */
1356
+ const STEP_RESULT_PROXY_SYMBOL = Symbol.for('RecordingContext.stepResultProxy');
1357
+ /** Symbol on step result proxy to get the operator string (for nested result paths) */
1358
+ const STEP_RESULT_REF_STRING = Symbol.for('RecordingContext.stepResultRefString');
1359
+ /** Symbol on component result proxy so step return shape uses $StepOutput{path} at runtime */
1360
+ const STEP_OUTPUT_REF_STRING = Symbol.for('RecordingContext.stepOutputRefString');
1361
+ /**
1362
+ * Create a proxy for ctx.input so that ctx.input.field compiles to $Input{field}.
1363
+ * When the whole proxy is used as a value (e.g. body: ctx.input), it is replaced
1364
+ * with an object of $Input{key} for each key in the feature input schema.
1365
+ */
1366
+ function createInputProxy(schemaKeys) {
1367
+ const refs = Object.fromEntries(schemaKeys.map((k) => [k, `$Input{${k}}`]));
1368
+ const proxy = new Proxy(refs, {
1369
+ get(target, prop) {
1370
+ if (prop === INPUT_PROXY_SYMBOL)
1371
+ return true;
1372
+ if (typeof prop === 'string' && prop in target)
1373
+ return target[prop];
1374
+ return `$Input{${String(prop)}}`;
1375
+ },
1376
+ });
1377
+ proxy[INPUT_PROXY_SYMBOL] = true;
1378
+ return proxy;
1379
+ }
1380
+ /**
1381
+ * Create a proxy for a step result so that result.field compiles to $Sequence{main}{stepTag}{field}.
1382
+ * Supports nested paths: result.data.nested.foo → $Sequence{main}{stepTag}{data.nested.foo}.
1383
+ * When a property is read, recordUsage(stepTag) is called so depends_on can be inferred.
1384
+ */
1385
+ function createStepResultProxy(featureTag, stepTag, pathPrefix = '', recordUsage) {
1386
+ const refString = pathPrefix ? `$Sequence{main}{${stepTag}}{${pathPrefix}}` : undefined;
1387
+ return new Proxy({}, {
1388
+ get(_, prop) {
1389
+ if (prop === STEP_RESULT_PROXY_SYMBOL)
1390
+ return true;
1391
+ if (prop === STEP_RESULT_REF_STRING)
1392
+ return refString;
1393
+ // So template literals like `${paymentResult.transactionId}` get the operator string
1394
+ if (prop === Symbol.toPrimitive) {
1395
+ return (hint) => (hint === 'string' && refString ? refString : undefined);
1396
+ }
1397
+ if (prop === 'toString' || prop === 'valueOf') {
1398
+ return () => refString !== null && refString !== void 0 ? refString : '';
1399
+ }
1400
+ if (typeof prop === 'string') {
1401
+ recordUsage === null || recordUsage === void 0 ? void 0 : recordUsage(stepTag);
1402
+ }
1403
+ const fullPath = pathPrefix ? `${pathPrefix}.${String(prop)}` : String(prop);
1404
+ return createStepResultProxy(featureTag, stepTag, fullPath, recordUsage);
1405
+ },
1406
+ });
1407
+ }
1408
+ /**
1409
+ * Create a proxy for the current step's component result (e.g. storage.upload return).
1410
+ * Any property access compiles to $StepOutput{path} so the step's return shape is resolved at runtime from the primitive result.
1411
+ * Supports nested paths: proxy.data.id → $StepOutput{data.id}.
1412
+ */
1413
+ function createStepOutputProxy(pathPrefix = '') {
1414
+ return new Proxy({}, {
1415
+ get(_, prop) {
1416
+ if (prop === STEP_OUTPUT_REF_STRING)
1417
+ return pathPrefix ? `$StepOutput{${pathPrefix}}` : undefined;
1418
+ const fullPath = pathPrefix ? `${pathPrefix}.${String(prop)}` : String(prop);
1419
+ return createStepOutputProxy(fullPath);
1420
+ },
1421
+ });
1422
+ }
1423
+ /**
1424
+ * Recursively convert recorded step input to operator strings per Ductape operator docs.
1425
+ * - Replaces input-proxy values with { key: '$Input{key}', ... } for all feature input keys.
1426
+ * - Leaves values that are already operator strings (e.g. from step result proxy) as-is.
1427
+ * - Strings containing operator refs ($Input{...}, $Sequence{main}{...}{...}, $Now) are converted to $Concat([...], "").
1428
+ * - Default delimiter is "" so template literals like `receipts/${ctx.input.orderId}.txt` become receipts/ORD-001.txt not receipts/ ORD-001 .txt.
1429
+ * - Recurses into plain objects and arrays; leaves other primitives unchanged.
1430
+ */
1431
+ const OPERATOR_REF_IN_STRING_REGEX = /(\$Input\{[^}]+\}|\$Sequence\{main\}\{[^}]+\}\{[^}]*\}|\$Now)/g;
1432
+ function convertStringToConcatIfOperatorRefs(str, delimiter = '') {
1433
+ OPERATOR_REF_IN_STRING_REGEX.lastIndex = 0;
1434
+ const tokens = str.split(OPERATOR_REF_IN_STRING_REGEX);
1435
+ const parts = [];
1436
+ for (const t of tokens) {
1437
+ if (!t)
1438
+ continue;
1439
+ const isOp = t.startsWith('$Input{') || t.startsWith('$Sequence{') || t === '$Now';
1440
+ if (isOp) {
1441
+ parts.push(t);
1442
+ }
1443
+ else {
1444
+ const trimmed = t.trim();
1445
+ if (trimmed.length > 0)
1446
+ parts.push(trimmed);
1447
+ }
1448
+ }
1449
+ if (parts.length <= 1)
1450
+ return str;
1451
+ return `$Concat([${parts.map((s) => (s.startsWith('$') ? s : JSON.stringify(s))).join(', ')}], "${delimiter.replace(/"/g, '\\"')}")`;
1452
+ }
1453
+ function convertToOperatorInput(value, inputProxy, schemaKeys) {
1454
+ if (value === null || value === undefined)
1455
+ return value;
1456
+ if (inputProxy && value === inputProxy) {
1457
+ return Object.fromEntries(schemaKeys.map((k) => [k, `$Input{${k}}`]));
1458
+ }
1459
+ const v = value;
1460
+ if (typeof v === 'object' && v !== null && v[INPUT_PROXY_SYMBOL] === true) {
1461
+ return Object.fromEntries(schemaKeys.map((k) => [k, `$Input{${k}}`]));
1462
+ }
1463
+ // Step result proxy (including nested result.data.foo) exposes operator string via STEP_RESULT_REF_STRING
1464
+ if (typeof v === 'object' && v !== null && typeof v[STEP_RESULT_REF_STRING] === 'string') {
1465
+ return v[STEP_RESULT_REF_STRING];
1466
+ }
1467
+ // Component result proxy (current step's primitive return) so step.output gets $StepOutput{path}
1468
+ if (typeof v === 'object' && v !== null && typeof v[STEP_OUTPUT_REF_STRING] === 'string') {
1469
+ return v[STEP_OUTPUT_REF_STRING];
1470
+ }
1471
+ if (Array.isArray(value)) {
1472
+ return value.map((item) => convertToOperatorInput(item, inputProxy, schemaKeys));
1473
+ }
1474
+ if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
1475
+ const out = {};
1476
+ for (const key of Object.keys(value)) {
1477
+ out[key] = convertToOperatorInput(value[key], inputProxy, schemaKeys);
1478
+ }
1479
+ return out;
1480
+ }
1481
+ if (typeof value === 'string') {
1482
+ return convertStringToConcatIfOperatorRefs(value);
1483
+ }
1484
+ return value;
1485
+ }
1486
+ /**
1487
+ * Recording context that captures step definitions during handler execution.
1488
+ * Uses operator strings ($Input{field}, $Sequence{main}{stepTag}{field}) per Ductape operator docs.
1489
+ */
1490
+ /** Build a step condition from a branch override so the step only runs when the overridden step's output matches (e.g. $Step{validate}{valid} == true). */
1491
+ function buildConditionFromOverride(stepTag, override) {
1492
+ if (override === null || override === undefined)
1493
+ return '';
1494
+ if (typeof override !== 'object' || Array.isArray(override)) {
1495
+ return `$Step{${stepTag}}{value} == ${JSON.stringify(override)}`;
1496
+ }
1497
+ const parts = [];
1498
+ for (const [key, val] of Object.entries(override)) {
1499
+ if (typeof val !== 'object' || val === null || Array.isArray(val)) {
1500
+ parts.push(`$Step{${stepTag}}{${key}} == ${JSON.stringify(val)}`);
1501
+ }
1502
+ }
1503
+ return parts.length === 0 ? '' : parts.join(' && ');
1504
+ }
1505
+ /** Build a step condition from a scenario (recordInput) so the step only runs when feature input matches (e.g. $Input{type} == 'a'). */
1506
+ function buildConditionFromScenario(scenario) {
1507
+ const parts = [];
1508
+ for (const [key, val] of Object.entries(scenario)) {
1509
+ if (val !== undefined && (typeof val !== 'object' || val === null || Array.isArray(val))) {
1510
+ parts.push(`$Input{${key}} == ${JSON.stringify(val)}`);
1511
+ }
1512
+ }
1513
+ return parts.length === 0 ? '' : parts.join(' && ');
1514
+ }
1515
+ /** Create ctx.input that prefers recordInput values (for loops/switch) and falls back to $Input{field} for other keys. */
1516
+ function createHybridInputProxy(recordInput, inputProxy) {
1517
+ const base = recordInput !== null && recordInput !== void 0 ? recordInput : {};
1518
+ if (!inputProxy)
1519
+ return base;
1520
+ return new Proxy(base, {
1521
+ get(target, prop) {
1522
+ if (typeof prop !== 'string')
1523
+ return target[prop];
1524
+ if (prop in target && target[prop] !== undefined) {
1525
+ return target[prop];
1526
+ }
1527
+ return inputProxy[prop];
1528
+ },
1529
+ has(target, prop) {
1530
+ return prop in target || (inputProxy && prop in inputProxy);
1531
+ },
1532
+ });
1533
+ }
1534
+ class RecordingContext {
1535
+ constructor(_input, featureTag, featureInputSchema, stepResultOverrides, scenarioCondition, recordInput) {
1536
+ this._steps = [];
1537
+ this._stepOrder = 0;
1538
+ this._currentStepTag = null;
1539
+ /** Step being built for the current ctx.step() call; updated in place by _updateCurrentStep so we emit one step per ctx.step(). */
1540
+ this._pendingStep = null;
1541
+ this._pendingRollback = null;
1542
+ this._inputProxy = null;
1543
+ /** Step tags whose result was read during the current step (for inferring depends_on) */
1544
+ this._stepResultDependencies = new Set();
1545
+ /** When set, the next step we push gets this condition (from a prior step's branch override). */
1546
+ this._pendingConditionFromOverride = null;
1547
+ this._stepResultOverrides = {};
1548
+ /** When set (from recordScenarios), every step we push gets this condition so it only runs when input matches. */
1549
+ this._scenarioCondition = null;
1550
+ this._recordInput = null;
1551
+ this._hybridInput = null;
1552
+ this._featureTag = featureTag;
1553
+ this._inputSchemaKeys = featureInputSchema ? Object.keys(featureInputSchema) : [];
1554
+ this._stepResultOverrides = stepResultOverrides !== null && stepResultOverrides !== void 0 ? stepResultOverrides : {};
1555
+ this._scenarioCondition = scenarioCondition !== null && scenarioCondition !== void 0 ? scenarioCondition : null;
1556
+ this._recordInput = recordInput !== null && recordInput !== void 0 ? recordInput : null;
1557
+ if (this._inputSchemaKeys.length > 0) {
1558
+ this._inputProxy = createInputProxy(this._inputSchemaKeys);
1559
+ }
1560
+ this._hybridInput = createHybridInputProxy(this._recordInput, this._inputProxy);
1561
+ }
1562
+ // Readonly feature metadata — prefer recordInput for control flow (loops, switch), fall back to $Input{field}
1563
+ get input() {
1564
+ var _a, _b;
1565
+ return ((_b = (_a = this._hybridInput) !== null && _a !== void 0 ? _a : this._inputProxy) !== null && _b !== void 0 ? _b : {});
1566
+ }
1567
+ get feature_id() {
1568
+ return `compile-${this._featureTag}`;
1569
+ }
1570
+ get feature_tag() {
1571
+ return this._featureTag;
1572
+ }
1573
+ get env() {
1574
+ return 'compile';
1575
+ }
1576
+ get product() {
1577
+ return 'compile';
1578
+ }
1579
+ get context() {
1580
+ return {};
1581
+ }
1582
+ get state() {
1583
+ return {};
1584
+ }
1585
+ get steps() {
1586
+ return {};
1587
+ }
1588
+ get completed_steps() {
1589
+ return [];
1590
+ }
1591
+ get current_step() {
1592
+ return this._currentStepTag;
1593
+ }
1594
+ /** At record time returns "$Now" so template literals like `at ${ctx.now()}` compile to $Concat(..., "$Now"). */
1595
+ get now() {
1596
+ return '$Now';
1597
+ }
1598
+ /**
1599
+ * Record a step definition
1600
+ */
1601
+ async step(tag, handler, rollback, options) {
1602
+ this._currentStepTag = tag;
1603
+ this._stepOrder++;
1604
+ this._stepResultDependencies = new Set();
1605
+ // Create step placeholder - will be updated in place by component calls (_updateCurrentStep)
1606
+ const step = {
1607
+ tag,
1608
+ name: tag,
1609
+ type: productsBuilder_types_1.FeatureStepType.ACTION,
1610
+ event: '',
1611
+ input: {},
1612
+ options,
1613
+ };
1614
+ this._pendingStep = step;
1615
+ // Execute handler to capture component calls and step return value
1616
+ let stepReturn;
1617
+ try {
1618
+ stepReturn = await handler();
1619
+ }
1620
+ catch (e) {
1621
+ // Ignore errors during compilation - we're just tracing
1622
+ }
1623
+ this._pendingStep = null;
1624
+ // Parse and store step output in schema (operator strings for downstream $Step{tag}{field} references)
1625
+ const valueToCapture = stepReturn !== undefined && stepReturn !== null && typeof stepReturn === 'object' && !Array.isArray(stepReturn)
1626
+ ? stepReturn.output !== undefined && stepReturn.process_id !== undefined
1627
+ ? stepReturn.output
1628
+ : stepReturn
1629
+ : stepReturn;
1630
+ if (valueToCapture !== undefined && valueToCapture !== null && typeof valueToCapture === 'object' && !Array.isArray(valueToCapture)) {
1631
+ step.output = convertToOperatorInput(valueToCapture, this._inputProxy, this._inputSchemaKeys);
1632
+ }
1633
+ else if (valueToCapture !== undefined && valueToCapture !== null) {
1634
+ step.output = { value: convertToOperatorInput(valueToCapture, this._inputProxy, this._inputSchemaKeys) };
1635
+ }
1636
+ else {
1637
+ step.output = {};
1638
+ }
1639
+ // If rollback was provided, try to capture it
1640
+ if (rollback && this._pendingRollback) {
1641
+ step.rollback = this._pendingRollback;
1642
+ this._pendingRollback = null;
1643
+ }
1644
+ // Always add the step (even when no component was called — e.g. validate-order that only returns)
1645
+ if (!step.event) {
1646
+ step.event = step.tag;
1647
+ }
1648
+ if (this._stepResultDependencies.size > 0) {
1649
+ step.depends_on = Array.from(this._stepResultDependencies);
1650
+ }
1651
+ // Apply conditions: scenario (e.g. $Input{type} == 'a') and/or branch override (e.g. $Step{validate}{valid} == true)
1652
+ const condParts = [];
1653
+ if (this._scenarioCondition)
1654
+ condParts.push(`(${this._scenarioCondition})`);
1655
+ if (this._pendingConditionFromOverride) {
1656
+ condParts.push(`(${this._pendingConditionFromOverride})`);
1657
+ this._pendingConditionFromOverride = null;
1658
+ }
1659
+ if (condParts.length > 0)
1660
+ step.condition = condParts.join(' && ');
1661
+ const overrideForThisStep = this._stepResultOverrides[tag];
1662
+ if (overrideForThisStep !== undefined) {
1663
+ this._pendingConditionFromOverride = buildConditionFromOverride(tag, overrideForThisStep);
1664
+ }
1665
+ this._steps.push(step);
1666
+ this._currentStepTag = null;
1667
+ // When branchOverrides[tag] is set, return it so the handler continues past if (!result.x) return (records both branches)
1668
+ if (overrideForThisStep !== undefined) {
1669
+ return overrideForThisStep;
1670
+ }
1671
+ // Return step-result proxy so result.field compiles to $Sequence{main}{stepTag}{field}
1672
+ const recordUsage = (depTag) => this._stepResultDependencies.add(depTag);
1673
+ return createStepResultProxy(this._featureTag, step.tag, '', recordUsage);
1674
+ }
1675
+ // ==================== COMPONENT CONTEXTS ====================
1676
+ get action() {
1677
+ return {
1678
+ run: async (options) => {
1679
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.ACTION, options.action, {
1680
+ app: options.app,
1681
+ input: options.input,
1682
+ options: { retries: options.retries, timeout: options.timeout },
1683
+ });
1684
+ return createStepOutputProxy();
1685
+ },
1686
+ };
1687
+ }
1688
+ get database() {
1689
+ return {
1690
+ execute: async (options) => {
1691
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, options.event, {
1692
+ database: options.database,
1693
+ input: options.input,
1694
+ options: { retries: options.retries, timeout: options.timeout },
1695
+ });
1696
+ return createStepOutputProxy();
1697
+ },
1698
+ query: async (options) => {
1699
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, options.event, {
1700
+ database: options.database,
1701
+ input: options.params || {},
1702
+ });
1703
+ return createStepOutputProxy();
1704
+ },
1705
+ insert: async (options) => {
1706
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, options.event, {
1707
+ database: options.database,
1708
+ input: { table: options.table, data: options.data },
1709
+ });
1710
+ return createStepOutputProxy();
1711
+ },
1712
+ update: async (options) => {
1713
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, options.event, {
1714
+ database: options.database,
1715
+ input: { table: options.table, data: options.data, where: options.where },
1716
+ });
1717
+ return createStepOutputProxy();
1718
+ },
1719
+ delete: async (options) => {
1720
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.DATABASE_ACTION, options.event, {
1721
+ database: options.database,
1722
+ input: { table: options.table, where: options.where },
1723
+ });
1724
+ return createStepOutputProxy();
1725
+ },
1726
+ };
1727
+ }
1728
+ get graph() {
1729
+ return {
1730
+ execute: async (options) => {
1731
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, options.action, {
1732
+ graph: options.graph,
1733
+ input: options.input,
1734
+ });
1735
+ return createStepOutputProxy();
1736
+ },
1737
+ createNode: async (options) => {
1738
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, 'createNode', {
1739
+ graph: options.graph,
1740
+ input: { labels: options.labels, properties: options.properties },
1741
+ });
1742
+ return createStepOutputProxy();
1743
+ },
1744
+ updateNode: async (options) => {
1745
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, 'updateNode', {
1746
+ graph: options.graph,
1747
+ input: { id: options.id, properties: options.properties },
1748
+ });
1749
+ return createStepOutputProxy();
1750
+ },
1751
+ deleteNode: async (options) => {
1752
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, 'deleteNode', {
1753
+ graph: options.graph,
1754
+ input: { id: options.id },
1755
+ });
1756
+ return createStepOutputProxy();
1757
+ },
1758
+ createRelationship: async (options) => {
1759
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, 'createRelationship', {
1760
+ graph: options.graph,
1761
+ input: { from: options.from, to: options.to, type: options.type, properties: options.properties },
1762
+ });
1763
+ return createStepOutputProxy();
1764
+ },
1765
+ deleteRelationship: async (options) => {
1766
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, 'deleteRelationship', {
1767
+ graph: options.graph,
1768
+ input: { id: options.id },
1769
+ });
1770
+ return createStepOutputProxy();
1771
+ },
1772
+ query: async (options) => {
1773
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.GRAPH, options.action, {
1774
+ graph: options.graph,
1775
+ input: options.params || {},
1776
+ });
1777
+ return createStepOutputProxy();
1778
+ },
1779
+ };
1780
+ }
1781
+ get notification() {
1782
+ return {
1783
+ send: async (options) => {
1784
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.NOTIFICATION, options.event, {
1785
+ notification: options.notification,
1786
+ input: options.input,
1787
+ options: { retries: options.retries },
1788
+ });
1789
+ return createStepOutputProxy();
1790
+ },
1791
+ email: async (options) => {
1792
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.NOTIFICATION, options.event, {
1793
+ notification: options.notification,
1794
+ input: { recipients: options.recipients, subject: options.subject, template: options.template },
1795
+ });
1796
+ return createStepOutputProxy();
1797
+ },
1798
+ push: async (options) => {
1799
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.NOTIFICATION, options.event, {
1800
+ notification: options.notification,
1801
+ input: { tokens: options.tokens, title: options.title, body: options.body, data: options.data },
1802
+ });
1803
+ return createStepOutputProxy();
1804
+ },
1805
+ sms: async (options) => {
1806
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.NOTIFICATION, options.event, {
1807
+ notification: options.notification,
1808
+ input: { phones: options.phones, message: options.message },
1809
+ });
1810
+ return createStepOutputProxy();
1811
+ },
1812
+ };
1813
+ }
1814
+ get storage() {
1815
+ return {
1816
+ upload: async (options) => {
1817
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.STORAGE, options.event, {
1818
+ storage: options.storage,
1819
+ input: options.input,
1820
+ options: { retries: options.retries },
1821
+ });
1822
+ return createStepOutputProxy();
1823
+ },
1824
+ download: async (options) => {
1825
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.STORAGE, options.event, {
1826
+ storage: options.storage,
1827
+ input: options.input,
1828
+ });
1829
+ return createStepOutputProxy();
1830
+ },
1831
+ delete: async (options) => {
1832
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.STORAGE, options.event, {
1833
+ storage: options.storage,
1834
+ input: options.input,
1835
+ });
1836
+ return createStepOutputProxy();
1837
+ },
1838
+ };
1839
+ }
1840
+ /** Ductape primitive: message broker produce. Records a produce step (event = "broker-tag:topic-tag"). */
1841
+ get events() {
1842
+ return {
1843
+ produce: async (options) => {
1844
+ const colon = options.event.indexOf(':');
1845
+ const broker = colon >= 0 ? options.event.slice(0, colon) : options.event;
1846
+ const topic = colon >= 0 ? options.event.slice(colon + 1) : options.event;
1847
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.PRODUCE, topic, {
1848
+ broker,
1849
+ input: { message: options.message },
1850
+ });
1851
+ return createStepOutputProxy();
1852
+ },
1853
+ };
1854
+ }
1855
+ /** @deprecated Prefer ctx.events.produce() */
1856
+ get publish() {
1857
+ return {
1858
+ send: async (options) => {
1859
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.PRODUCE, options.event, {
1860
+ broker: options.broker,
1861
+ input: options.input,
1862
+ options: { retries: options.retries },
1863
+ });
1864
+ return createStepOutputProxy();
1865
+ },
1866
+ };
1867
+ }
1868
+ get quota() {
1869
+ return {
1870
+ execute: async (options) => {
1871
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.QUOTA, 'execute', {
1872
+ quota: options.quota,
1873
+ input: options.input,
1874
+ options: { timeout: options.timeout },
1875
+ });
1876
+ return createStepOutputProxy();
1877
+ },
1878
+ };
1879
+ }
1880
+ get fallback() {
1881
+ return {
1882
+ execute: async (options) => {
1883
+ this._updateCurrentStep(productsBuilder_types_1.FeatureStepType.FALLBACK, 'execute', {
1884
+ fallback: options.fallback,
1885
+ input: options.input,
1886
+ options: { timeout: options.timeout },
1887
+ });
1888
+ return createStepOutputProxy();
1889
+ },
1890
+ };
1891
+ }
1892
+ get healthcheck() {
1893
+ return {
1894
+ getStatus: async (tag) => {
1895
+ return { status: 'available', lastChecked: new Date().toISOString(), lastLatency: 0 };
1896
+ },
1897
+ };
1898
+ }
1899
+ get transform() {
1900
+ return {
1901
+ size: (obj) => Object.keys(obj).length,
1902
+ length: (arr) => arr.length,
1903
+ parseJson: (str) => JSON.parse(str),
1904
+ stringify: (obj) => JSON.stringify(obj),
1905
+ upper: (str) => str.toUpperCase(),
1906
+ lower: (str) => str.toLowerCase(),
1907
+ trim: (str) => str.trim(),
1908
+ split: (str, separator) => str.split(separator),
1909
+ join: (arr, separator) => arr.join(separator),
1910
+ now: () => Date.now(),
1911
+ formatDate: (date, format) => new Date(date).toISOString(),
1912
+ };
1913
+ }
1914
+ // ==================== DATA REFERENCES ====================
1915
+ variable(app, key) {
1916
+ return `$Variable{${app}}{${key}}`;
1917
+ }
1918
+ constant(app, key) {
1919
+ return `$Constant{${app}}{${key}}`;
1920
+ }
1921
+ token(key) {
1922
+ return `$Secret{${key}}`;
1923
+ }
1924
+ get auth() {
1925
+ return {};
1926
+ }
1927
+ default(value, fallback) {
1928
+ return value !== null && value !== void 0 ? value : fallback;
1929
+ }
1930
+ // ==================== CONTROL FLOW ====================
1931
+ async sleep(duration) {
1932
+ const ms = typeof duration === 'string' ? this._parseDuration(duration) : duration;
1933
+ this._addControlStep(productsBuilder_types_1.FeatureStepType.SLEEP, 'sleep', { duration: ms });
1934
+ }
1935
+ async waitForSignal(signal, options) {
1936
+ const signals = Array.isArray(signal) ? signal : [signal];
1937
+ const timeout = (options === null || options === void 0 ? void 0 : options.timeout)
1938
+ ? typeof options.timeout === 'string'
1939
+ ? this._parseDuration(options.timeout)
1940
+ : options.timeout
1941
+ : undefined;
1942
+ this._addControlStep(productsBuilder_types_1.FeatureStepType.WAIT_FOR_SIGNAL, signals[0], {
1943
+ signals,
1944
+ options: timeout ? { timeout } : undefined,
1945
+ });
1946
+ return {};
1947
+ }
1948
+ async checkpoint(name, metadata) {
1949
+ this._addControlStep(productsBuilder_types_1.FeatureStepType.CHECKPOINT, name, { metadata });
1950
+ }
1951
+ // ==================== STATE MANAGEMENT ====================
1952
+ setState(key, value) {
1953
+ // No-op during compilation
1954
+ }
1955
+ getState(key) {
1956
+ return undefined;
1957
+ }
1958
+ // ==================== LOGGING ====================
1959
+ get log() {
1960
+ return {
1961
+ debug: (message, data) => { },
1962
+ info: (message, data) => { },
1963
+ warn: (message, data) => { },
1964
+ error: (message, data) => { },
1965
+ };
1966
+ }
1967
+ // ==================== CHILD WORKFLOWS ====================
1968
+ async feature(childId, tag, input, options) {
1969
+ this._addControlStep(productsBuilder_types_1.FeatureStepType.CHILD_FEATURE, 'execute', {
1970
+ feature: tag,
1971
+ input,
1972
+ options: options
1973
+ ? {
1974
+ timeout: options.timeout
1975
+ ? typeof options.timeout === 'string'
1976
+ ? this._parseDuration(options.timeout)
1977
+ : options.timeout
1978
+ : undefined,
1979
+ retries: options.retries,
1980
+ }
1981
+ : undefined,
1982
+ });
1983
+ return {};
1984
+ }
1985
+ // ==================== ROLLBACK ====================
1986
+ async triggerRollback(reason) {
1987
+ return { success: true, rolled_back_steps: [], reason };
1988
+ }
1989
+ // ==================== INTERNAL HELPERS ====================
1990
+ _updateCurrentStep(type, event, data) {
1991
+ if (!this._currentStepTag)
1992
+ return;
1993
+ // Update the step we're currently building (one step per ctx.step()), so we don't duplicate
1994
+ const step = this._pendingStep && this._pendingStep.tag === this._currentStepTag
1995
+ ? this._pendingStep
1996
+ : this._steps.find((s) => s.tag === this._currentStepTag);
1997
+ if (!step) {
1998
+ const newStep = {
1999
+ tag: this._currentStepTag,
2000
+ type,
2001
+ event,
2002
+ input: data.input || {},
2003
+ };
2004
+ this._steps.push(newStep);
2005
+ this._updateStepProperties(newStep, type, event, data);
2006
+ return;
2007
+ }
2008
+ this._updateStepProperties(step, type, event, data);
2009
+ }
2010
+ _updateStepProperties(step, type, event, data) {
2011
+ // Update step properties
2012
+ step.type = type;
2013
+ step.event = event;
2014
+ if (data.app)
2015
+ step.app = data.app;
2016
+ if (data.database)
2017
+ step.database = data.database;
2018
+ if (data.graph)
2019
+ step.graph = data.graph;
2020
+ if (data.storage)
2021
+ step.storage = data.storage;
2022
+ if (data.notification)
2023
+ step.notification = data.notification;
2024
+ if (data.broker)
2025
+ step.broker = data.broker;
2026
+ if (data.quota)
2027
+ step.quota = data.quota;
2028
+ if (data.fallback)
2029
+ step.fallback = data.fallback;
2030
+ if (data.feature)
2031
+ step.feature = data.feature;
2032
+ if (data.input) {
2033
+ step.input = convertToOperatorInput(data.input, this._inputProxy, this._inputSchemaKeys);
2034
+ }
2035
+ if (data.options)
2036
+ step.options = Object.assign(Object.assign({}, step.options), data.options);
2037
+ }
2038
+ _addControlStep(type, event, data) {
2039
+ const tag = `${type}-${this._stepOrder++}`;
2040
+ const input = convertToOperatorInput(data, this._inputProxy, this._inputSchemaKeys);
2041
+ const step = {
2042
+ tag,
2043
+ type,
2044
+ event,
2045
+ input,
2046
+ };
2047
+ if (data.feature != null)
2048
+ step.feature = data.feature;
2049
+ if (data.options != null)
2050
+ step.options = data.options;
2051
+ this._steps.push(step);
2052
+ }
2053
+ _parseDuration(duration) {
2054
+ const match = duration.match(/^(\d+)(ms|s|m|h|d)$/);
2055
+ if (!match)
2056
+ return parseInt(duration, 10) || 0;
2057
+ const value = parseInt(match[1], 10);
2058
+ const unit = match[2];
2059
+ switch (unit) {
2060
+ case 'ms':
2061
+ return value;
2062
+ case 's':
2063
+ return value * 1000;
2064
+ case 'm':
2065
+ return value * 60 * 1000;
2066
+ case 'h':
2067
+ return value * 60 * 60 * 1000;
2068
+ case 'd':
2069
+ return value * 24 * 60 * 60 * 1000;
2070
+ default:
2071
+ return value;
2072
+ }
2073
+ }
2074
+ /** Convert a value (e.g. handler return) to operator form for feature output schema */
2075
+ captureOutput(value) {
2076
+ if (value === undefined || value === null)
2077
+ return {};
2078
+ const converted = convertToOperatorInput(value, this._inputProxy, this._inputSchemaKeys);
2079
+ if (typeof converted === 'object' && converted !== null && !Array.isArray(converted)) {
2080
+ return converted;
2081
+ }
2082
+ return { value: converted };
2083
+ }
2084
+ /** Get collected steps */
2085
+ getSteps() {
2086
+ return this._steps;
2087
+ }
2088
+ }
2089
+ /**
2090
+ * Compiles code-first feature definitions to JSON schema
2091
+ * Uses runtime handler tracing to capture step definitions
2092
+ */
2093
+ class FeatureCompiler {
2094
+ constructor(options) {
2095
+ this.options = options;
2096
+ }
2097
+ /**
2098
+ * Compile the feature definition to JSON schema
2099
+ * Executes the handler with a recording context to capture step definitions
2100
+ */
2101
+ compile() {
2102
+ var _a, _b, _c, _d;
2103
+ const mockInput = ((_a = this.options.recordInput) !== null && _a !== void 0 ? _a : {});
2104
+ const recordingCtx = new RecordingContext(mockInput, this.options.tag, this.options.input, this.options.branchOverrides, undefined, ((_b = this.options.recordInput) !== null && _b !== void 0 ? _b : {}));
2105
+ // Execute handler to trace step definitions
2106
+ try {
2107
+ // Run the handler synchronously to capture all step calls
2108
+ // Note: This won't await async operations, but will capture the step structure
2109
+ const handlerPromise = this.options.handler(recordingCtx);
2110
+ // For synchronous tracing, we don't need to await
2111
+ // The recording context captures calls as they're made
2112
+ if (handlerPromise && typeof handlerPromise.then === 'function') {
2113
+ // Handler is async - we capture what we can synchronously
2114
+ // For full async tracing, use compileAsync()
2115
+ }
2116
+ }
2117
+ catch (e) {
2118
+ // Ignore errors during compilation - we're tracing structure, not executing
2119
+ }
2120
+ return {
2121
+ name: this.options.name,
2122
+ tag: this.options.tag,
2123
+ description: this.options.description,
2124
+ input: (_c = this.options.input) !== null && _c !== void 0 ? _c : {},
2125
+ output: (_d = this.options.output) !== null && _d !== void 0 ? _d : {},
2126
+ steps: recordingCtx.getSteps(),
2127
+ signals: this.options.signals
2128
+ ? Object.fromEntries(Object.entries(this.options.signals).map(([name, config]) => [name, { name, input: config.input }]))
2129
+ : undefined,
2130
+ queries: this.options.queries
2131
+ ? Object.fromEntries(Object.entries(this.options.queries).map(([name, config]) => {
2132
+ var _a;
2133
+ return [
2134
+ name,
2135
+ { name, handler: (_a = config.handler) === null || _a === void 0 ? void 0 : _a.toString() },
2136
+ ];
2137
+ }))
2138
+ : undefined,
2139
+ options: this.options.options,
2140
+ envs: this.options.envs,
2141
+ };
2142
+ }
2143
+ /**
2144
+ * Compile the feature definition asynchronously
2145
+ * Fully awaits the handler to capture all step definitions.
2146
+ * With recordScenarios, runs the handler once per scenario and merges steps (for switch-style branches).
2147
+ * With recordInput, uses that as ctx.input so loops and control flow see real values.
2148
+ */
2149
+ async compileAsync() {
2150
+ var _a, _b, _c, _d;
2151
+ const scenarios = this.options.recordScenarios && this.options.recordScenarios.length > 0
2152
+ ? this.options.recordScenarios
2153
+ : [(_a = this.options.recordInput) !== null && _a !== void 0 ? _a : {}];
2154
+ const stepsByTag = new Map();
2155
+ const stepOrder = [];
2156
+ let handlerOutput = undefined;
2157
+ let lastCtx = null;
2158
+ for (const scenario of scenarios) {
2159
+ const scenarioObj = typeof scenario === 'object' && scenario !== null ? scenario : {};
2160
+ const scenarioCondition = this.options.recordScenarios && Object.keys(scenarioObj).length > 0
2161
+ ? buildConditionFromScenario(scenarioObj)
2162
+ : null;
2163
+ const recordInput = Object.keys(scenarioObj).length > 0
2164
+ ? scenarioObj
2165
+ : ((_b = this.options.recordInput) !== null && _b !== void 0 ? _b : {});
2166
+ const recordingCtx = new RecordingContext((recordInput !== null && recordInput !== void 0 ? recordInput : {}), this.options.tag, this.options.input, this.options.branchOverrides, scenarioCondition, recordInput);
2167
+ lastCtx = recordingCtx;
2168
+ try {
2169
+ const out = await this.options.handler(recordingCtx);
2170
+ if (out !== undefined && out !== null)
2171
+ handlerOutput = out;
2172
+ }
2173
+ catch (_) { }
2174
+ for (const s of recordingCtx.getSteps()) {
2175
+ const existing = stepsByTag.get(s.tag);
2176
+ if (existing) {
2177
+ const c1 = existing.condition;
2178
+ const c2 = s.condition;
2179
+ if (c1 && c2)
2180
+ existing.condition = `(${c1}) || (${c2})`;
2181
+ else if (c2)
2182
+ existing.condition = c2;
2183
+ }
2184
+ else {
2185
+ stepsByTag.set(s.tag, Object.assign({}, s));
2186
+ stepOrder.push(s.tag);
2187
+ }
2188
+ }
2189
+ }
2190
+ const finalSteps = stepOrder.map((tag) => stepsByTag.get(tag)).filter(Boolean);
2191
+ const output = handlerOutput !== undefined && handlerOutput !== null && lastCtx
2192
+ ? lastCtx.captureOutput(handlerOutput)
2193
+ : ((_c = this.options.output) !== null && _c !== void 0 ? _c : {});
2194
+ return {
2195
+ name: this.options.name,
2196
+ tag: this.options.tag,
2197
+ description: this.options.description,
2198
+ input: (_d = this.options.input) !== null && _d !== void 0 ? _d : {},
2199
+ output: output,
2200
+ steps: finalSteps,
2201
+ signals: this.options.signals
2202
+ ? Object.fromEntries(Object.entries(this.options.signals).map(([name, config]) => [name, { name, input: config.input }]))
2203
+ : undefined,
2204
+ queries: this.options.queries
2205
+ ? Object.fromEntries(Object.entries(this.options.queries).map(([name, config]) => {
2206
+ var _a;
2207
+ return [
2208
+ name,
2209
+ { name, handler: (_a = config.handler) === null || _a === void 0 ? void 0 : _a.toString() },
2210
+ ];
2211
+ }))
2212
+ : undefined,
2213
+ options: this.options.options,
2214
+ envs: this.options.envs,
2215
+ };
2216
+ }
2217
+ }
2218
+ // Export singleton instance for convenience
2219
+ exports.featureService = new FeatureService();
2220
+ // Default export
2221
+ exports.default = FeatureService;
2222
+ //# sourceMappingURL=features.service.js.map