@salesforce/metadata-plugins 1.0.0

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 (54) hide show
  1. package/LICENSE.txt +27 -0
  2. package/README.md +121 -0
  3. package/dist/index.d.ts +25 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +45 -0
  6. package/dist/plugins/flexipage/FlexipageParser.d.ts +39 -0
  7. package/dist/plugins/flexipage/FlexipageParser.d.ts.map +1 -0
  8. package/dist/plugins/flexipage/FlexipageParser.js +124 -0
  9. package/dist/plugins/flexipage/FlexipageVisualizer.d.ts +19 -0
  10. package/dist/plugins/flexipage/FlexipageVisualizer.d.ts.map +1 -0
  11. package/dist/plugins/flexipage/FlexipageVisualizer.js +27 -0
  12. package/dist/plugins/flexipage/transformer/FlexipageToUemTransformer.d.ts +55 -0
  13. package/dist/plugins/flexipage/transformer/FlexipageToUemTransformer.d.ts.map +1 -0
  14. package/dist/plugins/flexipage/transformer/FlexipageToUemTransformer.js +302 -0
  15. package/dist/plugins/flexipage/transformer/FlexipageTransformerUtils.d.ts +34 -0
  16. package/dist/plugins/flexipage/transformer/FlexipageTransformerUtils.d.ts.map +1 -0
  17. package/dist/plugins/flexipage/transformer/FlexipageTransformerUtils.js +73 -0
  18. package/dist/plugins/flexipage/types/FlexipageMetadata.d.ts +76 -0
  19. package/dist/plugins/flexipage/types/FlexipageMetadata.d.ts.map +1 -0
  20. package/dist/plugins/flexipage/types/FlexipageMetadata.js +7 -0
  21. package/dist/plugins/flexipage/ui/assets/index-DUDC29Wu.js +55 -0
  22. package/dist/plugins/flexipage/ui/assets/index-U3SB2gLS.css +1 -0
  23. package/dist/plugins/flexipage/ui/index.html +16 -0
  24. package/dist/plugins/schema/SchemaParser.d.ts +80 -0
  25. package/dist/plugins/schema/SchemaParser.d.ts.map +1 -0
  26. package/dist/plugins/schema/SchemaParser.js +474 -0
  27. package/dist/plugins/schema/SchemaVisualizer.d.ts +42 -0
  28. package/dist/plugins/schema/SchemaVisualizer.d.ts.map +1 -0
  29. package/dist/plugins/schema/SchemaVisualizer.js +46 -0
  30. package/dist/plugins/schema/types/SchemaERDData.d.ts +91 -0
  31. package/dist/plugins/schema/types/SchemaERDData.d.ts.map +1 -0
  32. package/dist/plugins/schema/types/SchemaERDData.js +7 -0
  33. package/dist/plugins/schema/ui/assets/index-BbY653nW.js +62 -0
  34. package/dist/plugins/schema/ui/assets/index-CbNHuvPl.css +1 -0
  35. package/dist/plugins/schema/ui/index.html +14 -0
  36. package/dist/shared/uem/transformer/ITransformer.d.ts +58 -0
  37. package/dist/shared/uem/transformer/ITransformer.d.ts.map +1 -0
  38. package/dist/shared/uem/transformer/ITransformer.js +7 -0
  39. package/dist/shared/uem/transformer/TransformerRegistry.d.ts +54 -0
  40. package/dist/shared/uem/transformer/TransformerRegistry.d.ts.map +1 -0
  41. package/dist/shared/uem/transformer/TransformerRegistry.js +74 -0
  42. package/dist/shared/uem/transformer/UemBasedVisualizer.d.ts +43 -0
  43. package/dist/shared/uem/transformer/UemBasedVisualizer.d.ts.map +1 -0
  44. package/dist/shared/uem/transformer/UemBasedVisualizer.js +62 -0
  45. package/dist/shared/uem/types/UemMetadata.d.ts +84 -0
  46. package/dist/shared/uem/types/UemMetadata.d.ts.map +1 -0
  47. package/dist/shared/uem/types/UemMetadata.js +7 -0
  48. package/dist/shared/utils/uemTreeUtils.d.ts +51 -0
  49. package/dist/shared/utils/uemTreeUtils.d.ts.map +1 -0
  50. package/dist/shared/utils/uemTreeUtils.js +89 -0
  51. package/dist/shared/utils/vizDependentPathsHelper.d.ts +53 -0
  52. package/dist/shared/utils/vizDependentPathsHelper.d.ts.map +1 -0
  53. package/dist/shared/utils/vizDependentPathsHelper.js +108 -0
  54. package/package.json +111 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ **/
