newo 3.4.0 → 3.4.2

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 (79) hide show
  1. package/CHANGELOG.md +16 -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-attribute.js +1 -1
  13. package/dist/cli/commands/create-customer.d.ts +3 -0
  14. package/dist/cli/commands/create-customer.js +159 -0
  15. package/dist/cli/commands/diff.d.ts +6 -0
  16. package/dist/cli/commands/diff.js +288 -0
  17. package/dist/cli/commands/help.js +63 -3
  18. package/dist/cli/commands/logs.d.ts +18 -0
  19. package/dist/cli/commands/logs.js +283 -0
  20. package/dist/cli/commands/pull.js +114 -10
  21. package/dist/cli/commands/push.js +122 -12
  22. package/dist/cli/commands/update-attribute.d.ts +3 -0
  23. package/dist/cli/commands/update-attribute.js +78 -0
  24. package/dist/cli/commands/watch.d.ts +6 -0
  25. package/dist/cli/commands/watch.js +195 -0
  26. package/dist/cli-new/bootstrap.d.ts +74 -0
  27. package/dist/cli-new/bootstrap.js +154 -0
  28. package/dist/cli-new/di/Container.d.ts +64 -0
  29. package/dist/cli-new/di/Container.js +122 -0
  30. package/dist/cli-new/di/tokens.d.ts +77 -0
  31. package/dist/cli-new/di/tokens.js +76 -0
  32. package/dist/cli.js +20 -0
  33. package/dist/domain/resources/common/types.d.ts +71 -0
  34. package/dist/domain/resources/common/types.js +42 -0
  35. package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
  36. package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
  37. package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
  38. package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
  39. package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
  40. package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
  41. package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
  42. package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
  43. package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
  44. package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
  45. package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
  46. package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
  47. package/dist/domain/strategies/sync/index.d.ts +13 -0
  48. package/dist/domain/strategies/sync/index.js +19 -0
  49. package/dist/sync/migrate.js +99 -23
  50. package/dist/types.d.ts +124 -0
  51. package/package.json +3 -1
  52. package/src/api.ts +53 -2
  53. package/src/application/migration/MigrationEngine.ts +492 -0
  54. package/src/application/migration/index.ts +5 -0
  55. package/src/application/sync/SyncEngine.ts +467 -0
  56. package/src/application/sync/index.ts +5 -0
  57. package/src/cli/commands/create-attribute.ts +1 -1
  58. package/src/cli/commands/create-customer.ts +185 -0
  59. package/src/cli/commands/diff.ts +360 -0
  60. package/src/cli/commands/help.ts +63 -3
  61. package/src/cli/commands/logs.ts +329 -0
  62. package/src/cli/commands/pull.ts +128 -11
  63. package/src/cli/commands/push.ts +131 -13
  64. package/src/cli/commands/update-attribute.ts +82 -0
  65. package/src/cli/commands/watch.ts +227 -0
  66. package/src/cli-new/bootstrap.ts +252 -0
  67. package/src/cli-new/di/Container.ts +152 -0
  68. package/src/cli-new/di/tokens.ts +105 -0
  69. package/src/cli.ts +25 -0
  70. package/src/domain/resources/common/types.ts +106 -0
  71. package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
  72. package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
  73. package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
  74. package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
  75. package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
  76. package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
  77. package/src/domain/strategies/sync/index.ts +46 -0
  78. package/src/sync/migrate.ts +103 -24
  79. package/src/types.ts +135 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Generic Sync Strategy Interface
