@aws-amplify/data-schema 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/cjs/runtime/internals/APIClient.js +117 -26
  2. package/dist/cjs/runtime/internals/APIClient.js.map +1 -1
  3. package/dist/cjs/runtime/internals/operations/custom.js +10 -2
  4. package/dist/cjs/runtime/internals/operations/custom.js.map +1 -1
  5. package/dist/cjs/runtime/internals/operations/get.js +6 -3
  6. package/dist/cjs/runtime/internals/operations/get.js.map +1 -1
  7. package/dist/cjs/runtime/internals/operations/indexQuery.js +5 -4
  8. package/dist/cjs/runtime/internals/operations/indexQuery.js.map +1 -1
  9. package/dist/cjs/runtime/internals/operations/list.js +2 -2
  10. package/dist/cjs/runtime/internals/operations/list.js.map +1 -1
  11. package/dist/cjs/runtime/internals/operations/subscription.js +1 -0
  12. package/dist/cjs/runtime/internals/operations/subscription.js.map +1 -1
  13. package/dist/esm/runtime/internals/APIClient.d.ts +41 -3
  14. package/dist/esm/runtime/internals/APIClient.mjs +117 -26
  15. package/dist/esm/runtime/internals/APIClient.mjs.map +1 -1
  16. package/dist/esm/runtime/internals/operations/custom.mjs +11 -3
  17. package/dist/esm/runtime/internals/operations/custom.mjs.map +1 -1
  18. package/dist/esm/runtime/internals/operations/get.mjs +6 -3
  19. package/dist/esm/runtime/internals/operations/get.mjs.map +1 -1
  20. package/dist/esm/runtime/internals/operations/indexQuery.mjs +5 -4
  21. package/dist/esm/runtime/internals/operations/indexQuery.mjs.map +1 -1
  22. package/dist/esm/runtime/internals/operations/list.mjs +2 -2
  23. package/dist/esm/runtime/internals/operations/list.mjs.map +1 -1
  24. package/dist/esm/runtime/internals/operations/subscription.mjs +1 -0
  25. package/dist/esm/runtime/internals/operations/subscription.mjs.map +1 -1
  26. package/dist/meta/cjs.tsbuildinfo +1 -1
  27. package/package.json +1 -1
  28. package/src/runtime/internals/APIClient.ts +151 -48
  29. package/src/runtime/internals/operations/custom.ts +12 -3
  30. package/src/runtime/internals/operations/get.ts +5 -3
  31. package/src/runtime/internals/operations/indexQuery.ts +12 -2
  32. package/src/runtime/internals/operations/list.ts +8 -2
  33. package/src/runtime/internals/operations/subscription.ts +1 -0
