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,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
|
+
}
|