@vendure/dashboard 3.4.3-master-202509260228 → 3.5.0-minor-202509261210
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/dist/plugin/api/api-extensions.js +11 -14
- package/dist/plugin/api/metrics.resolver.d.ts +2 -2
- package/dist/plugin/api/metrics.resolver.js +2 -2
- package/dist/plugin/config/metrics-strategies.d.ts +9 -9
- package/dist/plugin/config/metrics-strategies.js +6 -6
- package/dist/plugin/constants.d.ts +2 -0
- package/dist/plugin/constants.js +3 -1
- package/dist/plugin/dashboard.plugin.js +13 -0
- package/dist/plugin/service/metrics.service.d.ts +3 -3
- package/dist/plugin/service/metrics.service.js +37 -53
- package/dist/plugin/types.d.ts +9 -12
- package/dist/plugin/types.js +7 -11
- package/dist/vite/vite-plugin-vendure-dashboard.js +2 -2
- package/package.json +4 -4
- package/src/app/routes/_authenticated/_collections/collections.tsx +7 -2
- package/src/app/routes/_authenticated/_collections/collections_.$id.tsx +15 -2
- package/src/app/routes/_authenticated/_facets/facets_.$id.tsx +14 -2
- package/src/app/routes/_authenticated/_product-variants/product-variants.graphql.ts +10 -0
- package/src/app/routes/_authenticated/_products/components/product-option-group-badge.tsx +19 -0
- package/src/app/routes/_authenticated/_products/components/product-options-table.tsx +111 -0
- package/src/app/routes/_authenticated/_products/product-option-groups.graphql.ts +103 -0
- package/src/app/routes/_authenticated/_products/products.graphql.ts +13 -1
- package/src/app/routes/_authenticated/_products/products.tsx +27 -3
- package/src/app/routes/_authenticated/_products/products_.$id.tsx +26 -9
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$id.tsx +181 -0
- package/src/app/routes/_authenticated/_products/products_.$productId.option-groups.$productOptionGroupId.options_.$id.tsx +208 -0
- package/src/app/routes/_authenticated/_zones/components/zone-countries-sheet.tsx +4 -1
- package/src/app/routes/_authenticated/index.tsx +41 -24
- package/src/lib/components/data-display/json.tsx +16 -1
- package/src/lib/components/data-input/index.ts +3 -0
- package/src/lib/components/data-input/slug-input.tsx +296 -0
- package/src/lib/components/data-table/add-filter-menu.tsx +13 -6
- package/src/lib/components/data-table/data-table-bulk-action-item.tsx +38 -1
- package/src/lib/components/data-table/data-table-context.tsx +91 -0
- package/src/lib/components/data-table/data-table-filter-badge.tsx +9 -5
- package/src/lib/components/data-table/data-table-view-options.tsx +17 -8
- package/src/lib/components/data-table/data-table.tsx +146 -94
- package/src/lib/components/data-table/global-views-bar.tsx +97 -0
- package/src/lib/components/data-table/global-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/manage-global-views-button.tsx +26 -0
- package/src/lib/components/data-table/my-views-button.tsx +47 -0
- package/src/lib/components/data-table/refresh-button.tsx +12 -3
- package/src/lib/components/data-table/save-view-button.tsx +45 -0
- package/src/lib/components/data-table/save-view-dialog.tsx +113 -0
- package/src/lib/components/data-table/use-generated-columns.tsx +3 -1
- package/src/lib/components/data-table/user-views-sheet.tsx +11 -0
- package/src/lib/components/data-table/views-sheet.tsx +297 -0
- package/src/lib/components/date-range-picker.tsx +184 -0
- package/src/lib/components/shared/paginated-list-data-table.tsx +59 -32
- package/src/lib/components/ui/button.tsx +1 -1
- package/src/lib/framework/dashboard-widget/latest-orders-widget/index.tsx +29 -2
- package/src/lib/framework/dashboard-widget/metrics-widget/index.tsx +10 -7
- package/src/lib/framework/dashboard-widget/metrics-widget/metrics-widget.graphql.ts +9 -3
- package/src/lib/framework/dashboard-widget/orders-summary/index.tsx +19 -75
- package/src/lib/framework/dashboard-widget/widget-filters-context.tsx +33 -0
- package/src/lib/framework/document-introspection/add-custom-fields.spec.ts +319 -9
- package/src/lib/framework/document-introspection/add-custom-fields.ts +60 -31
- package/src/lib/framework/document-introspection/get-document-structure.spec.ts +1 -159
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.spec.ts +1840 -0
- package/src/lib/framework/document-introspection/include-only-selected-list-fields.ts +940 -0
- package/src/lib/framework/document-introspection/testing-utils.ts +161 -0
- package/src/lib/framework/extension-api/display-component-extensions.tsx +2 -0
- package/src/lib/framework/extension-api/types/data-table.ts +62 -4
- package/src/lib/framework/extension-api/types/navigation.ts +16 -0
- package/src/lib/framework/form-engine/utils.ts +34 -0
- package/src/lib/framework/page/list-page.tsx +289 -4
- package/src/lib/framework/page/use-extended-router.tsx +59 -17
- package/src/lib/graphql/api.ts +4 -2
- package/src/lib/graphql/graphql-env.d.ts +13 -10
- package/src/lib/hooks/use-extended-list-query.ts +5 -0
- package/src/lib/hooks/use-saved-views.ts +230 -0
- package/src/lib/index.ts +15 -0
- package/src/lib/types/saved-views.ts +39 -0
- package/src/lib/utils/saved-views-utils.ts +40 -0
|
@@ -0,0 +1,1840 @@
|
|
|
1
|
+
import { DocumentNode, parse, print } from 'graphql';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { includeOnlySelectedListFields } from './include-only-selected-list-fields.js';
|
|
5
|
+
|
|
6
|
+
vi.mock('virtual:admin-api-schema', () => {
|
|
7
|
+
return import('./testing-utils.js').then(m => m.getMockSchemaInfo());
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('includeOnlySelectedListFields', () => {
|
|
11
|
+
const createTestDocument = (itemsFields: string): DocumentNode => {
|
|
12
|
+
return parse(`
|
|
13
|
+
query ProductList($options: ProductListOptions) {
|
|
14
|
+
products(options: $options) {
|
|
15
|
+
items {
|
|
16
|
+
${itemsFields}
|
|
17
|
+
}
|
|
18
|
+
totalItems
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const normalizeQuery = (query: string): string => {
|
|
25
|
+
// Remove extra whitespace and normalize for comparison
|
|
26
|
+
return query.replace(/\s+/g, ' ').trim();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('basic field selection', () => {
|
|
30
|
+
it('should return original document when no columns are selected', () => {
|
|
31
|
+
const document = createTestDocument('id name slug');
|
|
32
|
+
const result = includeOnlySelectedListFields(document, []);
|
|
33
|
+
|
|
34
|
+
expect(print(result)).toEqual(print(document));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should filter to only selected fields', () => {
|
|
38
|
+
const document = createTestDocument(`
|
|
39
|
+
id
|
|
40
|
+
name
|
|
41
|
+
slug
|
|
42
|
+
enabled
|
|
43
|
+
createdAt
|
|
44
|
+
updatedAt
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
const result = includeOnlySelectedListFields(document, [
|
|
48
|
+
{ name: 'id', isCustomField: false },
|
|
49
|
+
{ name: 'name', isCustomField: false },
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const resultQuery = normalizeQuery(print(result));
|
|
53
|
+
expect(resultQuery).toContain('items { id name }');
|
|
54
|
+
expect(resultQuery).not.toContain('slug');
|
|
55
|
+
expect(resultQuery).not.toContain('enabled');
|
|
56
|
+
expect(resultQuery).not.toContain('createdAt');
|
|
57
|
+
expect(resultQuery).not.toContain('updatedAt');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should handle single field selection', () => {
|
|
61
|
+
const document = createTestDocument(`
|
|
62
|
+
id
|
|
63
|
+
name
|
|
64
|
+
slug
|
|
65
|
+
enabled
|
|
66
|
+
`);
|
|
67
|
+
|
|
68
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
69
|
+
|
|
70
|
+
const resultQuery = normalizeQuery(print(result));
|
|
71
|
+
expect(resultQuery).toContain('items { name }');
|
|
72
|
+
expect(resultQuery).not.toContain('slug');
|
|
73
|
+
expect(resultQuery).not.toContain('enabled');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should preserve nested field structures', () => {
|
|
77
|
+
const document = createTestDocument(`
|
|
78
|
+
id
|
|
79
|
+
name
|
|
80
|
+
featuredAsset {
|
|
81
|
+
id
|
|
82
|
+
preview
|
|
83
|
+
source
|
|
84
|
+
}
|
|
85
|
+
slug
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
const result = includeOnlySelectedListFields(document, [
|
|
89
|
+
{ name: 'id', isCustomField: false },
|
|
90
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
const resultQuery = normalizeQuery(print(result));
|
|
94
|
+
expect(resultQuery).toContain('items { id featuredAsset { id preview source } }');
|
|
95
|
+
expect(resultQuery).not.toContain('name');
|
|
96
|
+
expect(resultQuery).not.toContain('slug');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should preserve __typename if present in original', () => {
|
|
100
|
+
const document = createTestDocument(`
|
|
101
|
+
__typename
|
|
102
|
+
id
|
|
103
|
+
name
|
|
104
|
+
slug
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
108
|
+
|
|
109
|
+
const resultQuery = normalizeQuery(print(result));
|
|
110
|
+
expect(resultQuery).toContain('__typename');
|
|
111
|
+
expect(resultQuery).toContain('name');
|
|
112
|
+
expect(resultQuery).not.toContain('slug');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('custom fields handling', () => {
|
|
117
|
+
it('should include custom fields when specified', () => {
|
|
118
|
+
const document = createTestDocument(`
|
|
119
|
+
id
|
|
120
|
+
name
|
|
121
|
+
customFields {
|
|
122
|
+
shortDescription
|
|
123
|
+
isEcoFriendly
|
|
124
|
+
warrantyMonths
|
|
125
|
+
}
|
|
126
|
+
`);
|
|
127
|
+
|
|
128
|
+
const result = includeOnlySelectedListFields(document, [
|
|
129
|
+
{ name: 'id', isCustomField: false },
|
|
130
|
+
{ name: 'shortDescription', isCustomField: true },
|
|
131
|
+
{ name: 'isEcoFriendly', isCustomField: true },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const resultQuery = normalizeQuery(print(result));
|
|
135
|
+
expect(resultQuery).toContain('items { id customFields { shortDescription isEcoFriendly } }');
|
|
136
|
+
expect(resultQuery).not.toContain('warrantyMonths');
|
|
137
|
+
expect(resultQuery).not.toContain('name');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should exclude customFields entirely if no custom fields are selected', () => {
|
|
141
|
+
const document = createTestDocument(`
|
|
142
|
+
id
|
|
143
|
+
name
|
|
144
|
+
customFields {
|
|
145
|
+
shortDescription
|
|
146
|
+
isEcoFriendly
|
|
147
|
+
}
|
|
148
|
+
`);
|
|
149
|
+
|
|
150
|
+
const result = includeOnlySelectedListFields(document, [
|
|
151
|
+
{ name: 'id', isCustomField: false },
|
|
152
|
+
{ name: 'name', isCustomField: false },
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
const resultQuery = normalizeQuery(print(result));
|
|
156
|
+
expect(resultQuery).toContain('items { id name }');
|
|
157
|
+
expect(resultQuery).not.toContain('customFields');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should handle mixed regular and custom field selection', () => {
|
|
161
|
+
const document = createTestDocument(`
|
|
162
|
+
id
|
|
163
|
+
name
|
|
164
|
+
slug
|
|
165
|
+
enabled
|
|
166
|
+
customFields {
|
|
167
|
+
shortDescription
|
|
168
|
+
warrantyMonths
|
|
169
|
+
isEcoFriendly
|
|
170
|
+
}
|
|
171
|
+
`);
|
|
172
|
+
|
|
173
|
+
const result = includeOnlySelectedListFields(document, [
|
|
174
|
+
{ name: 'name', isCustomField: false },
|
|
175
|
+
{ name: 'enabled', isCustomField: false },
|
|
176
|
+
{ name: 'shortDescription', isCustomField: true },
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const resultQuery = normalizeQuery(print(result));
|
|
180
|
+
expect(resultQuery).toContain('name');
|
|
181
|
+
expect(resultQuery).toContain('enabled');
|
|
182
|
+
expect(resultQuery).toContain('customFields { shortDescription }');
|
|
183
|
+
expect(resultQuery).not.toContain('warrantyMonths');
|
|
184
|
+
expect(resultQuery).not.toContain('isEcoFriendly');
|
|
185
|
+
expect(resultQuery).not.toContain('slug');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('fragment handling', () => {
|
|
190
|
+
it('should preserve inline fragments', () => {
|
|
191
|
+
const document = parse(`
|
|
192
|
+
query ProductList($options: ProductListOptions) {
|
|
193
|
+
products(options: $options) {
|
|
194
|
+
items {
|
|
195
|
+
id
|
|
196
|
+
name
|
|
197
|
+
... on Product {
|
|
198
|
+
slug
|
|
199
|
+
description
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
totalItems
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
`);
|
|
206
|
+
|
|
207
|
+
const result = includeOnlySelectedListFields(document, [
|
|
208
|
+
{ name: 'id', isCustomField: false },
|
|
209
|
+
{ name: 'name', isCustomField: false },
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const resultQuery = normalizeQuery(print(result));
|
|
213
|
+
expect(resultQuery).toContain('... on Product');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should preserve fragment spreads when they contain selected fields', () => {
|
|
217
|
+
const document = parse(`
|
|
218
|
+
query ProductList($options: ProductListOptions) {
|
|
219
|
+
products(options: $options) {
|
|
220
|
+
items {
|
|
221
|
+
id
|
|
222
|
+
...ProductFields
|
|
223
|
+
}
|
|
224
|
+
totalItems
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fragment ProductFields on Product {
|
|
229
|
+
name
|
|
230
|
+
slug
|
|
231
|
+
enabled
|
|
232
|
+
}
|
|
233
|
+
`);
|
|
234
|
+
|
|
235
|
+
const result = includeOnlySelectedListFields(document, [
|
|
236
|
+
{ name: 'id', isCustomField: false },
|
|
237
|
+
{ name: 'name', isCustomField: false },
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
const resultQuery = normalizeQuery(print(result));
|
|
241
|
+
expect(resultQuery).toContain('...ProductFields');
|
|
242
|
+
expect(resultQuery).toContain('fragment ProductFields');
|
|
243
|
+
expect(resultQuery).toContain(' name');
|
|
244
|
+
// Fragment should be filtered to only include selected fields
|
|
245
|
+
expect(resultQuery).not.toContain('slug');
|
|
246
|
+
expect(resultQuery).not.toContain('enabled');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('edge cases', () => {
|
|
251
|
+
it('should add id field if no fields selected to maintain valid query', () => {
|
|
252
|
+
const document = createTestDocument(`
|
|
253
|
+
id
|
|
254
|
+
name
|
|
255
|
+
slug
|
|
256
|
+
`);
|
|
257
|
+
|
|
258
|
+
// Select a field that doesn't exist in the document
|
|
259
|
+
const result = includeOnlySelectedListFields(document, [
|
|
260
|
+
{ name: 'nonExistentField', isCustomField: false },
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
const resultQuery = normalizeQuery(print(result));
|
|
264
|
+
expect(resultQuery).toContain('items { id }');
|
|
265
|
+
expect(resultQuery).not.toContain('name');
|
|
266
|
+
expect(resultQuery).not.toContain('slug');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should handle document without items selectionSet', () => {
|
|
270
|
+
const document = parse(`
|
|
271
|
+
query ProductList($options: ProductListOptions) {
|
|
272
|
+
products(options: $options) {
|
|
273
|
+
items
|
|
274
|
+
totalItems
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
`);
|
|
278
|
+
|
|
279
|
+
const result = includeOnlySelectedListFields(document, [
|
|
280
|
+
{ name: 'id', isCustomField: false },
|
|
281
|
+
{ name: 'name', isCustomField: false },
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const resultQuery = normalizeQuery(print(result));
|
|
285
|
+
expect(resultQuery).toContain('items');
|
|
286
|
+
expect(resultQuery).toContain('totalItems');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle multiple queries in document', () => {
|
|
290
|
+
const document = parse(`
|
|
291
|
+
query ProductList($options: ProductListOptions) {
|
|
292
|
+
products(options: $options) {
|
|
293
|
+
items {
|
|
294
|
+
id
|
|
295
|
+
name
|
|
296
|
+
slug
|
|
297
|
+
}
|
|
298
|
+
totalItems
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
query ProductCount {
|
|
303
|
+
products {
|
|
304
|
+
totalItems
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
`);
|
|
308
|
+
|
|
309
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
|
|
310
|
+
|
|
311
|
+
// Should only modify the first query's items field
|
|
312
|
+
const resultQuery = normalizeQuery(print(result));
|
|
313
|
+
expect(resultQuery).toContain('query ProductList');
|
|
314
|
+
expect(resultQuery).toContain('query ProductCount');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should handle deeply nested structures', () => {
|
|
318
|
+
const document = createTestDocument(`
|
|
319
|
+
id
|
|
320
|
+
name
|
|
321
|
+
slug
|
|
322
|
+
variants {
|
|
323
|
+
id
|
|
324
|
+
name
|
|
325
|
+
options {
|
|
326
|
+
id
|
|
327
|
+
code
|
|
328
|
+
name
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
`);
|
|
332
|
+
|
|
333
|
+
const result = includeOnlySelectedListFields(document, [
|
|
334
|
+
{ name: 'id', isCustomField: false },
|
|
335
|
+
{ name: 'variants', isCustomField: false },
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
const resultQuery = normalizeQuery(print(result));
|
|
339
|
+
expect(resultQuery).toContain('variants');
|
|
340
|
+
expect(resultQuery).toContain('options');
|
|
341
|
+
// The nested name fields within variants are preserved, but root level name and slug should not be
|
|
342
|
+
expect(resultQuery).not.toContain('slug');
|
|
343
|
+
// Check that the structure is preserved correctly
|
|
344
|
+
expect(resultQuery).toContain('variants { id name options');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should preserve totalItems and other pagination fields', () => {
|
|
348
|
+
const document = parse(`
|
|
349
|
+
query ProductList($options: ProductListOptions) {
|
|
350
|
+
products(options: $options) {
|
|
351
|
+
items {
|
|
352
|
+
id
|
|
353
|
+
name
|
|
354
|
+
slug
|
|
355
|
+
}
|
|
356
|
+
totalItems
|
|
357
|
+
hasNextPage
|
|
358
|
+
hasPreviousPage
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
`);
|
|
362
|
+
|
|
363
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'id', isCustomField: false }]);
|
|
364
|
+
|
|
365
|
+
const resultQuery = normalizeQuery(print(result));
|
|
366
|
+
expect(resultQuery).toContain('totalItems');
|
|
367
|
+
expect(resultQuery).toContain('hasNextPage');
|
|
368
|
+
expect(resultQuery).toContain('hasPreviousPage');
|
|
369
|
+
expect(resultQuery).toContain('items { id }');
|
|
370
|
+
expect(resultQuery).not.toContain('name');
|
|
371
|
+
expect(resultQuery).not.toContain('slug');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should handle empty customFields selection gracefully', () => {
|
|
375
|
+
const document = createTestDocument(`
|
|
376
|
+
id
|
|
377
|
+
name
|
|
378
|
+
customFields {
|
|
379
|
+
field1
|
|
380
|
+
field2
|
|
381
|
+
}
|
|
382
|
+
`);
|
|
383
|
+
|
|
384
|
+
const result = includeOnlySelectedListFields(document, [
|
|
385
|
+
{ name: 'id', isCustomField: false },
|
|
386
|
+
{ name: 'nonExistentCustomField', isCustomField: true },
|
|
387
|
+
]);
|
|
388
|
+
|
|
389
|
+
const resultQuery = normalizeQuery(print(result));
|
|
390
|
+
expect(resultQuery).toContain('items { id }');
|
|
391
|
+
expect(resultQuery).not.toContain('customFields');
|
|
392
|
+
expect(resultQuery).not.toContain('name');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should handle queries with aliases', () => {
|
|
396
|
+
const document = parse(`
|
|
397
|
+
query ProductList($options: ProductListOptions) {
|
|
398
|
+
allProducts: products(options: $options) {
|
|
399
|
+
items {
|
|
400
|
+
productId: id
|
|
401
|
+
productName: name
|
|
402
|
+
slug
|
|
403
|
+
}
|
|
404
|
+
totalItems
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
`);
|
|
408
|
+
|
|
409
|
+
const result = includeOnlySelectedListFields(document, [
|
|
410
|
+
{ name: 'id', isCustomField: false },
|
|
411
|
+
{ name: 'name', isCustomField: false },
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
const resultQuery = normalizeQuery(print(result));
|
|
415
|
+
// Note: aliases make this more complex - the function looks at field names
|
|
416
|
+
expect(resultQuery).toContain('productId: id');
|
|
417
|
+
expect(resultQuery).toContain('productName: name');
|
|
418
|
+
expect(resultQuery).not.toContain('slug');
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('fragment-based items selection', () => {
|
|
423
|
+
it('should handle items defined in fragments - user example case', () => {
|
|
424
|
+
const document = parse(`
|
|
425
|
+
query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
|
|
426
|
+
facets(options: $options) {
|
|
427
|
+
items {
|
|
428
|
+
...FacetWithValueList
|
|
429
|
+
}
|
|
430
|
+
totalItems
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fragment FacetWithValueList on Facet {
|
|
435
|
+
id
|
|
436
|
+
createdAt
|
|
437
|
+
updatedAt
|
|
438
|
+
name
|
|
439
|
+
code
|
|
440
|
+
isPrivate
|
|
441
|
+
valueList(options: $facetValueListOptions) {
|
|
442
|
+
totalItems
|
|
443
|
+
items {
|
|
444
|
+
...FacetValue
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fragment FacetValue on FacetValue {
|
|
450
|
+
id
|
|
451
|
+
createdAt
|
|
452
|
+
updatedAt
|
|
453
|
+
languageCode
|
|
454
|
+
code
|
|
455
|
+
name
|
|
456
|
+
translations {
|
|
457
|
+
id
|
|
458
|
+
languageCode
|
|
459
|
+
name
|
|
460
|
+
}
|
|
461
|
+
facet {
|
|
462
|
+
id
|
|
463
|
+
createdAt
|
|
464
|
+
updatedAt
|
|
465
|
+
name
|
|
466
|
+
code
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
`);
|
|
470
|
+
|
|
471
|
+
const result = includeOnlySelectedListFields(document, [
|
|
472
|
+
{ name: 'id', isCustomField: false },
|
|
473
|
+
{ name: 'name', isCustomField: false },
|
|
474
|
+
{ name: 'code', isCustomField: false },
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
const resultQuery = normalizeQuery(print(result));
|
|
478
|
+
|
|
479
|
+
// Should include selected fields
|
|
480
|
+
expect(resultQuery).toContain('...FacetWithValueList');
|
|
481
|
+
expect(resultQuery).toContain('fragment FacetWithValueList');
|
|
482
|
+
|
|
483
|
+
// Fragment should be filtered to only include selected fields
|
|
484
|
+
expect(resultQuery).toContain('id');
|
|
485
|
+
expect(resultQuery).toContain(' name');
|
|
486
|
+
expect(resultQuery).toContain(' code');
|
|
487
|
+
|
|
488
|
+
// Should exclude non-selected fields from fragment
|
|
489
|
+
expect(resultQuery).not.toContain('createdAt');
|
|
490
|
+
expect(resultQuery).not.toContain('updatedAt');
|
|
491
|
+
expect(resultQuery).not.toContain('isPrivate');
|
|
492
|
+
expect(resultQuery).not.toContain('valueList');
|
|
493
|
+
|
|
494
|
+
// Should remove unused FacetValue fragment
|
|
495
|
+
expect(resultQuery).not.toContain('fragment FacetValue');
|
|
496
|
+
expect(resultQuery).not.toContain('...FacetValue');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should handle nested fragments in items fragments', () => {
|
|
500
|
+
const document = parse(`
|
|
501
|
+
query ProductList($options: ProductListOptions) {
|
|
502
|
+
products(options: $options) {
|
|
503
|
+
items {
|
|
504
|
+
...ProductWithAssets
|
|
505
|
+
}
|
|
506
|
+
totalItems
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fragment ProductWithAssets on Product {
|
|
511
|
+
id
|
|
512
|
+
name
|
|
513
|
+
slug
|
|
514
|
+
featuredAsset {
|
|
515
|
+
...AssetInfo
|
|
516
|
+
}
|
|
517
|
+
assets {
|
|
518
|
+
...AssetInfo
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
fragment AssetInfo on Asset {
|
|
523
|
+
id
|
|
524
|
+
name
|
|
525
|
+
preview
|
|
526
|
+
source
|
|
527
|
+
width
|
|
528
|
+
height
|
|
529
|
+
}
|
|
530
|
+
`);
|
|
531
|
+
|
|
532
|
+
const result = includeOnlySelectedListFields(document, [
|
|
533
|
+
{ name: 'name', isCustomField: false },
|
|
534
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
const resultQuery = normalizeQuery(print(result));
|
|
538
|
+
|
|
539
|
+
// Should include selected fields
|
|
540
|
+
expect(resultQuery).toContain('...ProductWithAssets');
|
|
541
|
+
expect(resultQuery).toContain('fragment ProductWithAssets');
|
|
542
|
+
|
|
543
|
+
// Should include used nested fragments
|
|
544
|
+
expect(resultQuery).toContain('fragment AssetInfo');
|
|
545
|
+
expect(resultQuery).toContain('...AssetInfo');
|
|
546
|
+
|
|
547
|
+
// Fragment should be filtered
|
|
548
|
+
expect(resultQuery).toContain(' name');
|
|
549
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
550
|
+
|
|
551
|
+
// Should exclude non-selected fields
|
|
552
|
+
expect(resultQuery).not.toContain('slug');
|
|
553
|
+
expect(resultQuery).not.toContain('assets');
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it('should handle mixed direct fields and fragment spreads in items', () => {
|
|
557
|
+
const document = parse(`
|
|
558
|
+
query ProductList($options: ProductListOptions) {
|
|
559
|
+
products(options: $options) {
|
|
560
|
+
items {
|
|
561
|
+
id
|
|
562
|
+
enabled
|
|
563
|
+
...ProductCore
|
|
564
|
+
customFields {
|
|
565
|
+
shortDescription
|
|
566
|
+
warrantyMonths
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
totalItems
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
fragment ProductCore on Product {
|
|
574
|
+
name
|
|
575
|
+
slug
|
|
576
|
+
description
|
|
577
|
+
featuredAsset {
|
|
578
|
+
id
|
|
579
|
+
preview
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
`);
|
|
583
|
+
|
|
584
|
+
const result = includeOnlySelectedListFields(document, [
|
|
585
|
+
{ name: 'id', isCustomField: false },
|
|
586
|
+
{ name: 'name', isCustomField: false },
|
|
587
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
588
|
+
{ name: 'shortDescription', isCustomField: true },
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
const resultQuery = normalizeQuery(print(result));
|
|
592
|
+
|
|
593
|
+
// Should include direct fields
|
|
594
|
+
expect(resultQuery).toContain('id');
|
|
595
|
+
|
|
596
|
+
// Should include fragment with filtered content
|
|
597
|
+
expect(resultQuery).toContain('...ProductCore');
|
|
598
|
+
expect(resultQuery).toContain('fragment ProductCore');
|
|
599
|
+
expect(resultQuery).toContain(' name');
|
|
600
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
601
|
+
|
|
602
|
+
// Should include filtered custom fields
|
|
603
|
+
expect(resultQuery).toContain('customFields { shortDescription }');
|
|
604
|
+
|
|
605
|
+
// Should exclude non-selected fields
|
|
606
|
+
expect(resultQuery).not.toContain('enabled');
|
|
607
|
+
expect(resultQuery).not.toContain('slug');
|
|
608
|
+
expect(resultQuery).not.toContain('description');
|
|
609
|
+
expect(resultQuery).not.toContain('warrantyMonths');
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it('should handle items with only fragment spreads', () => {
|
|
613
|
+
const document = parse(`
|
|
614
|
+
query CustomerList($options: CustomerListOptions) {
|
|
615
|
+
customers(options: $options) {
|
|
616
|
+
items {
|
|
617
|
+
...CustomerBasic
|
|
618
|
+
...CustomerContact
|
|
619
|
+
}
|
|
620
|
+
totalItems
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
fragment CustomerBasic on Customer {
|
|
625
|
+
id
|
|
626
|
+
title
|
|
627
|
+
firstName
|
|
628
|
+
lastName
|
|
629
|
+
emailAddress
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
fragment CustomerContact on Customer {
|
|
633
|
+
phoneNumber
|
|
634
|
+
addresses {
|
|
635
|
+
id
|
|
636
|
+
streetLine1
|
|
637
|
+
city
|
|
638
|
+
country {
|
|
639
|
+
code
|
|
640
|
+
name
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
`);
|
|
645
|
+
|
|
646
|
+
const result = includeOnlySelectedListFields(document, [
|
|
647
|
+
{ name: 'firstName', isCustomField: false },
|
|
648
|
+
{ name: 'lastName', isCustomField: false },
|
|
649
|
+
{ name: 'emailAddress', isCustomField: false },
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
const resultQuery = normalizeQuery(print(result));
|
|
653
|
+
|
|
654
|
+
// Should include fragment with selected fields
|
|
655
|
+
expect(resultQuery).toContain('...CustomerBasic');
|
|
656
|
+
expect(resultQuery).toContain('fragment CustomerBasic');
|
|
657
|
+
expect(resultQuery).toContain('firstName');
|
|
658
|
+
expect(resultQuery).toContain('lastName');
|
|
659
|
+
expect(resultQuery).toContain('emailAddress');
|
|
660
|
+
|
|
661
|
+
// Should exclude unused fragment
|
|
662
|
+
expect(resultQuery).not.toContain('...CustomerContact');
|
|
663
|
+
expect(resultQuery).not.toContain('fragment CustomerContact');
|
|
664
|
+
|
|
665
|
+
// Should exclude non-selected fields from used fragment
|
|
666
|
+
expect(resultQuery).not.toContain('title');
|
|
667
|
+
expect(resultQuery).not.toContain('phoneNumber');
|
|
668
|
+
expect(resultQuery).not.toContain('addresses');
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('should handle deeply nested fragment spreads in items', () => {
|
|
672
|
+
const document = parse(`
|
|
673
|
+
query ProductList($options: ProductListOptions) {
|
|
674
|
+
products(options: $options) {
|
|
675
|
+
items {
|
|
676
|
+
...ProductWithVariants
|
|
677
|
+
}
|
|
678
|
+
totalItems
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
fragment ProductWithVariants on Product {
|
|
683
|
+
id
|
|
684
|
+
name
|
|
685
|
+
variants {
|
|
686
|
+
id
|
|
687
|
+
name
|
|
688
|
+
options {
|
|
689
|
+
...ProductOptionDetail
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fragment ProductOptionDetail on ProductOption {
|
|
695
|
+
id
|
|
696
|
+
code
|
|
697
|
+
name
|
|
698
|
+
group {
|
|
699
|
+
id
|
|
700
|
+
name
|
|
701
|
+
code
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
`);
|
|
705
|
+
|
|
706
|
+
const result = includeOnlySelectedListFields(document, [
|
|
707
|
+
{ name: 'name', isCustomField: false },
|
|
708
|
+
{ name: 'variants', isCustomField: false },
|
|
709
|
+
]);
|
|
710
|
+
|
|
711
|
+
const resultQuery = normalizeQuery(print(result));
|
|
712
|
+
|
|
713
|
+
// Should include main fragment
|
|
714
|
+
expect(resultQuery).toContain('...ProductWithVariants');
|
|
715
|
+
expect(resultQuery).toContain('fragment ProductWithVariants');
|
|
716
|
+
|
|
717
|
+
// Should include nested structures
|
|
718
|
+
expect(resultQuery).toContain(' name');
|
|
719
|
+
expect(resultQuery).toContain('variants');
|
|
720
|
+
expect(resultQuery).toContain('options');
|
|
721
|
+
|
|
722
|
+
// Should include nested fragment
|
|
723
|
+
expect(resultQuery).toContain('fragment ProductOptionDetail');
|
|
724
|
+
expect(resultQuery).toContain('...ProductOptionDetail');
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should handle fragment spreads with custom fields in items', () => {
|
|
728
|
+
const document = parse(`
|
|
729
|
+
query ProductList($options: ProductListOptions) {
|
|
730
|
+
products(options: $options) {
|
|
731
|
+
items {
|
|
732
|
+
...ProductWithCustomFields
|
|
733
|
+
}
|
|
734
|
+
totalItems
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
fragment ProductWithCustomFields on Product {
|
|
739
|
+
id
|
|
740
|
+
name
|
|
741
|
+
slug
|
|
742
|
+
customFields {
|
|
743
|
+
shortDescription
|
|
744
|
+
warrantyMonths
|
|
745
|
+
isEcoFriendly
|
|
746
|
+
featuredCollection {
|
|
747
|
+
id
|
|
748
|
+
name
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
`);
|
|
753
|
+
|
|
754
|
+
const result = includeOnlySelectedListFields(document, [
|
|
755
|
+
{ name: 'name', isCustomField: false },
|
|
756
|
+
{ name: 'shortDescription', isCustomField: true },
|
|
757
|
+
{ name: 'isEcoFriendly', isCustomField: true },
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
const resultQuery = normalizeQuery(print(result));
|
|
761
|
+
|
|
762
|
+
// Should include fragment
|
|
763
|
+
expect(resultQuery).toContain('...ProductWithCustomFields');
|
|
764
|
+
expect(resultQuery).toContain('fragment ProductWithCustomFields');
|
|
765
|
+
|
|
766
|
+
// Should include selected regular field
|
|
767
|
+
expect(resultQuery).toContain(' name');
|
|
768
|
+
|
|
769
|
+
// Should include filtered custom fields
|
|
770
|
+
expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
|
|
771
|
+
|
|
772
|
+
// Should exclude non-selected fields
|
|
773
|
+
expect(resultQuery).not.toContain('slug');
|
|
774
|
+
expect(resultQuery).not.toContain('warrantyMonths');
|
|
775
|
+
expect(resultQuery).not.toContain('featuredCollection');
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it('should only filter top-level items fragments, not nested fragments - user issue case', () => {
|
|
779
|
+
const document = parse(`
|
|
780
|
+
query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
|
|
781
|
+
facets(options: $options) {
|
|
782
|
+
items {
|
|
783
|
+
...FacetWithValueList
|
|
784
|
+
}
|
|
785
|
+
totalItems
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
fragment FacetWithValueList on Facet {
|
|
790
|
+
id
|
|
791
|
+
name
|
|
792
|
+
isPrivate
|
|
793
|
+
valueList(options: $facetValueListOptions) {
|
|
794
|
+
totalItems
|
|
795
|
+
items {
|
|
796
|
+
...FacetValue
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
fragment FacetValue on FacetValue {
|
|
802
|
+
id
|
|
803
|
+
name
|
|
804
|
+
code
|
|
805
|
+
translations {
|
|
806
|
+
name
|
|
807
|
+
languageCode
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
`);
|
|
811
|
+
|
|
812
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
813
|
+
|
|
814
|
+
const resultQuery = normalizeQuery(print(result));
|
|
815
|
+
|
|
816
|
+
// Should include the top-level fragment
|
|
817
|
+
expect(resultQuery).toContain('...FacetWithValueList');
|
|
818
|
+
expect(resultQuery).toContain('fragment FacetWithValueList');
|
|
819
|
+
|
|
820
|
+
// Top-level fragment should be filtered (only name, no isPrivate)
|
|
821
|
+
expect(resultQuery).toContain(' name');
|
|
822
|
+
expect(resultQuery).not.toContain('isPrivate');
|
|
823
|
+
expect(resultQuery).not.toContain('valueList'); // This should be filtered out
|
|
824
|
+
|
|
825
|
+
// Should NOT include the nested FacetValue fragment since valueList was removed
|
|
826
|
+
expect(resultQuery).not.toContain('fragment FacetValue');
|
|
827
|
+
expect(resultQuery).not.toContain('...FacetValue');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('should preserve nested fragments when their parent field is selected', () => {
|
|
831
|
+
const document = parse(`
|
|
832
|
+
query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
|
|
833
|
+
facets(options: $options) {
|
|
834
|
+
items {
|
|
835
|
+
...FacetWithValueList
|
|
836
|
+
}
|
|
837
|
+
totalItems
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
fragment FacetWithValueList on Facet {
|
|
842
|
+
id
|
|
843
|
+
name
|
|
844
|
+
isPrivate
|
|
845
|
+
valueList(options: $facetValueListOptions) {
|
|
846
|
+
totalItems
|
|
847
|
+
items {
|
|
848
|
+
...FacetValue
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
fragment FacetValue on FacetValue {
|
|
854
|
+
id
|
|
855
|
+
name
|
|
856
|
+
code
|
|
857
|
+
translations {
|
|
858
|
+
name
|
|
859
|
+
languageCode
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
`);
|
|
863
|
+
|
|
864
|
+
const result = includeOnlySelectedListFields(document, [
|
|
865
|
+
{ name: 'name', isCustomField: false },
|
|
866
|
+
{ name: 'valueList', isCustomField: false },
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
const resultQuery = normalizeQuery(print(result));
|
|
870
|
+
|
|
871
|
+
// Should include the top-level fragment
|
|
872
|
+
expect(resultQuery).toContain('...FacetWithValueList');
|
|
873
|
+
expect(resultQuery).toContain('fragment FacetWithValueList');
|
|
874
|
+
|
|
875
|
+
// Top-level fragment should be filtered to include name and valueList
|
|
876
|
+
expect(resultQuery).toContain(' name');
|
|
877
|
+
expect(resultQuery).toContain('valueList');
|
|
878
|
+
expect(resultQuery).not.toContain('isPrivate');
|
|
879
|
+
|
|
880
|
+
// Should include the nested FacetValue fragment UNCHANGED since it's not a top-level items fragment
|
|
881
|
+
expect(resultQuery).toContain('fragment FacetValue');
|
|
882
|
+
expect(resultQuery).toContain('...FacetValue');
|
|
883
|
+
expect(resultQuery).toContain(' code'); // This should be preserved in the nested fragment
|
|
884
|
+
expect(resultQuery).toContain('translations'); // This should be preserved in the nested fragment
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
describe('unused fragment removal', () => {
|
|
889
|
+
it('should remove unused fragments when fields using them are filtered out', () => {
|
|
890
|
+
const document = parse(`
|
|
891
|
+
query ProductVariantList($options: ProductVariantListOptions) {
|
|
892
|
+
productVariants(options: $options) {
|
|
893
|
+
items {
|
|
894
|
+
id
|
|
895
|
+
price
|
|
896
|
+
priceWithTax
|
|
897
|
+
featuredAsset {
|
|
898
|
+
...Asset
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
totalItems
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
fragment Asset on Asset {
|
|
906
|
+
id
|
|
907
|
+
createdAt
|
|
908
|
+
updatedAt
|
|
909
|
+
name
|
|
910
|
+
fileSize
|
|
911
|
+
mimeType
|
|
912
|
+
type
|
|
913
|
+
preview
|
|
914
|
+
source
|
|
915
|
+
width
|
|
916
|
+
height
|
|
917
|
+
focalPoint {
|
|
918
|
+
x
|
|
919
|
+
y
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
`);
|
|
923
|
+
|
|
924
|
+
// Select only price fields, excluding featuredAsset
|
|
925
|
+
const result = includeOnlySelectedListFields(document, [
|
|
926
|
+
{ name: 'price', isCustomField: false },
|
|
927
|
+
{ name: 'priceWithTax', isCustomField: false },
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
const resultQuery = normalizeQuery(print(result));
|
|
931
|
+
|
|
932
|
+
// Should include selected fields
|
|
933
|
+
expect(resultQuery).toContain('price');
|
|
934
|
+
expect(resultQuery).toContain('priceWithTax');
|
|
935
|
+
|
|
936
|
+
// Should exclude featuredAsset field
|
|
937
|
+
expect(resultQuery).not.toContain('featuredAsset');
|
|
938
|
+
|
|
939
|
+
// Should remove unused Asset fragment
|
|
940
|
+
expect(resultQuery).not.toContain('fragment Asset');
|
|
941
|
+
expect(resultQuery).not.toContain('...Asset');
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('should keep used fragments when fields using them are selected', () => {
|
|
945
|
+
const document = parse(`
|
|
946
|
+
query ProductVariantList($options: ProductVariantListOptions) {
|
|
947
|
+
productVariants(options: $options) {
|
|
948
|
+
items {
|
|
949
|
+
id
|
|
950
|
+
price
|
|
951
|
+
featuredAsset {
|
|
952
|
+
...Asset
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
totalItems
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
fragment Asset on Asset {
|
|
960
|
+
id
|
|
961
|
+
name
|
|
962
|
+
preview
|
|
963
|
+
source
|
|
964
|
+
}
|
|
965
|
+
`);
|
|
966
|
+
|
|
967
|
+
// Select fields including featuredAsset
|
|
968
|
+
const result = includeOnlySelectedListFields(document, [
|
|
969
|
+
{ name: 'price', isCustomField: false },
|
|
970
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
971
|
+
]);
|
|
972
|
+
|
|
973
|
+
const resultQuery = normalizeQuery(print(result));
|
|
974
|
+
|
|
975
|
+
// Should include selected fields
|
|
976
|
+
expect(resultQuery).toContain('price');
|
|
977
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
978
|
+
|
|
979
|
+
// Should keep used Asset fragment
|
|
980
|
+
expect(resultQuery).toContain('fragment Asset');
|
|
981
|
+
expect(resultQuery).toContain('...Asset');
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
it('should handle nested fragment dependencies', () => {
|
|
985
|
+
const document = parse(`
|
|
986
|
+
query ProductList($options: ProductListOptions) {
|
|
987
|
+
products(options: $options) {
|
|
988
|
+
items {
|
|
989
|
+
id
|
|
990
|
+
name
|
|
991
|
+
featuredAsset {
|
|
992
|
+
...AssetWithMetadata
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
totalItems
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
fragment AssetWithMetadata on Asset {
|
|
1000
|
+
...AssetCore
|
|
1001
|
+
metadata {
|
|
1002
|
+
key
|
|
1003
|
+
value
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
fragment AssetCore on Asset {
|
|
1008
|
+
id
|
|
1009
|
+
name
|
|
1010
|
+
preview
|
|
1011
|
+
source
|
|
1012
|
+
}
|
|
1013
|
+
`);
|
|
1014
|
+
|
|
1015
|
+
// Select only name, excluding featuredAsset
|
|
1016
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
1017
|
+
|
|
1018
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1019
|
+
|
|
1020
|
+
// Should include selected field
|
|
1021
|
+
expect(resultQuery).toContain(' name');
|
|
1022
|
+
|
|
1023
|
+
// Should exclude featuredAsset field
|
|
1024
|
+
expect(resultQuery).not.toContain('featuredAsset');
|
|
1025
|
+
|
|
1026
|
+
// Should remove all unused fragments
|
|
1027
|
+
expect(resultQuery).not.toContain('fragment AssetWithMetadata');
|
|
1028
|
+
expect(resultQuery).not.toContain('fragment AssetCore');
|
|
1029
|
+
expect(resultQuery).not.toContain('...AssetWithMetadata');
|
|
1030
|
+
expect(resultQuery).not.toContain('...AssetCore');
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
it('should keep transitive fragment dependencies when parent fragment is used', () => {
|
|
1034
|
+
const document = parse(`
|
|
1035
|
+
query ProductList($options: ProductListOptions) {
|
|
1036
|
+
products(options: $options) {
|
|
1037
|
+
items {
|
|
1038
|
+
id
|
|
1039
|
+
name
|
|
1040
|
+
featuredAsset {
|
|
1041
|
+
...AssetWithMetadata
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
totalItems
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
fragment AssetWithMetadata on Asset {
|
|
1049
|
+
...AssetCore
|
|
1050
|
+
metadata {
|
|
1051
|
+
key
|
|
1052
|
+
value
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
fragment AssetCore on Asset {
|
|
1057
|
+
id
|
|
1058
|
+
name
|
|
1059
|
+
preview
|
|
1060
|
+
source
|
|
1061
|
+
}
|
|
1062
|
+
`);
|
|
1063
|
+
|
|
1064
|
+
// Select featuredAsset, which should keep all related fragments
|
|
1065
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1066
|
+
{ name: 'name', isCustomField: false },
|
|
1067
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
1068
|
+
]);
|
|
1069
|
+
|
|
1070
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1071
|
+
|
|
1072
|
+
// Should include selected fields
|
|
1073
|
+
expect(resultQuery).toContain(' name');
|
|
1074
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
1075
|
+
|
|
1076
|
+
// Should keep all used fragments
|
|
1077
|
+
expect(resultQuery).toContain('fragment AssetWithMetadata');
|
|
1078
|
+
expect(resultQuery).toContain('fragment AssetCore');
|
|
1079
|
+
expect(resultQuery).toContain('...AssetWithMetadata');
|
|
1080
|
+
expect(resultQuery).toContain('...AssetCore');
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
it('should handle mixed used and unused fragments', () => {
|
|
1084
|
+
const document = parse(`
|
|
1085
|
+
query ProductList($options: ProductListOptions) {
|
|
1086
|
+
products(options: $options) {
|
|
1087
|
+
items {
|
|
1088
|
+
id
|
|
1089
|
+
name
|
|
1090
|
+
featuredAsset {
|
|
1091
|
+
...Asset
|
|
1092
|
+
}
|
|
1093
|
+
variants {
|
|
1094
|
+
...ProductVariant
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
totalItems
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
fragment Asset on Asset {
|
|
1102
|
+
id
|
|
1103
|
+
name
|
|
1104
|
+
preview
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
fragment ProductVariant on ProductVariant {
|
|
1108
|
+
id
|
|
1109
|
+
name
|
|
1110
|
+
sku
|
|
1111
|
+
price
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
fragment UnusedFragment on Product {
|
|
1115
|
+
description
|
|
1116
|
+
slug
|
|
1117
|
+
}
|
|
1118
|
+
`);
|
|
1119
|
+
|
|
1120
|
+
// Select name and featuredAsset, excluding variants
|
|
1121
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1122
|
+
{ name: 'name', isCustomField: false },
|
|
1123
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
1124
|
+
]);
|
|
1125
|
+
|
|
1126
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1127
|
+
|
|
1128
|
+
// Should include selected fields
|
|
1129
|
+
expect(resultQuery).toContain(' name');
|
|
1130
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
1131
|
+
|
|
1132
|
+
// Should keep used Asset fragment
|
|
1133
|
+
expect(resultQuery).toContain('fragment Asset');
|
|
1134
|
+
expect(resultQuery).toContain('...Asset');
|
|
1135
|
+
|
|
1136
|
+
// Should exclude variants field
|
|
1137
|
+
expect(resultQuery).not.toContain('variants');
|
|
1138
|
+
|
|
1139
|
+
// Should remove unused fragments
|
|
1140
|
+
expect(resultQuery).not.toContain('fragment ProductVariant');
|
|
1141
|
+
expect(resultQuery).not.toContain('fragment UnusedFragment');
|
|
1142
|
+
expect(resultQuery).not.toContain('...ProductVariant');
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it('should handle the exact case from user example', () => {
|
|
1146
|
+
const document = parse(`
|
|
1147
|
+
query ProductVariantList($options: ProductVariantListOptions) {
|
|
1148
|
+
productVariants(options: $options) {
|
|
1149
|
+
items {
|
|
1150
|
+
featuredAsset {
|
|
1151
|
+
...Asset
|
|
1152
|
+
}
|
|
1153
|
+
price
|
|
1154
|
+
priceWithTax
|
|
1155
|
+
stockLevels {
|
|
1156
|
+
id
|
|
1157
|
+
stockOnHand
|
|
1158
|
+
stockAllocated
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
totalItems
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
fragment Asset on Asset {
|
|
1166
|
+
id
|
|
1167
|
+
createdAt
|
|
1168
|
+
updatedAt
|
|
1169
|
+
name
|
|
1170
|
+
fileSize
|
|
1171
|
+
mimeType
|
|
1172
|
+
type
|
|
1173
|
+
preview
|
|
1174
|
+
source
|
|
1175
|
+
width
|
|
1176
|
+
height
|
|
1177
|
+
focalPoint {
|
|
1178
|
+
x
|
|
1179
|
+
y
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
`);
|
|
1183
|
+
|
|
1184
|
+
// Remove featuredAsset field as mentioned in the user's example
|
|
1185
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1186
|
+
{ name: 'price', isCustomField: false },
|
|
1187
|
+
{ name: 'priceWithTax', isCustomField: false },
|
|
1188
|
+
{ name: 'stockLevels', isCustomField: false },
|
|
1189
|
+
]);
|
|
1190
|
+
|
|
1191
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1192
|
+
|
|
1193
|
+
// Should include selected fields
|
|
1194
|
+
expect(resultQuery).toContain('price');
|
|
1195
|
+
expect(resultQuery).toContain('priceWithTax');
|
|
1196
|
+
expect(resultQuery).toContain('stockLevels');
|
|
1197
|
+
|
|
1198
|
+
// Should exclude featuredAsset
|
|
1199
|
+
expect(resultQuery).not.toContain('featuredAsset');
|
|
1200
|
+
|
|
1201
|
+
// Should remove unused Asset fragment to prevent GraphQL error
|
|
1202
|
+
expect(resultQuery).not.toContain('fragment Asset');
|
|
1203
|
+
expect(resultQuery).not.toContain('...Asset');
|
|
1204
|
+
|
|
1205
|
+
// Verify the document is valid GraphQL by checking basic structure
|
|
1206
|
+
expect(resultQuery).toContain('query ProductVariantList');
|
|
1207
|
+
expect(resultQuery).toContain('productVariants');
|
|
1208
|
+
expect(resultQuery).toContain('items');
|
|
1209
|
+
expect(resultQuery).toContain('totalItems');
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe('unused variable removal', () => {
|
|
1214
|
+
it('should remove unused variables when fields using them are filtered out - user issue case', () => {
|
|
1215
|
+
const document = parse(`
|
|
1216
|
+
query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
|
|
1217
|
+
facets(options: $options) {
|
|
1218
|
+
items {
|
|
1219
|
+
...FacetWithValueList
|
|
1220
|
+
}
|
|
1221
|
+
totalItems
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
fragment FacetWithValueList on Facet {
|
|
1226
|
+
id
|
|
1227
|
+
name
|
|
1228
|
+
isPrivate
|
|
1229
|
+
valueList(options: $facetValueListOptions) {
|
|
1230
|
+
totalItems
|
|
1231
|
+
items {
|
|
1232
|
+
...FacetValue
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
fragment FacetValue on FacetValue {
|
|
1238
|
+
name
|
|
1239
|
+
}
|
|
1240
|
+
`);
|
|
1241
|
+
|
|
1242
|
+
// Select only name, which should filter out valueList and its variable
|
|
1243
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
1244
|
+
|
|
1245
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1246
|
+
|
|
1247
|
+
// Should include selected field
|
|
1248
|
+
expect(resultQuery).toContain(' name');
|
|
1249
|
+
|
|
1250
|
+
// Should exclude valueList field
|
|
1251
|
+
expect(resultQuery).not.toContain('valueList');
|
|
1252
|
+
|
|
1253
|
+
// Should remove unused $facetValueListOptions variable
|
|
1254
|
+
expect(resultQuery).not.toContain('$facetValueListOptions');
|
|
1255
|
+
expect(resultQuery).not.toContain('FacetValueListOptions');
|
|
1256
|
+
|
|
1257
|
+
// Should keep used $options variable
|
|
1258
|
+
expect(resultQuery).toContain('$options');
|
|
1259
|
+
expect(resultQuery).toContain('FacetListOptions');
|
|
1260
|
+
|
|
1261
|
+
// Should remove unused FacetValue fragment
|
|
1262
|
+
expect(resultQuery).not.toContain('fragment FacetValue');
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it('should preserve variables that are still used', () => {
|
|
1266
|
+
const document = parse(`
|
|
1267
|
+
query FacetList($options: FacetListOptions, $facetValueListOptions: FacetValueListOptions) {
|
|
1268
|
+
facets(options: $options) {
|
|
1269
|
+
items {
|
|
1270
|
+
...FacetWithValueList
|
|
1271
|
+
}
|
|
1272
|
+
totalItems
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
fragment FacetWithValueList on Facet {
|
|
1277
|
+
id
|
|
1278
|
+
name
|
|
1279
|
+
valueList(options: $facetValueListOptions) {
|
|
1280
|
+
totalItems
|
|
1281
|
+
items {
|
|
1282
|
+
...FacetValue
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
fragment FacetValue on FacetValue {
|
|
1288
|
+
name
|
|
1289
|
+
}
|
|
1290
|
+
`);
|
|
1291
|
+
|
|
1292
|
+
// Select name and valueList, which should preserve the variable
|
|
1293
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1294
|
+
{ name: 'name', isCustomField: false },
|
|
1295
|
+
{ name: 'valueList', isCustomField: false },
|
|
1296
|
+
]);
|
|
1297
|
+
|
|
1298
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1299
|
+
|
|
1300
|
+
// Should include selected fields
|
|
1301
|
+
expect(resultQuery).toContain(' name');
|
|
1302
|
+
expect(resultQuery).toContain('valueList');
|
|
1303
|
+
|
|
1304
|
+
// Should keep both variables since both are used
|
|
1305
|
+
expect(resultQuery).toContain('$options');
|
|
1306
|
+
expect(resultQuery).toContain('$facetValueListOptions');
|
|
1307
|
+
expect(resultQuery).toContain('FacetListOptions');
|
|
1308
|
+
expect(resultQuery).toContain('FacetValueListOptions');
|
|
1309
|
+
|
|
1310
|
+
// Should keep FacetValue fragment since valueList is preserved
|
|
1311
|
+
expect(resultQuery).toContain('fragment FacetValue');
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it('should handle multiple variables with complex usage patterns', () => {
|
|
1315
|
+
const document = parse(`
|
|
1316
|
+
query ComplexQuery($listOptions: ListOptions, $searchTerm: String, $categoryId: ID, $priceRange: PriceRangeInput) {
|
|
1317
|
+
products(options: $listOptions) {
|
|
1318
|
+
items {
|
|
1319
|
+
id
|
|
1320
|
+
name
|
|
1321
|
+
searchResults(term: $searchTerm) {
|
|
1322
|
+
score
|
|
1323
|
+
highlight
|
|
1324
|
+
}
|
|
1325
|
+
category(id: $categoryId) {
|
|
1326
|
+
name
|
|
1327
|
+
path
|
|
1328
|
+
}
|
|
1329
|
+
variants(priceRange: $priceRange) {
|
|
1330
|
+
price
|
|
1331
|
+
priceWithTax
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
totalItems
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
`);
|
|
1338
|
+
|
|
1339
|
+
// Select only id and name, should remove most variables
|
|
1340
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1341
|
+
{ name: 'id', isCustomField: false },
|
|
1342
|
+
{ name: 'name', isCustomField: false },
|
|
1343
|
+
]);
|
|
1344
|
+
|
|
1345
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1346
|
+
|
|
1347
|
+
// Should include selected fields
|
|
1348
|
+
expect(resultQuery).toContain('id');
|
|
1349
|
+
expect(resultQuery).toContain(' name');
|
|
1350
|
+
|
|
1351
|
+
// Should exclude fields that use variables
|
|
1352
|
+
expect(resultQuery).not.toContain('searchResults');
|
|
1353
|
+
expect(resultQuery).not.toContain('category');
|
|
1354
|
+
expect(resultQuery).not.toContain('variants');
|
|
1355
|
+
|
|
1356
|
+
// Should keep only the used variable
|
|
1357
|
+
expect(resultQuery).toContain('$listOptions');
|
|
1358
|
+
expect(resultQuery).toContain('ListOptions');
|
|
1359
|
+
|
|
1360
|
+
// Should remove unused variables
|
|
1361
|
+
expect(resultQuery).not.toContain('$searchTerm');
|
|
1362
|
+
expect(resultQuery).not.toContain('$categoryId');
|
|
1363
|
+
expect(resultQuery).not.toContain('$priceRange');
|
|
1364
|
+
expect(resultQuery).not.toContain('String');
|
|
1365
|
+
expect(resultQuery).not.toContain('ID');
|
|
1366
|
+
expect(resultQuery).not.toContain('PriceRangeInput');
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
it('should handle variables in query-level arguments', () => {
|
|
1370
|
+
const document = parse(`
|
|
1371
|
+
query ProductSearch($options: ProductListOptions, $filters: ProductFilterInput) {
|
|
1372
|
+
products(options: $options, filters: $filters) {
|
|
1373
|
+
items {
|
|
1374
|
+
id
|
|
1375
|
+
name
|
|
1376
|
+
price
|
|
1377
|
+
category {
|
|
1378
|
+
name
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
totalItems
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
`);
|
|
1385
|
+
|
|
1386
|
+
// Select only name, which should remove fields but keep variables used at query level
|
|
1387
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
1388
|
+
|
|
1389
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1390
|
+
|
|
1391
|
+
// Should include selected field
|
|
1392
|
+
expect(resultQuery).toContain(' name');
|
|
1393
|
+
|
|
1394
|
+
// Should exclude non-selected fields
|
|
1395
|
+
expect(resultQuery).not.toContain('price');
|
|
1396
|
+
expect(resultQuery).not.toContain('category');
|
|
1397
|
+
|
|
1398
|
+
// Should keep both variables since they're used at query level
|
|
1399
|
+
expect(resultQuery).toContain('$options');
|
|
1400
|
+
expect(resultQuery).toContain('$filters');
|
|
1401
|
+
expect(resultQuery).toContain('ProductListOptions');
|
|
1402
|
+
expect(resultQuery).toContain('ProductFilterInput');
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
it('should handle variables used in fragment arguments', () => {
|
|
1406
|
+
const document = parse(`
|
|
1407
|
+
query ProductList($options: ProductListOptions, $assetOptions: AssetListOptions) {
|
|
1408
|
+
products(options: $options) {
|
|
1409
|
+
items {
|
|
1410
|
+
...ProductWithAssets
|
|
1411
|
+
}
|
|
1412
|
+
totalItems
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
fragment ProductWithAssets on Product {
|
|
1417
|
+
id
|
|
1418
|
+
name
|
|
1419
|
+
assets(options: $assetOptions) {
|
|
1420
|
+
id
|
|
1421
|
+
preview
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
`);
|
|
1425
|
+
|
|
1426
|
+
// Select only name, which should remove assets field and its variable
|
|
1427
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
1428
|
+
|
|
1429
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1430
|
+
|
|
1431
|
+
// Should include selected field
|
|
1432
|
+
expect(resultQuery).toContain(' name');
|
|
1433
|
+
|
|
1434
|
+
// Should exclude assets field
|
|
1435
|
+
expect(resultQuery).not.toContain('assets');
|
|
1436
|
+
|
|
1437
|
+
// Should keep used variable
|
|
1438
|
+
expect(resultQuery).toContain('$options');
|
|
1439
|
+
expect(resultQuery).toContain('ProductListOptions');
|
|
1440
|
+
|
|
1441
|
+
// Should remove unused variable
|
|
1442
|
+
expect(resultQuery).not.toContain('$assetOptions');
|
|
1443
|
+
expect(resultQuery).not.toContain('AssetListOptions');
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
it('should handle edge case with no variables', () => {
|
|
1447
|
+
const document = parse(`
|
|
1448
|
+
query ProductList {
|
|
1449
|
+
products {
|
|
1450
|
+
items {
|
|
1451
|
+
id
|
|
1452
|
+
name
|
|
1453
|
+
slug
|
|
1454
|
+
}
|
|
1455
|
+
totalItems
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
`);
|
|
1459
|
+
|
|
1460
|
+
const result = includeOnlySelectedListFields(document, [{ name: 'name', isCustomField: false }]);
|
|
1461
|
+
|
|
1462
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1463
|
+
|
|
1464
|
+
// Should include selected field
|
|
1465
|
+
expect(resultQuery).toContain(' name');
|
|
1466
|
+
|
|
1467
|
+
// Should exclude non-selected fields
|
|
1468
|
+
expect(resultQuery).not.toContain('slug');
|
|
1469
|
+
|
|
1470
|
+
// Query should remain valid with no variables
|
|
1471
|
+
expect(resultQuery).toContain('query ProductList {');
|
|
1472
|
+
});
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
describe('complex real-world scenarios', () => {
|
|
1476
|
+
it('should handle a complex product list query', () => {
|
|
1477
|
+
const document = parse(`
|
|
1478
|
+
query GetProducts($options: ProductListOptions) {
|
|
1479
|
+
products(options: $options) {
|
|
1480
|
+
items {
|
|
1481
|
+
id
|
|
1482
|
+
createdAt
|
|
1483
|
+
updatedAt
|
|
1484
|
+
name
|
|
1485
|
+
slug
|
|
1486
|
+
enabled
|
|
1487
|
+
featuredAsset {
|
|
1488
|
+
id
|
|
1489
|
+
preview
|
|
1490
|
+
source
|
|
1491
|
+
width
|
|
1492
|
+
height
|
|
1493
|
+
}
|
|
1494
|
+
variantList {
|
|
1495
|
+
totalItems
|
|
1496
|
+
items {
|
|
1497
|
+
id
|
|
1498
|
+
name
|
|
1499
|
+
sku
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
customFields {
|
|
1503
|
+
shortDescription
|
|
1504
|
+
warrantyMonths
|
|
1505
|
+
isEcoFriendly
|
|
1506
|
+
featuredCollection {
|
|
1507
|
+
id
|
|
1508
|
+
name
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
totalItems
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
`);
|
|
1516
|
+
|
|
1517
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1518
|
+
{ name: 'id', isCustomField: false },
|
|
1519
|
+
{ name: 'name', isCustomField: false },
|
|
1520
|
+
{ name: 'featuredAsset', isCustomField: false },
|
|
1521
|
+
{ name: 'shortDescription', isCustomField: true },
|
|
1522
|
+
{ name: 'isEcoFriendly', isCustomField: true },
|
|
1523
|
+
]);
|
|
1524
|
+
|
|
1525
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1526
|
+
|
|
1527
|
+
// Should include selected fields
|
|
1528
|
+
expect(resultQuery).toContain('id');
|
|
1529
|
+
expect(resultQuery).toContain(' name');
|
|
1530
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
1531
|
+
expect(resultQuery).toContain('customFields { shortDescription isEcoFriendly }');
|
|
1532
|
+
|
|
1533
|
+
// Should exclude non-selected fields
|
|
1534
|
+
expect(resultQuery).not.toContain('createdAt');
|
|
1535
|
+
expect(resultQuery).not.toContain('updatedAt');
|
|
1536
|
+
expect(resultQuery).not.toContain('slug');
|
|
1537
|
+
expect(resultQuery).not.toContain('enabled');
|
|
1538
|
+
expect(resultQuery).not.toContain('variantList');
|
|
1539
|
+
expect(resultQuery).not.toContain('warrantyMonths');
|
|
1540
|
+
expect(resultQuery).not.toContain('featuredCollection');
|
|
1541
|
+
|
|
1542
|
+
// Should preserve pagination
|
|
1543
|
+
expect(resultQuery).toContain('totalItems');
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
it('should handle customer list with addresses', () => {
|
|
1547
|
+
const document = parse(`
|
|
1548
|
+
query CustomerList($options: CustomerListOptions) {
|
|
1549
|
+
customers(options: $options) {
|
|
1550
|
+
items {
|
|
1551
|
+
id
|
|
1552
|
+
title
|
|
1553
|
+
firstName
|
|
1554
|
+
lastName
|
|
1555
|
+
emailAddress
|
|
1556
|
+
phoneNumber
|
|
1557
|
+
addresses {
|
|
1558
|
+
id
|
|
1559
|
+
streetLine1
|
|
1560
|
+
streetLine2
|
|
1561
|
+
city
|
|
1562
|
+
province
|
|
1563
|
+
postalCode
|
|
1564
|
+
country {
|
|
1565
|
+
id
|
|
1566
|
+
code
|
|
1567
|
+
name
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
orders {
|
|
1571
|
+
totalItems
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
totalItems
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
`);
|
|
1578
|
+
|
|
1579
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1580
|
+
{ name: 'id', isCustomField: false },
|
|
1581
|
+
{ name: 'emailAddress', isCustomField: false },
|
|
1582
|
+
{ name: 'firstName', isCustomField: false },
|
|
1583
|
+
{ name: 'lastName', isCustomField: false },
|
|
1584
|
+
]);
|
|
1585
|
+
|
|
1586
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1587
|
+
|
|
1588
|
+
// Should include selected fields
|
|
1589
|
+
expect(resultQuery).toContain('id');
|
|
1590
|
+
expect(resultQuery).toContain('emailAddress');
|
|
1591
|
+
expect(resultQuery).toContain('firstName');
|
|
1592
|
+
expect(resultQuery).toContain('lastName');
|
|
1593
|
+
|
|
1594
|
+
// Should exclude non-selected fields
|
|
1595
|
+
expect(resultQuery).not.toContain('title');
|
|
1596
|
+
expect(resultQuery).not.toContain('phoneNumber');
|
|
1597
|
+
expect(resultQuery).not.toContain('addresses');
|
|
1598
|
+
expect(resultQuery).not.toContain('orders');
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
describe('memoization', () => {
|
|
1603
|
+
it('should return the same reference for identical inputs', () => {
|
|
1604
|
+
const document = parse(`
|
|
1605
|
+
query ProductList($options: ProductListOptions) {
|
|
1606
|
+
products(options: $options) {
|
|
1607
|
+
items {
|
|
1608
|
+
id
|
|
1609
|
+
name
|
|
1610
|
+
slug
|
|
1611
|
+
enabled
|
|
1612
|
+
}
|
|
1613
|
+
totalItems
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
`);
|
|
1617
|
+
|
|
1618
|
+
const selectedColumns = [
|
|
1619
|
+
{ name: 'id', isCustomField: false },
|
|
1620
|
+
{ name: 'name', isCustomField: false },
|
|
1621
|
+
];
|
|
1622
|
+
|
|
1623
|
+
const result1 = includeOnlySelectedListFields(document, selectedColumns);
|
|
1624
|
+
const result2 = includeOnlySelectedListFields(document, selectedColumns);
|
|
1625
|
+
|
|
1626
|
+
// Should return the exact same reference from cache
|
|
1627
|
+
expect(result1).toBe(result2);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
it('should handle different column orders consistently', () => {
|
|
1631
|
+
const document = parse(`
|
|
1632
|
+
query ProductList($options: ProductListOptions) {
|
|
1633
|
+
products(options: $options) {
|
|
1634
|
+
items {
|
|
1635
|
+
id
|
|
1636
|
+
name
|
|
1637
|
+
slug
|
|
1638
|
+
}
|
|
1639
|
+
totalItems
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
`);
|
|
1643
|
+
|
|
1644
|
+
const columns1 = [
|
|
1645
|
+
{ name: 'name', isCustomField: false },
|
|
1646
|
+
{ name: 'id', isCustomField: false },
|
|
1647
|
+
];
|
|
1648
|
+
|
|
1649
|
+
const columns2 = [
|
|
1650
|
+
{ name: 'id', isCustomField: false },
|
|
1651
|
+
{ name: 'name', isCustomField: false },
|
|
1652
|
+
];
|
|
1653
|
+
|
|
1654
|
+
const result1 = includeOnlySelectedListFields(document, columns1);
|
|
1655
|
+
const result2 = includeOnlySelectedListFields(document, columns2);
|
|
1656
|
+
|
|
1657
|
+
// Should return the same result regardless of column order (due to sorting in cache key)
|
|
1658
|
+
expect(result1).toBe(result2);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
describe('column dependencies', () => {
|
|
1663
|
+
it('should include dependency fields even when not explicitly selected', () => {
|
|
1664
|
+
const document = parse(`
|
|
1665
|
+
query CollectionList($options: CollectionListOptions) {
|
|
1666
|
+
collections(options: $options) {
|
|
1667
|
+
items {
|
|
1668
|
+
id
|
|
1669
|
+
name
|
|
1670
|
+
slug
|
|
1671
|
+
children {
|
|
1672
|
+
id
|
|
1673
|
+
name
|
|
1674
|
+
}
|
|
1675
|
+
breadcrumbs {
|
|
1676
|
+
id
|
|
1677
|
+
name
|
|
1678
|
+
}
|
|
1679
|
+
position
|
|
1680
|
+
}
|
|
1681
|
+
totalItems
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
`);
|
|
1685
|
+
|
|
1686
|
+
// Select only 'name' but declare dependencies on 'children' and 'breadcrumbs'
|
|
1687
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1688
|
+
{
|
|
1689
|
+
name: 'name',
|
|
1690
|
+
isCustomField: false,
|
|
1691
|
+
dependencies: ['children', 'breadcrumbs'],
|
|
1692
|
+
},
|
|
1693
|
+
]);
|
|
1694
|
+
|
|
1695
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1696
|
+
|
|
1697
|
+
// Should include selected field
|
|
1698
|
+
expect(resultQuery).toContain('name');
|
|
1699
|
+
|
|
1700
|
+
// Should include dependency fields
|
|
1701
|
+
expect(resultQuery).toContain('children');
|
|
1702
|
+
expect(resultQuery).toContain('breadcrumbs');
|
|
1703
|
+
|
|
1704
|
+
// Should exclude non-selected, non-dependency fields
|
|
1705
|
+
expect(resultQuery).not.toContain('slug');
|
|
1706
|
+
expect(resultQuery).not.toContain('position');
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
it('should handle multiple columns with different dependencies', () => {
|
|
1710
|
+
const document = parse(`
|
|
1711
|
+
query ProductList($options: ProductListOptions) {
|
|
1712
|
+
products(options: $options) {
|
|
1713
|
+
items {
|
|
1714
|
+
id
|
|
1715
|
+
name
|
|
1716
|
+
slug
|
|
1717
|
+
enabled
|
|
1718
|
+
variants {
|
|
1719
|
+
id
|
|
1720
|
+
name
|
|
1721
|
+
}
|
|
1722
|
+
assets {
|
|
1723
|
+
id
|
|
1724
|
+
preview
|
|
1725
|
+
}
|
|
1726
|
+
featuredAsset {
|
|
1727
|
+
id
|
|
1728
|
+
preview
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
totalItems
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
`);
|
|
1735
|
+
|
|
1736
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1737
|
+
{
|
|
1738
|
+
name: 'name',
|
|
1739
|
+
isCustomField: false,
|
|
1740
|
+
dependencies: ['featuredAsset'], // For thumbnail display
|
|
1741
|
+
},
|
|
1742
|
+
{
|
|
1743
|
+
name: 'enabled',
|
|
1744
|
+
isCustomField: false,
|
|
1745
|
+
dependencies: ['variants'], // For variant count display
|
|
1746
|
+
},
|
|
1747
|
+
]);
|
|
1748
|
+
|
|
1749
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1750
|
+
|
|
1751
|
+
// Should include selected fields
|
|
1752
|
+
expect(resultQuery).toContain('name');
|
|
1753
|
+
expect(resultQuery).toContain('enabled');
|
|
1754
|
+
|
|
1755
|
+
// Should include dependency fields
|
|
1756
|
+
expect(resultQuery).toContain('featuredAsset');
|
|
1757
|
+
expect(resultQuery).toContain('variants');
|
|
1758
|
+
|
|
1759
|
+
// Should exclude non-selected, non-dependency fields
|
|
1760
|
+
expect(resultQuery).not.toContain('slug');
|
|
1761
|
+
expect(resultQuery).not.toContain('assets');
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
it('should cache correctly with dependencies', () => {
|
|
1765
|
+
const document = parse(`
|
|
1766
|
+
query TestList($options: TestListOptions) {
|
|
1767
|
+
tests(options: $options) {
|
|
1768
|
+
items {
|
|
1769
|
+
id
|
|
1770
|
+
name
|
|
1771
|
+
data
|
|
1772
|
+
metadata
|
|
1773
|
+
}
|
|
1774
|
+
totalItems
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
`);
|
|
1778
|
+
|
|
1779
|
+
const columnsWithDeps = [
|
|
1780
|
+
{
|
|
1781
|
+
name: 'name',
|
|
1782
|
+
isCustomField: false,
|
|
1783
|
+
dependencies: ['metadata'],
|
|
1784
|
+
},
|
|
1785
|
+
];
|
|
1786
|
+
|
|
1787
|
+
const result1 = includeOnlySelectedListFields(document, columnsWithDeps);
|
|
1788
|
+
const result2 = includeOnlySelectedListFields(document, columnsWithDeps);
|
|
1789
|
+
|
|
1790
|
+
// Should return the same reference from cache
|
|
1791
|
+
expect(result1).toBe(result2);
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
it('should handle dependencies with custom fields', () => {
|
|
1795
|
+
const document = parse(`
|
|
1796
|
+
query ProductList($options: ProductListOptions) {
|
|
1797
|
+
products(options: $options) {
|
|
1798
|
+
items {
|
|
1799
|
+
id
|
|
1800
|
+
name
|
|
1801
|
+
customFields {
|
|
1802
|
+
shortDescription
|
|
1803
|
+
isEcoFriendly
|
|
1804
|
+
relatedProducts {
|
|
1805
|
+
id
|
|
1806
|
+
name
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
totalItems
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
`);
|
|
1814
|
+
|
|
1815
|
+
const result = includeOnlySelectedListFields(document, [
|
|
1816
|
+
{
|
|
1817
|
+
name: 'name',
|
|
1818
|
+
isCustomField: false,
|
|
1819
|
+
dependencies: ['customFields'], // Depends on custom fields for rendering logic
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
name: 'shortDescription',
|
|
1823
|
+
isCustomField: true,
|
|
1824
|
+
},
|
|
1825
|
+
]);
|
|
1826
|
+
|
|
1827
|
+
const resultQuery = normalizeQuery(print(result));
|
|
1828
|
+
|
|
1829
|
+
// Should include selected fields
|
|
1830
|
+
expect(resultQuery).toContain('name');
|
|
1831
|
+
|
|
1832
|
+
// Should include customFields (as dependency and for custom field)
|
|
1833
|
+
expect(resultQuery).toContain('customFields');
|
|
1834
|
+
expect(resultQuery).toContain('shortDescription');
|
|
1835
|
+
|
|
1836
|
+
// Should include id for valid query
|
|
1837
|
+
expect(resultQuery).toContain('id');
|
|
1838
|
+
});
|
|
1839
|
+
});
|
|
1840
|
+
});
|