@@ -11,6 +11,7 @@ import {
11
11
  GraphQLAuthMode,
12
12
  ClientInternalsGetter,
13
13
  ListArgs,
14
+ Field,
14
15
  ModelFieldType,
15
16
  ModelIntrospectionSchema,
16
17
  NonModelFieldType,
@@ -72,32 +73,117 @@ const resolvedSkName = (sk: string[]): string => {
72
73
  };
73
74
 
74
75
  /**
76
+ * Crawls a model tree, starting with a given **individual** model instance record, looking
77
+ * for related hasMany children to extract from their `items` containers.
75
78
  *
76
- * @param GraphQL response object
77
- * @returns response object with `items` properties flattened
79
+ * E.g., if we have a record like this:
80
+ *
81
+ * ```js
82
+ * {
83
+ * id: 'some-id',
84
+ * children: {
85
+ * items: [
86
+ * { name: 'a' }
87
+ * { name: 'b' }
88
+ * { name: 'c' }
89
+ * ]
90
+ * }
91
+ * }
92
+ * ```
93
+ *
94
+ * And if `children` refers to *an array of another model* (as opposed to a custom type),
95
+ * the `items` will be extracted. We do this because `items` is just the mechanism for nesting
96
+ * child records -- we don't want customers to have to dig the items out in application code.
97
+ * Ultimately, we return this "flattened" structure:
98
+ *
99
+ * ```js
100
+ * {
101
+ * id: 'some-id',
102
+ * children: [
103
+ * { name: 'a' }
104
+ * { name: 'b' }
105
+ * { name: 'c' }
106
+ * ]
107
+ * }
108
+ * ```
109
+ *
110
+ * Notably, an identical record could be the result of a nested custom type that contains an
111
+ * `items` property. This will *not* be flattened, because in that case the `items` property is
112
+ * actually part of the customer's schema. Similarly if a model contains an explicit `items` field.
113
+ *
114
+ * @param modelIntrospection Top-level model introspection schema.
115
+ * @param modelName The name of the model. Can be `undefined`. E.g., for customOperation return types.
116
+ * @param modelRecord The individual "model instance record" to normalize.
78
117
  */
79
- export const flattenItems = (obj: Record<string, any>): Record<string, any> => {
80
- const res: Record<string, any> = {};
81
-
82
- Object.entries(obj).forEach(([prop, value]) => {
83
- if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
84
- if (value.items !== undefined) {
85
- res[prop] = value.items.map((item: Record<string, any>) =>
86
- flattenItems(item),
87
- );
88
-
89
- return;
90
- }
91
- res[prop] = flattenItems(value);
92
-
93
- return;
118
+ export const flattenItems = (
119
+ modelIntrospection: ModelIntrospectionSchema,
120
+ modelName: string | undefined,
121
+ modelRecord: Record<string, any>,
122
+ ): Record<string, any> | null => {
123
+ if (!modelRecord) return null;
124
+
125
+ const mapped = {} as Record<string, any>;
126
+ for (const [fieldName, value] of Object.entries(modelRecord)) {
127
+ const fieldDef = modelName
128
+ ? modelIntrospection.models[modelName]?.fields[fieldName]
129
+ : undefined;
130
+ const dvPair = { fieldDef, value };
131
+ if (isRelatedModelItemsArrayPair(dvPair)) {
132
+ mapped[fieldName] = dvPair.value.items.map((itemValue) =>
133
+ flattenItems(modelIntrospection, dvPair.fieldDef.type.model, itemValue),
134
+ );
135
+ } else if (isRelatedModelProperty(fieldDef)) {
136
+ mapped[fieldName] = flattenItems(
137
+ modelIntrospection,
138
+ fieldDef.type.model,
139
+ value,
140
+ );
141
+ } else {
142
+ mapped[fieldName] = value;
94
143
  }
144
+ }
145
+ return mapped;
146
+ };
95
147
 
96
- res[prop] = value;
97
- });
148
+ /**
149
+ * Determines whether the given field definition and associated result value
150
+ * represent a related model array from a HasMany-type relationship.
151
+ *
152
+ * @param dv Pair of field definition and associated result value
153
+ * @returns
154
+ */
155
+ function isRelatedModelItemsArrayPair(dv: {
156
+ fieldDef: Field | undefined;
157
+ value: any;
158
+ }): dv is {
159
+ fieldDef: Field & { type: ModelFieldType };
160
+ value: { items: Record<string, any>[] };
161
+ } {
162
+ return (
163
+ typeof dv.fieldDef?.type === 'object' &&
164
+ 'model' in dv.fieldDef.type &&
165
+ typeof dv.fieldDef.type.model === 'string' &&
166
+ dv.fieldDef.isArray &&
167
+ Array.isArray(dv.value?.items)
168
+ );
169
+ }
98
170
 
99
- return res;
100
- };
171
+ /**
172
+ * Determines whether the given field definition represents a relationship
173
+ * to another model.
174
+ *
175
+ * @param fieldDef
176
+ * @returns
177
+ */
178
+ function isRelatedModelProperty(
179
+ fieldDef: Field | undefined,
180
+ ): fieldDef is Field & { type: ModelFieldType } {
181
+ return (
182
+ typeof fieldDef?.type === 'object' &&
183
+ 'model' in fieldDef.type &&
184
+ typeof fieldDef.type.model === 'string'
185
+ );
186
+ }
101
187
 
102
188
  // TODO: this should accept single result to support CRUD methods; create helper for array/list
103
189
  export function initializeModel(
@@ -148,7 +234,6 @@ export function initializeModel(
148
234
  }
149
235
 
150
236
  switch (relationType) {
151
- case connectionType.HAS_ONE:
152
237
  case connectionType.BELONGS_TO: {
153
238
  const sortKeyValues = relatedModelSKFieldNames.reduce(
154
239
  // TODO(Eslint): is this implementation correct?
@@ -180,7 +265,7 @@ export function initializeModel(
180
265
  );
181
266
  }
182
267
 
183
- return undefined;
268
+ return { data: null };
184
269
  };
185
270
  } else {
186
271
  initializedRelationalFields[fieldName] = (
@@ -199,13 +284,29 @@ export function initializeModel(
199
284
  );
200
285
  }
201
286
 
202
- return undefined;
287
+ return { data: null };
203
288
  };
204
289
  }
205
290
 
206
291
  break;
207
292
  }
