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,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SyncEngine - Core synchronization orchestrator
|
|
3
|
+
*
|
|
4
|
+
* This is the central engine that coordinates sync operations across all resource types.
|
|
5
|
+
* It uses the Strategy pattern to handle different resources uniformly.
|
|
6
|
+
*
|
|
7
|
+
* Key benefits:
|
|
8
|
+
* - One engine handles projects, integrations, AKB, attributes, conversations
|
|
9
|
+
* - Adding new resource = implement one strategy class
|
|
10
|
+
* - No duplicate pull/push logic
|
|
11
|
+
* - Easy to test (mock strategies)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ISyncStrategy,
|
|
16
|
+
PullOptions,
|
|
17
|
+
PullResult,
|
|
18
|
+
PushResult,
|
|
19
|
+
StatusSummary,
|
|
20
|
+
ValidationResult
|
|
21
|
+
} from '../../domain/strategies/sync/ISyncStrategy.js';
|
|
22
|
+
import type { CustomerConfig, ILogger } from '../../domain/resources/common/types.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Combined pull result from all strategies
|
|
26
|
+
*/
|
|
27
|
+
export interface SyncPullResult {
|
|
28
|
+
customer: string;
|
|
29
|
+
resources: Array<{
|
|
30
|
+
resourceType: string;
|
|
31
|
+
displayName: string;
|
|
32
|
+
result: PullResult;
|
|
33
|
+
}>;
|
|
34
|
+
totalItems: number;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Combined push result from all strategies
|
|
40
|
+
*/
|
|
41
|
+
export interface SyncPushResult {
|
|
42
|
+
customer: string;
|
|
43
|
+
resources: Array<{
|
|
44
|
+
resourceType: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
result: PushResult;
|
|
47
|
+
}>;
|
|
48
|
+
totalCreated: number;
|
|
49
|
+
totalUpdated: number;
|
|
50
|
+
totalDeleted: number;
|
|
51
|
+
errors: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Status report for all resources
|
|
56
|
+
*/
|
|
57
|
+
export interface StatusReport {
|
|
58
|
+
customer: string;
|
|
59
|
+
resources: StatusSummary[];
|
|
60
|
+
totalChanges: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync error with context
|
|
65
|
+
*/
|
|
66
|
+
export class SyncError extends Error {
|
|
67
|
+
constructor(
|
|
68
|
+
message: string,
|
|
69
|
+
public resourceType: string,
|
|
70
|
+
public override cause?: Error
|
|
71
|
+
) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'SyncError';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Validation error with details
|
|
79
|
+
*/
|
|
80
|
+
export class ValidationError extends Error {
|
|
81
|
+
constructor(
|
|
82
|
+
message: string,
|
|
83
|
+
public results: ValidationResult[]
|
|
84
|
+
) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.name = 'ValidationError';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* SyncEngine Options
|
|
92
|
+
*/
|
|
93
|
+
export interface SyncEngineOptions {
|
|
94
|
+
/**
|
|
95
|
+
* Stop on first error instead of continuing
|
|
96
|
+
*/
|
|
97
|
+
stopOnError?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Run strategies in parallel where possible
|
|
101
|
+
*/
|
|
102
|
+
parallel?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* SyncEngine - Generic synchronization orchestrator
|
|
107
|
+
*
|
|
108
|
+
* Orchestrates pull/push/status operations across all registered strategies.
|
|
109
|
+
*/
|
|
110
|
+
export class SyncEngine {
|
|
111
|
+
private strategies: Map<string, ISyncStrategy> = new Map();
|
|
112
|
+
|
|
113
|
+
constructor(
|
|
114
|
+
strategies: ISyncStrategy[],
|
|
115
|
+
private logger: ILogger,
|
|
116
|
+
private options: SyncEngineOptions = {}
|
|
117
|
+
) {
|
|
118
|
+
for (const strategy of strategies) {
|
|
119
|
+
this.strategies.set(strategy.resourceType, strategy);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register a new strategy
|
|
125
|
+
*/
|
|
126
|
+
registerStrategy(strategy: ISyncStrategy): void {
|
|
127
|
+
this.strategies.set(strategy.resourceType, strategy);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get a specific strategy by resource type
|
|
132
|
+
*/
|
|
133
|
+
getStrategy(resourceType: string): ISyncStrategy | undefined {
|
|
134
|
+
return this.strategies.get(resourceType);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get all registered strategies
|
|
139
|
+
*/
|
|
140
|
+
getStrategies(): ISyncStrategy[] {
|
|
141
|
+
return Array.from(this.strategies.values());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Pull ALL resources using registered strategies
|
|
146
|
+
*/
|
|
147
|
+
async pullAll(customer: CustomerConfig, options: PullOptions = {}): Promise<SyncPullResult> {
|
|
148
|
+
this.logger.info(`📥 Pulling all resources for customer: ${customer.idn}`);
|
|
149
|
+
|
|
150
|
+
const result: SyncPullResult = {
|
|
151
|
+
customer: customer.idn,
|
|
152
|
+
resources: [],
|
|
153
|
+
totalItems: 0,
|
|
154
|
+
errors: []
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const strategies = Array.from(this.strategies.values());
|
|
158
|
+
|
|
159
|
+
if (this.options.parallel) {
|
|
160
|
+
// Parallel execution
|
|
161
|
+
const pullPromises = strategies.map(async (strategy) => {
|
|
162
|
+
try {
|
|
163
|
+
return await this.pullWithStrategy(strategy, customer, options);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
166
|
+
if (this.options.stopOnError) {
|
|
167
|
+
throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
|
|
168
|
+
}
|
|
169
|
+
result.errors.push(message);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const pullResults = await Promise.all(pullPromises);
|
|
175
|
+
|
|
176
|
+
for (const pullResult of pullResults) {
|
|
177
|
+
if (pullResult) {
|
|
178
|
+
result.resources.push(pullResult);
|
|
179
|
+
result.totalItems += pullResult.result.count;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// Sequential execution
|
|
184
|
+
for (const strategy of strategies) {
|
|
185
|
+
this.logger.info(` 📦 Pulling ${strategy.displayName}...`);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const pullResult = await this.pullWithStrategy(strategy, customer, options);
|
|
189
|
+
result.resources.push(pullResult);
|
|
190
|
+
result.totalItems += pullResult.result.count;
|
|
191
|
+
this.logger.info(` ✅ Pulled ${pullResult.result.count} ${strategy.displayName}`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
194
|
+
this.logger.error(message, error);
|
|
195
|
+
|
|
196
|
+
if (this.options.stopOnError) {
|
|
197
|
+
throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
|
|
198
|
+
}
|
|
199
|
+
result.errors.push(message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.logger.info(`✅ Pull completed: ${result.totalItems} items from ${result.resources.length} resource types`);
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Pull specific resource types
|
|
211
|
+
*/
|
|
212
|
+
async pullSelected(
|
|
213
|
+
customer: CustomerConfig,
|
|
214
|
+
resourceTypes: string[],
|
|
215
|
+
options: PullOptions = {}
|
|
216
|
+
): Promise<SyncPullResult> {
|
|
217
|
+
this.logger.info(`📥 Pulling selected resources for customer: ${customer.idn}`);
|
|
218
|
+
|
|
219
|
+
const result: SyncPullResult = {
|
|
220
|
+
customer: customer.idn,
|
|
221
|
+
resources: [],
|
|
222
|
+
totalItems: 0,
|
|
223
|
+
errors: []
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
for (const resourceType of resourceTypes) {
|
|
227
|
+
const strategy = this.strategies.get(resourceType);
|
|
228
|
+
|
|
229
|
+
if (!strategy) {
|
|
230
|
+
result.errors.push(`Unknown resource type: ${resourceType}`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.logger.info(` 📦 Pulling ${strategy.displayName}...`);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const pullResult = await this.pullWithStrategy(strategy, customer, options);
|
|
238
|
+
result.resources.push(pullResult);
|
|
239
|
+
result.totalItems += pullResult.result.count;
|
|
240
|
+
this.logger.info(` ✅ Pulled ${pullResult.result.count} ${strategy.displayName}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = `Failed to pull ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
243
|
+
this.logger.error(message, error);
|
|
244
|
+
|
|
245
|
+
if (this.options.stopOnError) {
|
|
246
|
+
throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
|
|
247
|
+
}
|
|
248
|
+
result.errors.push(message);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Push ALL changed resources using registered strategies
|
|
257
|
+
*/
|
|
258
|
+
async pushAll(customer: CustomerConfig): Promise<SyncPushResult> {
|
|
259
|
+
this.logger.info(`📤 Pushing changes for customer: ${customer.idn}`);
|
|
260
|
+
|
|
261
|
+
const result: SyncPushResult = {
|
|
262
|
+
customer: customer.idn,
|
|
263
|
+
resources: [],
|
|
264
|
+
totalCreated: 0,
|
|
265
|
+
totalUpdated: 0,
|
|
266
|
+
totalDeleted: 0,
|
|
267
|
+
errors: []
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
for (const strategy of this.strategies.values()) {
|
|
271
|
+
this.logger.info(` 🔍 Checking changes for ${strategy.displayName}...`);
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
const changes = await strategy.getChanges(customer);
|
|
275
|
+
|
|
276
|
+
if (changes.length === 0) {
|
|
277
|
+
this.logger.verbose(` No changes for ${strategy.displayName}`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.logger.info(` Found ${changes.length} changes in ${strategy.displayName}`);
|
|
282
|
+
|
|
283
|
+
// Validate before push
|
|
284
|
+
const items = changes.map(c => c.item);
|
|
285
|
+
const validation = await strategy.validate(customer, items);
|
|
286
|
+
|
|
287
|
+
if (!validation.valid) {
|
|
288
|
+
const errorMessages = validation.errors.map(e => `${e.field}: ${e.message}`).join(', ');
|
|
289
|
+
throw new ValidationError(`Validation failed: ${errorMessages}`, [validation]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Push changes
|
|
293
|
+
const pushResult = await strategy.push(customer, changes);
|
|
294
|
+
|
|
295
|
+
result.resources.push({
|
|
296
|
+
resourceType: strategy.resourceType,
|
|
297
|
+
displayName: strategy.displayName,
|
|
298
|
+
result: pushResult
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
result.totalCreated += pushResult.created;
|
|
302
|
+
result.totalUpdated += pushResult.updated;
|
|
303
|
+
result.totalDeleted += pushResult.deleted;
|
|
304
|
+
result.errors.push(...pushResult.errors);
|
|
305
|
+
|
|
306
|
+
this.logger.info(` ✅ Pushed: ${pushResult.created} created, ${pushResult.updated} updated, ${pushResult.deleted} deleted`);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
const message = `Failed to push ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
309
|
+
this.logger.error(message, error);
|
|
310
|
+
|
|
311
|
+
if (this.options.stopOnError) {
|
|
312
|
+
throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
|
|
313
|
+
}
|
|
314
|
+
result.errors.push(message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.logger.info(`✅ Push completed: ${result.totalCreated} created, ${result.totalUpdated} updated, ${result.totalDeleted} deleted`);
|
|
319
|
+
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Push specific resource types
|
|
325
|
+
*/
|
|
326
|
+
async pushSelected(customer: CustomerConfig, resourceTypes: string[]): Promise<SyncPushResult> {
|
|
327
|
+
this.logger.info(`📤 Pushing selected resources for customer: ${customer.idn}`);
|
|
328
|
+
|
|
329
|
+
const result: SyncPushResult = {
|
|
330
|
+
customer: customer.idn,
|
|
331
|
+
resources: [],
|
|
332
|
+
totalCreated: 0,
|
|
333
|
+
totalUpdated: 0,
|
|
334
|
+
totalDeleted: 0,
|
|
335
|
+
errors: []
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
for (const resourceType of resourceTypes) {
|
|
339
|
+
const strategy = this.strategies.get(resourceType);
|
|
340
|
+
|
|
341
|
+
if (!strategy) {
|
|
342
|
+
result.errors.push(`Unknown resource type: ${resourceType}`);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const changes = await strategy.getChanges(customer);
|
|
348
|
+
|
|
349
|
+
if (changes.length === 0) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Validate before push
|
|
354
|
+
const items = changes.map(c => c.item);
|
|
355
|
+
const validation = await strategy.validate(customer, items);
|
|
356
|
+
|
|
357
|
+
if (!validation.valid) {
|
|
358
|
+
const errorMessages = validation.errors.map(e => `${e.field}: ${e.message}`).join(', ');
|
|
359
|
+
throw new ValidationError(`Validation failed: ${errorMessages}`, [validation]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const pushResult = await strategy.push(customer, changes);
|
|
363
|
+
|
|
364
|
+
result.resources.push({
|
|
365
|
+
resourceType: strategy.resourceType,
|
|
366
|
+
displayName: strategy.displayName,
|
|
367
|
+
result: pushResult
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
result.totalCreated += pushResult.created;
|
|
371
|
+
result.totalUpdated += pushResult.updated;
|
|
372
|
+
result.totalDeleted += pushResult.deleted;
|
|
373
|
+
result.errors.push(...pushResult.errors);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const message = `Failed to push ${strategy.displayName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
376
|
+
this.logger.error(message, error);
|
|
377
|
+
|
|
378
|
+
if (this.options.stopOnError) {
|
|
379
|
+
throw new SyncError(message, strategy.resourceType, error instanceof Error ? error : undefined);
|
|
380
|
+
}
|
|
381
|
+
result.errors.push(message);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get status for ALL resources
|
|
390
|
+
*/
|
|
391
|
+
async getStatus(customer: CustomerConfig): Promise<StatusReport> {
|
|
392
|
+
const report: StatusReport = {
|
|
393
|
+
customer: customer.idn,
|
|
394
|
+
resources: [],
|
|
395
|
+
totalChanges: 0
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
for (const strategy of this.strategies.values()) {
|
|
399
|
+
try {
|
|
400
|
+
const status = await strategy.getStatus(customer);
|
|
401
|
+
report.resources.push(status);
|
|
402
|
+
report.totalChanges += status.changedCount;
|
|
403
|
+
} catch (error) {
|
|
404
|
+
this.logger.error(`Failed to get status for ${strategy.displayName}`, error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return report;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get status for specific resource types
|
|
413
|
+
*/
|
|
414
|
+
async getStatusSelected(customer: CustomerConfig, resourceTypes: string[]): Promise<StatusReport> {
|
|
415
|
+
const report: StatusReport = {
|
|
416
|
+
customer: customer.idn,
|
|
417
|
+
resources: [],
|
|
418
|
+
totalChanges: 0
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
for (const resourceType of resourceTypes) {
|
|
422
|
+
const strategy = this.strategies.get(resourceType);
|
|
423
|
+
|
|
424
|
+
if (!strategy) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
const status = await strategy.getStatus(customer);
|
|
430
|
+
report.resources.push(status);
|
|
431
|
+
report.totalChanges += status.changedCount;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.logger.error(`Failed to get status for ${strategy.displayName}`, error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return report;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Helper to execute pull with a single strategy
|
|
442
|
+
*/
|
|
443
|
+
private async pullWithStrategy(
|
|
444
|
+
strategy: ISyncStrategy,
|
|
445
|
+
customer: CustomerConfig,
|
|
446
|
+
options: PullOptions
|
|
447
|
+
): Promise<{ resourceType: string; displayName: string; result: PullResult }> {
|
|
448
|
+
const result = await strategy.pull(customer, options);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
resourceType: strategy.resourceType,
|
|
452
|
+
displayName: strategy.displayName,
|
|
453
|
+
result
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Factory function for creating SyncEngine with default strategies
|
|
460
|
+
*/
|
|
461
|
+
export function createSyncEngine(
|
|
462
|
+
strategies: ISyncStrategy[],
|
|
463
|
+
logger: ILogger,
|
|
464
|
+
options?: SyncEngineOptions
|
|
465
|
+
): SyncEngine {
|
|
466
|
+
return new SyncEngine(strategies, logger, options);
|
|
467
|
+
}
|
|
@@ -16,7 +16,7 @@ export async function handleCreateAttributeCommand(
|
|
|
16
16
|
|
|
17
17
|
// Parse arguments
|
|
18
18
|
const idn = args._[1] as string;
|
|
19
|
-
const value = args.value
|
|
19
|
+
const value = args.value !== undefined ? String(args.value) : '';
|
|
20
20
|
const title = args.title as string || idn;
|
|
21
21
|
const description = args.description as string || '';
|
|
22
22
|
const group = args.group as string || 'General';
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Customer Command Handler - Creates a new NEWO customer account
|
|
3
|
+
*
|
|
4
|
+
* This command creates an empty NEWO customer using the v3 API.
|
|
5
|
+
* It requires the api_secret attribute from the source customer account.
|
|
6
|
+
*/
|
|
7
|
+
import { makeClient, createNewoCustomer, getCustomerAttributes } from '../../api.js';
|
|
8
|
+
import { getValidAccessToken } from '../../auth.js';
|
|
9
|
+
import { requireSingleCustomer } from '../customer-selection.js';
|
|
10
|
+
import type {
|
|
11
|
+
MultiCustomerConfig,
|
|
12
|
+
CliArgs,
|
|
13
|
+
CreateNewoCustomerRequest,
|
|
14
|
+
CustomerMember,
|
|
15
|
+
CustomerProjectInput
|
|
16
|
+
} from '../../types.js';
|
|
17
|
+
|
|
18
|
+
export async function handleCreateCustomerCommand(
|
|
19
|
+
customerConfig: MultiCustomerConfig,
|
|
20
|
+
args: CliArgs,
|
|
21
|
+
verbose: boolean = false
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
const selectedCustomer = requireSingleCustomer(customerConfig, args.customer as string | undefined);
|
|
25
|
+
|
|
26
|
+
// Parse arguments
|
|
27
|
+
const organizationName = args._[1] as string;
|
|
28
|
+
const email = args.email as string;
|
|
29
|
+
const tenant = (args.tenant as string) || 'newo';
|
|
30
|
+
const phone = (args.phone as string) || '';
|
|
31
|
+
const comment = (args.comment as string) || '';
|
|
32
|
+
const status = (args.status as string) || 'temporal';
|
|
33
|
+
const projectIdn = args.project as string | undefined;
|
|
34
|
+
const externalId = args['external-id'] as string | undefined;
|
|
35
|
+
|
|
36
|
+
// Validate required parameters
|
|
37
|
+
if (!organizationName) {
|
|
38
|
+
console.error('Error: Organization name is required');
|
|
39
|
+
console.error('Usage: newo create-customer <organization_name> --email <email> [options]');
|
|
40
|
+
console.error('');
|
|
41
|
+
console.error('Options:');
|
|
42
|
+
console.error(' --email <email> Owner email (required)');
|
|
43
|
+
console.error(' --tenant <tenant> Tenant name (default: newo)');
|
|
44
|
+
console.error(' --phone <phone> Contact phone number');
|
|
45
|
+
console.error(' --comment <comment> Comment or notes');
|
|
46
|
+
console.error(' --status <status> temporal or permanent (default: temporal)');
|
|
47
|
+
console.error(' --project <idn> Project IDN to install (e.g., naf)');
|
|
48
|
+
console.error(' --external-id <id> External customer ID for tracking');
|
|
49
|
+
console.error('');
|
|
50
|
+
console.error('Example:');
|
|
51
|
+
console.error(' newo create-customer "Acme Corp" --email owner@acme.com --project naf');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!email) {
|
|
56
|
+
console.error('Error: Owner email is required (--email <email>)');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (verbose) {
|
|
61
|
+
console.log(`📝 Creating new NEWO customer...`);
|
|
62
|
+
console.log(` Organization: ${organizationName}`);
|
|
63
|
+
console.log(` Owner Email: ${email}`);
|
|
64
|
+
console.log(` Tenant: ${tenant}`);
|
|
65
|
+
console.log(` Status: ${status}`);
|
|
66
|
+
if (phone) console.log(` Phone: ${phone}`);
|
|
67
|
+
if (comment) console.log(` Comment: ${comment}`);
|
|
68
|
+
if (projectIdn) console.log(` Project: ${projectIdn}`);
|
|
69
|
+
if (externalId) console.log(` External ID: ${externalId}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Get access token and create client
|
|
73
|
+
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
74
|
+
const client = await makeClient(verbose, accessToken);
|
|
75
|
+
|
|
76
|
+
// Get api_secret from customer attributes
|
|
77
|
+
console.log('🔑 Fetching API secret from customer attributes...');
|
|
78
|
+
const attributesResponse = await getCustomerAttributes(client, true);
|
|
79
|
+
const apiSecretAttr = attributesResponse.attributes.find(attr => attr.idn === 'api_secret');
|
|
80
|
+
|
|
81
|
+
if (!apiSecretAttr || !apiSecretAttr.value) {
|
|
82
|
+
console.error('Error: api_secret attribute not found in customer account');
|
|
83
|
+
console.error('This command requires the api_secret attribute to create new customers.');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const apiSecret = typeof apiSecretAttr.value === 'string' ? apiSecretAttr.value : String(apiSecretAttr.value);
|
|
88
|
+
|
|
89
|
+
if (verbose) {
|
|
90
|
+
console.log(`✅ API secret found (${apiSecret.substring(0, 6)}...)`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build members array
|
|
94
|
+
const members: CustomerMember[] = [
|
|
95
|
+
{
|
|
96
|
+
email: email,
|
|
97
|
+
role: 'owner',
|
|
98
|
+
tenants: [tenant]
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// Build projects array if project specified
|
|
103
|
+
const projects: CustomerProjectInput[] = [];
|
|
104
|
+
if (projectIdn) {
|
|
105
|
+
projects.push({
|
|
106
|
+
idn: projectIdn
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Build customer object - only include optional fields if they have values
|
|
111
|
+
const customerData: CreateNewoCustomerRequest['customer'] = {
|
|
112
|
+
organization_name: organizationName,
|
|
113
|
+
tenant: tenant,
|
|
114
|
+
members: members,
|
|
115
|
+
contact_email: email,
|
|
116
|
+
organization_type: 'customer',
|
|
117
|
+
organization_status: status as 'temporal' | 'permanent',
|
|
118
|
+
attributes: [
|
|
119
|
+
{
|
|
120
|
+
idn: 'empty',
|
|
121
|
+
value: 'True'
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Add optional fields only if they have values
|
|
127
|
+
if (comment) {
|
|
128
|
+
customerData.comment = comment;
|
|
129
|
+
}
|
|
130
|
+
if (phone) {
|
|
131
|
+
customerData.contact_phone = phone;
|
|
132
|
+
}
|
|
133
|
+
if (externalId) {
|
|
134
|
+
customerData.external_customer_id = externalId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Build customer creation request
|
|
138
|
+
const createRequest: CreateNewoCustomerRequest = {
|
|
139
|
+
secret: apiSecret,
|
|
140
|
+
customer: customerData
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Add projects only if specified
|
|
144
|
+
if (projects.length > 0) {
|
|
145
|
+
createRequest.projects = projects;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (verbose) {
|
|
149
|
+
console.log('📤 Creating customer with request:');
|
|
150
|
+
console.log(JSON.stringify(createRequest, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create the customer
|
|
154
|
+
console.log('🚀 Creating customer...');
|
|
155
|
+
const response = await createNewoCustomer(client, createRequest);
|
|
156
|
+
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log('✅ Customer created successfully!');
|
|
159
|
+
console.log(` Customer IDN: ${response.idn}`);
|
|
160
|
+
console.log(` Customer ID: ${response.id}`);
|
|
161
|
+
console.log(` Organization: ${organizationName}`);
|
|
162
|
+
console.log(` Owner: ${email}`);
|
|
163
|
+
if (projectIdn) {
|
|
164
|
+
console.log(` Project: ${projectIdn}`);
|
|
165
|
+
}
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log('📝 Next steps:');
|
|
168
|
+
console.log(` 1. Add the new customer to your .env file:`);
|
|
169
|
+
console.log(` NEWO_${response.idn}_API_KEY=<api_key>`);
|
|
170
|
+
console.log(` 2. Or use the customer_intercom integration to manage the new customer`);
|
|
171
|
+
|
|
172
|
+
} catch (error: unknown) {
|
|
173
|
+
const errMessage = error instanceof Error ? error.message : String(error);
|
|
174
|
+
console.error('❌ Failed to create customer:', errMessage);
|
|
175
|
+
|
|
176
|
+
// Provide more detailed error info if available
|
|
177
|
+
if (error && typeof error === 'object' && 'response' in error) {
|
|
178
|
+
const axiosError = error as any;
|
|
179
|
+
if (axiosError.response?.data) {
|
|
180
|
+
console.error(' API Error:', JSON.stringify(axiosError.response.data, null, 2));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|