newo 3.4.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/api.d.ts +3 -1
  3. package/dist/api.js +49 -1
  4. package/dist/application/migration/MigrationEngine.d.ts +141 -0
  5. package/dist/application/migration/MigrationEngine.js +322 -0
  6. package/dist/application/migration/index.d.ts +5 -0
  7. package/dist/application/migration/index.js +5 -0
  8. package/dist/application/sync/SyncEngine.d.ts +134 -0
  9. package/dist/application/sync/SyncEngine.js +335 -0
  10. package/dist/application/sync/index.d.ts +5 -0
  11. package/dist/application/sync/index.js +5 -0
  12. package/dist/cli/commands/create-customer.d.ts +3 -0
  13. package/dist/cli/commands/create-customer.js +159 -0
  14. package/dist/cli/commands/diff.d.ts +6 -0
  15. package/dist/cli/commands/diff.js +288 -0
  16. package/dist/cli/commands/help.js +63 -3
  17. package/dist/cli/commands/logs.d.ts +18 -0
  18. package/dist/cli/commands/logs.js +283 -0
  19. package/dist/cli/commands/pull.js +114 -10
  20. package/dist/cli/commands/push.js +122 -12
  21. package/dist/cli/commands/watch.d.ts +6 -0
  22. package/dist/cli/commands/watch.js +195 -0
  23. package/dist/cli-new/bootstrap.d.ts +74 -0
  24. package/dist/cli-new/bootstrap.js +154 -0
  25. package/dist/cli-new/di/Container.d.ts +64 -0
  26. package/dist/cli-new/di/Container.js +122 -0
  27. package/dist/cli-new/di/tokens.d.ts +77 -0
  28. package/dist/cli-new/di/tokens.js +76 -0
  29. package/dist/cli.js +16 -0
  30. package/dist/domain/resources/common/types.d.ts +71 -0
  31. package/dist/domain/resources/common/types.js +42 -0
  32. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  33. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  34. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  35. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  36. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  37. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  38. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  39. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  40. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  41. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  42. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  43. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  44. package/dist/domain/strategies/sync/index.d.ts +13 -0
  45. package/dist/domain/strategies/sync/index.js +19 -0
  46. package/dist/sync/migrate.js +99 -23
  47. package/dist/types.d.ts +124 -0
  48. package/package.json +3 -1
  49. package/src/api.ts +53 -2
  50. package/src/application/migration/MigrationEngine.ts +492 -0
  51. package/src/application/migration/index.ts +5 -0
  52. package/src/application/sync/SyncEngine.ts +467 -0
  53. package/src/application/sync/index.ts +5 -0
  54. package/src/cli/commands/create-customer.ts +185 -0
  55. package/src/cli/commands/diff.ts +360 -0
  56. package/src/cli/commands/help.ts +63 -3
  57. package/src/cli/commands/logs.ts +329 -0
  58. package/src/cli/commands/pull.ts +128 -11
  59. package/src/cli/commands/push.ts +131 -13
  60. package/src/cli/commands/watch.ts +227 -0
  61. package/src/cli-new/bootstrap.ts +252 -0
  62. package/src/cli-new/di/Container.ts +152 -0
  63. package/src/cli-new/di/tokens.ts +105 -0
  64. package/src/cli.ts +20 -0
  65. package/src/domain/resources/common/types.ts +106 -0
  66. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  67. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  68. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  69. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  70. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  71. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  72. package/src/domain/strategies/sync/index.ts +46 -0
  73. package/src/sync/migrate.ts +103 -24
  74. package/src/types.ts +135 -0
