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,508 @@
1
+ /**
2
+ * AttributeSyncStrategy - Handles synchronization of Customer and Project Attributes
3
+ *
4
+ * This strategy implements ISyncStrategy for the Attributes resource.
5
+ *
6
+ * Key responsibilities:
7
+ * - Pull customer attributes from NEWO platform
8
+ * - Pull project attributes for all projects
9
+ * - Push changed attributes back to platform
10
+ * - Detect changes using stored hashes
11
+ */
12
+
13
+ import type {
14
+ ISyncStrategy,
15
+ PullOptions,
16
+ PullResult,
17
+ PushResult,
18
+ ChangeItem,
19
+ ValidationResult,
20
+ ValidationError,
21
+ StatusSummary
22
+ } from './ISyncStrategy.js';
23
+ import type { CustomerConfig, ILogger, HashStore } from '../../resources/common/types.js';
24
+ import type { AxiosInstance } from 'axios';
25
+ import type { CustomerAttribute, CustomerAttributesResponse } from '../../../types.js';
26
+ import fs from 'fs-extra';
27
+ import yaml from 'js-yaml';
28
+ import path from 'path';
29
+ import {
30
+ getCustomerAttributes,
31
+ getProjectAttributes,
32
+ updateCustomerAttribute,
33
+ updateProjectAttribute,
34
+ listProjects
35
+ } from '../../../api.js';
36
+ import {
37
+ writeFileSafe,
38
+ customerAttributesPath,
39
+ customerAttributesMapPath
40
+ } from '../../../fsutil.js';
41
+ import { sha256, saveHashes, loadHashes } from '../../../hash.js';
42
+
43
+ /**
44
+ * Local attribute data for storage
45
+ */
46
+ export interface LocalAttributeData {
47
+ type: 'customer' | 'project';
48
+ projectIdn?: string;
49
+ attributes: CustomerAttribute[];
50
+ idMapping: Record<string, string>;
51
+ }
52
+
53
+ /**
54
+ * API client factory type
55
+ */
56
+ export type ApiClientFactory = (customer: CustomerConfig, verbose: boolean) => Promise<AxiosInstance>;
57
+
58
+ /**
59
+ * AttributeSyncStrategy - Handles attribute synchronization
60
+ */
61
+ export class AttributeSyncStrategy implements ISyncStrategy<CustomerAttributesResponse, LocalAttributeData> {
62
+ readonly resourceType = 'attributes';
63
+ readonly displayName = 'Attributes';
64
+
65
+ constructor(
66
+ private apiClientFactory: ApiClientFactory,
67
+ private logger: ILogger
68
+ ) {}
69
+
70
+ /**
71
+ * Pull all attributes from NEWO platform
72
+ */
73
+ async pull(customer: CustomerConfig, options: PullOptions = {}): Promise<PullResult<LocalAttributeData>> {
74
+ const client = await this.apiClientFactory(customer, options.verbose ?? false);
75
+ const hashes: HashStore = {};
76
+ const items: LocalAttributeData[] = [];
77
+
78
+ this.logger.verbose(`🔍 Fetching attributes for ${customer.idn}...`);
79
+
80
+ // Pull customer attributes
81
+ const customerAttrs = await this.pullCustomerAttributes(client, customer, hashes, options);
82
+ items.push(customerAttrs);
83
+
84
+ // Pull project attributes
85
+ const projects = await listProjects(client);
86
+ this.logger.verbose(`📁 Pulling attributes for ${projects.length} projects`);
87
+
88
+ for (const project of projects) {
89
+ try {
90
+ const projectAttrs = await this.pullProjectAttributes(
91
+ client, customer, project.id, project.idn, hashes, options
92
+ );
93
+ if (projectAttrs) {
94
+ items.push(projectAttrs);
95
+ }
96
+ } catch (error) {
97
+ this.logger.warn(`Failed to pull attributes for project ${project.idn}`);
98
+ }
99
+ }
100
+
101
+ // Save hashes
102
+ const existingHashes = await loadHashes(customer.idn);
103
+ await saveHashes({ ...existingHashes, ...hashes }, customer.idn);
104
+
105
+ return {
106
+ items,
107
+ count: items.length,
108
+ hashes
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Pull customer attributes
114
+ */
115
+ private async pullCustomerAttributes(
116
+ client: AxiosInstance,
117
+ customer: CustomerConfig,
118
+ hashes: HashStore,
119
+ options: PullOptions
120
+ ): Promise<LocalAttributeData> {
121
+ this.logger.verbose(` 📦 Fetching customer attributes...`);
122
+
123
+ const response = await getCustomerAttributes(client, true);
124
+ const attributes = response.attributes || [];
125
+
126
+ // Create ID mapping
127
+ const idMapping: Record<string, string> = {};
128
+ const cleanAttributes = attributes.map(attr => {
129
+ if (attr.id) {
130
+ idMapping[attr.idn] = attr.id;
131
+ }
132
+ return this.cleanAttribute(attr);
133
+ });
134
+
135
+ // Format as YAML
136
+ const yamlContent = this.formatAttributesYaml(cleanAttributes);
137
+
138
+ // Save files
139
+ const attributesPath = customerAttributesPath(customer.idn);
140
+ await writeFileSafe(attributesPath, yamlContent);
141
+ await writeFileSafe(customerAttributesMapPath(customer.idn), JSON.stringify(idMapping, null, 2));
142
+
143
+ hashes[attributesPath] = sha256(yamlContent);
144
+
145
+ if (options.verbose) {
146
+ this.logger.info(` ✓ Saved ${cleanAttributes.length} customer attributes`);
147
+ }
148
+
149
+ return {
150
+ type: 'customer',
151
+ attributes: cleanAttributes,
152
+ idMapping
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Pull project attributes
158
+ */
159
+ private async pullProjectAttributes(
160
+ client: AxiosInstance,
161
+ customer: CustomerConfig,
162
+ projectId: string,
163
+ projectIdn: string,
164
+ hashes: HashStore,
165
+ options: PullOptions
166
+ ): Promise<LocalAttributeData | null> {
167
+ try {
168
+ const response = await getProjectAttributes(client, projectId, true);
169
+ const attributes = response.attributes || [];
170
+
171
+ if (attributes.length === 0) {
172
+ return null;
173
+ }
174
+
175
+ // Create ID mapping
176
+ const idMapping: Record<string, string> = {};
177
+ const cleanAttributes = attributes.map(attr => {
178
+ if (attr.id) {
179
+ idMapping[attr.idn] = attr.id;
180
+ }
181
+ return this.cleanAttribute(attr);
182
+ });
183
+
184
+ // Format as YAML
185
+ const yamlContent = this.formatAttributesYaml(cleanAttributes);
186
+
187
+ // Save files
188
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
189
+ const projectDir = path.join(customerDir, 'projects', projectIdn);
190
+ await fs.ensureDir(projectDir);
191
+
192
+ const attributesFile = path.join(projectDir, 'attributes.yaml');
193
+ const mapFile = path.join(process.cwd(), '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
194
+
195
+ await writeFileSafe(attributesFile, yamlContent);
196
+ await fs.ensureDir(path.dirname(mapFile));
197
+ await writeFileSafe(mapFile, JSON.stringify(idMapping, null, 2));
198
+
199
+ hashes[attributesFile] = sha256(yamlContent);
200
+
201
+ if (options.verbose) {
202
+ this.logger.verbose(` ✓ Saved ${cleanAttributes.length} attributes for project ${projectIdn}`);
203
+ }
204
+
205
+ return {
206
+ type: 'project',
207
+ projectIdn,
208
+ attributes: cleanAttributes,
209
+ idMapping
210
+ };
211
+ } catch (error) {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Clean an attribute for local storage
218
+ */
219
+ private cleanAttribute(attr: CustomerAttribute): CustomerAttribute {
220
+ let processedValue = attr.value;
221
+
222
+ // Handle JSON string values
223
+ if (typeof attr.value === 'string' && attr.value.startsWith('[{') && attr.value.endsWith('}]')) {
224
+ try {
225
+ const parsed = JSON.parse(attr.value);
226
+ processedValue = JSON.stringify(parsed, null, 0);
227
+ } catch {
228
+ processedValue = attr.value;
229
+ }
230
+ }
231
+
232
+ return {
233
+ idn: attr.idn,
234
+ value: processedValue,
235
+ title: attr.title || '',
236
+ description: attr.description || '',
237
+ group: attr.group || '',
238
+ is_hidden: attr.is_hidden,
239
+ possible_values: attr.possible_values || [],
240
+ value_type: attr.value_type
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Format attributes as YAML
246
+ */
247
+ private formatAttributesYaml(attributes: CustomerAttribute[]): string {
248
+ // Add enum placeholders for value_type
249
+ const attributesWithPlaceholders = attributes.map(attr => ({
250
+ ...attr,
251
+ value_type: `__ENUM_PLACEHOLDER_${attr.value_type}__`
252
+ }));
253
+
254
+ let yamlContent = yaml.dump({ attributes: attributesWithPlaceholders }, {
255
+ indent: 2,
256
+ quotingType: '"',
257
+ forceQuotes: false,
258
+ lineWidth: 80,
259
+ noRefs: true,
260
+ sortKeys: false,
261
+ flowLevel: -1
262
+ });
263
+
264
+ // Replace placeholders with enum syntax
265
+ yamlContent = yamlContent.replace(/__ENUM_PLACEHOLDER_(\w+)__/g, '!enum "AttributeValueTypes.$1"');
266
+ yamlContent = yamlContent.replace(/\\"/g, '"');
267
+
268
+ return yamlContent;
269
+ }
270
+
271
+ /**
272
+ * Push changed attributes to NEWO platform
273
+ */
274
+ async push(customer: CustomerConfig, changes?: ChangeItem<LocalAttributeData>[]): Promise<PushResult> {
275
+ const result: PushResult = { created: 0, updated: 0, deleted: 0, errors: [] };
276
+
277
+ if (!changes) {
278
+ changes = await this.getChanges(customer);
279
+ }
280
+
281
+ if (changes.length === 0) {
282
+ return result;
283
+ }
284
+
285
+ const client = await this.apiClientFactory(customer, false);
286
+
287
+ for (const change of changes) {
288
+ try {
289
+ if (change.item.type === 'customer') {
290
+ const updateCount = await this.pushCustomerAttributes(client, customer, change.item);
291
+ result.updated += updateCount;
292
+ } else if (change.item.type === 'project' && change.item.projectIdn) {
293
+ const updateCount = await this.pushProjectAttributes(
294
+ client, customer, change.item.projectIdn, change.item
295
+ );
296
+ result.updated += updateCount;
297
+ }
298
+ } catch (error) {
299
+ result.errors.push(`Failed to push ${change.path}: ${error instanceof Error ? error.message : String(error)}`);
300
+ }
301
+ }
302
+
303
+ return result;
304
+ }
305
+
306
+ /**
307
+ * Push customer attributes
308
+ */
309
+ private async pushCustomerAttributes(
310
+ client: AxiosInstance,
311
+ customer: CustomerConfig,
312
+ _data: LocalAttributeData
313
+ ): Promise<number> {
314
+ // Load current attributes from file
315
+ const attributesFile = customerAttributesPath(customer.idn);
316
+ const mapFile = customerAttributesMapPath(customer.idn);
317
+
318
+ if (!(await fs.pathExists(attributesFile)) || !(await fs.pathExists(mapFile))) {
319
+ return 0;
320
+ }
321
+
322
+ let content = await fs.readFile(attributesFile, 'utf-8');
323
+ content = content.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
324
+
325
+ const localData = yaml.load(content) as { attributes: CustomerAttribute[] };
326
+ const idMapping = JSON.parse(await fs.readFile(mapFile, 'utf-8')) as Record<string, string>;
327
+
328
+ // Get remote attributes
329
+ const remoteResponse = await getCustomerAttributes(client, true);
330
+ const remoteMap = new Map<string, CustomerAttribute>();
331
+ remoteResponse.attributes.forEach(attr => remoteMap.set(attr.idn, attr));
332
+
333
+ let updatedCount = 0;
334
+
335
+ for (const localAttr of localData.attributes) {
336
+ const attributeId = idMapping[localAttr.idn];
337
+ if (!attributeId) continue;
338
+
339
+ const remoteAttr = remoteMap.get(localAttr.idn);
340
+ if (!remoteAttr) continue;
341
+
342
+ if (String(localAttr.value) !== String(remoteAttr.value)) {
343
+ await updateCustomerAttribute(client, {
344
+ id: attributeId,
345
+ ...localAttr
346
+ });
347
+ updatedCount++;
348
+ this.logger.info(` ✓ Updated customer attribute: ${localAttr.idn}`);
349
+ }
350
+ }
351
+
352
+ return updatedCount;
353
+ }
354
+
355
+ /**
356
+ * Push project attributes
357
+ */
358
+ private async pushProjectAttributes(
359
+ client: AxiosInstance,
360
+ customer: CustomerConfig,
361
+ projectIdn: string,
362
+ _data: LocalAttributeData
363
+ ): Promise<number> {
364
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn);
365
+ const attributesFile = path.join(customerDir, 'projects', projectIdn, 'attributes.yaml');
366
+ const mapFile = path.join(process.cwd(), '.newo', customer.idn, `project_${projectIdn}_attributes-map.json`);
367
+
368
+ if (!(await fs.pathExists(attributesFile)) || !(await fs.pathExists(mapFile))) {
369
+ return 0;
370
+ }
371
+
372
+ let content = await fs.readFile(attributesFile, 'utf-8');
373
+ content = content.replace(/!enum "AttributeValueTypes\.(\w+)"/g, '$1');
374
+
375
+ const localData = yaml.load(content) as { attributes: CustomerAttribute[] };
376
+ const idMapping = JSON.parse(await fs.readFile(mapFile, 'utf-8')) as Record<string, string>;
377
+
378
+ // Get project ID from projects list
379
+ const projects = await listProjects(client);
380
+ const project = projects.find(p => p.idn === projectIdn);
381
+ if (!project) {
382
+ return 0;
383
+ }
384
+
385
+ // Get remote attributes
386
+ const remoteResponse = await getProjectAttributes(client, project.id, true);
387
+ const remoteMap = new Map<string, CustomerAttribute>();
388
+ remoteResponse.attributes.forEach(attr => remoteMap.set(attr.idn, attr));
389
+
390
+ let updatedCount = 0;
391
+
392
+ for (const localAttr of localData.attributes) {
393
+ const attributeId = idMapping[localAttr.idn];
394
+ if (!attributeId) continue;
395
+
396
+ const remoteAttr = remoteMap.get(localAttr.idn);
397
+ if (!remoteAttr) continue;
398
+
399
+ if (String(localAttr.value) !== String(remoteAttr.value)) {
400
+ await updateProjectAttribute(client, project.id, {
401
+ id: attributeId,
402
+ ...localAttr
403
+ });
404
+ updatedCount++;
405
+ this.logger.info(` ✓ Updated project attribute: ${projectIdn}/${localAttr.idn}`);
406
+ }
407
+ }
408
+
409
+ return updatedCount;
410
+ }
411
+
412
+ /**
413
+ * Detect changes in attribute files
414
+ */
415
+ async getChanges(customer: CustomerConfig): Promise<ChangeItem<LocalAttributeData>[]> {
416
+ const changes: ChangeItem<LocalAttributeData>[] = [];
417
+ const hashes = await loadHashes(customer.idn);
418
+
419
+ // Check customer attributes
420
+ const customerAttrsPath = customerAttributesPath(customer.idn);
421
+ if (await fs.pathExists(customerAttrsPath)) {
422
+ const content = await fs.readFile(customerAttrsPath, 'utf-8');
423
+ const currentHash = sha256(content);
424
+ const storedHash = hashes[customerAttrsPath];
425
+
426
+ if (storedHash !== currentHash) {
427
+ changes.push({
428
+ item: { type: 'customer', attributes: [], idMapping: {} },
429
+ operation: 'modified',
430
+ path: customerAttrsPath
431
+ });
432
+ }
433
+ }
434
+
435
+ // Check project attributes
436
+ const customerDir = path.join(process.cwd(), 'newo_customers', customer.idn, 'projects');
437
+ if (await fs.pathExists(customerDir)) {
438
+ const projectDirs = await fs.readdir(customerDir);
439
+
440
+ for (const projectIdn of projectDirs) {
441
+ const attributesFile = path.join(customerDir, projectIdn, 'attributes.yaml');
442
+
443
+ if (await fs.pathExists(attributesFile)) {
444
+ const content = await fs.readFile(attributesFile, 'utf-8');
445
+ const currentHash = sha256(content);
446
+ const storedHash = hashes[attributesFile];
447
+
448
+ if (storedHash !== currentHash) {
449
+ changes.push({
450
+ item: { type: 'project', projectIdn, attributes: [], idMapping: {} },
451
+ operation: 'modified',
452
+ path: attributesFile
453
+ });
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ return changes;
460
+ }
461
+
462
+ /**
463
+ * Validate attribute data
464
+ */
465
+ async validate(_customer: CustomerConfig, items: LocalAttributeData[]): Promise<ValidationResult> {
466
+ const errors: ValidationError[] = [];
467
+
468
+ for (const item of items) {
469
+ for (const attr of item.attributes) {
470
+ if (!attr.idn) {
471
+ errors.push({
472
+ field: 'idn',
473
+ message: 'Attribute IDN is required'
474
+ });
475
+ }
476
+ }
477
+ }
478
+
479
+ return { valid: errors.length === 0, errors };
480
+ }
481
+
482
+ /**
483
+ * Get status summary
484
+ */
485
+ async getStatus(customer: CustomerConfig): Promise<StatusSummary> {
486
+ const changes = await this.getChanges(customer);
487
+
488
+ return {
489
+ resourceType: this.resourceType,
490
+ displayName: this.displayName,
491
+ changedCount: changes.length,
492
+ changes: changes.map(c => ({
493
+ path: c.path,
494
+ operation: c.operation
495
+ }))
496
+ };
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Factory function for creating AttributeSyncStrategy
502
+ */
503
+ export function createAttributeSyncStrategy(
504
+ apiClientFactory: ApiClientFactory,
505
+ logger: ILogger
506
+ ): AttributeSyncStrategy {
507
+ return new AttributeSyncStrategy(apiClientFactory, logger);
508
+ }