@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.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 (78) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +1 -1
  3. package/dist/assets/index-DD9fzkbz.js +232 -0
  4. package/dist/index.html +70 -0
  5. package/index.html +0 -1
  6. package/npm-shrinkwrap.json +6329 -0
  7. package/package.json +95 -51
  8. package/CHANGELOG.md +0 -120
  9. package/eslint.config.js +0 -23
  10. package/public/vite.svg +0 -1
  11. package/src/app/AppShell.tsx +0 -1733
  12. package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
  13. package/src/assets/react.svg +0 -1
  14. package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
  15. package/src/features/fetchxml/api/excelExport.ts +0 -318
  16. package/src/features/fetchxml/api/formattedValues.ts +0 -107
  17. package/src/features/fetchxml/api/pptbClient.ts +0 -2635
  18. package/src/features/fetchxml/model/displaySettings.ts +0 -18
  19. package/src/features/fetchxml/model/fetchxml.ts +0 -346
  20. package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
  21. package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
  22. package/src/features/fetchxml/model/layoutxml.ts +0 -372
  23. package/src/features/fetchxml/model/nodes.ts +0 -255
  24. package/src/features/fetchxml/model/operators.ts +0 -624
  25. package/src/features/fetchxml/model/treeUtils.ts +0 -174
  26. package/src/features/fetchxml/state/builderStore.tsx +0 -1160
  27. package/src/features/fetchxml/state/cache.ts +0 -393
  28. package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
  29. package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
  30. package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
  31. package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
  32. package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
  33. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
  34. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
  35. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
  36. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
  37. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
  38. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
  39. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
  40. package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
  41. package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
  42. package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
  43. package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
  44. package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
  45. package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
  46. package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
  47. package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
  48. package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
  49. package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
  50. package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
  51. package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
  52. package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
  53. package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
  54. package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
  55. package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
  56. package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
  57. package/src/main.tsx +0 -11
  58. package/src/shared/components/AttributePicker.tsx +0 -138
  59. package/src/shared/components/BetweenInputPicker.tsx +0 -120
  60. package/src/shared/components/BooleanValuePicker.tsx +0 -165
  61. package/src/shared/components/DateTimeInputPicker.tsx +0 -170
  62. package/src/shared/components/DynamicValueList.tsx +0 -128
  63. package/src/shared/components/ExcelIcon.tsx +0 -30
  64. package/src/shared/components/MultiValuePicker.tsx +0 -178
  65. package/src/shared/components/NumericInputPicker.tsx +0 -156
  66. package/src/shared/components/OptionSetValuePicker.tsx +0 -150
  67. package/src/shared/components/RelationshipPicker.tsx +0 -268
  68. package/src/shared/contexts/ThemeContext.tsx +0 -19
  69. package/src/shared/hooks/useAccessMode.ts +0 -77
  70. package/src/shared/hooks/useLazyMetadata.ts +0 -132
  71. package/src/shared/hooks/usePptbContext.ts +0 -170
  72. package/src/shared/hooks/usePublisherFilter.ts +0 -214
  73. package/src/shared/hooks/useSolutionFilter.ts +0 -162
  74. package/src/shared/utils/debug.ts +0 -87
  75. package/tsconfig.app.json +0 -28
  76. package/tsconfig.json +0 -7
  77. package/tsconfig.node.json +0 -26
  78. package/vite.config.ts +0 -7
