dynamo-query-engine 1.0.6 → 1.0.8
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/package.json +1 -1
- package/src/core/expandResolver.js +170 -20
- package/tests/unit/expandResolver.test.js +17 -0
package/package.json
CHANGED
|
@@ -35,7 +35,11 @@ export async function resolveExpand({
|
|
|
35
35
|
field,
|
|
36
36
|
args = {},
|
|
37
37
|
}) {
|
|
38
|
+
console.log(`[expandResolver] resolveExpand called for field: ${field}`);
|
|
39
|
+
console.log(`[expandResolver] parentItems count: ${parentItems?.length || 0}`);
|
|
40
|
+
|
|
38
41
|
if (!parentItems || parentItems.length === 0) {
|
|
42
|
+
console.log(`[expandResolver] No parent items, skipping expand for field: ${field}`);
|
|
39
43
|
return;
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -52,11 +56,13 @@ export async function resolveExpand({
|
|
|
52
56
|
const policy = expandPolicy[field];
|
|
53
57
|
|
|
54
58
|
if (!policy) {
|
|
59
|
+
console.error(`[expandResolver] Field '${field}' does not have expand configuration`);
|
|
55
60
|
throw new Error(
|
|
56
61
|
`Field '${field}' does not have expand configuration in parent model`
|
|
57
62
|
);
|
|
58
63
|
}
|
|
59
64
|
|
|
65
|
+
|
|
60
66
|
// Get target model from registry
|
|
61
67
|
const targetModel = modelRegistry.get(policy.type);
|
|
62
68
|
|
|
@@ -76,30 +82,87 @@ export async function resolveExpand({
|
|
|
76
82
|
// Resolve relations for each parent item
|
|
77
83
|
const expandPromises = parentItems.map(async (parent) => {
|
|
78
84
|
try {
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
const
|
|
85
|
+
// Get partition key value from parent
|
|
86
|
+
// First, determine the target model's hash key name
|
|
87
|
+
const targetAttributes = targetModel.schema.getAttributes();
|
|
88
|
+
const targetHashKeyName = Object.keys(targetAttributes).find(
|
|
89
|
+
(key) => targetAttributes[key].hashKey === true
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
// Get the partition key value from the parent using the target's hash key name
|
|
94
|
+
const partitionKeyValue = parent[targetHashKeyName];
|
|
82
95
|
|
|
96
|
+
|
|
83
97
|
if (!partitionKeyValue) {
|
|
84
98
|
console.warn(
|
|
85
|
-
`Cannot expand '${field}' for parent: missing partition key value`
|
|
99
|
+
`[expandResolver] Cannot expand '${field}' for parent: missing partition key value`
|
|
86
100
|
);
|
|
87
|
-
parent[expandPropertyName] = [];
|
|
101
|
+
parent[expandPropertyName] = policy.relation === "ONE" ? null : [];
|
|
88
102
|
return;
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
let results;
|
|
106
|
+
|
|
107
|
+
// For ONE relations with a foreignKey, we can optimize by directly querying for that specific item
|
|
108
|
+
if (policy.relation === "ONE" && policy.foreignKey) {
|
|
109
|
+
const foreignKeyValue = parent[policy.foreignKey];
|
|
110
|
+
|
|
111
|
+
if (!foreignKeyValue) {
|
|
112
|
+
console.warn(
|
|
113
|
+
`[expandResolver] Cannot expand '${field}' for parent: missing foreign key value '${policy.foreignKey}'`
|
|
114
|
+
);
|
|
115
|
+
parent[expandPropertyName] = null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build a query that filters for the specific range key value
|
|
120
|
+
const targetAttributes = targetModel.schema.getAttributes();
|
|
121
|
+
const rangeKeyName = Object.keys(targetAttributes).find(
|
|
122
|
+
(key) => targetAttributes[key].rangeKey === true
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (!rangeKeyName) {
|
|
126
|
+
console.warn(
|
|
127
|
+
`[expandResolver] Cannot expand '${field}': target model '${policy.type}' has no range key defined`
|
|
128
|
+
);
|
|
129
|
+
parent[expandPropertyName] = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
133
|
+
// Build query with range key filter
|
|
134
|
+
const filterModel = JSON.stringify({
|
|
135
|
+
items: [
|
|
136
|
+
{
|
|
137
|
+
field: rangeKeyName,
|
|
138
|
+
operator: "eq",
|
|
139
|
+
value: foreignKeyValue,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const { query } = buildGridQuery({
|
|
146
|
+
model: targetModel,
|
|
147
|
+
partitionKeyValue,
|
|
148
|
+
filterModel,
|
|
149
|
+
limit: 1,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
results = await query.exec();
|
|
153
|
+
} else {
|
|
154
|
+
// For MANY relations or ONE without foreignKey, use standard query
|
|
155
|
+
const { query } = buildGridQuery({
|
|
156
|
+
model: targetModel,
|
|
157
|
+
partitionKeyValue,
|
|
158
|
+
filterModel: args.filter,
|
|
159
|
+
sortModel: args.sort,
|
|
160
|
+
paginationModel: { pageSize: limit },
|
|
161
|
+
// No cursor for nested expands
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
results = await query.exec();
|
|
165
|
+
}
|
|
103
166
|
|
|
104
167
|
// Add __typename to each result
|
|
105
168
|
const resultsWithTypename = results.map((item) => ({
|
|
@@ -107,6 +170,7 @@ export async function resolveExpand({
|
|
|
107
170
|
__typename: graphqlTypeName,
|
|
108
171
|
}));
|
|
109
172
|
|
|
173
|
+
|
|
110
174
|
// Assign results to parent
|
|
111
175
|
if (policy.relation === "ONE") {
|
|
112
176
|
parent[expandPropertyName] = resultsWithTypename.length > 0 ? resultsWithTypename[0] : null;
|
|
@@ -114,7 +178,8 @@ export async function resolveExpand({
|
|
|
114
178
|
parent[expandPropertyName] = resultsWithTypename;
|
|
115
179
|
}
|
|
116
180
|
} catch (error) {
|
|
117
|
-
console.error(`Error expanding '${field}' for parent:`, error);
|
|
181
|
+
console.error(`[expandResolver] Error expanding '${field}' for parent:`, error);
|
|
182
|
+
console.error(`[expandResolver] Error stack:`, error.stack);
|
|
118
183
|
// Gracefully handle errors - set empty result
|
|
119
184
|
parent[expandPropertyName] = policy.relation === "ONE" ? null : [];
|
|
120
185
|
}
|
|
@@ -145,19 +210,102 @@ function createGraphQLToSchemaFieldMap(schema) {
|
|
|
145
210
|
return map;
|
|
146
211
|
}
|
|
147
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Parse AppSync selectionSetList to extract requested expand fields
|
|
215
|
+
* @param {string[]} selectionSetList - AppSync selectionSetList array
|
|
216
|
+
* @param {string} parentPath - Parent path prefix (e.g., "rows")
|
|
217
|
+
* @returns {Set<string>} Set of field names that were requested
|
|
218
|
+
*/
|
|
219
|
+
function parseAppSyncSelectionSet(selectionSetList, parentPath = "rows") {
|
|
220
|
+
const requestedFields = new Set();
|
|
221
|
+
const prefix = parentPath + "/";
|
|
222
|
+
|
|
223
|
+
for (const path of selectionSetList) {
|
|
224
|
+
if (path.startsWith(prefix)) {
|
|
225
|
+
const afterPrefix = path.substring(prefix.length);
|
|
226
|
+
const firstSlash = afterPrefix.indexOf("/");
|
|
227
|
+
|
|
228
|
+
if (firstSlash === -1) {
|
|
229
|
+
// This is a direct child field (e.g., "rows/unitId")
|
|
230
|
+
requestedFields.add(afterPrefix);
|
|
231
|
+
} else {
|
|
232
|
+
// This is a nested field (e.g., "rows/Unit/name")
|
|
233
|
+
const fieldName = afterPrefix.substring(0, firstSlash);
|
|
234
|
+
requestedFields.add(fieldName);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return requestedFields;
|
|
240
|
+
}
|
|
241
|
+
|
|
148
242
|
/**
|
|
149
243
|
* Resolve multiple expands from GraphQL resolve info
|
|
150
244
|
* @param {Array} parentItems - Parent items to expand
|
|
151
245
|
* @param {*} parentModel - Parent Dynamoose model
|
|
152
|
-
* @param {Object} fieldsByTypeName - Fields by type name from graphql-parse-resolve-info
|
|
246
|
+
* @param {Object|string[]} fieldsByTypeName - Fields by type name from graphql-parse-resolve-info OR AppSync selectionSetList
|
|
247
|
+
* @param {Object} [options] - Additional options
|
|
248
|
+
* @param {boolean} [options.isAppSync] - Whether to parse AppSync selectionSetList format
|
|
249
|
+
* @param {string} [options.parentPath] - Parent path for AppSync (default: "rows")
|
|
153
250
|
* @returns {Promise<void>}
|
|
154
251
|
*/
|
|
155
252
|
export async function resolveExpands(
|
|
156
253
|
parentItems,
|
|
157
254
|
parentModel,
|
|
158
|
-
fieldsByTypeName
|
|
255
|
+
fieldsByTypeName,
|
|
256
|
+
options = {}
|
|
159
257
|
) {
|
|
258
|
+
const { isAppSync = false, parentPath = "rows" } = options;
|
|
259
|
+
|
|
260
|
+
// Handle AppSync format
|
|
261
|
+
if (isAppSync) {
|
|
262
|
+
const selectionSetList = fieldsByTypeName;
|
|
263
|
+
|
|
264
|
+
if (!selectionSetList || selectionSetList.length === 0) {
|
|
265
|
+
console.log(`[expandResolver] No selectionSetList, skipping`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Parse AppSync selection set to find requested fields
|
|
270
|
+
const requestedFields = parseAppSyncSelectionSet(selectionSetList, parentPath);
|
|
271
|
+
|
|
272
|
+
// Get expand policy to filter only expandable fields
|
|
273
|
+
const expandPolicy = extractExpandPolicy(parentModel.schema);
|
|
274
|
+
|
|
275
|
+
// Create mapping from GraphQL field names to schema field names
|
|
276
|
+
const graphqlToSchemaMap = createGraphQLToSchemaFieldMap(parentModel.schema);
|
|
277
|
+
|
|
278
|
+
// Resolve all expands in parallel
|
|
279
|
+
const expandPromises = Array.from(requestedFields)
|
|
280
|
+
.map((graphqlFieldName) => {
|
|
281
|
+
// Map GraphQL field name to schema field name
|
|
282
|
+
const schemaFieldName = graphqlToSchemaMap[graphqlFieldName];
|
|
283
|
+
|
|
284
|
+
// Check if this field is expandable
|
|
285
|
+
if (!schemaFieldName || !expandPolicy[schemaFieldName]) {
|
|
286
|
+
console.log(`[expandResolver] Field '${graphqlFieldName}' is not expandable (schemaFieldName: ${schemaFieldName}, has policy: ${!!expandPolicy[schemaFieldName]})`);
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log(`[expandResolver] Expanding field '${graphqlFieldName}' (schema: ${schemaFieldName})`);
|
|
291
|
+
return resolveExpand({
|
|
292
|
+
parentItems,
|
|
293
|
+
parentModel,
|
|
294
|
+
field: schemaFieldName,
|
|
295
|
+
args: {},
|
|
296
|
+
});
|
|
297
|
+
})
|
|
298
|
+
.filter(Boolean); // Remove null entries
|
|
299
|
+
|
|
300
|
+
console.log(`[expandResolver] Executing ${expandPromises.length} expand promises`);
|
|
301
|
+
await Promise.all(expandPromises);
|
|
302
|
+
console.log(`[expandResolver] All expands completed`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Handle standard graphql-parse-resolve-info format
|
|
160
307
|
if (!fieldsByTypeName || Object.keys(fieldsByTypeName).length === 0) {
|
|
308
|
+
console.log(`[expandResolver] No fieldsByTypeName, skipping`);
|
|
161
309
|
return;
|
|
162
310
|
}
|
|
163
311
|
|
|
@@ -166,12 +314,13 @@ export async function resolveExpands(
|
|
|
166
314
|
const fields = fieldsByTypeName[typeName];
|
|
167
315
|
|
|
168
316
|
if (!fields) {
|
|
317
|
+
console.log(`[expandResolver] No fields found for type ${typeName}`);
|
|
169
318
|
return;
|
|
170
319
|
}
|
|
171
320
|
|
|
172
321
|
// Get expand policy to filter only expandable fields
|
|
173
322
|
const expandPolicy = extractExpandPolicy(parentModel.schema);
|
|
174
|
-
|
|
323
|
+
|
|
175
324
|
// Create mapping from GraphQL field names to schema field names
|
|
176
325
|
const graphqlToSchemaMap = createGraphQLToSchemaFieldMap(parentModel.schema);
|
|
177
326
|
|
|
@@ -183,6 +332,7 @@ export async function resolveExpands(
|
|
|
183
332
|
|
|
184
333
|
// Check if this field is expandable
|
|
185
334
|
if (!schemaFieldName || !expandPolicy[schemaFieldName]) {
|
|
335
|
+
console.log(`[expandResolver] Field '${graphqlFieldName}' is not expandable (schemaFieldName: ${schemaFieldName}, has policy: ${!!expandPolicy[schemaFieldName]})`);
|
|
186
336
|
return null;
|
|
187
337
|
}
|
|
188
338
|
|
|
@@ -33,6 +33,7 @@ describe("ExpandResolver", () => {
|
|
|
33
33
|
expand: {
|
|
34
34
|
type: "Unit",
|
|
35
35
|
relation: "ONE",
|
|
36
|
+
foreignKey: "unitId", // Field in parent that references the child
|
|
36
37
|
defaultLimit: 1,
|
|
37
38
|
maxLimit: 1,
|
|
38
39
|
},
|
|
@@ -177,6 +178,22 @@ describe("ExpandResolver", () => {
|
|
|
177
178
|
expect(parentItems[0].Units[1].__typename).toBe("Units");
|
|
178
179
|
});
|
|
179
180
|
|
|
181
|
+
it("should use foreignKey to filter for specific item in ONE relation", async () => {
|
|
182
|
+
await resolveExpand({
|
|
183
|
+
parentItems,
|
|
184
|
+
parentModel,
|
|
185
|
+
field: "unitId",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Should have expanded exactly one unit
|
|
189
|
+
expect(parentItems[0].Unit).toBeDefined();
|
|
190
|
+
expect(parentItems[0].Unit.unitId).toBe("unit-1");
|
|
191
|
+
expect(parentItems[0].Unit.name).toBe("Unit A");
|
|
192
|
+
|
|
193
|
+
// Verify that buildGridQuery was called with a filterModel
|
|
194
|
+
// (This would require spy/mock inspection in a more detailed test)
|
|
195
|
+
});
|
|
196
|
+
|
|
180
197
|
it("should fallback to field name when graphql.type is not defined", async () => {
|
|
181
198
|
// Clear and re-register with different config
|
|
182
199
|
modelRegistry.clear();
|