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.
- package/CHANGELOG.md +16 -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-attribute.js +1 -1
- 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/update-attribute.d.ts +3 -0
- package/dist/cli/commands/update-attribute.js +78 -0
- 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 +20 -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-attribute.ts +1 -1
- 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/update-attribute.ts +82 -0
- 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 +25 -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,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
|