@@ -1,708 +0,0 @@
1
- /**
2
- * FetchXML Parser - Convert FetchXML string to FetchNode tree
3
- * Strict XML syntax validation, lenient schema validation (warnings)
4
- */
5
-
6
- import type {
7
- FetchNode,
8
- EntityNode,
9
- AttributeNode,
10
- AllAttributesNode,
11
- OrderNode,
12
- FilterNode,
13
- ConditionNode,
14
- LinkEntityNode,
15
- LinkType,
16
- OperatorType,
17
- NodeId,
18
- } from "./nodes";
19
-
20
- // ID generator for parsed nodes
21
- let parseIdCounter = 0;
22
- const generateParseId = (): NodeId => `parsed_${++parseIdCounter}`;
23
-
24
- /**
25
- * Reset the ID counter (useful for testing)
26
- */
27
- export function resetParserIdCounter(): void {
28
- parseIdCounter = 0;
29
- }
30
-
31
- /**
32
- * Parsing result with optional warnings
33
- */
34
- export interface ParseResult {
35
- success: boolean;
36
- fetchNode?: FetchNode;
37
- errors: ParseError[];
38
- warnings: ParseWarning[];
39
- }
40
-
41
- export interface ParseError {
42
- message: string;
43
- line?: number;
44
- column?: number;
45
- }
46
-
47
- export interface ParseWarning {
48
- message: string;
49
- element?: string;
50
- attribute?: string;
51
- }
52
-
53
- /**
54
- * Main entry point: Parse FetchXML string to FetchNode
55
- */
56
- export function parseFetchXml(xmlString: string): ParseResult {
57
- const errors: ParseError[] = [];
58
- const warnings: ParseWarning[] = [];
59
-
60
- // Validate input
61
- if (!xmlString || typeof xmlString !== "string") {
62
- return {
63
- success: false,
64
- errors: [{ message: "FetchXML string is required" }],
65
- warnings: [],
66
- };
67
- }
68
-
69
- const trimmedXml = xmlString.trim();
70
- if (!trimmedXml) {
71
- return {
72
- success: false,
73
- errors: [{ message: "FetchXML string is empty" }],
74
- warnings: [],
75
- };
76
- }
77
-
78
- // Parse XML using DOMParser
79
- let doc: Document;
80
- try {
81
- const parser = new DOMParser();
82
- doc = parser.parseFromString(trimmedXml, "application/xml");
83
-
84
- // Check for parsing errors
85
- const parserError = doc.querySelector("parsererror");
86
- if (parserError) {
87
- // Extract error message from parsererror element
88
- const errorText = parserError.textContent || "Invalid XML syntax";
89
- return {
90
- success: false,
91
- errors: [{ message: `XML Parsing Error: ${errorText}` }],
92
- warnings: [],
93
- };
94
- }
95
- } catch (e) {
96
- return {
97
- success: false,
98
- errors: [{ message: `XML Parsing Error: ${e instanceof Error ? e.message : String(e)}` }],
99
- warnings: [],
100
- };
101
- }
102
-
103
- // Validate root element is <fetch>
104
- const fetchElement = doc.documentElement;
105
- if (!fetchElement || fetchElement.nodeName.toLowerCase() !== "fetch") {
106
- return {
107
- success: false,
108
- errors: [
109
- { message: `Root element must be <fetch>, found <${fetchElement?.nodeName || "none"}>` },
110
- ],
111
- warnings: [],
112
- };
113
- }
114
-
115
- // Find the entity element (required)
116
- const entityElement = fetchElement.querySelector(":scope > entity");
117
- if (!entityElement) {
118
- return {
119
- success: false,
120
- errors: [{ message: "Missing required <entity> element inside <fetch>" }],
121
- warnings: [],
122
- };
123
- }
124
-
125
- // Validate entity has a name attribute
126
- const entityName = entityElement.getAttribute("name");
127
- if (!entityName) {
128
- return {
129
- success: false,
130
- errors: [{ message: "Entity element must have a 'name' attribute" }],
131
- warnings: [],
132
- };
133
- }
134
-
135
- try {
136
- // Parse the fetch options
137
- const options = parseFetchOptions(fetchElement, warnings);
138
-
139
- // Parse the entity tree
140
- const entity = parseEntity(entityElement, warnings);
141
-
142
- const fetchNode: FetchNode = {
143
- id: generateParseId(),
144
- type: "fetch",
145
- entity,
146
- options,
147
- };
148
-
149
- return {
150
- success: true,
151
- fetchNode,
152
- errors,
153
- warnings,
154
- };
155
- } catch (e) {
156
- return {
157
- success: false,
158
- errors: [{ message: `Parsing Error: ${e instanceof Error ? e.message : String(e)}` }],
159
- warnings,
160
- };
161
- }
162
- }
163
-
164
- /**
165
- * Parse fetch element attributes into options
166
- */
167
- function parseFetchOptions(fetchElement: Element, warnings: ParseWarning[]): FetchNode["options"] {
168
- const options: FetchNode["options"] = {};
169
-
170
- // Boolean options
171
- if (fetchElement.getAttribute("aggregate") === "true") {
172
- options.aggregate = true;
173
- }
174
- if (fetchElement.getAttribute("distinct") === "true") {
175
- options.distinct = true;
176
- }
177
- if (fetchElement.getAttribute("returntotalrecordcount") === "true") {
178
- options.returnTotalRecordCount = true;
179
- }
180
- if (fetchElement.getAttribute("no-lock") === "true") {
181
- options.noLock = true;
182
- }
183
- if (fetchElement.getAttribute("latematerialize") === "true") {
184
- options.latematerialize = true;
185
- }
186
-
187
- // Numeric options
188
- const top = fetchElement.getAttribute("top");
189
- if (top !== null) {
190
- const topNum = parseInt(top, 10);
191
- if (!isNaN(topNum)) {
192
- options.top = topNum;
193
- } else {
194
- warnings.push({ message: `Invalid 'top' value: ${top}`, element: "fetch", attribute: "top" });
195
- }
196
- }
197
-
198
- const count = fetchElement.getAttribute("count");
199
- if (count !== null) {
200
- const countNum = parseInt(count, 10);
201
- if (!isNaN(countNum)) {
202
- options.count = countNum;
203
- } else {
204
- warnings.push({
205
- message: `Invalid 'count' value: ${count}`,
206
- element: "fetch",
207
- attribute: "count",
208
- });
209
- }
210
- }
211
-
212
- const page = fetchElement.getAttribute("page");
213
- if (page !== null) {
214
- const pageNum = parseInt(page, 10);
215
- if (!isNaN(pageNum)) {
216
- options.page = pageNum;
217
- } else {
218
- warnings.push({
219
- message: `Invalid 'page' value: ${page}`,
220
- element: "fetch",
221
- attribute: "page",
222
- });
223
- }
224
- }
225
-
226
- const utcOffset = fetchElement.getAttribute("utc-offset");
227
- if (utcOffset !== null) {
228
- const utcNum = parseInt(utcOffset, 10);
229
- if (!isNaN(utcNum)) {
230
- options.utcOffset = utcNum;
231
- } else {
232
- warnings.push({
233
- message: `Invalid 'utc-offset' value: ${utcOffset}`,
234
- element: "fetch",
235
- attribute: "utc-offset",
236
- });
237
- }
238
- }
239
-
240
- // String options
241
- const pagingCookie = fetchElement.getAttribute("paging-cookie");
242
- if (pagingCookie) {
243
- options.pagingCookie = pagingCookie;
244
- }
245
-
246
- // Check for unknown attributes (schema warnings)
247
- const knownFetchAttrs = new Set([
248
- "aggregate",
249
- "distinct",
250
- "top",
251
- "count",
252
- "page",
253
- "paging-cookie",
254
- "returntotalrecordcount",
255
- "no-lock",
256
- "utc-offset",
257
- "latematerialize",
258
- "version",
259
- "mapping",
260
- "output-format",
261
- "min-active-row-version",
262
- "datasource",
263
- "options",
264
- ]);
265
-
266
- for (const attr of fetchElement.attributes) {
267
- if (!knownFetchAttrs.has(attr.name.toLowerCase())) {
268
- warnings.push({
269
- message: `Unknown attribute '${attr.name}' on <fetch> element`,
270
- element: "fetch",
271
- attribute: attr.name,
272
- });
273
- }
274
- }
275
-
276
- return options;
277
- }
278
-
279
- /**
280
- * Parse entity element
281
- */
282
- function parseEntity(entityElement: Element, warnings: ParseWarning[]): EntityNode {
283
- const name = entityElement.getAttribute("name") || "";
284
-
285
- const entity: EntityNode = {
286
- id: generateParseId(),
287
- type: "entity",
288
- name,
289
- attributes: [],
290
- orders: [],
291
- filters: [],
292
- links: [],
293
- };
294
-
295
- // Parse optional entity attributes
296
- if (entityElement.getAttribute("enableprefiltering") === "true") {
297
- entity.enablePrefiltering = true;
298
- }
299
- const prefilterParam = entityElement.getAttribute("prefilterparametername");
300
- if (prefilterParam) {
301
- entity.prefilterParameterName = prefilterParam;
302
- }
303
-
304
- // Parse child elements
305
- for (const child of entityElement.children) {
306
- const tagName = child.nodeName.toLowerCase();
307
-
308
- switch (tagName) {
309
- case "all-attributes":
310
- entity.allAttributes = parseAllAttributes(child, warnings);
311
- break;
312
- case "attribute":
313
- entity.attributes.push(parseAttribute(child, warnings));
314
- break;
315
- case "order":
316
- entity.orders.push(parseOrder(child, warnings));
317
- break;
318
- case "filter":
319
- entity.filters.push(parseFilter(child, warnings));
320
- break;
321
- case "link-entity":
322
- entity.links.push(parseLinkEntity(child, warnings));
323
- break;
324
- default:
325
- warnings.push({
326
- message: `Unknown element <${tagName}> inside <entity>`,
327
- element: tagName,
328
- });
329
- }
330
- }
331
-
332
- return entity;
333
- }
334
-
335
- /**
336
- * Parse all-attributes element
337
- * Note: Parameters reserved for future use (potential warnings for deprecated usage)
338
- */
339
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
340
- function parseAllAttributes(_element: Element, _warnings: ParseWarning[]): AllAttributesNode {
341
- return {
342
- id: generateParseId(),
343
- type: "all-attributes",
344
- enabled: true,
345
- };
346
- }
347
-
348
- /**
349
- * Parse attribute element
350
- */
351
- function parseAttribute(attrElement: Element, warnings: ParseWarning[]): AttributeNode {
352
- const name = attrElement.getAttribute("name") || "";
353
-
354
- if (!name) {
355
- warnings.push({
356
- message: "Attribute element missing 'name'",
357
- element: "attribute",
358
- });
359
- }
360
-
361
- const attr: AttributeNode = {
362
- id: generateParseId(),
363
- type: "attribute",
364
- name,
365
- };
366
-
367
- // Optional attributes
368
- const alias = attrElement.getAttribute("alias");
369
- if (alias) attr.alias = alias;
370
-
371
- if (attrElement.getAttribute("groupby") === "true") {
372
- attr.groupby = true;
373
- }
374
-
375
- const aggregate = attrElement.getAttribute("aggregate");
376
- if (aggregate) {
377
- const validAggregates = ["sum", "count", "countcolumn", "min", "max", "avg", "rowaggregate"];
378
- if (validAggregates.includes(aggregate)) {
379
- attr.aggregate = aggregate as AttributeNode["aggregate"];
380
- } else {
381
- warnings.push({
382
- message: `Invalid aggregate value '${aggregate}'`,
383
- element: "attribute",
384
- attribute: "aggregate",
385
- });
386
- }
387
- }
388
-
389
- const dategrouping = attrElement.getAttribute("dategrouping");
390
- if (dategrouping) {
391
- const validDateGroupings = [
392
- "day",
393
- "week",
394
- "month",
395
- "quarter",
396
- "year",
397
- "fiscal-period",
398
- "fiscal-year",
399
- ];
400
- if (validDateGroupings.includes(dategrouping)) {
401
- attr.dategrouping = dategrouping as AttributeNode["dategrouping"];
402
- } else {
403
- warnings.push({
404
- message: `Invalid dategrouping value '${dategrouping}'`,
405
- element: "attribute",
406
- attribute: "dategrouping",
407
- });
408
- }
409
- }
410
-
411
- if (attrElement.hasAttribute("usertimezone")) {
412
- attr.usertimezone = attrElement.getAttribute("usertimezone") === "true";
413
- }
414
-
415
- return attr;
416
- }
417
-
418
- /**
419
- * Parse order element
420
- */
421
- function parseOrder(orderElement: Element, warnings: ParseWarning[]): OrderNode {
422
- const attribute = orderElement.getAttribute("attribute") || "";
423
-
424
- if (!attribute) {
425
- warnings.push({
426
- message: "Order element missing 'attribute'",
427
- element: "order",
428
- });
429
- }
430
-
431
- const order: OrderNode = {
432
- id: generateParseId(),
433
- type: "order",
434
- attribute,
435
- };
436
-
437
- if (orderElement.getAttribute("descending") === "true") {
438
- order.descending = true;
439
- }
440
-
441
- const entityname = orderElement.getAttribute("entityname");
442
- if (entityname) {
443
- order.entityname = entityname;
444
- }
445
-
446
- return order;
447
- }
448
-
449
- /**
450
- * Parse filter element (recursive for subfilters)
451
- */
452
- function parseFilter(filterElement: Element, warnings: ParseWarning[]): FilterNode {
453
- const type = filterElement.getAttribute("type") || "and";
454
-
455
- const filter: FilterNode = {
456
- id: generateParseId(),
457
- type: "filter",
458
- conjunction: type === "or" ? "or" : "and",
459
- conditions: [],
460
- subfilters: [],
461
- links: [],
462
- };
463
-
464
- const hint = filterElement.getAttribute("hint");
465
- if (hint === "union") {
466
- filter.hint = "union";
467
- }
468
-
469
- // Parse child elements
470
- for (const child of filterElement.children) {
471
- const tagName = child.nodeName.toLowerCase();
472
-
473
- switch (tagName) {
474
- case "condition":
475
- filter.conditions.push(parseCondition(child, warnings));
476
- break;
477
- case "filter":
478
- filter.subfilters.push(parseFilter(child, warnings));
479
- break;
480
- case "link-entity":
481
- // Parse link-entity inside filter (for any/not any/all/not all)
482
- filter.links.push(parseLinkEntity(child, warnings));
483
- break;
484
- default:
485
- warnings.push({
486
- message: `Unknown element <${tagName}> inside <filter>`,
487
- element: tagName,
488
- });
489
- }
490
- }
491
-
492
- return filter;
493
- }
494
-
495
- /**
496
- * Parse condition element
497
- */
498
- function parseCondition(condElement: Element, warnings: ParseWarning[]): ConditionNode {
499
- const attribute = condElement.getAttribute("attribute") || "";
500
- const operator = condElement.getAttribute("operator") || "eq";
501
-
502
- if (!attribute) {
503
- warnings.push({
504
- message: "Condition element missing 'attribute'",
505
- element: "condition",
506
- });
507
- }
508
-
509
- const condition: ConditionNode = {
510
- id: generateParseId(),
511
- type: "condition",
512
- attribute,
513
- operator: operator as OperatorType,
514
- };
515
-
516
- // Optional attributes
517
- const entityname = condElement.getAttribute("entityname");
518
- if (entityname) condition.entityname = entityname;
519
-
520
- const valueof = condElement.getAttribute("valueof");
521
- if (valueof) condition.valueof = valueof;
522
-
523
- const aggregate = condElement.getAttribute("aggregate");
524
- if (aggregate) {
525
- const validAggregates = ["sum", "count", "countcolumn", "min", "max", "avg"];
526
- if (validAggregates.includes(aggregate)) {
527
- condition.aggregate = aggregate as ConditionNode["aggregate"];
528
- }
529
- }
530
-
531
- // Parse value - can be from attribute or child <value> elements
532
- const valueAttr = condElement.getAttribute("value");
533
- const valueElements = condElement.querySelectorAll(":scope > value");
534
-
535
- if (valueElements.length > 0) {
536
- // Multiple value elements (for in, not-in, between, etc.)
537
- const values: (string | number)[] = [];
538
- for (const valEl of valueElements) {
539
- const valText = valEl.textContent?.trim() || "";
540
- // Try to parse as number if it looks like one
541
- const numVal = parseFloat(valText);
542
- if (!isNaN(numVal) && valText === numVal.toString()) {
543
- values.push(numVal);
544
- } else {
545
- values.push(valText);
546
- }
547
- }
548
- condition.value = values;
549
- } else if (valueAttr !== null) {
550
- // Single value attribute
551
- const numVal = parseFloat(valueAttr);
552
- if (!isNaN(numVal) && valueAttr === numVal.toString()) {
553
- condition.value = numVal;
554
- } else if (valueAttr === "true") {
555
- condition.value = true;
556
- } else if (valueAttr === "false") {
557
- condition.value = false;
558
- } else {
559
- condition.value = valueAttr;
560
- }
561
- }
562
-
563
- return condition;
564
- }
565
-
566
- /**
567
- * Parse link-entity element (recursive)
568
- */
569
- function parseLinkEntity(linkElement: Element, warnings: ParseWarning[]): LinkEntityNode {
570
- const name = linkElement.getAttribute("name") || "";
571
- const from = linkElement.getAttribute("from") || "";
572
- const to = linkElement.getAttribute("to") || "";
573
-
574
- if (!name) {
575
- warnings.push({
576
- message: "Link-entity element missing 'name'",
577
- element: "link-entity",
578
- });
579
- }
580
- if (!from) {
581
- warnings.push({
582
- message: "Link-entity element missing 'from'",
583
- element: "link-entity",
584
- });
585
- }
586
- if (!to) {
587
- warnings.push({
588
- message: "Link-entity element missing 'to'",
589
- element: "link-entity",
590
- });
591
- }
592
-
593
- const linkTypeAttr = linkElement.getAttribute("link-type") || "inner";
594
- const validLinkTypes: LinkType[] = [
595
- "inner",
596
- "outer",
597
- "any",
598
- "not any",
599
- "all",
600
- "not all",
601
- "exists",
602
- "in",
603
- "matchfirstrowusingcrossapply",
604
- ];
605
-
606
- let linkType: LinkType = "inner";
607
- if (validLinkTypes.includes(linkTypeAttr as LinkType)) {
608
- linkType = linkTypeAttr as LinkType;
609
- } else {
610
- warnings.push({
611
- message: `Invalid link-type '${linkTypeAttr}', defaulting to 'inner'`,
612
- element: "link-entity",
613
- attribute: "link-type",
614
- });
615
- }
616
-
617
- const link: LinkEntityNode = {
618
- id: generateParseId(),
619
- type: "link-entity",
620
- name,
621
- from,
622
- to,
623
- linkType,
624
- attributes: [],
625
- orders: [],
626
- filters: [],
627
- links: [],
628
- };
629
-
630
- // Optional attributes
631
- const alias = linkElement.getAttribute("alias");
632
- if (alias) link.alias = alias;
633
-
634
- if (linkElement.getAttribute("intersect") === "true") {
635
- link.intersect = true;
636
- }
637
-
638
- if (linkElement.hasAttribute("visible")) {
639
- link.visible = linkElement.getAttribute("visible") === "true";
640
- }
641
-
642
- // Parse child elements
643
- for (const child of linkElement.children) {
644
- const tagName = child.nodeName.toLowerCase();
645
-
646
- switch (tagName) {
647
- case "all-attributes":
648
- link.allAttributes = parseAllAttributes(child, warnings);
649
- break;
650
- case "attribute":
651
- link.attributes.push(parseAttribute(child, warnings));
652
- break;
653
- case "order":
654
- link.orders.push(parseOrder(child, warnings));
655
- break;
656
- case "filter":
657
- link.filters.push(parseFilter(child, warnings));
658
- break;
659
- case "link-entity":
660
- link.links.push(parseLinkEntity(child, warnings));
661
- break;
662
- default:
663
- warnings.push({
664
- message: `Unknown element <${tagName}> inside <link-entity>`,
665
- element: tagName,
666
- });
667
- }
668
- }
669
-
670
- return link;
671
- }
672
-
673
- /**
674
- * Validate a FetchXML string without fully parsing it
675
- * Quick syntax check
676
- */
677
- export function validateFetchXmlSyntax(xmlString: string): { valid: boolean; error?: string } {
678
- if (!xmlString?.trim()) {
679
- return { valid: false, error: "FetchXML string is empty" };
680
- }
681
-
682
- try {
683
- const parser = new DOMParser();
684
- const doc = parser.parseFromString(xmlString.trim(), "application/xml");
685
- const parserError = doc.querySelector("parsererror");
686
- if (parserError) {
687
- return { valid: false, error: parserError.textContent || "Invalid XML syntax" };
688
- }
689
-
690
- const fetchElement = doc.documentElement;
691
- if (!fetchElement || fetchElement.nodeName.toLowerCase() !== "fetch") {
692
- return { valid: false, error: "Root element must be <fetch>" };
693
- }
694
-
695
- const entityElement = fetchElement.querySelector(":scope > entity");
696
- if (!entityElement) {
697
- return { valid: false, error: "Missing required <entity> element" };
698
- }
699
-
700
- if (!entityElement.getAttribute("name")) {
701
- return { valid: false, error: "Entity element must have a 'name' attribute" };
702
- }
703
-
704
- return { valid: true };
705
- } catch (e) {
706
- return { valid: false, error: e instanceof Error ? e.message : String(e) };
707
- }
708
- }