7
+ /**
8
+ * Schema Parser
9
+ *
10
+ * Parses Salesforce SFDX decomposed source format (.object-meta.xml and .field-meta.xml)
11
+ * into a SchemaERDData structure for interactive ERD visualization.
12
+ *
13
+ * Unlike the ObjectParser which parses a single object at a time, the SchemaParser
14
+ * discovers and parses ALL objects in the project to build a complete ERD graph
15
+ * with resolved cross-object relationships.
16
+ *
17
+ * Supports:
18
+ * - Decomposed source format (separate .field-meta.xml files in fields/ subdirectory)
19
+ * - Inline fields in .object-meta.xml (flat format)
20
+ * - Standard objects (Account, Asset, etc.) and custom objects (*__c)
21
+ * - Lookup and MasterDetail relationship resolution
22
+ * - Full transitive BFS traversal of related objects
23
+ * - Platform limit validation (max 2 Master-Detail per object)
24
+ *
25
+ * @author Schema Team
26
+ */
27
+ import { IMetadataParser, SalesforceXMLParser, ValidationResult, IPlatformContext, VersionInfo } from '@salesforce/metadata-core-sdk';
28
+ import { SchemaERDData } from './types/SchemaERDData.js';
29
+ export declare class SchemaParser implements IMetadataParser<SchemaERDData> {
30
+ private platformContext?;
31
+ private telemetry?;
32
+ readonly xmlParser: SalesforceXMLParser;
33
+ setPlatformContext(context: IPlatformContext): void;
34
+ /**
35
+ * Parse all objects in the project starting from the given file path.
36
+ * Discovers all sibling objects, parses their fields, resolves relationships,
37
+ * and returns a complete SchemaERDData structure.
38
+ */
39
+ parse(filePath: string, _versionInfo: VersionInfo): Promise<SchemaERDData>;
40
+ /**
41
+ * Validate parsed SchemaERDData against Salesforce platform limits.
42
+ */
43
+ validate?(data: SchemaERDData): ValidationResult;
44
+ /**
45
+ * Get all file paths that this visualization depends on.
46
+ * Used by the framework to trigger re-renders when any of these files change.
47
+ */
48
+ getVizDependentFilePaths(filePath: string): Promise<string[]>;
49
+ /**
50
+ * Walk up from the given file path to find the "objects" directory.
51
+ */
52
+ private findObjectsRoot;
53
+ /**
54
+ * Extract the object API name from the clicked file path.
55
+ * e.g. ".../objects/Video__c/Video__c.object-meta.xml" → "Video__c"
56
+ * e.g. ".../objects/Video__c/fields/Status__c.field-meta.xml" → "Video__c"
57
+ */
58
+ private getTriggerObjectName;
59
+ /**
60
+ * Find all objects transitively related to the trigger object via BFS.
61
+ * Walks the full relationship graph: if A→B→C, triggering from A
62
+ * includes B and C (and anything reachable from them).
63
+ */
64
+ private findRelatedObjects;
65
+ /**
66
+ * Discover all object directories under the objects root.
67
+ * Uses the platform fileSystem API.
68
+ */
69
+ private discoverObjects;
70
+ /**
71
+ * Parse a single object and its fields into an ObjectNode + RelationshipEdge[].
72
+ */
73
+ private parseObject;
74
+ /**
75
+ * Parse a single field XML element into a SchemaField and optional RelationshipEdge.
76
+ */
77
+ private parseFieldXml;
78
+ private categorizeParseError;
79
+ }
80
+ //# sourceMappingURL=SchemaParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaParser.d.ts","sourceRoot":"","sources":["../../../src/plugins/schema/SchemaParser.ts"],"names":[],"mappings":"AAAA;;;;;IAKI;AAEJ;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAGhB,WAAW,EAGZ,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAA6C,MAAM,0BAA0B,CAAC;AASpG,qBAAa,YAAa,YAAW,eAAe,CAAC,aAAa,CAAC;IACjE,OAAO,CAAC,eAAe,CAAC,CAAmB;IAC3C,OAAO,CAAC,SAAS,CAAC,CAAa;IAE/B,QAAQ,CAAC,SAAS,sBAA6B;IAE/C,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAKnD;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IA2HhF;;OAEG;IACH,QAAQ,CAAC,CAAC,IAAI,EAAE,aAAa,GAAG,gBAAgB;IAyBhD;;;OAGG;IACG,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmDnE;;OAEG;IACH,OAAO,CAAC,eAAe;IAqBvB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAa5B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAuC1B;;;OAGG;YACW,eAAe;IA6D7B;;OAEG;YACW,WAAW;IAqGzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAqDrB,OAAO,CAAC,oBAAoB;CAiB7B"}
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ **/
7
+ /**
8
+ * Schema Parser
9
+ *
10
+ * Parses Salesforce SFDX decomposed source format (.object-meta.xml and .field-meta.xml)
11
+ * into a SchemaERDData structure for interactive ERD visualization.
12
+ *
13
+ * Unlike the ObjectParser which parses a single object at a time, the SchemaParser
14
+ * discovers and parses ALL objects in the project to build a complete ERD graph
15
+ * with resolved cross-object relationships.
16
+ *
17
+ * Supports:
18
+ * - Decomposed source format (separate .field-meta.xml files in fields/ subdirectory)
19
+ * - Inline fields in .object-meta.xml (flat format)
20
+ * - Standard objects (Account, Asset, etc.) and custom objects (*__c)
21
+ * - Lookup and MasterDetail relationship resolution
22
+ * - Full transitive BFS traversal of related objects
23
+ * - Platform limit validation (max 2 Master-Detail per object)
24
+ *
25
+ * @author Schema Team
26
+ */
27
+ import { SalesforceXMLParser, ErrorBuilder, ErrorCategory, } from '@salesforce/metadata-core-sdk';
28
+ export class SchemaParser {
29
+ constructor() {
30
+ this.xmlParser = new SalesforceXMLParser();
31
+ }
32
+ setPlatformContext(context) {
33
+ this.platformContext = context;
34
+ this.telemetry = context.telemetry.createChild({ component: 'SchemaParser' });
35
+ }
36
+ /**
37
+ * Parse all objects in the project starting from the given file path.
38
+ * Discovers all sibling objects, parses their fields, resolves relationships,
39
+ * and returns a complete SchemaERDData structure.
40
+ */
41
+ async parse(filePath, _versionInfo) {
42
+ if (!this.platformContext) {
43
+ throw ErrorBuilder.create()
44
+ .category(ErrorCategory.Configuration)
45
+ .message('SchemaParser not initialized with platform context')
46
+ .build();
47
+ }
48
+ return this.telemetry.withSpan('schema.parse', async (span) => {
49
+ const logger = this.platformContext.logger.createChild('SchemaParser');
50
+ const triggerFileName = this.platformContext.fileSystem.basename(filePath);
51
+ logger.info(`Parsing schema from trigger file: ${filePath}`);
52
+ span.setAttribute('triggerFile', triggerFileName);
53
+ try {
54
+ const triggerFilePath = filePath;
55
+ const objectsRoot = this.findObjectsRoot(triggerFilePath);
56
+ if (!objectsRoot) {
57
+ throw ErrorBuilder.create()
58
+ .category(ErrorCategory.Dependency)
59
+ .message(`Cannot determine objects root from path: ${triggerFileName}`)
60
+ .context({ filePath })
61
+ .build();
62
+ }
63
+ logger.info(`Objects root: ${objectsRoot}`);
64
+ const discovered = await this.discoverObjects(objectsRoot, logger);
65
+ logger.info(`Discovered ${discovered.length} objects`);
66
+ this.telemetry.sendEvent('schema.discovery_complete', {
67
+ pluginId: 'schema',
68
+ }, {
69
+ discoveredObjectCount: discovered.length,
70
+ fieldOnlyObjectCount: discovered.filter((d) => !d.objectMetaPath).length,
71
+ });
72
+ const allObjects = [];
73
+ const allRelationships = [];
74
+ let parseFailures = 0;
75
+ for (const disc of discovered) {
76
+ try {
77
+ const { objectNode, rels } = await this.parseObject(disc, logger);
78
+ allObjects.push(objectNode);
79
+ allRelationships.push(...rels);
80
+ }
81
+ catch (error) {
82
+ parseFailures++;
83
+ logger.error(`Failed to parse ${disc.objectName}:`, error);
84
+ this.telemetry.sendException('schema.object_parse_error', error, {
85
+ pluginId: 'schema',
86
+ objectName: disc.objectName,
87
+ hasObjectMeta: String(!!disc.objectMetaPath),
88
+ });
89
+ }
90
+ }
91
+ const triggerObjectName = this.getTriggerObjectName(triggerFilePath);
92
+ logger.info(`Trigger object: ${triggerObjectName}`);
93
+ span.setAttribute('triggerObject', triggerObjectName);
94
+ const relatedNames = this.findRelatedObjects(triggerObjectName, allObjects, allRelationships, logger);
95
+ const objects = allObjects.filter((o) => relatedNames.has(o.apiName));
96
+ const objectNames = new Set(objects.map((o) => o.apiName));
97
+ const validRelationships = allRelationships.filter((r) => objectNames.has(r.sourceObject) && objectNames.has(r.targetObject));
98
+ logger.info(`Filtered to ${objects.length} related objects (from ${allObjects.length} total), ${validRelationships.length} relationships`);
99
+ span.setAttribute('totalObjectsDiscovered', allObjects.length);
100
+ span.setAttribute('objectsRendered', objects.length);
101
+ span.setAttribute('relationshipsRendered', validRelationships.length);
102
+ span.setAttribute('parseFailures', parseFailures);
103
+ return {
104
+ objects,
105
+ relationships: validRelationships,
106
+ metadata: {
107
+ objectCount: objects.length,
108
+ relationshipCount: validRelationships.length,
109
+ objectsRoot,
110
+ parsedAt: new Date().toISOString(),
111
+ },
112
+ };
113
+ }
114
+ catch (error) {
115
+ logger.error('Schema parsing failed:', error);
116
+ if (typeof error === 'object' &&
117
+ error !== null &&
118
+ 'category' in error &&
119
+ Object.values(ErrorCategory).includes(error.category)) {
120
+ throw error;
121
+ }
122
+ const pluginError = ErrorBuilder.fromError(this.categorizeParseError(error), error, {
123
+ filePath,
124
+ objectName: triggerFileName,
125
+ }).build();
126
+ throw pluginError;
127
+ }
128
+ }, { pluginId: 'schema' });
129
+ }
130
+ /**
131
+ * Validate parsed SchemaERDData against Salesforce platform limits.
132
+ */
133
+ validate(data) {
134
+ const errors = [];
135
+ const warnings = [];
136
+ for (const obj of data.objects) {
137
+ const mdCount = data.relationships.filter((r) => r.sourceObject === obj.apiName && r.type === 'MasterDetail').length;
138
+ if (mdCount > 2) {
139
+ errors.push(`${obj.apiName}: Exceeds max 2 Master-Detail relationships (has ${mdCount})`);
140
+ }
141
+ if (obj.fields.length === 0 && obj.type === 'custom') {
142
+ warnings.push(`${obj.apiName}: Custom object has no custom fields`);
143
+ }
144
+ }
145
+ return {
146
+ valid: errors.length === 0,
147
+ errors,
148
+ warnings,
149
+ };
150
+ }
151
+ /**
152
+ * Get all file paths that this visualization depends on.
153
+ * Used by the framework to trigger re-renders when any of these files change.
154
+ */
155
+ async getVizDependentFilePaths(filePath) {
156
+ if (!this.platformContext) {
157
+ return [filePath];
158
+ }
159
+ const objectsRoot = this.findObjectsRoot(filePath);
160
+ if (!objectsRoot) {
161
+ return [filePath];
162
+ }
163
+ const pfs = this.platformContext.fileSystem;
164
+ const paths = [];
165
+ try {
166
+ const dirResult = await pfs.readDirectory(objectsRoot);
167
+ if (!dirResult.success || !dirResult.data) {
168
+ return [filePath];
169
+ }
170
+ for (const entry of dirResult.data) {
171
+ if (entry.isFile) {
172
+ continue;
173
+ }
174
+ const objDir = pfs.join(objectsRoot, entry.name);
175
+ const objectMetaFile = pfs.join(objDir, `${entry.name}.object-meta.xml`);
176
+ if (await pfs.exists(objectMetaFile)) {
177
+ paths.push(objectMetaFile);
178
+ }
179
+ const fieldsDir = pfs.join(objDir, 'fields');
180
+ if (await pfs.exists(fieldsDir)) {
181
+ const fieldDirResult = await pfs.readDirectory(fieldsDir);
182
+ if (fieldDirResult.success && fieldDirResult.data) {
183
+ for (const fe of fieldDirResult.data) {
184
+ if (fe.isFile && fe.name.endsWith('.field-meta.xml')) {
185
+ paths.push(pfs.join(fieldsDir, fe.name));
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ catch {
193
+ return [filePath];
194
+ }
195
+ return paths;
196
+ }
197
+ // ─── Private Helpers ───────────────────────────────────────────────
198
+ /**
199
+ * Walk up from the given file path to find the "objects" directory.
200
+ */
201
+ findObjectsRoot(filePath) {
202
+ const sep = this.platformContext?.fileSystem.separator ?? '/';
203
+ let dir = filePath;
204
+ if (dir.includes('.')) {
205
+ const lastSep = dir.lastIndexOf(sep);
206
+ if (lastSep >= 0) {
207
+ dir = dir.substring(0, lastSep);
208
+ }
209
+ }
210
+ for (let i = 0; i < 6; i++) {
211
+ const lastSep = dir.lastIndexOf(sep);
212
+ const base = lastSep >= 0 ? dir.substring(lastSep + 1) : dir;
213
+ if (base === 'objects') {
214
+ return dir;
215
+ }
216
+ dir = lastSep >= 0 ? dir.substring(0, lastSep) : dir;
217
+ }
218
+ return null;
219
+ }
220
+ /**
221
+ * Extract the object API name from the clicked file path.
222
+ * e.g. ".../objects/Video__c/Video__c.object-meta.xml" → "Video__c"
223
+ * e.g. ".../objects/Video__c/fields/Status__c.field-meta.xml" → "Video__c"
224
+ */
225
+ getTriggerObjectName(filePath) {
226
+ const sep = this.platformContext?.fileSystem.separator ?? '/';
227
+ const parts = filePath.split(sep);
228
+ const objectsIdx = parts.lastIndexOf('objects');
229
+ if (objectsIdx >= 0 && objectsIdx + 1 < parts.length) {
230
+ return parts[objectsIdx + 1];
231
+ }
232
+ const lastSep = filePath.lastIndexOf(sep);
233
+ const dir = lastSep >= 0 ? filePath.substring(0, lastSep) : filePath;
234
+ const dirLastSep = dir.lastIndexOf(sep);
235
+ return dirLastSep >= 0 ? dir.substring(dirLastSep + 1) : dir;
236
+ }
237
+ /**
238
+ * Find all objects transitively related to the trigger object via BFS.
239
+ * Walks the full relationship graph: if A→B→C, triggering from A
240
+ * includes B and C (and anything reachable from them).
241
+ */
242
+ findRelatedObjects(triggerName, _allObjects, allRelationships, logger) {
243
+ const adjacency = new Map();
244
+ for (const rel of allRelationships) {
245
+ if (!adjacency.has(rel.sourceObject)) {
246
+ adjacency.set(rel.sourceObject, new Set());
247
+ }
248
+ if (!adjacency.has(rel.targetObject)) {
249
+ adjacency.set(rel.targetObject, new Set());
250
+ }
251
+ adjacency.get(rel.sourceObject).add(rel.targetObject);
252
+ adjacency.get(rel.targetObject).add(rel.sourceObject);
253
+ }
254
+ const visited = new Set();
255
+ const queue = [triggerName];
256
+ visited.add(triggerName);
257
+ while (queue.length > 0) {
258
+ const current = queue.shift();
259
+ const neighbors = adjacency.get(current);
260
+ if (neighbors) {
261
+ for (const neighbor of neighbors) {
262
+ if (!visited.has(neighbor)) {
263
+ visited.add(neighbor);
264
+ queue.push(neighbor);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ logger.info(`Related objects for ${triggerName} (full BFS): ${Array.from(visited).join(', ')}`);
270
+ return visited;
271
+ }
272
+ /**
273
+ * Discover all object directories under the objects root.
274
+ * Uses the platform fileSystem API.
275
+ */
276
+ async discoverObjects(objectsRoot, logger) {
277
+ if (!this.platformContext) {
278
+ throw ErrorBuilder.create()
279
+ .category(ErrorCategory.Configuration)
280
+ .message('SchemaParser not initialized with platform context')
281
+ .build();
282
+ }
283
+ const discovered = [];
284
+ // Find all .object-meta.xml files
285
+ const objectFilesResult = await this.platformContext.fileSystem.findFiles('**/*.object-meta.xml');
286
+ if (!objectFilesResult.success || !objectFilesResult.data) {
287
+ logger.error('Error finding object files:', objectFilesResult.error);
288
+ return [];
289
+ }
290
+ // Filter to only files under our objectsRoot
291
+ const objectFiles = objectFilesResult.data.filter((fp) => fp.startsWith(objectsRoot));
292
+ // Find all .field-meta.xml files
293
+ const fieldFilesResult = await this.platformContext.fileSystem.findFiles('**/*.field-meta.xml');
294
+ const allFieldFiles = fieldFilesResult.success && fieldFilesResult.data
295
+ ? fieldFilesResult.data.filter((fp) => fp.startsWith(objectsRoot))
296
+ : [];
297
+ const pfs = this.platformContext.fileSystem;
298
+ const discoveredNames = new Set();
299
+ for (const objectMetaPath of objectFiles) {
300
+ const objectDir = pfs.dirname(objectMetaPath);
301
+ const objectName = pfs.basename(objectDir);
302
+ const fieldsDir = pfs.join(objectDir, 'fields');
303
+ const fieldMetaPaths = allFieldFiles.filter((fp) => fp.startsWith(fieldsDir + pfs.separator));
304
+ discovered.push({ objectName, objectMetaPath, fieldMetaPaths });
305
+ discoveredNames.add(objectName);
306
+ }
307
+ // Standard objects (Account, Contact, etc.) often have only a fields/
308
+ // subdirectory with no .object-meta.xml in SFDX source. Discover those
309
+ // by finding field files whose parent object wasn't already discovered.
310
+ for (const fieldPath of allFieldFiles) {
311
+ const fieldsDir = pfs.dirname(fieldPath);
312
+ const objectDir = pfs.dirname(fieldsDir);
313
+ const objectName = pfs.basename(objectDir);
314
+ if (discoveredNames.has(objectName) || pfs.basename(fieldsDir) !== 'fields') {
315
+ continue;
316
+ }
317
+ const fieldMetaPaths = allFieldFiles.filter((fp) => fp.startsWith(fieldsDir + pfs.separator));
318
+ discovered.push({ objectName, fieldMetaPaths });
319
+ discoveredNames.add(objectName);
320
+ logger.info(`Discovered field-only object (no .object-meta.xml): ${objectName}`);
321
+ }
322
+ return discovered;
323
+ }
324
+ /**
325
+ * Parse a single object and its fields into an ObjectNode + RelationshipEdge[].
326
+ */
327
+ async parseObject(disc, logger) {
328
+ if (!this.platformContext) {
329
+ throw ErrorBuilder.create()
330
+ .category(ErrorCategory.Configuration)
331
+ .message('SchemaParser not initialized with platform context')
332
+ .build();
333
+ }
334
+ const { objectName, objectMetaPath, fieldMetaPaths } = disc;
335
+ const isCustom = objectName.endsWith('__c');
336
+ logger.info(`Parsing: ${objectName} (${isCustom ? 'custom' : 'standard'})`);
337
+ let customObject = {};
338
+ let nameFieldType = 'Text';
339
+ if (objectMetaPath) {
340
+ const readResult = await this.platformContext.fileSystem.readFile(objectMetaPath);
341
+ if (!readResult.success || !readResult.data) {
342
+ const msg = readResult.error?.message ?? 'Unknown error';
343
+ throw ErrorBuilder.create()
344
+ .category(ErrorCategory.Dependency)
345
+ .message(`Failed to read file: ${msg}`)
346
+ .context({ filePath: objectMetaPath, objectName })
347
+ .build();
348
+ }
349
+ const parsed = this.xmlParser.parse(readResult.data);
350
+ customObject = parsed.CustomObject || {};
351
+ if (customObject.nameField?.type === 'AutoNumber') {
352
+ nameFieldType = 'AutoNumber';
353
+ }
354
+ }
355
+ const fields = [];
356
+ const rels = [];
357
+ let inlineFields = customObject.fields;
358
+ if (inlineFields) {
359
+ if (!Array.isArray(inlineFields)) {
360
+ inlineFields = [inlineFields];
361
+ }
362
+ for (const f of inlineFields) {
363
+ const { field, rel } = this.parseFieldXml(f, objectName);
364
+ fields.push(field);
365
+ if (rel) {
366
+ rels.push(rel);
367
+ }
368
+ }
369
+ }
370
+ // Parse decomposed .field-meta.xml files
371
+ for (const fieldPath of fieldMetaPaths) {
372
+ try {
373
+ const fieldReadResult = await this.platformContext.fileSystem.readFile(fieldPath);
374
+ if (!fieldReadResult.success || !fieldReadResult.data) {
375
+ logger.warn(`Failed to read field file: ${fieldPath}`);
376
+ continue;
377
+ }
378
+ const fieldParsed = this.xmlParser.parse(fieldReadResult.data);
379
+ const fieldXml = fieldParsed.CustomField;
380
+ if (!fieldXml) {
381
+ logger.warn(`No CustomField root in ${fieldPath}`);
382
+ continue;
383
+ }
384
+ if (!fieldXml.fullName) {
385
+ fieldXml.fullName = this.platformContext.fileSystem
386
+ .basename(fieldPath)
387
+ .replace('.field-meta.xml', '');
388
+ }
389
+ const { field, rel } = this.parseFieldXml(fieldXml, objectName);
390
+ fields.push(field);
391
+ if (rel) {
392
+ rels.push(rel);
393
+ }
394
+ }
395
+ catch (error) {
396
+ logger.error(`Error parsing field ${fieldPath}:`, error);
397
+ this.telemetry?.sendException('schema.field_parse_error', error, {
398
+ pluginId: 'schema',
399
+ objectName,
400
+ fieldFile: this.platformContext.fileSystem.basename(fieldPath),
401
+ });
402
+ }
403
+ }
404
+ const objectNode = {
405
+ apiName: objectName,
406
+ label: customObject.label || objectName,
407
+ type: isCustom ? 'custom' : 'standard',
408
+ nameFieldType,
409
+ fields,
410
+ };
411
+ return { objectNode, rels };
412
+ }
413
+ /**
414
+ * Parse a single field XML element into a SchemaField and optional RelationshipEdge.
415
+ */
416
+ parseFieldXml(fieldXml, objectName) {
417
+ const apiName = fieldXml.fullName || '';
418
+ const rawType = fieldXml.type || 'Text';
419
+ const isRelationship = rawType === 'Lookup' || rawType === 'MasterDetail';
420
+ const referenceTo = fieldXml.referenceTo;
421
+ // Build display type string
422
+ let dataType = rawType;
423
+ if (rawType === 'Text' && fieldXml.length) {
424
+ dataType = `Text(${fieldXml.length})`;
425
+ }
426
+ else if (rawType === 'Html' && fieldXml.length) {
427
+ dataType = `Html(${fieldXml.length})`;
428
+ }
429
+ else if (rawType === 'Number') {
430
+ const p = fieldXml.precision || '';
431
+ const s = fieldXml.scale || '';
432
+ if (p || s) {
433
+ dataType = `Number(${p},${s})`;
434
+ }
435
+ }
436
+ const field = {
437
+ apiName,
438
+ label: fieldXml.label || apiName,
439
+ dataType,
440
+ isRelationship,
441
+ referenceTo: isRelationship ? referenceTo : undefined,
442
+ relationshipType: isRelationship ? rawType : undefined,
443
+ description: fieldXml.description,
444
+ helpText: fieldXml.inlineHelpText,
445
+ required: fieldXml.required === 'true' || fieldXml.required === true || rawType === 'MasterDetail',
446
+ length: fieldXml.length ? parseInt(fieldXml.length, 10) : undefined,
447
+ };
448
+ let rel = null;
449
+ if (isRelationship && referenceTo) {
450
+ rel = {
451
+ sourceObject: objectName,
452
+ targetObject: referenceTo,
453
+ fieldApiName: apiName,
454
+ type: rawType,
455
+ };
456
+ }
457
+ return { field, rel };
458
+ }
459
+ categorizeParseError(error) {
460
+ if (error instanceof Error) {
461
+ const message = error.message.toLowerCase();
462
+ if (message.includes('xml') || message.includes('parse')) {
463
+ return ErrorCategory.Parsing;
464
+ }
465
+ if (message.includes('not found') || message.includes('enoent')) {
466
+ return ErrorCategory.Dependency;
467
+ }
468
+ if (message.includes('permission') || message.includes('eacces')) {
469
+ return ErrorCategory.Permission;
470
+ }
471
+ }
472
+ return ErrorCategory.Runtime;
473
+ }
474
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ **/
7
+ /**
8
+ * Schema Visualizer Plugin
9
+ *
10
+ * Handles .object-meta.xml and .field-meta.xml files and displays an interactive
11
+ * ERD (Entity Relationship Diagram) using React Flow.
12
+ *
13
+ * This plugin discovers ALL objects in the project, parses their fields and
14
+ * relationships, and renders a full ERD graph with:
15
+ * - Color-coded object cards (blue=standard, purple=custom)
16
+ * - Crow's foot notation for relationships
17
+ * - Solid lines for Master-Detail, dashed for Lookup
18
+ * - "NEW" badges for custom objects/fields
19
+ * - Auto-layout with dagre, zoom/pan, minimap
20
+ *
21
+ * @author Schema Team
22
+ */
23
+ import { IVisualizationPlugin, MetadataPluginInfo, IMetadataParser, IPlatformContext } from '@salesforce/metadata-core-sdk';
24
+ import { SchemaERDData } from './types/SchemaERDData.js';
25
+ /**
26
+ * SchemaVisualizer implementation for Salesforce object metadata ERD
27
+ */
28
+ export declare class SchemaVisualizer implements IVisualizationPlugin<SchemaERDData> {
29
+ private readonly pluginMetadata;
30
+ readonly parser: IMetadataParser<SchemaERDData>;
31
+ private logger?;
32
+ constructor();
33
+ /**
34
+ * Get plugin metadata information.
35
+ * Framework calls this during plugin registration.
36
+ */
37
+ getMetadataPluginInfo(): MetadataPluginInfo;
38
+ canHandle(filePath: string): boolean;
39
+ initialize(context: IPlatformContext): Promise<void>;
40
+ dispose?(): void;
41
+ }
42
+ //# sourceMappingURL=SchemaVisualizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SchemaVisualizer.d.ts","sourceRoot":"","sources":["../../../src/plugins/schema/SchemaVisualizer.ts"],"names":[],"mappings":"AAAA;;;;;IAKI;AAEJ;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EACL,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD;;GAEG;AACH,qBAAa,gBAAiB,YAAW,oBAAoB,CAAC,aAAa,CAAC;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAO7B;IAEF,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;IAEhD,OAAO,CAAC,MAAM,CAAC,CAAU;;IAMzB;;;OAGG;IACH,qBAAqB,IAAI,kBAAkB;IAI3C,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO;IAS9B,UAAU,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1D,OAAO,CAAC,IAAI,IAAI;CAGjB"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Copyright (c) 2025, Salesforce, Inc.
3
+ * All rights reserved.
4
+ * Licensed under the BSD 3-Clause license.
5
+ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ **/
7
+ import { SchemaParser } from './SchemaParser.js';
8
+ /**
9
+ * SchemaVisualizer implementation for Salesforce object metadata ERD
10
+ */
11
+ export class SchemaVisualizer {
12
+ constructor() {
13
+ this.pluginMetadata = {
14
+ id: 'schema',
15
+ name: 'Salesforce Schema (Data Model) Visualizer',
16
+ description: 'Interactive ERD visualization of Salesforce objects, fields, and relationships',
17
+ author: 'Schema Team',
18
+ filePatterns: ['.object-meta.xml', '.field-meta.xml'],
19
+ priority: 10,
20
+ };
21
+ this.parser = new SchemaParser();
22
+ }
23
+ /**
24
+ * Get plugin metadata information.
25
+ * Framework calls this during plugin registration.
26
+ */
27
+ getMetadataPluginInfo() {
28
+ return this.pluginMetadata;
29
+ }
30
+ canHandle(filePath) {
31
+ return this.getMetadataPluginInfo().filePatterns.some((pattern) => {
32
+ if (pattern.startsWith('*')) {
33
+ return filePath.endsWith(pattern.slice(1));
34
+ }
35
+ return filePath === pattern || filePath.endsWith(pattern);
36
+ });
37
+ }
38
+ async initialize(context) {
39
+ this.logger = context.logger.createChild(this.getMetadataPluginInfo().id);
40
+ this.logger.info('Schema visualizer initialized');
41
+ this.parser.setPlatformContext(context);
42
+ }
43
+ dispose() {
44
+ this.logger?.info(`Disposing ${this.getMetadataPluginInfo().name}`);
45
+ }
46
+ }