Skip to content

Commit b7f35a4

Browse files
authored
chore(@aws-amplify/datastore): docstrings, small refactors, cleanup unused predicate function (#11040)
* added a few targeted TODO's from PR; in general, look for ops to cleanup as anys and bangs * datastore.ts refactors and docstrings * docstrings, small refactor, remove unused method
1 parent 7320ef8 commit b7f35a4

File tree

3 files changed

+200
-109
lines changed

3 files changed

+200
-109
lines changed

packages/datastore/src/datastore/datastore.ts

+108-60
Original file line numberDiff line numberDiff line change
@@ -139,9 +139,13 @@ const modelNamespaceMap = new WeakMap<
139139
PersistentModelConstructor<any>,
140140
string
141141
>();
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+
*/
145149
const modelPatchesMap = new WeakMap<
146150
PersistentModel,
147151
[Patch[], PersistentModel]
@@ -158,17 +162,6 @@ const getModelDefinition = (
158162
return definition;
159163
};
160164

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-
172165
/**
173166
* Determine what the managed timestamp field names are for the given model definition
174167
* and return the mapping.
@@ -202,14 +195,16 @@ const getTimestampFields = (
202195
};
203196
};
204197

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+
*/
205204
const isValidModelConstructor = <T extends PersistentModel>(
206205
obj: any
207206
): 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);
213208
};
214209

215210
const namespaceResolver: NamespaceResolver = modelConstructor => {
@@ -222,6 +217,25 @@ const namespaceResolver: NamespaceResolver = modelConstructor => {
222217
return resolver;
223218
};
224219

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+
*/
225239
const buildSeedPredicate = <T extends PersistentModel>(
226240
modelConstructor: PersistentModelConstructor<T>
227241
) => {
@@ -232,9 +246,7 @@ const buildSeedPredicate = <T extends PersistentModel>(
232246
);
233247
if (!modelSchema) throw new Error('Missing modelSchema');
234248

235-
const pks = getModelPKFieldName(
236-
modelConstructor as PersistentModelConstructor<T>
237-
);
249+
const pks = extractPrimaryKeyFieldNames(modelSchema);
238250
if (!pks) throw new Error('Could not determine PK');
239251

240252
return recursivePredicateFor<T>({
@@ -387,27 +399,23 @@ const initSchema = (userSchema: Schema) => {
387399

388400
modelAssociations.set(model.name, connectedModels);
389401

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.
390405
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)) {
398408
Object.defineProperty(field.type, 'modelConstructor', {
399409
get: () => {
410+
const relatedModelDefinition = getModelDefinition(relatedModel);
411+
if (!relatedModelDefinition)
412+
throw new Error(
413+
`Could not find model definition for ${relatedModel.name}`
414+
);
400415
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),
411419
};
412420
},
413421
});
@@ -548,7 +556,7 @@ const createTypeClasses: (
548556

549557
Object.entries(namespace.nonModels || {}).forEach(
550558
([typeName, typeDefinition]) => {
551-
const clazz = createNonModelClass(typeDefinition) as any;
559+
const clazz = createNonModelClass(typeDefinition);
552560
classes[typeName] = clazz;
553561
}
554562
);
@@ -965,25 +973,16 @@ const createModelClass = <T extends PersistentModel>(
965973

966974
Object.defineProperty(clazz, 'name', { value: modelDefinition.name });
967975

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;
987986

988987
Object.defineProperty(clazz.prototype, modelDefinition.fields[field].name, {
989988
set(model: PersistentModel | undefined | null) {
@@ -1018,6 +1017,7 @@ const createModelClass = <T extends PersistentModel>(
10181017
}
10191018
}
10201019

1020+
// if the relationship can be managed automagically, set the FK's
10211021
if (relationship.isComplete) {
10221022
for (let i = 0; i < relationship.localJoinFields.length; i++) {
10231023
this[relationship.localJoinFields[i]] =
@@ -1030,27 +1030,46 @@ const createModelClass = <T extends PersistentModel>(
10301030
}
10311031
},
10321032
get() {
1033+
/**
1034+
* Bucket for holding related models instances specific to `this` instance.
1035+
*/
10331036
const instanceMemos = modelInstanceAssociationsMap.has(this)
10341037
? modelInstanceAssociationsMap.get(this)!
10351038
: modelInstanceAssociationsMap.set(this, {}).get(this)!;
10361039

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.
10371043
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.
10381046
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.
10391051
const resultPromise = instance.query(
10401052
relationship.remoteModelConstructor as PersistentModelConstructor<T>,
10411053
base =>
10421054
base.and(q => {
10431055
return relationship.remoteJoinFields.map((field, index) => {
1056+
// TODO: anything we can use instead of `any` here?
10441057
return (q[field] as any).eq(
10451058
this[relationship.localJoinFields[index]]
10461059
);
10471060
});
10481061
})
10491062
);
10501063

1064+
// results in hand, how we return them to the caller depends on the relationship type.
10511065
if (relationship.type === 'HAS_MANY') {
1066+
// collections should support async iteration, even though we don't
1067+
// leverage it fully [yet].
10521068
instanceMemos[field] = new AsyncCollection(resultPromise);
10531069
} 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."
10541073
instanceMemos[field] = resultPromise.then(rows => {
10551074
if (rows.length > 1) {
10561075
// should never happen for a HAS_ONE or BELONGS_TO.
@@ -1084,15 +1103,34 @@ const createModelClass = <T extends PersistentModel>(
10841103
return clazz;
10851104
};
10861105

1106+
/**
1107+
* An eventually loaded related model instance.
1108+
*/
10871109
export class AsyncItem<T> extends Promise<T> {}
10881110

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+
*/
10891116
export class AsyncCollection<T> implements AsyncIterable<T> {
10901117
private values: Array<any> | Promise<Array<any>>;
10911118

10921119
constructor(values: Array<any> | Promise<Array<any>>) {
10931120
this.values = values;
10941121
}
10951122

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+
*/
10961134
[Symbol.asyncIterator](): AsyncIterator<T> {
10971135
let values;
10981136
let index = 0;
@@ -1115,6 +1153,14 @@ export class AsyncCollection<T> implements AsyncIterable<T> {
11151153
};
11161154
}
11171155

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+
*/
11181164
async toArray({
11191165
max = Number.MAX_SAFE_INTEGER,
11201166
}: { max?: number } = {}): Promise<T[]> {
@@ -1332,6 +1378,8 @@ enum DataStoreState {
13321378
Clearing = 'Clearing',
13331379
}
13341380

1381+
// TODO: How can we get rid of the non-null assertions?
1382+
// https://github.com/aws-amplify/amplify-js/pull/10477/files#r1007363485
13351383
class DataStore {
13361384
// reference to configured category instances. Used for preserving SSR context
13371385
private Auth = Auth;
@@ -1656,7 +1704,7 @@ class DataStore {
16561704
const seedPredicate = recursivePredicateFor<T>({
16571705
builder: modelConstructor,
16581706
schema: modelDefinition,
1659-
pkField: getModelPKFieldName(modelConstructor),
1707+
pkField: extractPrimaryKeyFieldNames(modelDefinition),
16601708
});
16611709
const predicate = internals(
16621710
(identifierOrCriteria as RecursiveModelPredicateExtender<T>)(

0 commit comments

Comments
 (0)