@@ -0,0 +1,522 @@
1
+ /**
2
+ * IntegrationSyncStrategy - Handles synchronization of Integrations, Connectors, and Webhooks
3
+ *
4
+ * This strategy implements ISyncStrategy for the Integrations resource.
5
+ *
6
+ * Key responsibilities:
7
+ * - Pull integrations from NEWO platform
8
+ * - Pull connectors for each integration
9
+ * - Pull webhooks (outgoing and incoming)
10
+ * - Push connector changes back to platform
11
+ * - Detect changes using stored hashes
12
+ */
13
+
14
+ import type {
15
+ ISyncStrategy,
16
+ PullOptions,
17
+ PullResult,
18
+ PushResult,
19
+ ChangeItem,
20
+ ValidationResult,
21
+ ValidationError,
22
+ StatusSummary
23
+ } from './ISyncStrategy.js';
24
+ import type { CustomerConfig, ILogger, HashStore } from '../../resources/common/types.js';
25
+ import type { AxiosInstance } from 'axios';
26
+ import type {
27
+ Integration,
28
+ Connector,
29
+ IntegrationMetadata,
30
+ ConnectorMetadata,
31
+ OutgoingWebhook,
32
+ IncomingWebhook
33
+ } from '../../../types.js';
34
+ import fs from 'fs-extra';
35
+ import yaml from 'js-yaml';
36
+ import path from 'path';
37
+ import {
38
+ listIntegrations,
39
+ listConnectors,
40
+ getIntegrationSettings,
41
+ createConnector,
42
+ updateConnector,
43
+ deleteConnector,
44
+ listOutgoingWebhooks,
45
+ listIncomingWebhooks
46
+ } from '../../../api.js';
47
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
48
+
49
+ /**
50
+ * Local integration data for storage
51
+ */
52
+ export interface LocalIntegrationData {
53
+ integration: IntegrationMetadata;
54
+ connectors: ConnectorMetadata[];
55
+ outgoingWebhooks: OutgoingWebhook[];
56
+ incomingWebhooks: IncomingWebhook[];
57
+ }
58
+
59
+ /**
60
+ * API client factory type
61
+ */
62
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
63
+
64
+ /**
65
+ * IntegrationSyncStrategy - Handles integration synchronization
66
+ */
67
+ export class IntegrationSyncStrategy implements ISyncStrategy<Integration, LocalIntegrationData> {
68
+ readonly resourceType = 'integrations';
69
+ readonly displayName = 'Integrations';
70
+
71
+ constructor(
72
+ private apiClientFactory: ApiClientFactory,
73
+ private logger: ILogger
74
+ ) {}
75
+
76
+ /**
77
+ * Pull all integrations from NEWO platform
78
+ */
79
+ async pull(customer: CustomerConfig, options: PullOptions = {}): Promise<PullResult<LocalIntegrationData>> {
80
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
81
+ const hashes: HashStore = {};
82
+ const items: LocalIntegrationData[] = [];
83
+
84
+ this.logger.verbose(`🔍 Fetching integrations for ${customer.idn}...`);
85
+
86
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
87
+ const integrationsDir = path.join(customerDir, 'integrations');
88
+ await fs.ensureDir(integrationsDir);
89
+
90
+ // Fetch all integrations
91
+ const integrations = await listIntegrations(client);
92
+ this.logger.verbose(`📦 Found ${integrations.length} integrations`);
93
+
94
+ const integrationsMetadata: IntegrationMetadata[] = [];
95
+
96
+ // Fetch webhooks once for all integrations
97
+ let allOutgoingWebhooks: OutgoingWebhook[] = [];
98
+ let allIncomingWebhooks: IncomingWebhook[] = [];
99
+
100
+ try {
101
+ allOutgoingWebhooks = await listOutgoingWebhooks(client);
102
+ allIncomingWebhooks = await listIncomingWebhooks(client);
103
+ this.logger.verbose(`📡 Found ${allOutgoingWebhooks.length} outgoing, ${allIncomingWebhooks.length} incoming webhooks`);
104
+ } catch (error) {
105
+ this.logger.warn('Could not fetch webhooks');
106
+ }
107
+
108
+ // Group webhooks by connector_idn
109
+ const outgoingByConnector = new Map<string, OutgoingWebhook[]>();
110
+ const incomingByConnector = new Map<string, IncomingWebhook[]>();
111
+
112
+ allOutgoingWebhooks.forEach(webhook => {
113
+ if (!outgoingByConnector.has(webhook.connector_idn)) {
114
+ outgoingByConnector.set(webhook.connector_idn, []);
115
+ }
116
+ outgoingByConnector.get(webhook.connector_idn)!.push(webhook);
117
+ });
118
+
119
+ allIncomingWebhooks.forEach(webhook => {
120
+ if (!incomingByConnector.has(webhook.connector_idn)) {
121
+ incomingByConnector.set(webhook.connector_idn, []);
122
+ }
123
+ incomingByConnector.get(webhook.connector_idn)!.push(webhook);
124
+ });
125
+
126
+ // Process each integration
127
+ for (const integration of integrations) {
128
+ this.logger.verbose(` 📦 Processing: ${integration.title} (${integration.idn})`);
129
+
130
+ const metadata: IntegrationMetadata = {
131
+ id: integration.id,
132
+ idn: integration.idn,
133
+ title: integration.title,
134
+ description: integration.description,
135
+ channel: integration.channel,
136
+ is_disabled: integration.is_disabled
137
+ };
138
+ integrationsMetadata.push(metadata);
139
+
140
+ // Create integration directory
141
+ const integrationDir = path.join(integrationsDir, integration.idn);
142
+ await fs.ensureDir(integrationDir);
143
+
144
+ // Fetch integration settings
145
+ let integrationSettings: unknown[] = [];
146
+ try {
147
+ integrationSettings = await getIntegrationSettings(client, integration.id);
148
+ } catch (_error) {
149
+ // Settings endpoint may not be available for all integrations
150
+ }
151
+
152
+ // Save combined integration file (metadata + settings)
153
+ const integrationData: Record<string, unknown> = {
154
+ id: integration.id,
155
+ idn: integration.idn,
156
+ title: integration.title,
157
+ description: integration.description,
158
+ channel: integration.channel,
159
+ is_disabled: integration.is_disabled
160
+ };
161
+
162
+ if (integrationSettings.length > 0) {
163
+ integrationData.settings = integrationSettings;
164
+ }
165
+
166
+ const integrationFile = path.join(integrationDir, `${integration.idn}.yaml`);
167
+ const integrationYaml = yaml.dump(integrationData, { lineWidth: -1 });
168
+ await fs.writeFile(integrationFile, integrationYaml);
169
+ hashes[integrationFile] = sha256(integrationYaml);
170
+
171
+ // Fetch and save connectors
172
+ const connectors = await listConnectors(client, integration.id);
173
+ const connectorMetadatas: ConnectorMetadata[] = [];
174
+
175
+ if (connectors.length > 0) {
176
+ const connectorsDir = path.join(integrationDir, 'connectors');
177
+ await fs.ensureDir(connectorsDir);
178
+
179
+ for (const connector of connectors) {
180
+ const connectorMetadata: ConnectorMetadata = {
181
+ id: connector.id,
182
+ connector_idn: connector.connector_idn,
183
+ title: connector.title,
184
+ status: connector.status,
185
+ integration_idn: integration.idn,
186
+ settings: connector.settings
187
+ };
188
+ connectorMetadatas.push(connectorMetadata);
189
+
190
+ // Create subdirectory for this connector
191
+ const connectorDir = path.join(connectorsDir, connector.connector_idn);
192
+ await fs.ensureDir(connectorDir);
193
+
194
+ // Save connector YAML file
195
+ const connectorFile = path.join(connectorDir, `${connector.connector_idn}.yaml`);
196
+ const connectorYaml = yaml.dump(connectorMetadata, { lineWidth: -1 });
197
+ await fs.writeFile(connectorFile, connectorYaml);
198
+ hashes[connectorFile] = sha256(connectorYaml);
199
+
200
+ // Save webhooks if any
201
+ const outgoing = outgoingByConnector.get(connector.connector_idn) || [];
202
+ const incoming = incomingByConnector.get(connector.connector_idn) || [];
203
+
204
+ if (outgoing.length > 0 || incoming.length > 0) {
205
+ const webhooksDir = path.join(connectorDir, 'webhooks');
206
+ await fs.ensureDir(webhooksDir);
207
+
208
+ if (outgoing.length > 0) {
209
+ const outgoingFile = path.join(webhooksDir, 'outgoing.yaml');
210
+ const outgoingYaml = yaml.dump({ webhooks: outgoing }, { lineWidth: -1 });
211
+ await fs.writeFile(outgoingFile, outgoingYaml);
212
+ hashes[outgoingFile] = sha256(outgoingYaml);
213
+ }
214
+
215
+ if (incoming.length > 0) {
216
+ const incomingFile = path.join(webhooksDir, 'incoming.yaml');
217
+ const incomingYaml = yaml.dump({ webhooks: incoming }, { lineWidth: -1 });
218
+ await fs.writeFile(incomingFile, incomingYaml);
219
+ hashes[incomingFile] = sha256(incomingYaml);
220
+ }
221
+ }
222
+
223
+ this.logger.verbose(` ✓ Saved: ${connector.title}`);
224
+ }
225
+ }
226
+
227
+ items.push({
228
+ integration: metadata,
229
+ connectors: connectorMetadatas,
230
+ outgoingWebhooks: allOutgoingWebhooks.filter(w =>
231
+ connectorMetadatas.some(c => c.connector_idn === w.connector_idn)
232
+ ),
233
+ incomingWebhooks: allIncomingWebhooks.filter(w =>
234
+ connectorMetadatas.some(c => c.connector_idn === w.connector_idn)
235
+ )
236
+ });
237
+ }
238
+
239
+ // Save master integrations list
240
+ const integrationsFile = path.join(integrationsDir, 'integrations.yaml');
241
+ const integrationsYaml = yaml.dump({ integrations: integrationsMetadata }, { lineWidth: -1 });
242
+ await fs.writeFile(integrationsFile, integrationsYaml);
243
+ hashes[integrationsFile] = sha256(integrationsYaml);
244
+
245
+ // Save hashes
246
+ const existingHashes = await loadHashes(customer.idn);
247
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
248
+
249
+ this.logger.info(`✅ Saved ${integrations.length} integrations`);
250
+
251
+ return {
252
+ items,
253
+ count: items.length,
254
+ hashes
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Push changed connectors to NEWO platform
260
+ */
261
+ async push(customer: CustomerConfig, changes?: ChangeItem<LocalIntegrationData>[]): Promise<PushResult> {
262
+ const result: PushResult = { created: 0, updated: 0, deleted: 0, errors: [] };
263
+
264
+ if (!changes) {
265
+ changes = await this.getChanges(customer);
266
+ }
267
+
268
+ if (changes.length === 0) {
269
+ return result;
270
+ }
271
+
272
+ const client = await this.apiClientFactory(customer, false);
273
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
274
+ const integrationsDir = path.join(customerDir, 'integrations');
275
+
276
+ // Load remote integrations for ID mapping
277
+ const remoteIntegrations = await listIntegrations(client);
278
+ const integrationMap = new Map<string, string>(); // idn -> id
279
+ remoteIntegrations.forEach(int => integrationMap.set(int.idn, int.id));
280
+
281
+ // Read integration folders
282
+ const integrationFolders = await fs.readdir(integrationsDir);
283
+
284
+ for (const folder of integrationFolders) {
285
+ if (folder === 'integrations.yaml') continue;
286
+
287
+ const integrationDir = path.join(integrationsDir, folder);
288
+ const stat = await fs.stat(integrationDir);
289
+ if (!stat.isDirectory()) continue;
290
+
291
+ const integrationIdn = folder;
292
+ const integrationId = integrationMap.get(integrationIdn);
293
+
294
+ if (!integrationId) {
295
+ this.logger.warn(`Integration ${integrationIdn} not found on platform, skipping...`);
296
+ continue;
297
+ }
298
+
299
+ // Process connectors
300
+ const connectorsDir = path.join(integrationDir, 'connectors');
301
+ if (await fs.pathExists(connectorsDir)) {
302
+ const remoteConnectors = await listConnectors(client, integrationId);
303
+ const remoteConnectorMap = new Map<string, Connector>();
304
+ remoteConnectors.forEach(conn => remoteConnectorMap.set(conn.connector_idn, conn));
305
+
306
+ const connectorDirs = await fs.readdir(connectorsDir);
307
+ const localConnectorIdns = new Set<string>();
308
+
309
+ for (const connectorDirName of connectorDirs) {
310
+ const connectorPath = path.join(connectorsDir, connectorDirName);
311
+ const stat = await fs.stat(connectorPath);
312
+ if (!stat.isDirectory()) continue;
313
+
314
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
315
+ if (!await fs.pathExists(connectorFile)) continue;
316
+
317
+ const connectorData = yaml.load(await fs.readFile(connectorFile, 'utf-8')) as ConnectorMetadata;
318
+ localConnectorIdns.add(connectorData.connector_idn);
319
+
320
+ const remoteConnector = remoteConnectorMap.get(connectorData.connector_idn);
321
+
322
+ if (!remoteConnector) {
323
+ // Create new connector
324
+ try {
325
+ await createConnector(client, integrationId, {
326
+ title: connectorData.title,
327
+ connector_idn: connectorData.connector_idn,
328
+ integration_idn: integrationIdn,
329
+ settings: connectorData.settings
330
+ });
331
+ result.created++;
332
+ this.logger.info(` ✓ Created connector: ${connectorData.title}`);
333
+ } catch (error) {
334
+ result.errors.push(`Failed to create connector ${connectorData.connector_idn}: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ } else if (this.hasConnectorChanged(remoteConnector, connectorData)) {
337
+ // Update connector
338
+ try {
339
+ await updateConnector(client, remoteConnector.id, {
340
+ title: connectorData.title,
341
+ status: connectorData.status,
342
+ settings: connectorData.settings
343
+ });
344
+ result.updated++;
345
+ this.logger.info(` ✓ Updated connector: ${connectorData.title}`);
346
+ } catch (error) {
347
+ result.errors.push(`Failed to update connector ${connectorData.connector_idn}: ${error instanceof Error ? error.message : String(error)}`);
348
+ }
349
+ }
350
+ }
351
+
352
+ // Delete connectors that exist remotely but not locally
353
+ for (const [connectorIdn, remoteConnector] of remoteConnectorMap) {
354
+ if (!localConnectorIdns.has(connectorIdn)) {
355
+ try {
356
+ await deleteConnector(client, remoteConnector.id);
357
+ result.deleted++;
358
+ this.logger.info(` ✓ Deleted connector: ${remoteConnector.title}`);
359
+ } catch (error) {
360
+ result.errors.push(`Failed to delete connector ${connectorIdn}: ${error instanceof Error ? error.message : String(error)}`);
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+
367
+ return result;
368
+ }
369
+
370
+ /**
371
+ * Check if connector has changed compared to remote version
372
+ */
373
+ private hasConnectorChanged(remote: Connector, local: ConnectorMetadata): boolean {
374
+ if (remote.title !== local.title) return true;
375
+ if (remote.status !== local.status) return true;
376
+ if (remote.settings.length !== local.settings.length) return true;
377
+
378
+ const remoteSettingsMap = new Map<string, string>();
379
+ remote.settings.forEach(s => remoteSettingsMap.set(s.idn, s.value));
380
+
381
+ for (const localSetting of local.settings) {
382
+ const remoteValue = remoteSettingsMap.get(localSetting.idn);
383
+ if (remoteValue !== localSetting.value) return true;
384
+ }
385
+
386
+ return false;
387
+ }
388
+
389
+ /**
390
+ * Detect changes in integration files
391
+ */
392
+ async getChanges(customer: CustomerConfig): Promise<ChangeItem<LocalIntegrationData>[]> {
393
+ const changes: ChangeItem<LocalIntegrationData>[] = [];
394
+ const hashes = await loadHashes(customer.idn);
395
+
396
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
397
+ const integrationsDir = path.join(customerDir, 'integrations');
398
+
399
+ if (!await fs.pathExists(integrationsDir)) {
400
+ return changes;
401
+ }
402
+
403
+ const integrationFolders = await fs.readdir(integrationsDir);
404
+
405
+ for (const folder of integrationFolders) {
406
+ if (folder === 'integrations.yaml') continue;
407
+
408
+ const integrationDir = path.join(integrationsDir, folder);
409
+ const stat = await fs.stat(integrationDir);
410
+ if (!stat.isDirectory()) continue;
411
+
412
+ // Check integration file
413
+ const integrationFile = path.join(integrationDir, `${folder}.yaml`);
414
+ if (await fs.pathExists(integrationFile)) {
415
+ const content = await fs.readFile(integrationFile, 'utf-8');
416
+ const currentHash = sha256(content);
417
+ const storedHash = hashes[integrationFile];
418
+
419
+ if (storedHash !== currentHash) {
420
+ changes.push({
421
+ item: {
422
+ integration: yaml.load(content) as IntegrationMetadata,
423
+ connectors: [],
424
+ outgoingWebhooks: [],
425
+ incomingWebhooks: []
426
+ },
427
+ operation: storedHash ? 'modified' : 'created',
428
+ path: integrationFile
429
+ });
430
+ }
431
+ }
432
+
433
+ // Check connector files
434
+ const connectorsDir = path.join(integrationDir, 'connectors');
435
+ if (await fs.pathExists(connectorsDir)) {
436
+ const connectorDirs = await fs.readdir(connectorsDir);
437
+
438
+ for (const connectorDirName of connectorDirs) {
439
+ const connectorPath = path.join(connectorsDir, connectorDirName);
440
+ const stat = await fs.stat(connectorPath);
441
+ if (!stat.isDirectory()) continue;
442
+
443
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
444
+ if (await fs.pathExists(connectorFile)) {
445
+ const content = await fs.readFile(connectorFile, 'utf-8');
446
+ const currentHash = sha256(content);
447
+ const storedHash = hashes[connectorFile];
448
+
449
+ if (storedHash !== currentHash) {
450
+ changes.push({
451
+ item: {
452
+ integration: { id: '', idn: folder, title: '', description: '', channel: '', is_disabled: false },
453
+ connectors: [yaml.load(content) as ConnectorMetadata],
454
+ outgoingWebhooks: [],
455
+ incomingWebhooks: []
456
+ },
457
+ operation: storedHash ? 'modified' : 'created',
458
+ path: connectorFile
459
+ });
460
+ }
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ return changes;
467
+ }
468
+
469
+ /**
470
+ * Validate integration data
471
+ */
472
+ async validate(_customer: CustomerConfig, items: LocalIntegrationData[]): Promise<ValidationResult> {
473
+ const errors: ValidationError[] = [];
474
+
475
+ for (const item of items) {
476
+ if (!item.integration.idn) {
477
+ errors.push({
478
+ field: 'idn',
479
+ message: 'Integration IDN is required'
480
+ });
481
+ }
482
+
483
+ for (const connector of item.connectors) {
484
+ if (!connector.connector_idn) {
485
+ errors.push({
486
+ field: 'connector_idn',
487
+ message: 'Connector IDN is required'
488
+ });
489
+ }
490
+ }
491
+ }
492
+
493
+ return { valid: errors.length === 0, errors };
494
+ }
495
+
496
+ /**
497
+ * Get status summary
498
+ */
499
+ async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
500
+ const changes = await this.getChanges(customer);
501
+
502
+ return {
503
+ resourceType: this.resourceType,
504
+ displayName: this.displayName,
505
+ changedCount: changes.length,
506
+ changes: changes.map(c => ({
507
+ path: c.path,
508
+ operation: c.operation
509
+ }))
510
+ };
511
+ }
512
+ }
513
+
514
+ /**
515
+ * Factory function for creating IntegrationSyncStrategy
516
+ */
517
+ export function createIntegrationSyncStrategy(
518
+ apiClientFactory: ApiClientFactory,
519
+ logger: ILogger
520
+ ): IntegrationSyncStrategy {
521
+ return new IntegrationSyncStrategy(apiClientFactory, logger);
522
+ }