dynamo-query-engine 1.0.4 → 1.0.6
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
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import "../types/jsdoc.js";
|
|
6
6
|
import { modelRegistry } from "./modelRegistry.js";
|
|
7
|
-
import { buildGridQuery } from "./gridQueryBuilder.js";
|
|
8
|
-
import { validateExpandField } from "../utils/validation.js";
|
|
9
|
-
import { extractGridConfig } from "./gridQueryBuilder.js";
|
|
7
|
+
import { buildGridQuery, extractGridConfig } from "./gridQueryBuilder.js";
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Extract expand policy from schema
|
|
@@ -62,6 +60,15 @@ export async function resolveExpand({
|
|
|
62
60
|
// Get target model from registry
|
|
63
61
|
const targetModel = modelRegistry.get(policy.type);
|
|
64
62
|
|
|
63
|
+
// Get the GraphQL type name from the schema (if defined)
|
|
64
|
+
const attributes = parentModel.schema.getAttributes();
|
|
65
|
+
const fieldConfig = attributes[field];
|
|
66
|
+
const graphqlTypeName = fieldConfig?.graphql?.type || policy.type;
|
|
67
|
+
|
|
68
|
+
// Determine which property name to use for storing the expanded data
|
|
69
|
+
// If graphql.type is defined, use it as the property name, otherwise use the field name
|
|
70
|
+
const expandPropertyName = fieldConfig?.graphql?.type || field;
|
|
71
|
+
|
|
65
72
|
// Determine limit based on args and policy
|
|
66
73
|
const requestedLimit = args.pagination?.pageSize ?? policy.defaultLimit;
|
|
67
74
|
const limit = Math.min(requestedLimit, policy.maxLimit);
|
|
@@ -77,7 +84,7 @@ export async function resolveExpand({
|
|
|
77
84
|
console.warn(
|
|
78
85
|
`Cannot expand '${field}' for parent: missing partition key value`
|
|
79
86
|
);
|
|
80
|
-
parent[
|
|
87
|
+
parent[expandPropertyName] = [];
|
|
81
88
|
return;
|
|
82
89
|
}
|
|
83
90
|
|
|
@@ -94,16 +101,22 @@ export async function resolveExpand({
|
|
|
94
101
|
// Execute query
|
|
95
102
|
const results = await query.exec();
|
|
96
103
|
|
|
104
|
+
// Add __typename to each result
|
|
105
|
+
const resultsWithTypename = results.map((item) => ({
|
|
106
|
+
...item,
|
|
107
|
+
__typename: graphqlTypeName,
|
|
108
|
+
}));
|
|
109
|
+
|
|
97
110
|
// Assign results to parent
|
|
98
111
|
if (policy.relation === "ONE") {
|
|
99
|
-
parent[
|
|
112
|
+
parent[expandPropertyName] = resultsWithTypename.length > 0 ? resultsWithTypename[0] : null;
|
|
100
113
|
} else {
|
|
101
|
-
parent[
|
|
114
|
+
parent[expandPropertyName] = resultsWithTypename;
|
|
102
115
|
}
|
|
103
116
|
} catch (error) {
|
|
104
117
|
console.error(`Error expanding '${field}' for parent:`, error);
|
|
105
118
|
// Gracefully handle errors - set empty result
|
|
106
|
-
parent[
|
|
119
|
+
parent[expandPropertyName] = policy.relation === "ONE" ? null : [];
|
|
107
120
|
}
|
|
108
121
|
});
|
|
109
122
|
|
|
@@ -111,6 +124,27 @@ export async function resolveExpand({
|
|
|
111
124
|
await Promise.all(expandPromises);
|
|
112
125
|
}
|
|
113
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Create a mapping from GraphQL field names to schema field names
|
|
129
|
+
* @param {*} schema - Dynamoose schema
|
|
130
|
+
* @returns {Object.<string, string>} Map of GraphQL field name to schema field name
|
|
131
|
+
*/
|
|
132
|
+
function createGraphQLToSchemaFieldMap(schema) {
|
|
133
|
+
const attributes = schema.getAttributes();
|
|
134
|
+
const map = {};
|
|
135
|
+
|
|
136
|
+
for (const [fieldName, config] of Object.entries(attributes)) {
|
|
137
|
+
// If the field has a graphql.type config, map from graphql type to field name
|
|
138
|
+
if (config?.graphql?.type) {
|
|
139
|
+
map[config.graphql.type] = fieldName;
|
|
140
|
+
}
|
|
141
|
+
// Also keep the original field name mapping (for backward compatibility)
|
|
142
|
+
map[fieldName] = fieldName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return map;
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
/**
|
|
115
149
|
* Resolve multiple expands from GraphQL resolve info
|
|
116
150
|
* @param {Array} parentItems - Parent items to expand
|
|
@@ -137,18 +171,29 @@ export async function resolveExpands(
|
|
|
137
171
|
|
|
138
172
|
// Get expand policy to filter only expandable fields
|
|
139
173
|
const expandPolicy = extractExpandPolicy(parentModel.schema);
|
|
174
|
+
|
|
175
|
+
// Create mapping from GraphQL field names to schema field names
|
|
176
|
+
const graphqlToSchemaMap = createGraphQLToSchemaFieldMap(parentModel.schema);
|
|
140
177
|
|
|
141
178
|
// Resolve all expands in parallel
|
|
142
179
|
const expandPromises = Object.entries(fields)
|
|
143
|
-
.
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
.map(([graphqlFieldName, fieldInfo]) => {
|
|
181
|
+
// Map GraphQL field name to schema field name
|
|
182
|
+
const schemaFieldName = graphqlToSchemaMap[graphqlFieldName];
|
|
183
|
+
|
|
184
|
+
// Check if this field is expandable
|
|
185
|
+
if (!schemaFieldName || !expandPolicy[schemaFieldName]) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return resolveExpand({
|
|
146
190
|
parentItems,
|
|
147
191
|
parentModel,
|
|
148
|
-
field:
|
|
192
|
+
field: schemaFieldName,
|
|
149
193
|
args: fieldInfo.args || {},
|
|
150
|
-
})
|
|
151
|
-
)
|
|
194
|
+
});
|
|
195
|
+
})
|
|
196
|
+
.filter(Boolean); // Remove null entries
|
|
152
197
|
|
|
153
198
|
await Promise.all(expandPromises);
|
|
154
199
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
validateFilterField,
|
|
10
10
|
validateSortField,
|
|
11
11
|
validateFilterOperator,
|
|
12
|
-
validatePaginationModel,
|
|
13
12
|
} from "../utils/validation.js";
|
|
14
13
|
import { decodeCursor, validateCursor } from "../utils/cursor.js";
|
|
15
14
|
|
|
@@ -18,13 +17,13 @@ import { decodeCursor, validateCursor } from "../utils/cursor.js";
|
|
|
18
17
|
* @param {Object} params - Query parameters
|
|
19
18
|
* @param {FilterModel} params.filterModel - Filter model
|
|
20
19
|
* @param {SortModel} params.sortModel - Sort model
|
|
21
|
-
* @param {number} params.
|
|
20
|
+
* @param {number} params.limit - Query limit
|
|
22
21
|
* @returns {string} SHA256 hash of the query parameters
|
|
23
22
|
*/
|
|
24
|
-
function hashQuery({ filterModel, sortModel,
|
|
23
|
+
function hashQuery({ filterModel, sortModel, limit }) {
|
|
25
24
|
return crypto
|
|
26
25
|
.createHash("sha256")
|
|
27
|
-
.update(JSON.stringify({ filterModel, sortModel,
|
|
26
|
+
.update(JSON.stringify({ filterModel, sortModel, limit }))
|
|
28
27
|
.digest("hex");
|
|
29
28
|
}
|
|
30
29
|
|
|
@@ -199,7 +198,7 @@ export function buildGridQuery({
|
|
|
199
198
|
partitionKeyValue,
|
|
200
199
|
filterModel,
|
|
201
200
|
sortModel,
|
|
202
|
-
|
|
201
|
+
limit = 10,
|
|
203
202
|
cursor,
|
|
204
203
|
}) {
|
|
205
204
|
// Validate inputs
|
|
@@ -209,7 +208,21 @@ export function buildGridQuery({
|
|
|
209
208
|
if (!partitionKeyValue) {
|
|
210
209
|
throw new Error("partitionKeyValue is required");
|
|
211
210
|
}
|
|
212
|
-
|
|
211
|
+
|
|
212
|
+
// Validate and normalize limit
|
|
213
|
+
if (limit !== undefined && limit !== null) {
|
|
214
|
+
const numLimit = typeof limit === "string" ? Number(limit) : limit;
|
|
215
|
+
if (Number.isNaN(numLimit) || numLimit < 1) {
|
|
216
|
+
throw new Error("limit must be a positive number");
|
|
217
|
+
}
|
|
218
|
+
if (numLimit > 1000) {
|
|
219
|
+
throw new Error("limit cannot exceed 1000 (DynamoDB limit)");
|
|
220
|
+
}
|
|
221
|
+
limit = numLimit;
|
|
222
|
+
} else {
|
|
223
|
+
limit = 10;
|
|
224
|
+
}
|
|
225
|
+
|
|
213
226
|
validateSingleSort(sortModel);
|
|
214
227
|
|
|
215
228
|
// Extract grid config from schema
|
|
@@ -225,7 +238,7 @@ export function buildGridQuery({
|
|
|
225
238
|
const queryHash = hashQuery({
|
|
226
239
|
filterModel,
|
|
227
240
|
sortModel,
|
|
228
|
-
|
|
241
|
+
limit,
|
|
229
242
|
});
|
|
230
243
|
|
|
231
244
|
// Initialize query with partition key
|
|
@@ -242,8 +255,8 @@ export function buildGridQuery({
|
|
|
242
255
|
// Apply sort
|
|
243
256
|
query = applySort(query, sortModel, gridConfig);
|
|
244
257
|
|
|
245
|
-
// Apply
|
|
246
|
-
query = query.limit(
|
|
258
|
+
// Apply limit
|
|
259
|
+
query = query.limit(limit);
|
|
247
260
|
|
|
248
261
|
// Handle cursor for pagination
|
|
249
262
|
if (cursor) {
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for expand resolver with GraphQL type names
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
6
|
+
import { resolveExpand, resolveExpands } from "../../src/core/expandResolver.js";
|
|
7
|
+
import { modelRegistry } from "../../src/core/modelRegistry.js";
|
|
8
|
+
import { createMockSchema, createMockModel, createMockQuery } from "../mocks/dynamoose.mock.js";
|
|
9
|
+
|
|
10
|
+
describe("ExpandResolver", () => {
|
|
11
|
+
describe("resolveExpand with GraphQL type names", () => {
|
|
12
|
+
let parentModel;
|
|
13
|
+
let targetModel;
|
|
14
|
+
let parentItems;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Clear registry
|
|
18
|
+
modelRegistry.clear();
|
|
19
|
+
|
|
20
|
+
// Create parent schema with unitId field that has graphql.type config
|
|
21
|
+
const parentSchema = createMockSchema({
|
|
22
|
+
tenantId: {
|
|
23
|
+
type: String,
|
|
24
|
+
hashKey: true,
|
|
25
|
+
},
|
|
26
|
+
reservationId: {
|
|
27
|
+
type: String,
|
|
28
|
+
rangeKey: true,
|
|
29
|
+
},
|
|
30
|
+
unitId: {
|
|
31
|
+
type: String,
|
|
32
|
+
query: {
|
|
33
|
+
expand: {
|
|
34
|
+
type: "Unit",
|
|
35
|
+
relation: "ONE",
|
|
36
|
+
defaultLimit: 1,
|
|
37
|
+
maxLimit: 1,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
graphql: {
|
|
41
|
+
type: "Unit", // GraphQL type name
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Create target schema (Unit)
|
|
47
|
+
const targetSchema = createMockSchema({
|
|
48
|
+
tenantId: {
|
|
49
|
+
type: String,
|
|
50
|
+
hashKey: true,
|
|
51
|
+
},
|
|
52
|
+
unitId: {
|
|
53
|
+
type: String,
|
|
54
|
+
rangeKey: true,
|
|
55
|
+
},
|
|
56
|
+
name: {
|
|
57
|
+
type: String,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Create parent model
|
|
62
|
+
parentModel = createMockModel(parentSchema);
|
|
63
|
+
|
|
64
|
+
// Create target model with mock query result
|
|
65
|
+
const unitData = [
|
|
66
|
+
{
|
|
67
|
+
tenantId: "tenant-1",
|
|
68
|
+
unitId: "unit-1",
|
|
69
|
+
name: "Unit A",
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
targetModel = createMockModel(targetSchema, unitData);
|
|
73
|
+
|
|
74
|
+
// Register target model
|
|
75
|
+
modelRegistry.register("Unit", targetModel);
|
|
76
|
+
|
|
77
|
+
// Create parent items
|
|
78
|
+
parentItems = [
|
|
79
|
+
{
|
|
80
|
+
tenantId: "tenant-1",
|
|
81
|
+
reservationId: "res-1",
|
|
82
|
+
unitId: "unit-1",
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should store expanded data under GraphQL type name (Unit) not field name (unitId)", async () => {
|
|
88
|
+
await resolveExpand({
|
|
89
|
+
parentItems,
|
|
90
|
+
parentModel,
|
|
91
|
+
field: "unitId",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// The expanded data should be under 'Unit', not 'unitId'
|
|
95
|
+
expect(parentItems[0].Unit).toBeDefined();
|
|
96
|
+
expect(parentItems[0].Unit).toEqual({
|
|
97
|
+
tenantId: "tenant-1",
|
|
98
|
+
unitId: "unit-1",
|
|
99
|
+
name: "Unit A",
|
|
100
|
+
__typename: "Unit",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should add __typename field to expanded data", async () => {
|
|
105
|
+
await resolveExpand({
|
|
106
|
+
parentItems,
|
|
107
|
+
parentModel,
|
|
108
|
+
field: "unitId",
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(parentItems[0].Unit.__typename).toBe("Unit");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should handle ONE relation correctly", async () => {
|
|
115
|
+
await resolveExpand({
|
|
116
|
+
parentItems,
|
|
117
|
+
parentModel,
|
|
118
|
+
field: "unitId",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ONE relation should return a single object, not an array
|
|
122
|
+
expect(parentItems[0].Unit).toBeDefined();
|
|
123
|
+
expect(Array.isArray(parentItems[0].Unit)).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should handle MANY relation correctly", async () => {
|
|
127
|
+
// Clear and re-register with different config
|
|
128
|
+
modelRegistry.clear();
|
|
129
|
+
|
|
130
|
+
// Update parent schema for MANY relation
|
|
131
|
+
const parentSchemaMany = createMockSchema({
|
|
132
|
+
tenantId: {
|
|
133
|
+
type: String,
|
|
134
|
+
hashKey: true,
|
|
135
|
+
},
|
|
136
|
+
reservationId: {
|
|
137
|
+
type: String,
|
|
138
|
+
rangeKey: true,
|
|
139
|
+
},
|
|
140
|
+
unitId: {
|
|
141
|
+
type: String,
|
|
142
|
+
query: {
|
|
143
|
+
expand: {
|
|
144
|
+
type: "Unit",
|
|
145
|
+
relation: "MANY",
|
|
146
|
+
defaultLimit: 10,
|
|
147
|
+
maxLimit: 100,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
graphql: {
|
|
151
|
+
type: "Units", // Plural for MANY relation
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const unitsData = [
|
|
157
|
+
{ tenantId: "tenant-1", unitId: "unit-1", name: "Unit A" },
|
|
158
|
+
{ tenantId: "tenant-1", unitId: "unit-2", name: "Unit B" },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const parentModelMany = createMockModel(parentSchemaMany);
|
|
162
|
+
const targetModelMany = createMockModel(targetModel.schema, unitsData);
|
|
163
|
+
|
|
164
|
+
modelRegistry.register("Unit", targetModelMany);
|
|
165
|
+
|
|
166
|
+
await resolveExpand({
|
|
167
|
+
parentItems,
|
|
168
|
+
parentModel: parentModelMany,
|
|
169
|
+
field: "unitId",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// MANY relation should return an array
|
|
173
|
+
expect(parentItems[0].Units).toBeDefined();
|
|
174
|
+
expect(Array.isArray(parentItems[0].Units)).toBe(true);
|
|
175
|
+
expect(parentItems[0].Units).toHaveLength(2);
|
|
176
|
+
expect(parentItems[0].Units[0].__typename).toBe("Units");
|
|
177
|
+
expect(parentItems[0].Units[1].__typename).toBe("Units");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should fallback to field name when graphql.type is not defined", async () => {
|
|
181
|
+
// Clear and re-register with different config
|
|
182
|
+
modelRegistry.clear();
|
|
183
|
+
|
|
184
|
+
// Create schema without graphql.type config
|
|
185
|
+
const parentSchemaNoGraphQL = createMockSchema({
|
|
186
|
+
tenantId: {
|
|
187
|
+
type: String,
|
|
188
|
+
hashKey: true,
|
|
189
|
+
},
|
|
190
|
+
reservationId: {
|
|
191
|
+
type: String,
|
|
192
|
+
rangeKey: true,
|
|
193
|
+
},
|
|
194
|
+
unitId: {
|
|
195
|
+
type: String,
|
|
196
|
+
query: {
|
|
197
|
+
expand: {
|
|
198
|
+
type: "Unit",
|
|
199
|
+
relation: "ONE",
|
|
200
|
+
defaultLimit: 1,
|
|
201
|
+
maxLimit: 1,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
// No graphql config
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const unitData = [
|
|
209
|
+
{ tenantId: "tenant-1", unitId: "unit-1", name: "Unit A" },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const parentModelNoGraphQL = createMockModel(parentSchemaNoGraphQL);
|
|
213
|
+
const targetModelNoGraphQL = createMockModel(targetModel.schema, unitData);
|
|
214
|
+
|
|
215
|
+
modelRegistry.register("Unit", targetModelNoGraphQL);
|
|
216
|
+
|
|
217
|
+
await resolveExpand({
|
|
218
|
+
parentItems,
|
|
219
|
+
parentModel: parentModelNoGraphQL,
|
|
220
|
+
field: "unitId",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Should fallback to field name 'unitId'
|
|
224
|
+
expect(parentItems[0].unitId).toBeDefined();
|
|
225
|
+
expect(parentItems[0].unitId.__typename).toBe("Unit");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("resolveExpands with GraphQL field mapping", () => {
|
|
230
|
+
let parentModel;
|
|
231
|
+
let targetModel;
|
|
232
|
+
let parentItems;
|
|
233
|
+
|
|
234
|
+
beforeEach(() => {
|
|
235
|
+
modelRegistry.clear();
|
|
236
|
+
|
|
237
|
+
const parentSchema = createMockSchema({
|
|
238
|
+
tenantId: {
|
|
239
|
+
type: String,
|
|
240
|
+
hashKey: true,
|
|
241
|
+
},
|
|
242
|
+
reservationId: {
|
|
243
|
+
type: String,
|
|
244
|
+
rangeKey: true,
|
|
245
|
+
},
|
|
246
|
+
unitId: {
|
|
247
|
+
type: String,
|
|
248
|
+
query: {
|
|
249
|
+
expand: {
|
|
250
|
+
type: "Unit",
|
|
251
|
+
relation: "ONE",
|
|
252
|
+
defaultLimit: 1,
|
|
253
|
+
maxLimit: 1,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
graphql: {
|
|
257
|
+
type: "Unit",
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const targetSchema = createMockSchema({
|
|
263
|
+
tenantId: {
|
|
264
|
+
type: String,
|
|
265
|
+
hashKey: true,
|
|
266
|
+
},
|
|
267
|
+
unitId: {
|
|
268
|
+
type: String,
|
|
269
|
+
rangeKey: true,
|
|
270
|
+
},
|
|
271
|
+
name: {
|
|
272
|
+
type: String,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const unitData = [
|
|
277
|
+
{ tenantId: "tenant-1", unitId: "unit-1", name: "Unit A" },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
parentModel = createMockModel(parentSchema);
|
|
281
|
+
targetModel = createMockModel(targetSchema, unitData);
|
|
282
|
+
|
|
283
|
+
modelRegistry.register("Unit", targetModel);
|
|
284
|
+
|
|
285
|
+
parentItems = [
|
|
286
|
+
{
|
|
287
|
+
tenantId: "tenant-1",
|
|
288
|
+
reservationId: "res-1",
|
|
289
|
+
unitId: "unit-1",
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should map GraphQL field name (Unit) to schema field name (unitId)", async () => {
|
|
295
|
+
// Simulate GraphQL resolve info with field name 'Unit'
|
|
296
|
+
const fieldsByTypeName = {
|
|
297
|
+
Reservation: {
|
|
298
|
+
Unit: {
|
|
299
|
+
// GraphQL field name
|
|
300
|
+
fieldsByTypeName: {
|
|
301
|
+
Unit: {
|
|
302
|
+
unitId: {},
|
|
303
|
+
name: {},
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
await resolveExpands(parentItems, parentModel, fieldsByTypeName);
|
|
311
|
+
|
|
312
|
+
// Should map 'Unit' -> 'unitId' and store result under 'Unit'
|
|
313
|
+
expect(parentItems[0].Unit).toBeDefined();
|
|
314
|
+
expect(parentItems[0].Unit.__typename).toBe("Unit");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("should skip non-expandable fields", async () => {
|
|
318
|
+
const fieldsByTypeName = {
|
|
319
|
+
Reservation: {
|
|
320
|
+
reservationId: {}, // Not an expandable field
|
|
321
|
+
Unit: {
|
|
322
|
+
fieldsByTypeName: {
|
|
323
|
+
Unit: {
|
|
324
|
+
unitId: {},
|
|
325
|
+
name: {},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
await resolveExpands(parentItems, parentModel, fieldsByTypeName);
|
|
333
|
+
|
|
334
|
+
// Only Unit should be expanded
|
|
335
|
+
expect(parentItems[0].Unit).toBeDefined();
|
|
336
|
+
expect(parentItems[0].reservationId).toBe("res-1"); // Original value unchanged
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -189,7 +189,7 @@ describe("GridQueryBuilder", () => {
|
|
|
189
189
|
const { query } = buildGridQuery({
|
|
190
190
|
model: mockModel,
|
|
191
191
|
partitionKeyValue: "ORG#123",
|
|
192
|
-
|
|
192
|
+
limit: 20,
|
|
193
193
|
});
|
|
194
194
|
|
|
195
195
|
expect(mockModel.query).toHaveBeenCalledWith("pk");
|
|
@@ -203,7 +203,7 @@ describe("GridQueryBuilder", () => {
|
|
|
203
203
|
buildGridQuery({
|
|
204
204
|
model: null,
|
|
205
205
|
partitionKeyValue: "ORG#123",
|
|
206
|
-
|
|
206
|
+
limit: 20,
|
|
207
207
|
});
|
|
208
208
|
}).toThrow("model is required");
|
|
209
209
|
});
|
|
@@ -213,18 +213,19 @@ describe("GridQueryBuilder", () => {
|
|
|
213
213
|
buildGridQuery({
|
|
214
214
|
model: mockModel,
|
|
215
215
|
partitionKeyValue: null,
|
|
216
|
-
|
|
216
|
+
limit: 20,
|
|
217
217
|
});
|
|
218
218
|
}).toThrow("partitionKeyValue is required");
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
it("should
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
221
|
+
it("should use default limit when not provided", () => {
|
|
222
|
+
const { query } = buildGridQuery({
|
|
223
|
+
model: mockModel,
|
|
224
|
+
partitionKeyValue: "ORG#123",
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(10);
|
|
228
|
+
expect(query).toBe(mockQuery);
|
|
228
229
|
});
|
|
229
230
|
|
|
230
231
|
it("should use custom hash key name from schema", () => {
|
|
@@ -248,7 +249,7 @@ describe("GridQueryBuilder", () => {
|
|
|
248
249
|
buildGridQuery({
|
|
249
250
|
model: customMockModel,
|
|
250
251
|
partitionKeyValue: "tenant-123",
|
|
251
|
-
|
|
252
|
+
limit: 20,
|
|
252
253
|
});
|
|
253
254
|
|
|
254
255
|
// Should query with 'tenantId' not 'pk'
|
|
@@ -277,7 +278,7 @@ describe("GridQueryBuilder", () => {
|
|
|
277
278
|
model: mockModel,
|
|
278
279
|
partitionKeyValue: "ORG#123",
|
|
279
280
|
filterModel,
|
|
280
|
-
|
|
281
|
+
limit: 20,
|
|
281
282
|
});
|
|
282
283
|
|
|
283
284
|
expect(mockQuery.where).toHaveBeenCalledWith("status");
|
|
@@ -295,7 +296,7 @@ describe("GridQueryBuilder", () => {
|
|
|
295
296
|
model: mockModel,
|
|
296
297
|
partitionKeyValue: "ORG#123",
|
|
297
298
|
filterModel,
|
|
298
|
-
|
|
299
|
+
limit: 20,
|
|
299
300
|
});
|
|
300
301
|
|
|
301
302
|
expect(mockQuery.where).toHaveBeenCalledWith("createdAt");
|
|
@@ -317,7 +318,7 @@ describe("GridQueryBuilder", () => {
|
|
|
317
318
|
model: mockModel,
|
|
318
319
|
partitionKeyValue: "ORG#123",
|
|
319
320
|
filterModel,
|
|
320
|
-
|
|
321
|
+
limit: 20,
|
|
321
322
|
});
|
|
322
323
|
|
|
323
324
|
expect(mockQuery.between).toHaveBeenCalledWith("2024-01-01", "2024-12-31");
|
|
@@ -332,7 +333,7 @@ describe("GridQueryBuilder", () => {
|
|
|
332
333
|
model: mockModel,
|
|
333
334
|
partitionKeyValue: "ORG#123",
|
|
334
335
|
filterModel,
|
|
335
|
-
|
|
336
|
+
limit: 20,
|
|
336
337
|
});
|
|
337
338
|
|
|
338
339
|
expect(mockQuery.using).toHaveBeenCalledWith("status-createdAt-index");
|
|
@@ -348,7 +349,7 @@ describe("GridQueryBuilder", () => {
|
|
|
348
349
|
model: mockModel,
|
|
349
350
|
partitionKeyValue: "ORG#123",
|
|
350
351
|
filterModel,
|
|
351
|
-
|
|
352
|
+
limit: 20,
|
|
352
353
|
});
|
|
353
354
|
}).toThrow("Filtering not allowed");
|
|
354
355
|
});
|
|
@@ -363,7 +364,7 @@ describe("GridQueryBuilder", () => {
|
|
|
363
364
|
model: mockModel,
|
|
364
365
|
partitionKeyValue: "ORG#123",
|
|
365
366
|
filterModel,
|
|
366
|
-
|
|
367
|
+
limit: 20,
|
|
367
368
|
});
|
|
368
369
|
}).toThrow("not allowed");
|
|
369
370
|
});
|
|
@@ -387,7 +388,7 @@ describe("GridQueryBuilder", () => {
|
|
|
387
388
|
model: mockModel,
|
|
388
389
|
partitionKeyValue: "ORG#123",
|
|
389
390
|
sortModel,
|
|
390
|
-
|
|
391
|
+
limit: 20,
|
|
391
392
|
});
|
|
392
393
|
|
|
393
394
|
expect(mockQuery.sort).toHaveBeenCalledWith("descending");
|
|
@@ -400,7 +401,7 @@ describe("GridQueryBuilder", () => {
|
|
|
400
401
|
model: mockModel,
|
|
401
402
|
partitionKeyValue: "ORG#123",
|
|
402
403
|
sortModel,
|
|
403
|
-
|
|
404
|
+
limit: 20,
|
|
404
405
|
});
|
|
405
406
|
|
|
406
407
|
// Ascending is default, so sort() should not be called
|
|
@@ -418,7 +419,7 @@ describe("GridQueryBuilder", () => {
|
|
|
418
419
|
model: mockModel,
|
|
419
420
|
partitionKeyValue: "ORG#123",
|
|
420
421
|
sortModel,
|
|
421
|
-
|
|
422
|
+
limit: 20,
|
|
422
423
|
});
|
|
423
424
|
}).toThrow(/sorting by one field/);
|
|
424
425
|
});
|
|
@@ -431,13 +432,13 @@ describe("GridQueryBuilder", () => {
|
|
|
431
432
|
model: mockModel,
|
|
432
433
|
partitionKeyValue: "ORG#123",
|
|
433
434
|
sortModel,
|
|
434
|
-
|
|
435
|
+
limit: 20,
|
|
435
436
|
});
|
|
436
437
|
}).toThrow("Sorting not allowed");
|
|
437
438
|
});
|
|
438
439
|
});
|
|
439
440
|
|
|
440
|
-
describe("buildGridQuery -
|
|
441
|
+
describe("buildGridQuery - Limit and Cursor", () => {
|
|
441
442
|
let mockModel;
|
|
442
443
|
let mockQuery;
|
|
443
444
|
|
|
@@ -448,11 +449,11 @@ describe("GridQueryBuilder", () => {
|
|
|
448
449
|
mockModel.query = vi.fn(() => mockQuery);
|
|
449
450
|
});
|
|
450
451
|
|
|
451
|
-
it("should apply
|
|
452
|
+
it("should apply limit", () => {
|
|
452
453
|
buildGridQuery({
|
|
453
454
|
model: mockModel,
|
|
454
455
|
partitionKeyValue: "ORG#123",
|
|
455
|
-
|
|
456
|
+
limit: 50,
|
|
456
457
|
});
|
|
457
458
|
|
|
458
459
|
expect(mockQuery.limit).toHaveBeenCalledWith(50);
|
|
@@ -465,7 +466,7 @@ describe("GridQueryBuilder", () => {
|
|
|
465
466
|
const firstResult = buildGridQuery({
|
|
466
467
|
model: mockModel,
|
|
467
468
|
partitionKeyValue: "ORG#123",
|
|
468
|
-
|
|
469
|
+
limit: 20,
|
|
469
470
|
});
|
|
470
471
|
|
|
471
472
|
// Create cursor with the actual queryHash
|
|
@@ -477,7 +478,7 @@ describe("GridQueryBuilder", () => {
|
|
|
477
478
|
buildGridQuery({
|
|
478
479
|
model: mockModel,
|
|
479
480
|
partitionKeyValue: "ORG#123",
|
|
480
|
-
|
|
481
|
+
limit: 20,
|
|
481
482
|
cursor,
|
|
482
483
|
});
|
|
483
484
|
|
|
@@ -496,7 +497,7 @@ describe("GridQueryBuilder", () => {
|
|
|
496
497
|
buildGridQuery({
|
|
497
498
|
model: mockModel,
|
|
498
499
|
partitionKeyValue: "ORG#123",
|
|
499
|
-
|
|
500
|
+
limit: 50, // Different limit
|
|
500
501
|
cursor,
|
|
501
502
|
});
|
|
502
503
|
}).toThrow("Cursor does not match");
|
|
@@ -519,7 +520,7 @@ describe("GridQueryBuilder", () => {
|
|
|
519
520
|
items: [{ field: "status", operator: "eq", value: "active" }],
|
|
520
521
|
},
|
|
521
522
|
sortModel: [{ field: "createdAt", sort: "desc" }],
|
|
522
|
-
|
|
523
|
+
limit: 20,
|
|
523
524
|
};
|
|
524
525
|
|
|
525
526
|
const result1 = buildGridQuery(params);
|
|
@@ -532,7 +533,7 @@ describe("GridQueryBuilder", () => {
|
|
|
532
533
|
const baseParams = {
|
|
533
534
|
model: mockModel,
|
|
534
535
|
partitionKeyValue: "ORG#123",
|
|
535
|
-
|
|
536
|
+
limit: 20,
|
|
536
537
|
};
|
|
537
538
|
|
|
538
539
|
const result1 = buildGridQuery({
|
|
@@ -552,7 +553,7 @@ describe("GridQueryBuilder", () => {
|
|
|
552
553
|
expect(result1.queryHash).not.toBe(result2.queryHash);
|
|
553
554
|
});
|
|
554
555
|
|
|
555
|
-
it("should return different hash when
|
|
556
|
+
it("should return different hash when limit changes", () => {
|
|
556
557
|
const baseParams = {
|
|
557
558
|
model: mockModel,
|
|
558
559
|
partitionKeyValue: "ORG#123",
|
|
@@ -560,12 +561,12 @@ describe("GridQueryBuilder", () => {
|
|
|
560
561
|
|
|
561
562
|
const result1 = buildGridQuery({
|
|
562
563
|
...baseParams,
|
|
563
|
-
|
|
564
|
+
limit: 20,
|
|
564
565
|
});
|
|
565
566
|
|
|
566
567
|
const result2 = buildGridQuery({
|
|
567
568
|
...baseParams,
|
|
568
|
-
|
|
569
|
+
limit: 50,
|
|
569
570
|
});
|
|
570
571
|
|
|
571
572
|
expect(result1.queryHash).not.toBe(result2.queryHash);
|
|
@@ -583,7 +584,7 @@ describe("GridQueryBuilder", () => {
|
|
|
583
584
|
mockModel.query = vi.fn(() => mockQuery);
|
|
584
585
|
});
|
|
585
586
|
|
|
586
|
-
it("should handle query with filter, sort, and
|
|
587
|
+
it("should handle query with filter, sort, and limit", () => {
|
|
587
588
|
buildGridQuery({
|
|
588
589
|
model: mockModel,
|
|
589
590
|
partitionKeyValue: "ORG#123",
|
|
@@ -591,7 +592,7 @@ describe("GridQueryBuilder", () => {
|
|
|
591
592
|
items: [{ field: "status", operator: "eq", value: "active" }],
|
|
592
593
|
},
|
|
593
594
|
sortModel: [{ field: "createdAt", sort: "desc" }],
|
|
594
|
-
|
|
595
|
+
limit: 25,
|
|
595
596
|
});
|
|
596
597
|
|
|
597
598
|
// Verify all operations were applied
|
|
@@ -609,7 +610,7 @@ describe("GridQueryBuilder", () => {
|
|
|
609
610
|
partitionKeyValue: "ORG#123",
|
|
610
611
|
filterModel: { items: [] },
|
|
611
612
|
sortModel: [],
|
|
612
|
-
|
|
613
|
+
limit: 20,
|
|
613
614
|
});
|
|
614
615
|
|
|
615
616
|
expect(query).toBeDefined();
|