293
+ case connectionType.HAS_ONE:
208
294
  case connectionType.HAS_MANY: {
295
+ /**
296
+ * If the loader is a HAS_ONE, we just need to attempt to grab the first item
297
+ * from the result.
298
+ */
299
+ const mapResult =
300
+ relationType === connectionType.HAS_ONE
301
+ ? (result: Record<string, any>) => {
302
+ return {
303
+ data: result?.data.shift() || null,
304
+ errors: result.errors,
305
+ extensions: result.extensions,
306
+ };
307
+ }
308
+ : (result: Record<string, any>) => result;
309
+
209
310
  const parentPk = introModel.primaryKeyInfo.primaryKeyFieldName;
210
311
  const parentSK = introModel.primaryKeyInfo.sortKeyFieldNames;
211
312
 
@@ -238,16 +339,15 @@ export function initializeModel(
238
339
  options?: LazyLoadOptions,
239
340
  ) => {
240
341
  if (record[parentPk]) {
241
- return (client as any).models[relatedModelName].list(
242
- contextSpec,
243
- {
342
+ return (client as any).models[relatedModelName]
343
+ .list(contextSpec, {
244
344
  filter: { and: hasManyFilter },
245
345
  limit: options?.limit,
246
346
  nextToken: options?.nextToken,
247
347
  authMode: options?.authMode || authMode,
248
348
  authToken: options?.authToken || authToken,
249
- },
250
- );
349
+ })
350
+ .then(mapResult);
251
351
  }
252
352
 
253
353
  return [];
@@ -257,13 +357,15 @@ export function initializeModel(
257
357
  options?: LazyLoadOptions,
258
358
  ) => {
259
359
  if (record[parentPk]) {
260
- return (client as any).models[relatedModelName].list({
261
- filter: { and: hasManyFilter },
262
- limit: options?.limit,
263
- nextToken: options?.nextToken,
264
- authMode: options?.authMode || authMode,
265
- authToken: options?.authToken || authToken,
266
- });
360
+ return (client as any).models[relatedModelName]
361
+ .list({
362
+ filter: { and: hasManyFilter },
363
+ limit: options?.limit,
364
+ nextToken: options?.nextToken,
365
+ authMode: options?.authMode || authMode,
366
+ authToken: options?.authToken || authToken,
367
+ })
368
+ .then(mapResult);
267
369
  }
268
370
 
269
371
  return [];
@@ -289,16 +391,15 @@ export function initializeModel(
289
391
  options?: LazyLoadOptions,
290
392
  ) => {
291
393
  if (record[parentPk]) {
292
- return (client as any).models[relatedModelName].list(
293
- contextSpec,
294
- {
394
+ return (client as any).models[relatedModelName]
395
+ .list(contextSpec, {
295
396
  filter: { and: hasManyFilter },
296
397
  limit: options?.limit,
297
398
  nextToken: options?.nextToken,
298
399
  authMode: options?.authMode || authMode,
299
400
  authToken: options?.authToken || authToken,
300
- },
301
- );
401
+ })
402
+ .then(mapResult);
302
403
  }
303
404
 
304
405
  return [];
@@ -308,13 +409,15 @@ export function initializeModel(
308
409
  options?: LazyLoadOptions,
309
410
  ) => {
310
411
  if (record[parentPk]) {
311
- return (client as any).models[relatedModelName].list({
312
- filter: { and: hasManyFilter },
313
- limit: options?.limit,
314
- nextToken: options?.nextToken,
315
- authMode: options?.authMode || authMode,
316
- authToken: options?.authToken || authToken,
317
- });
412
+ return (client as any).models[relatedModelName]
413
+ .list({
414
+ filter: { and: hasManyFilter },
415
+ limit: options?.limit,
416
+ nextToken: options?.nextToken,
417
+ authMode: options?.authMode || authMode,
418
+ authToken: options?.authToken || authToken,
419
+ })
420
+ .then(mapResult);
318
421
  }
319
422
 
320
423
  return [];
@@ -20,7 +20,6 @@ import { map } from 'rxjs';
20
20
  import {
21
21
  authModeParams,
22
22
  getDefaultSelectionSetForNonModelWithIR,
23
- flattenItems,
24
23
  generateSelectionSet,
25
24
  getCustomHeaders,
26
25
  initializeModel,
@@ -398,7 +397,12 @@ async function _op(
398
397
  // flatten response
399
398
  if (data) {
400
399
  const [key] = Object.keys(data);
401
- const flattenedResult = flattenItems(data)[key];
400
+
401
+ // TODO: when adding support for custom selection set, flattening will need
402
+ // to occur recursively. For now, it's expected that related models are not
403
+ // present in the result. Only FK's are present. Any related model properties
404
+ // should be replaced with lazy loaders under the current implementation.
405
+ const flattenedResult = data[key];
402
406
 
403
407
  // TODO: custom selection set. current selection set is default selection set only
404
408
  // custom selection set requires data-schema-type + runtime updates above.
@@ -433,7 +437,12 @@ async function _op(
433
437
  */
434
438
  if (data && Object.keys(data).length !== 0 && errors) {
435
439
  const [key] = Object.keys(data);
436
- const flattenedResult = flattenItems(data)[key];
440
+
441
+ // TODO: when adding support for custom selection set, flattening will need
442
+ // to occur recursively. For now, it's expected that related models are not
443
+ // present in the result. Only FK's are present. Any related model properties
444
+ // should be replaced with lazy loaders under the current implementation.
445
+ const flattenedResult = data[key];
437
446
 
438
447
  /**
439
448
  * `flattenedResult` could be `null` here (e.g. `data: { getPost: null }`)
@@ -119,9 +119,11 @@ async function _get(
119
119
  // flatten response
120
120
  if (data) {
121
121
  const [key] = Object.keys(data);
122
- const flattenedResult = flattenItems(data)[key];
122
+ const flattenedResult = flattenItems(modelIntrospection, name, data[key]);
123
123
 
124
- if (options?.selectionSet) {
124
+ if (flattenedResult === null) {
125
+ return { data: null, extensions };
126
+ } else if (options?.selectionSet) {
125
127
  return { data: flattenedResult, extensions };
126
128
  } else {
127
129
  // TODO: refactor to avoid destructuring here
@@ -155,7 +157,7 @@ async function _get(
155
157
  */
156
158
  if (data && Object.keys(data).length !== 0 && errors) {
157
159
  const [key] = Object.keys(data);
158
- const flattenedResult = flattenItems(data)[key];
160
+ const flattenedResult = flattenItems(modelIntrospection, name, data[key]);
159
161
 
160
162
  /**
161
163
  * `flattenedResult` could be `null` here (e.g. `data: { getPost: null }`)
@@ -74,6 +74,8 @@ export function indexQueryFactory(
74
74
  }
75
75
 
76
76
  function processGraphQlResponse(
77
+ modelIntroSchema: ModelIntrospectionSchema,
78
+ modelName: string,
77
79
  result: GraphQLResult,
78
80
  selectionSet: undefined | string[],
79
81
  modelInitializer: (flattenedResult: any[]) => any[],
@@ -83,7 +85,9 @@ function processGraphQlResponse(
83
85
  const [key] = Object.keys(data);
84
86
 
85
87
  if (data[key].items) {
86
- const flattenedResult = flattenItems(data)[key];
88
+ const flattenedResult = data[key].items.map((value: Record<string, any>) =>
89
+ flattenItems(modelIntroSchema, modelName, value),
90
+ );
87
91
 
88
92
  return {
89
93
  data: selectionSet ? flattenedResult : modelInitializer(flattenedResult),
@@ -92,6 +96,7 @@ function processGraphQlResponse(
92
96
  };
93
97
  }
94
98
 
99
+ // Index queries are always list queries. No `items`? No flattening needed.
95
100
  return {
96
101
  data: data[key],
97
102
  nextToken: data[key].nextToken,
@@ -159,6 +164,8 @@ async function _indexQuery(
159
164
 
160
165
  if (response.data !== undefined) {
161
166
  return processGraphQlResponse(
167
+ modelIntrospection,
168
+ name,
162
169
  response,
163
170
  args?.selectionSet,
164
171
  modelInitializer,
@@ -181,7 +188,10 @@ async function _indexQuery(
181
188
  const [key] = Object.keys(data);
182
189
 
183
190
  if (data[key]?.items) {
184
- const flattenedResult = flattenItems(data)[key];
191
+ const flattenedResult = data[key]?.items.map(
192
+ (value: Record<string, any>) =>
193
+ flattenItems(modelIntrospection, name, value),
194
+ );
185
195
 
186
196
  /**
187
197
  * Check exists since `flattenedResult` could be `null`.
@@ -99,7 +99,10 @@ async function _list(
99
99
  const [key] = Object.keys(data);
100
100
 
101
101
  if (data[key].items) {
102
- const flattenedResult = flattenItems(data)[key];
102
+ const flattenedResult = data[key].items.map(
103
+ (value: Record<string, any>) =>
104
+ flattenItems(modelIntrospection, name, value),
105
+ );
103
106
 
104
107
  // don't init if custom selection set
105
108
  if (args?.selectionSet) {
@@ -153,7 +156,10 @@ async function _list(
153
156
  const [key] = Object.keys(data);
154
157
 
155
158
  if (data[key]?.items) {
156
- const flattenedResult = flattenItems(data)[key];
159
+ const flattenedResult = data[key].items.map(
160
+ (value: Record<string, any>) =>
161
+ flattenItems(modelIntrospection, name, value),
162
+ );
157
163
 
158
164
  /**
159
165
  * Check exists since `flattenedResult` could be `null`.
@@ -59,6 +59,7 @@ export function subscriptionFactory(
59
59
  return observable.pipe(
60
60
  map((value) => {
61
61
  const [key] = Object.keys(value.data);
62
+ // Will need flattening here if/when custom selection set support is added:
62
63
  const data = (value.data as any)[key];
63
64
  const [initialized] = initializeModel(
64
65
  client,