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.
- package/CHANGELOG.md +6 -0
- package/dist/api.d.ts +3 -1
- package/dist/api.js +49 -1
- package/dist/application/migration/MigrationEngine.d.ts +141 -0
- package/dist/application/migration/MigrationEngine.js +322 -0
- package/dist/application/migration/index.d.ts +5 -0
- package/dist/application/migration/index.js +5 -0
- package/dist/application/sync/SyncEngine.d.ts +134 -0
- package/dist/application/sync/SyncEngine.js +335 -0
- package/dist/application/sync/index.d.ts +5 -0
- package/dist/application/sync/index.js +5 -0
- package/dist/cli/commands/create-customer.d.ts +3 -0
- package/dist/cli/commands/create-customer.js +159 -0
- package/dist/cli/commands/diff.d.ts +6 -0
- package/dist/cli/commands/diff.js +288 -0
- package/dist/cli/commands/help.js +63 -3
- package/dist/cli/commands/logs.d.ts +18 -0
- package/dist/cli/commands/logs.js +283 -0
- package/dist/cli/commands/pull.js +114 -10
- package/dist/cli/commands/push.js +122 -12
- package/dist/cli/commands/watch.d.ts +6 -0
- package/dist/cli/commands/watch.js +195 -0
- package/dist/cli-new/bootstrap.d.ts +74 -0
- package/dist/cli-new/bootstrap.js +154 -0
- package/dist/cli-new/di/Container.d.ts +64 -0
- package/dist/cli-new/di/Container.js +122 -0
- package/dist/cli-new/di/tokens.d.ts +77 -0
- package/dist/cli-new/di/tokens.js +76 -0
- package/dist/cli.js +16 -0
- package/dist/domain/resources/common/types.d.ts +71 -0
- package/dist/domain/resources/common/types.js +42 -0
- package/dist/domain/strategies/sync/AkbSyncStrategy.d.ts +63 -0
- package/dist/domain/strategies/sync/AkbSyncStrategy.js +274 -0
- package/dist/domain/strategies/sync/AttributeSyncStrategy.d.ts +87 -0
- package/dist/domain/strategies/sync/AttributeSyncStrategy.js +378 -0
- package/dist/domain/strategies/sync/ConversationSyncStrategy.d.ts +61 -0
- package/dist/domain/strategies/sync/ConversationSyncStrategy.js +232 -0
- package/dist/domain/strategies/sync/ISyncStrategy.d.ts +149 -0
- package/dist/domain/strategies/sync/ISyncStrategy.js +24 -0
- package/dist/domain/strategies/sync/IntegrationSyncStrategy.d.ts +68 -0
- package/dist/domain/strategies/sync/IntegrationSyncStrategy.js +413 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.d.ts +111 -0
- package/dist/domain/strategies/sync/ProjectSyncStrategy.js +523 -0
- package/dist/domain/strategies/sync/index.d.ts +13 -0
- package/dist/domain/strategies/sync/index.js +19 -0
- package/dist/sync/migrate.js +99 -23
- package/dist/types.d.ts +124 -0
- package/package.json +3 -1
- package/src/api.ts +53 -2
- package/src/application/migration/MigrationEngine.ts +492 -0
- package/src/application/migration/index.ts +5 -0
- package/src/application/sync/SyncEngine.ts +467 -0
- package/src/application/sync/index.ts +5 -0
- package/src/cli/commands/create-customer.ts +185 -0
- package/src/cli/commands/diff.ts +360 -0
- package/src/cli/commands/help.ts +63 -3
- package/src/cli/commands/logs.ts +329 -0
- package/src/cli/commands/pull.ts +128 -11
- package/src/cli/commands/push.ts +131 -13
- package/src/cli/commands/watch.ts +227 -0
- package/src/cli-new/bootstrap.ts +252 -0
- package/src/cli-new/di/Container.ts +152 -0
- package/src/cli-new/di/tokens.ts +105 -0
- package/src/cli.ts +20 -0
- package/src/domain/resources/common/types.ts +106 -0
- package/src/domain/strategies/sync/AkbSyncStrategy.ts +358 -0
- package/src/domain/strategies/sync/AttributeSyncStrategy.ts +508 -0
- package/src/domain/strategies/sync/ConversationSyncStrategy.ts +299 -0
- package/src/domain/strategies/sync/ISyncStrategy.ts +182 -0
- package/src/domain/strategies/sync/IntegrationSyncStrategy.ts +522 -0
- package/src/domain/strategies/sync/ProjectSyncStrategy.ts +747 -0
- package/src/domain/strategies/sync/index.ts +46 -0
- package/src/sync/migrate.ts +103 -24
- 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
|
+
}
|