@mohsinonxrm/pptb-fetchxml-studio 1.0.0 → 1.0.1
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/LICENSE +661 -0
- package/dist/assets/index-DD9fzkbz.js +232 -0
- package/dist/index.html +70 -0
- package/index.html +0 -1
- package/npm-shrinkwrap.json +6329 -0
- package/package.json +95 -51
- package/CHANGELOG.md +0 -120
- package/eslint.config.js +0 -23
- package/public/vite.svg +0 -1
- package/src/app/AppShell.tsx +0 -1733
- package/src/assets/20px-Microsoft_Office_Excel_(2025/342/200/223present).svg.png +0 -0
- package/src/assets/react.svg +0 -1
- package/src/features/fetchxml/api/dataverseMetadata.ts +0 -192
- package/src/features/fetchxml/api/excelExport.ts +0 -318
- package/src/features/fetchxml/api/formattedValues.ts +0 -107
- package/src/features/fetchxml/api/pptbClient.ts +0 -2635
- package/src/features/fetchxml/model/displaySettings.ts +0 -18
- package/src/features/fetchxml/model/fetchxml.ts +0 -346
- package/src/features/fetchxml/model/fetchxmlIntellisense.ts +0 -583
- package/src/features/fetchxml/model/fetchxmlParser.ts +0 -708
- package/src/features/fetchxml/model/layoutxml.ts +0 -372
- package/src/features/fetchxml/model/nodes.ts +0 -255
- package/src/features/fetchxml/model/operators.ts +0 -624
- package/src/features/fetchxml/model/treeUtils.ts +0 -174
- package/src/features/fetchxml/state/builderStore.tsx +0 -1160
- package/src/features/fetchxml/state/cache.ts +0 -393
- package/src/features/fetchxml/ui/Dialogs/BulkDeleteDialog.tsx +0 -352
- package/src/features/fetchxml/ui/Dialogs/DeleteConfirmDialog.tsx +0 -396
- package/src/features/fetchxml/ui/Dialogs/SaveViewDialog.tsx +0 -612
- package/src/features/fetchxml/ui/Dialogs/SolutionPicker.tsx +0 -114
- package/src/features/fetchxml/ui/Dialogs/WorkflowPickerDialog.tsx +0 -437
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/AttributeEditor.tsx +0 -214
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ConditionEditor.tsx +0 -533
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FetchOptionsEditor.tsx +0 -296
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/FilterEditor.tsx +0 -106
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/LinkEntityEditor.tsx +0 -433
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/OrderEditor.tsx +0 -161
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel/editors/ValueOfPicker.tsx +0 -194
- package/src/features/fetchxml/ui/LeftPane/PropertiesPanel.tsx +0 -225
- package/src/features/fetchxml/ui/LeftPane/TreeView.tsx +0 -604
- package/src/features/fetchxml/ui/RightPane/AddColumnsPanel.tsx +0 -800
- package/src/features/fetchxml/ui/RightPane/ColumnConfigDialog.tsx +0 -170
- package/src/features/fetchxml/ui/RightPane/DataGridCellRenderers.tsx +0 -356
- package/src/features/fetchxml/ui/RightPane/EditColumnsPanel.tsx +0 -372
- package/src/features/fetchxml/ui/RightPane/FetchXmlEditor.tsx +0 -399
- package/src/features/fetchxml/ui/RightPane/FormattedValueUtils.ts +0 -189
- package/src/features/fetchxml/ui/RightPane/LayoutXmlViewer.tsx +0 -171
- package/src/features/fetchxml/ui/RightPane/PreviewTabs.tsx +0 -471
- package/src/features/fetchxml/ui/RightPane/ResultsCommandBar.tsx +0 -430
- package/src/features/fetchxml/ui/RightPane/ResultsGrid.tsx +0 -983
- package/src/features/fetchxml/ui/RightPane/XmlTextArea.tsx +0 -65
- package/src/features/fetchxml/ui/Settings/SettingsDrawer.tsx +0 -175
- package/src/features/fetchxml/ui/Toolbar/EntitySelector.tsx +0 -788
- package/src/features/fetchxml/ui/Toolbar/LoadViewPicker.tsx +0 -274
- package/src/features/fetchxml/ui/Toolbar/SaveViewButton.tsx +0 -174
- package/src/main.tsx +0 -11
- package/src/shared/components/AttributePicker.tsx +0 -138
- package/src/shared/components/BetweenInputPicker.tsx +0 -120
- package/src/shared/components/BooleanValuePicker.tsx +0 -165
- package/src/shared/components/DateTimeInputPicker.tsx +0 -170
- package/src/shared/components/DynamicValueList.tsx +0 -128
- package/src/shared/components/ExcelIcon.tsx +0 -30
- package/src/shared/components/MultiValuePicker.tsx +0 -178
- package/src/shared/components/NumericInputPicker.tsx +0 -156
- package/src/shared/components/OptionSetValuePicker.tsx +0 -150
- package/src/shared/components/RelationshipPicker.tsx +0 -268
- package/src/shared/contexts/ThemeContext.tsx +0 -19
- package/src/shared/hooks/useAccessMode.ts +0 -77
- package/src/shared/hooks/useLazyMetadata.ts +0 -132
- package/src/shared/hooks/usePptbContext.ts +0 -170
- package/src/shared/hooks/usePublisherFilter.ts +0 -214
- package/src/shared/hooks/useSolutionFilter.ts +0 -162
- package/src/shared/utils/debug.ts +0 -87
- package/tsconfig.app.json +0 -28
- package/tsconfig.json +0 -7
- package/tsconfig.node.json +0 -26
- 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
|
-
}
|