3
+ *
4
+ * All resource types (Projects, Integrations, AKB, Attributes) implement this interface.
5
+ * This enables the SyncEngine to handle all resources uniformly.
6
+ */
7
+ import type { CustomerConfig } from '../../resources/common/types.js';
8
+ /**
9
+ * Validation result for pre-sync validation
10
+ */
11
+ export interface ValidationResult {
12
+ valid: boolean;
13
+ errors: ValidationError[];
14
+ }
15
+ export interface ValidationError {
16
+ field: string;
17
+ message: string;
18
+ path?: string;
19
+ }
20
+ /**
21
+ * Change operation types
22
+ */
23
+ export type ChangeOperation = 'created' | 'modified' | 'deleted';
24
+ /**
25
+ * Generic change item representing a resource change
26
+ */
27
+ export interface ChangeItem<T = unknown> {
28
+ item: T;
29
+ operation: ChangeOperation;
30
+ path: string;
31
+ }
32
+ /**
33
+ * Pull result containing resources and metadata
34
+ */
35
+ export interface PullResult<T = unknown> {
36
+ items: T[];
37
+ count: number;
38
+ hashes: Record<string, string>;
39
+ }
40
+ /**
41
+ * Push result containing operation outcomes
42
+ */
43
+ export interface PushResult {
44
+ created: number;
45
+ updated: number;
46
+ deleted: number;
47
+ errors: string[];
48
+ }
49
+ /**
50
+ * Generic Sync Strategy Interface
51
+ *
52
+ * TRemote - Type from the API (e.g., API response types)
53
+ * TLocal - Type for local storage (e.g., YAML/JSON file types)
54
+ */
55
+ export interface ISyncStrategy<_TRemote = unknown, TLocal = unknown> {
56
+ /**
57
+ * Resource type identifier (e.g., 'projects', 'integrations', 'akb', 'attributes')
58
+ */
59
+ readonly resourceType: string;
60
+ /**
61
+ * Display name for logging/UI (e.g., 'Projects', 'Integrations')
62
+ */
63
+ readonly displayName: string;
64
+ /**
65
+ * Pull resources from NEWO platform to local filesystem
66
+ *
67
+ * @param customer - Customer configuration
68
+ * @param options - Optional pull options
69
+ * @returns Pull result with items and hashes
70
+ */
71
+ pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<TLocal>>;
72
+ /**
73
+ * Push local changes to NEWO platform
74
+ *
75
+ * @param customer - Customer configuration
76
+ * @param changes - Changes to push (if not provided, detect changes automatically)
77
+ * @returns Push result with counts
78
+ */
79
+ push(customer: CustomerConfig, changes?: ChangeItem<TLocal>[]): Promise<PushResult>;
80
+ /**
81
+ * Detect what has changed locally since last sync
82
+ *
83
+ * @param customer - Customer configuration
84
+ * @returns Array of changed items
85
+ */
86
+ getChanges(customer: CustomerConfig): Promise<ChangeItem<TLocal>[]>;
87
+ /**
88
+ * Validate local state before push
89
+ *
90
+ * @param customer - Customer configuration
91
+ * @param items - Items to validate
92
+ * @returns Validation result
93
+ */
94
+ validate(customer: CustomerConfig, items: TLocal[]): Promise<ValidationResult>;
95
+ /**
96
+ * Get status summary for display
97
+ *
98
+ * @param customer - Customer configuration
99
+ * @returns Status summary
100
+ */
101
+ getStatus(customer: CustomerConfig): Promise<StatusSummary>;
102
+ }
103
+ /**
104
+ * Pull operation options
105
+ */
106
+ export interface PullOptions {
107
+ /**
108
+ * Overwrite local changes without prompting
109
+ */
110
+ silentOverwrite?: boolean;
111
+ /**
112
+ * Enable verbose logging
113
+ */
114
+ verbose?: boolean;
115
+ /**
116
+ * Specific project ID to pull (for projects strategy)
117
+ */
118
+ projectId?: string | null;
119
+ /**
120
+ * Skip deletion detection and cleanup
121
+ */
122
+ skipCleanup?: boolean;
123
+ }
124
+ /**
125
+ * Status summary for a resource type
126
+ */
127
+ export interface StatusSummary {
128
+ resourceType: string;
129
+ displayName: string;
130
+ changedCount: number;
131
+ changes: Array<{
132
+ path: string;
133
+ operation: ChangeOperation;
134
+ details?: string;
135
+ }>;
136
+ }
137
+ /**
138
+ * Abstract base class with common strategy functionality
139
+ */
140
+ export declare abstract class BaseSyncStrategy<TRemote = unknown, TLocal = unknown> implements ISyncStrategy<TRemote, TLocal> {
141
+ abstract readonly resourceType: string;
142
+ abstract readonly displayName: string;
143
+ abstract pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<TLocal>>;
144
+ abstract push(customer: CustomerConfig, changes?: ChangeItem<TLocal>[]): Promise<PushResult>;
145
+ abstract getChanges(customer: CustomerConfig): Promise<ChangeItem<TLocal>[]>;
146
+ abstract validate(customer: CustomerConfig, items: TLocal[]): Promise<ValidationResult>;
147
+ getStatus(customer: CustomerConfig): Promise<StatusSummary>;
148
+ }
149
+ //# sourceMappingURL=ISyncStrategy.d.ts.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generic Sync Strategy Interface
3
+ *
4
+ * All resource types (Projects, Integrations, AKB, Attributes) implement this interface.
5
+ * This enables the SyncEngine to handle all resources uniformly.
6
+ */
7
+ /**
8
+ * Abstract base class with common strategy functionality
9
+ */
10
+ export class BaseSyncStrategy {
11
+ async getStatus(customer) {
12
+ const changes = await this.getChanges(customer);
13
+ return {
14
+ resourceType: this.resourceType,
15
+ displayName: this.displayName,
16
+ changedCount: changes.length,
17
+ changes: changes.map(c => ({
18
+ path: c.path,
19
+ operation: c.operation
20
+ }))
21
+ };
22
+ }
23
+ }
24
+ //# sourceMappingURL=ISyncStrategy.js.map
@@ -0,0 +1,68 @@
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
+ import type { ISyncStrategy, PullOptions, PullResult, PushResult, ChangeItem, ValidationResult, StatusSummary } from './ISyncStrategy.js';
14
+ import type { CustomerConfig, ILogger } from '../../resources/common/types.js';
15
+ import type { AxiosInstance } from 'axios';
16
+ import type { Integration, IntegrationMetadata, ConnectorMetadata, OutgoingWebhook, IncomingWebhook } from '../../../types.js';
17
+ /**
18
+ * Local integration data for storage
19
+ */
20
+ export interface LocalIntegrationData {
21
+ integration: IntegrationMetadata;
22
+ connectors: ConnectorMetadata[];
23
+ outgoingWebhooks: OutgoingWebhook[];
24
+ incomingWebhooks: IncomingWebhook[];
25
+ }
26
+ /**
27
+ * API client factory type
28
+ */
29
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
30
+ /**
31
+ * IntegrationSyncStrategy - Handles integration synchronization
32
+ */
33
+ export declare class IntegrationSyncStrategy implements ISyncStrategy<Integration, LocalIntegrationData> {
34
+ private apiClientFactory;
35
+ private logger;
36
+ readonly resourceType = "integrations";
37
+ readonly displayName = "Integrations";
38
+ constructor(apiClientFactory: ApiClientFactory, logger: ILogger);
39
+ /**
40
+ * Pull all integrations from NEWO platform
41
+ */
42
+ pull(customer: CustomerConfig, options?: PullOptions): Promise<PullResult<LocalIntegrationData>>;
43
+ /**
44
+ * Push changed connectors to NEWO platform
45
+ */
46
+ push(customer: CustomerConfig, changes?: ChangeItem<LocalIntegrationData>[]): Promise<PushResult>;
47
+ /**
48
+ * Check if connector has changed compared to remote version
49
+ */
50
+ private hasConnectorChanged;
51
+ /**
52
+ * Detect changes in integration files
53
+ */
54
+ getChanges(customer: CustomerConfig): Promise<ChangeItem<LocalIntegrationData>[]>;
55
+ /**
56
+ * Validate integration data
57
+ */
58
+ validate(_customer: CustomerConfig, items: LocalIntegrationData[]): Promise<ValidationResult>;
59
+ /**
60
+ * Get status summary
61
+ */
62
+ getStatus(customer: CustomerConfig): Promise<StatusSummary>;
63
+ }
64
+ /**
65
+ * Factory function for creating IntegrationSyncStrategy
66
+ */
67
+ export declare function createIntegrationSyncStrategy(apiClientFactory: ApiClientFactory, logger: ILogger): IntegrationSyncStrategy;
68
+ //# sourceMappingURL=IntegrationSyncStrategy.d.ts.map
@@ -0,0 +1,413 @@
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
+ import fs from 'fs-extra';
14
+ import yaml from 'js-yaml';
15
+ import path from 'path';
16
+ import { listIntegrations, listConnectors, getIntegrationSettings, createConnector, updateConnector, deleteConnector, listOutgoingWebhooks, listIncomingWebhooks } from '../../../api.js';
17
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
18
+ /**
19
+ * IntegrationSyncStrategy - Handles integration synchronization
20
+ */
21
+ export class IntegrationSyncStrategy {
22
+ apiClientFactory;
23
+ logger;
24
+ resourceType = 'integrations';
25
+ displayName = 'Integrations';
26
+ constructor(apiClientFactory, logger) {
27
+ this.apiClientFactory = apiClientFactory;
28
+ this.logger = logger;
29
+ }
30
+ /**
31
+ * Pull all integrations from NEWO platform
32
+ */
33
+ async pull(customer, options = {}) {
34
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
35
+ const hashes = {};
36
+ const items = [];
37
+ this.logger.verbose(`🔍 Fetching integrations for ${customer.idn}...`);
38
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
39
+ const integrationsDir = path.join(customerDir, 'integrations');
40
+ await fs.ensureDir(integrationsDir);
41
+ // Fetch all integrations
42
+ const integrations = await listIntegrations(client);
43
+ this.logger.verbose(`📦 Found ${integrations.length} integrations`);
44
+ const integrationsMetadata = [];
45
+ // Fetch webhooks once for all integrations
46
+ let allOutgoingWebhooks = [];
47
+ let allIncomingWebhooks = [];
48
+ try {
49
+ allOutgoingWebhooks = await listOutgoingWebhooks(client);
50
+ allIncomingWebhooks = await listIncomingWebhooks(client);
51
+ this.logger.verbose(`📡 Found ${allOutgoingWebhooks.length} outgoing, ${allIncomingWebhooks.length} incoming webhooks`);
52
+ }
53
+ catch (error) {
54
+ this.logger.warn('Could not fetch webhooks');
55
+ }
56
+ // Group webhooks by connector_idn
57
+ const outgoingByConnector = new Map();
58
+ const incomingByConnector = new Map();
59
+ allOutgoingWebhooks.forEach(webhook => {
60
+ if (!outgoingByConnector.has(webhook.connector_idn)) {
61
+ outgoingByConnector.set(webhook.connector_idn, []);
62
+ }
63
+ outgoingByConnector.get(webhook.connector_idn).push(webhook);
64
+ });
65
+ allIncomingWebhooks.forEach(webhook => {
66
+ if (!incomingByConnector.has(webhook.connector_idn)) {
67
+ incomingByConnector.set(webhook.connector_idn, []);
68
+ }
69
+ incomingByConnector.get(webhook.connector_idn).push(webhook);
70
+ });
71
+ // Process each integration
72
+ for (const integration of integrations) {
73
+ this.logger.verbose(` 📦 Processing: ${integration.title} (${integration.idn})`);
74
+ const metadata = {
75
+ id: integration.id,
76
+ idn: integration.idn,
77
+ title: integration.title,
78
+ description: integration.description,
79
+ channel: integration.channel,
80
+ is_disabled: integration.is_disabled
81
+ };
82
+ integrationsMetadata.push(metadata);
83
+ // Create integration directory
84
+ const integrationDir = path.join(integrationsDir, integration.idn);
85
+ await fs.ensureDir(integrationDir);
86
+ // Fetch integration settings
87
+ let integrationSettings = [];
88
+ try {
89
+ integrationSettings = await getIntegrationSettings(client, integration.id);
90
+ }
91
+ catch (_error) {
92
+ // Settings endpoint may not be available for all integrations
93
+ }
94
+ // Save combined integration file (metadata + settings)
95
+ const integrationData = {
96
+ id: integration.id,
97
+ idn: integration.idn,
98
+ title: integration.title,
99
+ description: integration.description,
100
+ channel: integration.channel,
101
+ is_disabled: integration.is_disabled
102
+ };
103
+ if (integrationSettings.length > 0) {
104
+ integrationData.settings = integrationSettings;
105
+ }
106
+ const integrationFile = path.join(integrationDir, `${integration.idn}.yaml`);
107
+ const integrationYaml = yaml.dump(integrationData, { lineWidth: -1 });
108
+ await fs.writeFile(integrationFile, integrationYaml);
109
+ hashes[integrationFile] = sha256(integrationYaml);
110
+ // Fetch and save connectors
111
+ const connectors = await listConnectors(client, integration.id);
112
+ const connectorMetadatas = [];
113
+ if (connectors.length > 0) {
114
+ const connectorsDir = path.join(integrationDir, 'connectors');
115
+ await fs.ensureDir(connectorsDir);
116
+ for (const connector of connectors) {
117
+ const connectorMetadata = {
118
+ id: connector.id,
119
+ connector_idn: connector.connector_idn,
120
+ title: connector.title,
121
+ status: connector.status,
122
+ integration_idn: integration.idn,
123
+ settings: connector.settings
124
+ };
125
+ connectorMetadatas.push(connectorMetadata);
126
+ // Create subdirectory for this connector
127
+ const connectorDir = path.join(connectorsDir, connector.connector_idn);
128
+ await fs.ensureDir(connectorDir);
129
+ // Save connector YAML file
130
+ const connectorFile = path.join(connectorDir, `${connector.connector_idn}.yaml`);
131
+ const connectorYaml = yaml.dump(connectorMetadata, { lineWidth: -1 });
132
+ await fs.writeFile(connectorFile, connectorYaml);
133
+ hashes[connectorFile] = sha256(connectorYaml);
134
+ // Save webhooks if any
135
+ const outgoing = outgoingByConnector.get(connector.connector_idn) || [];
136
+ const incoming = incomingByConnector.get(connector.connector_idn) || [];
137
+ if (outgoing.length > 0 || incoming.length > 0) {
138
+ const webhooksDir = path.join(connectorDir, 'webhooks');
139
+ await fs.ensureDir(webhooksDir);
140
+ if (outgoing.length > 0) {
141
+ const outgoingFile = path.join(webhooksDir, 'outgoing.yaml');
142
+ const outgoingYaml = yaml.dump({ webhooks: outgoing }, { lineWidth: -1 });
143
+ await fs.writeFile(outgoingFile, outgoingYaml);
144
+ hashes[outgoingFile] = sha256(outgoingYaml);
145
+ }
146
+ if (incoming.length > 0) {
147
+ const incomingFile = path.join(webhooksDir, 'incoming.yaml');
148
+ const incomingYaml = yaml.dump({ webhooks: incoming }, { lineWidth: -1 });
149
+ await fs.writeFile(incomingFile, incomingYaml);
150
+ hashes[incomingFile] = sha256(incomingYaml);
151
+ }
152
+ }
153
+ this.logger.verbose(` ✓ Saved: ${connector.title}`);
154
+ }
155
+ }
156
+ items.push({
157
+ integration: metadata,
158
+ connectors: connectorMetadatas,
159
+ outgoingWebhooks: allOutgoingWebhooks.filter(w => connectorMetadatas.some(c => c.connector_idn === w.connector_idn)),
160
+ incomingWebhooks: allIncomingWebhooks.filter(w => connectorMetadatas.some(c => c.connector_idn === w.connector_idn))
161
+ });
162
+ }
163
+ // Save master integrations list
164
+ const integrationsFile = path.join(integrationsDir, 'integrations.yaml');
165
+ const integrationsYaml = yaml.dump({ integrations: integrationsMetadata }, { lineWidth: -1 });
166
+ await fs.writeFile(integrationsFile, integrationsYaml);
167
+ hashes[integrationsFile] = sha256(integrationsYaml);
168
+ // Save hashes
169
+ const existingHashes = await loadHashes(customer.idn);
170
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
171
+ this.logger.info(`✅ Saved ${integrations.length} integrations`);
172
+ return {
173
+ items,
174
+ count: items.length,
175
+ hashes
176
+ };
177
+ }
178
+ /**
179
+ * Push changed connectors to NEWO platform
180
+ */
181
+ async push(customer, changes) {
182
+ const result = { created: 0, updated: 0, deleted: 0, errors: [] };
183
+ if (!changes) {
184
+ changes = await this.getChanges(customer);
185
+ }
186
+ if (changes.length === 0) {
187
+ return result;
188
+ }
189
+ const client = await this.apiClientFactory(customer, false);
190
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
191
+ const integrationsDir = path.join(customerDir, 'integrations');
192
+ // Load remote integrations for ID mapping
193
+ const remoteIntegrations = await listIntegrations(client);
194
+ const integrationMap = new Map(); // idn -> id
195
+ remoteIntegrations.forEach(int => integrationMap.set(int.idn, int.id));
196
+ // Read integration folders
197
+ const integrationFolders = await fs.readdir(integrationsDir);
198
+ for (const folder of integrationFolders) {
199
+ if (folder === 'integrations.yaml')
200
+ continue;
201
+ const integrationDir = path.join(integrationsDir, folder);
202
+ const stat = await fs.stat(integrationDir);
203
+ if (!stat.isDirectory())
204
+ continue;
205
+ const integrationIdn = folder;
206
+ const integrationId = integrationMap.get(integrationIdn);
207
+ if (!integrationId) {
208
+ this.logger.warn(`Integration ${integrationIdn} not found on platform, skipping...`);
209
+ continue;
210
+ }
211
+ // Process connectors
212
+ const connectorsDir = path.join(integrationDir, 'connectors');
213
+ if (await fs.pathExists(connectorsDir)) {
214
+ const remoteConnectors = await listConnectors(client, integrationId);
215
+ const remoteConnectorMap = new Map();
216
+ remoteConnectors.forEach(conn => remoteConnectorMap.set(conn.connector_idn, conn));
217
+ const connectorDirs = await fs.readdir(connectorsDir);
218
+ const localConnectorIdns = new Set();
219
+ for (const connectorDirName of connectorDirs) {
220
+ const connectorPath = path.join(connectorsDir, connectorDirName);
221
+ const stat = await fs.stat(connectorPath);
222
+ if (!stat.isDirectory())
223
+ continue;
224
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
225
+ if (!await fs.pathExists(connectorFile))
226
+ continue;
227
+ const connectorData = yaml.load(await fs.readFile(connectorFile, 'utf-8'));
228
+ localConnectorIdns.add(connectorData.connector_idn);
229
+ const remoteConnector = remoteConnectorMap.get(connectorData.connector_idn);
230
+ if (!remoteConnector) {
231
+ // Create new connector
232
+ try {
233
+ await createConnector(client, integrationId, {
234
+ title: connectorData.title,
235
+ connector_idn: connectorData.connector_idn,
236
+ integration_idn: integrationIdn,
237
+ settings: connectorData.settings
238
+ });
239
+ result.created++;
240
+ this.logger.info(` ✓ Created connector: ${connectorData.title}`);
241
+ }
242
+ catch (error) {
243
+ result.errors.push(`Failed to create connector ${connectorData.connector_idn}: ${error instanceof Error ? error.message : String(error)}`);
244
+ }
245
+ }
246
+ else if (this.hasConnectorChanged(remoteConnector, connectorData)) {
247
+ // Update connector
248
+ try {
249
+ await updateConnector(client, remoteConnector.id, {
250
+ title: connectorData.title,
251
+ status: connectorData.status,
252
+ settings: connectorData.settings
253
+ });
254
+ result.updated++;
255
+ this.logger.info(` ✓ Updated connector: ${connectorData.title}`);
256
+ }
257
+ catch (error) {
258
+ result.errors.push(`Failed to update connector ${connectorData.connector_idn}: ${error instanceof Error ? error.message : String(error)}`);
259
+ }
260
+ }
261
+ }
262
+ // Delete connectors that exist remotely but not locally
263
+ for (const [connectorIdn, remoteConnector] of remoteConnectorMap) {
264
+ if (!localConnectorIdns.has(connectorIdn)) {
265
+ try {
266
+ await deleteConnector(client, remoteConnector.id);
267
+ result.deleted++;
268
+ this.logger.info(` ✓ Deleted connector: ${remoteConnector.title}`);
269
+ }
270
+ catch (error) {
271
+ result.errors.push(`Failed to delete connector ${connectorIdn}: ${error instanceof Error ? error.message : String(error)}`);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ }
277
+ return result;
278
+ }
279
+ /**
280
+ * Check if connector has changed compared to remote version
281
+ */
282
+ hasConnectorChanged(remote, local) {
283
+ if (remote.title !== local.title)
284
+ return true;
285
+ if (remote.status !== local.status)
286
+ return true;
287
+ if (remote.settings.length !== local.settings.length)
288
+ return true;
289
+ const remoteSettingsMap = new Map();
290
+ remote.settings.forEach(s => remoteSettingsMap.set(s.idn, s.value));
291
+ for (const localSetting of local.settings) {
292
+ const remoteValue = remoteSettingsMap.get(localSetting.idn);
293
+ if (remoteValue !== localSetting.value)
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ /**
299
+ * Detect changes in integration files
300
+ */
301
+ async getChanges(customer) {
302
+ const changes = [];
303
+ const hashes = await loadHashes(customer.idn);
304
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
305
+ const integrationsDir = path.join(customerDir, 'integrations');
306
+ if (!await fs.pathExists(integrationsDir)) {
307
+ return changes;
308
+ }
309
+ const integrationFolders = await fs.readdir(integrationsDir);
310
+ for (const folder of integrationFolders) {
311
+ if (folder === 'integrations.yaml')
312
+ continue;
313
+ const integrationDir = path.join(integrationsDir, folder);
314
+ const stat = await fs.stat(integrationDir);
315
+ if (!stat.isDirectory())
316
+ continue;
317
+ // Check integration file
318
+ const integrationFile = path.join(integrationDir, `${folder}.yaml`);
319
+ if (await fs.pathExists(integrationFile)) {
320
+ const content = await fs.readFile(integrationFile, 'utf-8');
321
+ const currentHash = sha256(content);
322
+ const storedHash = hashes[integrationFile];
323
+ if (storedHash !== currentHash) {
324
+ changes.push({
325
+ item: {
326
+ integration: yaml.load(content),
327
+ connectors: [],
328
+ outgoingWebhooks: [],
329
+ incomingWebhooks: []
330
+ },
331
+ operation: storedHash ? 'modified' : 'created',
332
+ path: integrationFile
333
+ });
334
+ }
335
+ }
336
+ // Check connector files
337
+ const connectorsDir = path.join(integrationDir, 'connectors');
338
+ if (await fs.pathExists(connectorsDir)) {
339
+ const connectorDirs = await fs.readdir(connectorsDir);
340
+ for (const connectorDirName of connectorDirs) {
341
+ const connectorPath = path.join(connectorsDir, connectorDirName);
342
+ const stat = await fs.stat(connectorPath);
343
+ if (!stat.isDirectory())
344
+ continue;
345
+ const connectorFile = path.join(connectorPath, `${connectorDirName}.yaml`);
346
+ if (await fs.pathExists(connectorFile)) {
347
+ const content = await fs.readFile(connectorFile, 'utf-8');
348
+ const currentHash = sha256(content);
349
+ const storedHash = hashes[connectorFile];
350
+ if (storedHash !== currentHash) {
351
+ changes.push({
352
+ item: {
353
+ integration: { id: '', idn: folder, title: '', description: '', channel: '', is_disabled: false },
354
+ connectors: [yaml.load(content)],
355
+ outgoingWebhooks: [],
356
+ incomingWebhooks: []
357
+ },
358
+ operation: storedHash ? 'modified' : 'created',
359
+ path: connectorFile
360
+ });
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ return changes;
367
+ }
368
+ /**
369
+ * Validate integration data
370
+ */
371
+ async validate(_customer, items) {
372
+ const errors = [];
373
+ for (const item of items) {
374
+ if (!item.integration.idn) {
375
+ errors.push({
376
+ field: 'idn',
377
+ message: 'Integration IDN is required'
378
+ });
379
+ }
380
+ for (const connector of item.connectors) {
381
+ if (!connector.connector_idn) {
382
+ errors.push({
383
+ field: 'connector_idn',
384
+ message: 'Connector IDN is required'
385
+ });
386
+ }
387
+ }
388
+ }
389
+ return { valid: errors.length === 0, errors };
390
+ }
391
+ /**
392
+ * Get status summary
393
+ */
394
+ async getStatus(customer) {
395
+ const changes = await this.getChanges(customer);
396
+ return {
397
+ resourceType: this.resourceType,
398
+ displayName: this.displayName,
399
+ changedCount: changes.length,
400
+ changes: changes.map(c => ({
401
+ path: c.path,
402
+ operation: c.operation
403
+ }))
404
+ };
405
+ }
406
+ }
407
+ /**
408
+ * Factory function for creating IntegrationSyncStrategy
409
+ */
410
+ export function createIntegrationSyncStrategy(apiClientFactory, logger) {
411
+ return new IntegrationSyncStrategy(apiClientFactory, logger);
412
+ }
413
+ //# sourceMappingURL=IntegrationSyncStrategy.js.map