@@ -139,9 +139,13 @@ const modelNamespaceMap = new WeakMap<
139
139
PersistentModelConstructor < any > ,
140
140
string
141
141
> ( ) ;
142
- // stores data for crafting the correct update mutation input for a model
143
- // Patch[] - array of changed fields and metadata
144
- // PersistentModel - the source model, used for diffing object-type fields
142
+
143
+ /**
144
+ * Stores data for crafting the correct update mutation input for a model.
145
+ *
146
+ * - `Patch[]` - array of changed fields and metadata.
147
+ * - `PersistentModel` - the source model, used for diffing object-type fields.
148
+ */
145
149
const modelPatchesMap = new WeakMap <
146
150
PersistentModel ,
147
151
[ Patch [ ] , PersistentModel ]
@@ -158,17 +162,6 @@ const getModelDefinition = (
158
162
return definition ;
159
163
} ;
160
164
161
- const getModelPKFieldName = (
162
- modelConstructor : PersistentModelConstructor < any >
163
- ) => {
164
- const namespace = modelNamespaceMap . get ( modelConstructor ) ;
165
- return (
166
- ( namespace &&
167
- schema . namespaces ?. [ namespace ] ?. keys ?. [ modelConstructor . name ]
168
- . primaryKey ) || [ 'id' ]
169
- ) ;
170
- } ;
171
-
172
165
/**
173
166
* Determine what the managed timestamp field names are for the given model definition
174
167
* and return the mapping.
@@ -202,14 +195,16 @@ const getTimestampFields = (
202
195
} ;
203
196
} ;
204
197
198
+ /**
199
+ * Determines whether the given object is a Model Constructor that DataStore can
200
+ * safely use to construct objects and discover related metadata.
201
+ *
202
+ * @param obj The object to test.
203
+ */
205
204
const isValidModelConstructor = < T extends PersistentModel > (
206
205
obj : any
207
206
) : obj is PersistentModelConstructor < T > => {
208
- if ( isModelConstructor ( obj ) && modelNamespaceMap . has ( obj ) ) {
209
- return true ;
210
- } else {
211
- return false ;
212
- }
207
+ return isModelConstructor ( obj ) && modelNamespaceMap . has ( obj ) ;
213
208
} ;
214
209
215
210
const namespaceResolver : NamespaceResolver = modelConstructor => {
@@ -222,6 +217,25 @@ const namespaceResolver: NamespaceResolver = modelConstructor => {
222
217
return resolver ;
223
218
} ;
224
219
220
+ /**
221
+ * Creates a predicate without any conditions that can be passed to customer
222
+ * code to have conditions added to it.
223
+ *
224
+ * For example, in this query:
225
+ *
226
+ * ```ts
227
+ * await DataStore.query(
228
+ * Model,
229
+ * item => item.field.eq('value')
230
+ * );
231
+ * ```
232
+ *
233
+ * `buildSeedPredicate(Model)` is used to create `item`, which is passed to the
234
+ * predicate function, which in turn uses that "seed" predicate (`item`) to build
235
+ * a predicate tree.
236
+ *
237
+ * @param modelConstructor The model the predicate will query.
238
+ */
225
239
const buildSeedPredicate = < T extends PersistentModel > (
226
240
modelConstructor : PersistentModelConstructor < T >
227
241
) => {
@@ -232,9 +246,7 @@ const buildSeedPredicate = <T extends PersistentModel>(
232
246
) ;
233
247
if ( ! modelSchema ) throw new Error ( 'Missing modelSchema' ) ;
234
248
235
- const pks = getModelPKFieldName (
236
- modelConstructor as PersistentModelConstructor < T >
237
- ) ;
249
+ const pks = extractPrimaryKeyFieldNames ( modelSchema ) ;
238
250
if ( ! pks ) throw new Error ( 'Could not determine PK' ) ;
239
251
240
252
return recursivePredicateFor < T > ( {
@@ -387,27 +399,23 @@ const initSchema = (userSchema: Schema) => {
387
399
388
400
modelAssociations . set ( model . name , connectedModels ) ;
389
401
402
+ // Precompute model info (such as pk fields) so that downstream schema consumers
403
+ // (such as predicate builders) don't have to reach back into "DataStore" space
404
+ // to go looking for it.
390
405
Object . values ( model . fields ) . forEach ( field => {
391
- if (
392
- typeof field . type === 'object' &&
393
- ! Object . getOwnPropertyDescriptor (
394
- < ModelFieldType > field . type ,
395
- 'modelConstructor'
396
- )
397
- ) {
406
+ const relatedModel = userClasses [ ( < ModelFieldType > field . type ) . model ] ;
407
+ if ( isModelConstructor ( relatedModel ) ) {
398
408
Object . defineProperty ( field . type , 'modelConstructor' , {
399
409
get : ( ) => {
410
+ const relatedModelDefinition = getModelDefinition ( relatedModel ) ;
411
+ if ( ! relatedModelDefinition )
412
+ throw new Error (
413
+ `Could not find model definition for ${ relatedModel . name } `
414
+ ) ;
400
415
return {
401
- builder : userClasses [ ( < ModelFieldType > field . type ) . model ] ,
402
- schema :
403
- schema . namespaces [ namespace ] . models [
404
- ( < ModelFieldType > field . type ) . model
405
- ] ,
406
- pkField : getModelPKFieldName (
407
- userClasses [
408
- ( < ModelFieldType > field . type ) . model
409
- ] as PersistentModelConstructor < any >
410
- ) ,
416
+ builder : relatedModel ,
417
+ schema : relatedModelDefinition ,
418
+ pkField : extractPrimaryKeyFieldNames ( relatedModelDefinition ) ,
411
419
} ;
412
420
} ,
413
421
} ) ;
@@ -548,7 +556,7 @@ const createTypeClasses: (
548
556
549
557
Object . entries ( namespace . nonModels || { } ) . forEach (
550
558
( [ typeName , typeDefinition ] ) => {
551
- const clazz = createNonModelClass ( typeDefinition ) as any ;
559
+ const clazz = createNonModelClass ( typeDefinition ) ;
552
560
classes [ typeName ] = clazz ;
553
561
}
554
562
) ;
@@ -965,25 +973,16 @@ const createModelClass = <T extends PersistentModel>(
965
973
966
974
Object . defineProperty ( clazz , 'name' , { value : modelDefinition . name } ) ;
967
975
968
- for ( const field in modelDefinition . fields ) {
969
- if ( ! isFieldAssociation ( modelDefinition , field ) ) {
970
- continue ;
971
- }
972
-
973
- const {
974
- type,
975
- association : localAssociation ,
976
- association : { targetName, targetNames } ,
977
- } = modelDefinition . fields [ field ] as Required < ModelField > ;
978
-
979
- const relationship = new ModelRelationship (
980
- {
981
- builder : clazz ,
982
- schema : modelDefinition ,
983
- pkField : extractPrimaryKeyFieldNames ( modelDefinition ) ,
984
- } ,
985
- field
986
- ) ;
976
+ // Add getters/setters for relationship fields.
977
+ // getter - for lazy loading
978
+ // setter - for FK management
979
+ const allModelRelationships = ModelRelationship . allFrom ( {
980
+ builder : clazz ,
981
+ schema : modelDefinition ,
982
+ pkField : extractPrimaryKeyFieldNames ( modelDefinition ) ,
983
+ } ) ;
984
+ for ( const relationship of allModelRelationships ) {
985
+ const field = relationship . field ;
987
986
988
987
Object . defineProperty ( clazz . prototype , modelDefinition . fields [ field ] . name , {
989
988
set ( model : PersistentModel | undefined | null ) {
@@ -1018,6 +1017,7 @@ const createModelClass = <T extends PersistentModel>(
1018
1017
}
1019
1018
}
1020
1019
1020
+ // if the relationship can be managed automagically, set the FK's
1021
1021
if ( relationship . isComplete ) {
1022
1022
for ( let i = 0 ; i < relationship . localJoinFields . length ; i ++ ) {
1023
1023
this [ relationship . localJoinFields [ i ] ] =
@@ -1030,27 +1030,46 @@ const createModelClass = <T extends PersistentModel>(
1030
1030
}
1031
1031
} ,
1032
1032
get ( ) {
1033
+ /**
1034
+ * Bucket for holding related models instances specific to `this` instance.
1035
+ */
1033
1036
const instanceMemos = modelInstanceAssociationsMap . has ( this )
1034
1037
? modelInstanceAssociationsMap . get ( this ) !
1035
1038
: modelInstanceAssociationsMap . set ( this , { } ) . get ( this ) ! ;
1036
1039
1040
+ // if the memos already has a result for this field, we'll use it.
1041
+ // there is no "cache" invalidation of any kind; memos are permanent to
1042
+ // keep an immutable perception of the instance.
1037
1043
if ( ! instanceMemos . hasOwnProperty ( field ) ) {
1044
+ // before we populate the memo, we need to know where to look for relatives.
1045
+ // today, this only supports DataStore. Models aren't managed elsewhere in Amplify.
1038
1046
if ( getAttachment ( this ) === ModelAttachment . DataStore ) {
1047
+ // when we fetch the results using a query constructed under the guidance
1048
+ // of the relationship metadata, we DO NOT AWAIT resolution. we want to
1049
+ // drop the promise into the memo's synchronously, eliminating the chance
1050
+ // for a race.
1039
1051
const resultPromise = instance . query (
1040
1052
relationship . remoteModelConstructor as PersistentModelConstructor < T > ,
1041
1053
base =>
1042
1054
base . and ( q => {
1043
1055
return relationship . remoteJoinFields . map ( ( field , index ) => {
1056
+ // TODO: anything we can use instead of `any` here?
1044
1057
return ( q [ field ] as any ) . eq (
1045
1058
this [ relationship . localJoinFields [ index ] ]
1046
1059
) ;
1047
1060
} ) ;
1048
1061
} )
1049
1062
) ;
1050
1063
1064
+ // results in hand, how we return them to the caller depends on the relationship type.
1051
1065
if ( relationship . type === 'HAS_MANY' ) {
1066
+ // collections should support async iteration, even though we don't
1067
+ // leverage it fully [yet].
1052
1068
instanceMemos [ field ] = new AsyncCollection ( resultPromise ) ;
1053
1069
} else {
1070
+ // non-collections should only ever return 1 value *or nothing*.
1071
+ // if we have more than 1 record, something's amiss. it's not our job
1072
+ // pick a result for the customer. it's our job to say "something's wrong."
1054
1073
instanceMemos [ field ] = resultPromise . then ( rows => {
1055
1074
if ( rows . length > 1 ) {
1056
1075
// should never happen for a HAS_ONE or BELONGS_TO.
@@ -1084,15 +1103,34 @@ const createModelClass = <T extends PersistentModel>(
1084
1103
return clazz ;
1085
1104
} ;
1086
1105
1106
+ /**
1107
+ * An eventually loaded related model instance.
1108
+ */
1087
1109
export class AsyncItem < T > extends Promise < T > { }
1088
1110
1111
+ /**
1112
+ * A collection of related model instances.
1113
+ *
1114
+ * This collection can be async-iterated or turned directly into an array using `toArray()`.
1115
+ */
1089
1116
export class AsyncCollection < T > implements AsyncIterable < T > {
1090
1117
private values : Array < any > | Promise < Array < any > > ;
1091
1118
1092
1119
constructor ( values : Array < any > | Promise < Array < any > > ) {
1093
1120
this . values = values ;
1094
1121
}
1095
1122
1123
+ /**
1124
+ * Facilitates async iteration.
1125
+ *
1126
+ * ```ts
1127
+ * for await (const item of collection) {
1128
+ * handle(item)
1129
+ * }
1130
+ * ```
1131
+ *
1132
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of
1133
+ */
1096
1134
[ Symbol . asyncIterator ] ( ) : AsyncIterator < T > {
1097
1135
let values ;
1098
1136
let index = 0 ;
@@ -1115,6 +1153,14 @@ export class AsyncCollection<T> implements AsyncIterable<T> {
1115
1153
} ;
1116
1154
}
1117
1155
1156
+ /**
1157
+ * Turns the collection into an array, up to the amount specified in `max` param.
1158
+ *
1159
+ * ```ts
1160
+ * const all = await collection.toArray();
1161
+ * const first100 = await collection.toArray({max: 100});
1162
+ * ```
1163
+ */
1118
1164
async toArray ( {
1119
1165
max = Number . MAX_SAFE_INTEGER ,
1120
1166
} : { max ?: number } = { } ) : Promise < T [ ] > {
@@ -1332,6 +1378,8 @@ enum DataStoreState {
1332
1378
Clearing = 'Clearing' ,
1333
1379
}
1334
1380
1381
+ // TODO: How can we get rid of the non-null assertions?
1382
+ // https://github.com/aws-amplify/amplify-js/pull/10477/files#r1007363485
1335
1383
class DataStore {
1336
1384
// reference to configured category instances. Used for preserving SSR context
1337
1385
private Auth = Auth ;
@@ -1656,7 +1704,7 @@ class DataStore {
1656
1704
const seedPredicate = recursivePredicateFor < T > ( {
1657
1705
builder : modelConstructor ,
1658
1706
schema : modelDefinition ,
1659
- pkField : getModelPKFieldName ( modelConstructor ) ,
1707
+ pkField : extractPrimaryKeyFieldNames ( modelDefinition ) ,
1660
1708
} ) ;
1661
1709
const predicate = internals (
1662
1710
( identifierOrCriteria as RecursiveModelPredicateExtender < T > ) (
0 commit comments