@aws-amplify/datastore 3.7.4-custom-pk.79 → 3.7.4-custom-pk.87

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 (42) hide show
  1. package/dist/aws-amplify-datastore.js +458 -256
  2. package/dist/aws-amplify-datastore.js.map +1 -1
  3. package/dist/aws-amplify-datastore.min.js +3 -3
  4. package/dist/aws-amplify-datastore.min.js.map +1 -1
  5. package/lib/datastore/datastore.js +47 -11
  6. package/lib/datastore/datastore.js.map +1 -1
  7. package/lib/storage/adapter/AsyncStorageAdapter.js +229 -135
  8. package/lib/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  9. package/lib/storage/adapter/AsyncStorageDatabase.js +3 -2
  10. package/lib/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  11. package/lib/storage/adapter/IndexedDBAdapter.js +12 -17
  12. package/lib/storage/adapter/IndexedDBAdapter.js.map +1 -1
  13. package/lib/sync/utils.js +7 -3
  14. package/lib/sync/utils.js.map +1 -1
  15. package/lib/types.d.ts +5 -1
  16. package/lib/types.js.map +1 -1
  17. package/lib/util.d.ts +7 -2
  18. package/lib/util.js +76 -54
  19. package/lib/util.js.map +1 -1
  20. package/lib-esm/datastore/datastore.js +48 -12
  21. package/lib-esm/datastore/datastore.js.map +1 -1
  22. package/lib-esm/storage/adapter/AsyncStorageAdapter.js +229 -135
  23. package/lib-esm/storage/adapter/AsyncStorageAdapter.js.map +1 -1
  24. package/lib-esm/storage/adapter/AsyncStorageDatabase.js +4 -3
  25. package/lib-esm/storage/adapter/AsyncStorageDatabase.js.map +1 -1
  26. package/lib-esm/storage/adapter/IndexedDBAdapter.js +12 -17
  27. package/lib-esm/storage/adapter/IndexedDBAdapter.js.map +1 -1
  28. package/lib-esm/sync/utils.js +7 -3
  29. package/lib-esm/sync/utils.js.map +1 -1
  30. package/lib-esm/types.d.ts +5 -1
  31. package/lib-esm/types.js.map +1 -1
  32. package/lib-esm/util.d.ts +7 -2
  33. package/lib-esm/util.js +74 -54
  34. package/lib-esm/util.js.map +1 -1
  35. package/package.json +7 -7
  36. package/src/datastore/datastore.ts +60 -13
  37. package/src/storage/adapter/AsyncStorageAdapter.ts +162 -35
  38. package/src/storage/adapter/AsyncStorageDatabase.ts +4 -2
  39. package/src/storage/adapter/IndexedDBAdapter.ts +14 -25
  40. package/src/sync/utils.ts +7 -4
  41. package/src/types.ts +10 -1
  42. package/src/util.ts +71 -35
@@ -65,8 +65,9 @@ export class AsyncStorageAdapter implements Adapter {
65
65
 
66
66
  // Returns primary keys for a model
67
67
  private getIndexKeys(namespaceName: string, modelName: string): string[] {
68
- const keyPath =
69
- this.schema.namespaces[namespaceName]?.keys[modelName]?.primaryKey;
68
+ const namespace = this.schema.namespaces[namespaceName];
69
+
70
+ const keyPath = namespace?.keys[modelName]?.primaryKey;
70
71
 
71
72
  if (keyPath) {
72
73
  return keyPath;
@@ -239,7 +240,8 @@ export class AsyncStorageAdapter implements Adapter {
239
240
  }
240
241
 
241
242
  for await (const relation of relations) {
242
- const { fieldName, modelName, targetName, relationType } = relation;
243
+ const { fieldName, modelName, targetName, targetNames, relationType } =
244
+ relation;
243
245
  const storeName = this.getStorename(namespaceName, modelName);
244
246
  const modelConstructor = this.getModelConstructorByModelName(
245
247
  namespaceName,
@@ -249,22 +251,84 @@ export class AsyncStorageAdapter implements Adapter {
249
251
  switch (relationType) {
250
252
  case 'HAS_ONE':
251
253
  for await (const recordItem of records) {
252
- const getByfield = recordItem[targetName] ? targetName : fieldName;
253
- if (!recordItem[getByfield]) break;
254
+ // ASYNC CPK TODO: make this cleaner
255
+ if (targetNames?.length) {
256
+ let getByFields = [];
257
+ let allPresent;
258
+ // iterate through all targetnames to make sure they are all present in the recordItem
259
+ allPresent = targetNames.every(targetName => {
260
+ return recordItem[targetName] != null;
261
+ });
262
+
263
+ if (!allPresent) {
264
+ break;
265
+ }
266
+
267
+ getByFields = targetNames as any;
268
+
269
+ // keys are the key values
270
+ const keys = getByFields
271
+ .map(getByField => recordItem[getByField])
272
+ .join(DEFAULT_PRIMARY_KEY_SEPARATOR);
273
+
274
+ const connectionRecord = await this.db.get(
275
+ keys as any,
276
+ storeName
277
+ );
278
+
279
+ recordItem[fieldName] =
280
+ connectionRecord &&
281
+ this.modelInstanceCreator(modelConstructor, connectionRecord);
282
+ } else {
283
+ const getByfield = recordItem[targetName]
284
+ ? targetName
285
+ : fieldName;
286
+ if (!recordItem[getByfield]) break;
254
287
 
255
- const key = recordItem[getByfield];
288
+ const key = recordItem[getByfield];
256
289
 
257
- const connectionRecord = await this.db.get(key, storeName);
290
+ const connectionRecord = await this.db.get(key, storeName);
258
291
 
259
- recordItem[fieldName] =
260
- connectionRecord &&
261
- this.modelInstanceCreator(modelConstructor, connectionRecord);
292
+ recordItem[fieldName] =
293
+ connectionRecord &&
294
+ this.modelInstanceCreator(modelConstructor, connectionRecord);
295
+ }
262
296
  }
263
297
 
264
298
  break;
265
299
  case 'BELONGS_TO':
266
300
  for await (const recordItem of records) {
267
- if (recordItem[targetName]) {
301
+ // ASYNC CPK TODO: make this cleaner
302
+ if (targetNames?.length) {
303
+ let allPresent;
304
+ // iterate through all targetnames to make sure they are all present in the recordItem
305
+ allPresent = targetNames.every(targetName => {
306
+ return recordItem[targetName] != null;
307
+ });
308
+
309
+ // If not present, there is not yet a connected record
310
+ if (!allPresent) {
311
+ break;
312
+ }
313
+
314
+ const keys = targetNames
315
+ .map(targetName => recordItem[targetName])
316
+ .join(DEFAULT_PRIMARY_KEY_SEPARATOR);
317
+
318
+ // Retrieve the connected record
319
+ const connectionRecord = await this.db.get(
320
+ keys as any,
321
+ storeName
322
+ );
323
+
324
+ recordItem[fieldName] =
325
+ connectionRecord &&
326
+ this.modelInstanceCreator(modelConstructor, connectionRecord);
327
+
328
+ targetNames?.map(targetName => {
329
+ delete recordItem[targetName];
330
+ });
331
+ } else if (recordItem[targetName as any]) {
268
332
  const key = recordItem[targetName];
269
333
 
270
334
  const connectionRecord = await this.db.get(key, storeName);
@@ -600,36 +664,99 @@ export class AsyncStorageAdapter implements Adapter {
600
664
  switch (relationType) {
601
665
  case 'HAS_ONE':
602
666
  for await (const model of models) {
603
- const hasOneIndex = index || associatedWith;
604
- const hasOneCustomField = targetName in model;
605
- const keyValuesPath: string = this.getIndexKeyValuesPath(model);
606
- const value = hasOneCustomField ? model[targetName] : keyValuesPath;
607
-
608
- if (!value) break;
609
-
610
- const allRecords = await this.db.getAll(storeName);
611
-
612
- const recordToDelete = allRecords.filter(
613
- childItem => childItem[hasOneIndex] === value
614
- ) as T[];
615
-
616
- await this.deleteTraverse<T>(
617
- this.schema.namespaces[nameSpace].relationships[modelName]
618
- .relationTypes,
619
- recordToDelete,
620
- modelName,
621
- nameSpace,
622
- deleteQueue
623
- );
667
+ if (targetNames && targetNames?.length) {
668
+ const hasOneIndex = index || associatedWith;
669
+
670
+ // iterate over targetNames array and see if each item is present in model object
671
+ // targetNames here being the keys for the CHILD model
672
+ const hasConnectedModelFields = targetNames.every(targetName =>
673
+ model.hasOwnProperty(targetName)
674
+ );
675
+
676
+ // PK / Composite key for the parent model
677
+ const keyValuesPath: string = this.getIndexKeyValuesPath(model);
678
+
679
+ let values;
680
+
681
+ if (hasConnectedModelFields) {
682
+ // Values will be that of the child model
683
+ values = targetNames.map(
684
+ targetName => model[targetName]
685
+ ) as any;
686
+ } else {
687
+ // values will be that of the parent model
688
+ values = keyValuesPath;
689
+ }
690
+
691
+ if (values.length === 0) break;
692
+
693
+ const allRecords = await this.db.getAll(storeName);
694
+
695
+ let recordToDelete;
696
+
697
+ // values === targetNames
698
+ if (hasConnectedModelFields) {
699
+ /**
700
+ * Retrieve record by finding the record where all
701
+ * targetNames are present on the connected model
702
+ */
703
+ recordToDelete = allRecords.filter(childItem =>
704
+ values.every(value => childItem[value] != null)
705
+ ) as T[];
706
+ } else {
707
+ // values === keyValuePath
708
+ recordToDelete = allRecords.filter(
709
+ childItem => childItem[hasOneIndex] === values
710
+ ) as T[];
711
+ }
712
+
713
+ await this.deleteTraverse<T>(
714
+ this.schema.namespaces[nameSpace].relationships[modelName]
715
+ .relationTypes,
716
+ recordToDelete,
717
+ modelName,
718
+ nameSpace,
719
+ deleteQueue
720
+ );
721
+ } else {
722
+ const hasOneIndex = index || associatedWith;
723
+ const hasOneCustomField = targetName in model;
724
+ const keyValuesPath: string = this.getIndexKeyValuesPath(model);
725
+ const value = hasOneCustomField
726
+ ? model[targetName]
727
+ : keyValuesPath;
728
+
729
+ if (!value) break;
730
+
731
+ const allRecords = await this.db.getAll(storeName);
732
+
733
+ const recordToDelete = allRecords.filter(
734
+ childItem => childItem[hasOneIndex] === value
735
+ ) as T[];
736
+
737
+ await this.deleteTraverse<T>(
738
+ this.schema.namespaces[nameSpace].relationships[modelName]
739
+ .relationTypes,
740
+ recordToDelete,
741
+ modelName,
742
+ nameSpace,
743
+ deleteQueue
744
+ );
745
+ }
624
746
  }
625
747
  break;
626
748
  case 'HAS_MANY':
627
749
  for await (const model of models) {
628
- const keyValuesPath: string = this.getIndexKeyValuesPath(model);
750
+ // Key values for the parent model:
751
+ const keyValues: string[] = this.getIndexKeyValues(model);
629
752
 
630
753
  const allRecords = await this.db.getAll(storeName);
631
- const childrenArray = allRecords.filter(
632
- childItem => childItem[index] === keyValuesPath
754
+
755
+ // Use constant if we go with this approach
756
+ const indices = index.split('-');
757
+
758
+ const childrenArray = allRecords.filter(childItem =>
759
+ indices.every(index => keyValues.includes(childItem[index]))
633
760
  ) as T[];
634
761
 
635
762
  await this.deleteTraverse<T>(
@@ -6,7 +6,7 @@ import {
6
6
  PersistentModel,
7
7
  QueryOne,
8
8
  } from '../../types';
9
- import { monotonicUlidFactory } from '../../util';
9
+ import { indexNameFromKeys, monotonicUlidFactory } from '../../util';
10
10
  import { createInMemoryStore } from './InMemoryStore';
11
11
 
12
12
  const DB_NAME = '@AmplifyDatastore';
@@ -96,8 +96,10 @@ class AsyncStorageDatabase {
96
96
  keys: string[],
97
97
  keyValuesPath: string
98
98
  ) {
99
+ const idxName = indexNameFromKeys(keys);
100
+
99
101
  const ulid =
100
- this.getCollectionIndex(storeName).get(keys[0]) ||
102
+ this.getCollectionIndex(storeName).get(idxName) ||
101
103
  this.getMonotonicFactory(storeName)();
102
104
 
103
105
  // Retrieve db key for item
@@ -29,7 +29,6 @@ import {
29
29
  traverseModel,
30
30
  validatePredicate,
31
31
  sortCompareFunction,
32
- extractTargetNamesFromSrc,
33
32
  } from '../../util';
34
33
  import { Adapter } from './index';
35
34
 
@@ -88,8 +87,9 @@ class IndexedDBAdapter implements Adapter {
88
87
  }
89
88
 
90
89
  private getIndexKeyPath(namespaceName: string, modelName: string): string[] {
91
- const keyPath =
92
- this.schema.namespaces[namespaceName]?.keys[modelName]?.primaryKey;
90
+ const namespace = this.schema.namespaces[namespaceName];
91
+
92
+ const keyPath = namespace?.keys[modelName]?.primaryKey;
93
93
 
94
94
  if (keyPath) {
95
95
  return keyPath;
@@ -102,6 +102,7 @@ class IndexedDBAdapter implements Adapter {
102
102
  const modelConstructor = Object.getPrototypeOf(model)
103
103
  .constructor as PersistentModelConstructor<T>;
104
104
  const namespaceName = this.namespaceResolver(modelConstructor);
105
+
105
106
  const keys = this.getIndexKeyPath(namespaceName, modelConstructor.name);
106
107
 
107
108
  const keyValues = keys.map(field => model[field]);
@@ -860,7 +861,6 @@ class IndexedDBAdapter implements Adapter {
860
861
 
861
862
  const storeName = this.getStorename(nameSpace, modelName);
862
863
 
863
- // Explain:
864
864
  const index: string | string[] | undefined =
865
865
  getIndex(
866
866
  this.schema.namespaces[nameSpace].relationships[modelName]
@@ -909,7 +909,7 @@ class IndexedDBAdapter implements Adapter {
909
909
  const recordToDelete = <T>await this.db
910
910
  .transaction(storeName, 'readwrite')
911
911
  .objectStore(storeName)
912
- .index(hasOneIndex as any)
912
+ .index(hasOneIndex as string)
913
913
  .get(values);
914
914
 
915
915
  await this.deleteTraverse(
@@ -927,19 +927,14 @@ class IndexedDBAdapter implements Adapter {
927
927
 
928
928
  const hasOneCustomField = targetName in model;
929
929
  const keyValues = this.getIndexKeyValues(model);
930
- let value = hasOneCustomField ? model[targetName] : keyValues[0];
931
-
932
- if (hasOneIndex === 'byPk') {
933
- // byPk requires an array keyValue
934
- value = [value];
935
- }
930
+ const value = hasOneCustomField ? [model[targetName]] : keyValues;
936
931
 
937
932
  if (!value) break;
938
933
 
939
934
  const recordToDelete = <T>await this.db
940
935
  .transaction(storeName, 'readwrite')
941
936
  .objectStore(storeName)
942
- .index(hasOneIndex as any)
937
+ .index(hasOneIndex as string)
943
938
  .get(value);
944
939
 
945
940
  await this.deleteTraverse(
@@ -957,16 +952,11 @@ class IndexedDBAdapter implements Adapter {
957
952
  for await (const model of models) {
958
953
  const keyValues = this.getIndexKeyValues(model);
959
954
 
960
- // CPK TODO: Double check this, unsure that we encounter `byPk` here, ever.
961
- const hasManyIndex = index || 'byPk';
962
-
963
- // CPK TODO: get by all key values
964
955
  const childrenArray = await this.db
965
956
  .transaction(storeName, 'readwrite')
966
957
  .objectStore(storeName)
967
- .index(hasManyIndex as any)
968
- .getAll(keyValues[0]);
969
- // .getAll(keyValues); // TODO
958
+ .index(index as string)
959
+ .getAll(keyValues);
970
960
 
971
961
  await this.deleteTraverse(
972
962
  this.schema.namespaces[nameSpace].relationships[modelName]
@@ -1080,13 +1070,12 @@ class IndexedDBAdapter implements Adapter {
1080
1070
  autoIncrement: true,
1081
1071
  });
1082
1072
 
1083
- const indexes =
1084
- this.schema.namespaces[namespaceName].relationships[modelName].indexes;
1085
- indexes.forEach(index => store.createIndex(index, index));
1073
+ const { indexes } =
1074
+ this.schema.namespaces[namespaceName].relationships[modelName];
1086
1075
 
1087
- const keyPath = this.getIndexKeyPath(namespaceName, modelName);
1088
-
1089
- store.createIndex('byPk', keyPath, { unique: true });
1076
+ indexes.forEach(([idxName, keyPath, options]) => {
1077
+ store.createIndex(idxName, keyPath, options);
1078
+ });
1090
1079
  }
1091
1080
  }
1092
1081
 
package/src/sync/utils.ts CHANGED
@@ -147,7 +147,7 @@ function getConnectionFields(
147
147
  modelDefinition: SchemaModel,
148
148
  namespace: SchemaNamespace
149
149
  ): string[] {
150
- const result = [];
150
+ const result: string[] = [];
151
151
 
152
152
  Object.values(modelDefinition.fields)
153
153
  .filter(({ association }) => association && Object.keys(association).length)
@@ -169,11 +169,14 @@ function getConnectionFields(
169
169
  const connectedModelName =
170
170
  modelDefinition.fields[name].type['model'];
171
171
 
172
- // keyFields are fields of the connected model
173
- const keyFields = relations[connectedModelName].indexes;
172
+ const byPkIndex = relations[connectedModelName].indexes.find(
173
+ ([name]) => name === 'byPk'
174
+ );
175
+ const keyFields = byPkIndex && byPkIndex[1];
176
+ const keyFieldSelectionSet = keyFields?.join(' ');
174
177
 
175
178
  // We rely on `_deleted` when we process the sync query (e.g. in batchSave in the adapters)
176
- result.push(`${name} { ${keyFields} _deleted }`);
179
+ result.push(`${name} { ${keyFieldSelectionSet} _deleted }`);
177
180
  } else {
178
181
  // backwards-compatability for schema generated prior to custom primary key support
179
182
  result.push(`${name} { id _deleted }`);
package/src/types.ts CHANGED
@@ -800,8 +800,17 @@ export type RelationType = {
800
800
  associatedWith?: string | string[];
801
801
  };
802
802
 
803
+ type IndexOptions = {
804
+ unique?: boolean;
805
+ };
806
+
807
+ export type IndexesType = Array<[string, string[], IndexOptions?]>;
808
+
803
809
  export type RelationshipType = {
804
- [modelName: string]: { indexes: string[]; relationTypes: RelationType[] };
810
+ [modelName: string]: {
811
+ indexes: IndexesType;
812
+ relationTypes: RelationType[];
813
+ };
805
814
  };
806
815
 
807
816
  //#endregion
package/src/util.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  AllOperators,
8
8
  isPredicateGroup,
9
9
  isPredicateObj,
10
- ModelAssociation,
11
10
  ModelInstanceMetadata,
12
11
  PersistentModel,
13
12
  PersistentModelConstructor,
@@ -29,10 +28,16 @@ import {
29
28
  LimitTimerRaceResolvedValues,
30
29
  SchemaModel,
31
30
  ModelAttribute,
31
+ IndexesType,
32
32
  } from './types';
33
33
  import { WordArray } from 'amazon-cognito-identity-js';
34
+
34
35
  const ID = 'id';
35
36
 
37
+ export const errorMessages = {
38
+ idEmptyString: 'An index field cannot contain an empty string value',
39
+ };
40
+
36
41
  export function extractKeyIfExists(
37
42
  modelDefinition: SchemaModel
38
43
  ): ModelAttribute | undefined {
@@ -59,6 +64,15 @@ export function extractPrimaryKeyValues<T extends PersistentModel>(
59
64
  return keyFields.map(key => model[key]);
60
65
  }
61
66
 
67
+ export function extractPrimaryKeysAndValues<T extends PersistentModel>(
68
+ model: T,
69
+ keyFields: string[]
70
+ ): any {
71
+ const primaryKeysAndValues = {};
72
+ keyFields.forEach(key => (primaryKeysAndValues[key] = model[key]));
73
+ return primaryKeysAndValues;
74
+ }
75
+
62
76
  // IdentifierFields<ManagedIdentifier>
63
77
  // Default behavior without explicit @primaryKey defined
64
78
  export function isIdManaged(modelDefinition: SchemaModel): boolean {
@@ -335,25 +349,13 @@ export const establishRelationAndKeys = (
335
349
  });
336
350
 
337
351
  if (connectionType === 'BELONGS_TO') {
338
- // const targetNames: string[] = extractTargetNamesFromSrc(
339
- // fieldAttribute.association
340
- // );
341
-
342
- // targetNames.forEach((targetName: string) => {
343
- // relationship[mKey].indexes.push(targetName);
344
- // });
345
- if (fieldAttribute.association.targetName) {
346
- // backwards-compatability for schema generated prior to custom primary key support
347
- relationship[mKey].indexes.push(
348
- fieldAttribute.association['targetName']
349
- );
350
- } else {
351
- // iterate through fieldAttribute.association.targetNames, and push to relationship[mKey].indexes
352
- fieldAttribute.association.targetNames.forEach(
353
- (targetName: string) => {
354
- relationship[mKey].indexes.push(targetName);
355
- }
356
- );
352
+ const targetNames = extractTargetNamesFromSrc(
353
+ fieldAttribute.association
354
+ );
355
+
356
+ if (targetNames) {
357
+ const idxName = indexNameFromKeys(targetNames);
358
+ relationship[mKey].indexes.push([idxName, targetNames]);
357
359
  }
358
360
  }
359
361
  }
@@ -367,24 +369,36 @@ export const establishRelationAndKeys = (
367
369
  continue;
368
370
  }
369
371
 
372
+ const { fields } = attribute.properties;
373
+
370
374
  if (isModelAttributePrimaryKey(attribute)) {
371
- keys[mKey].primaryKey = attribute.properties.fields;
375
+ keys[mKey].primaryKey = fields;
376
+ continue;
372
377
  }
373
378
 
374
- const { fields } = attribute.properties;
375
- for (const field of fields) {
376
- // only add index if it hasn't already been added
377
- const exists = relationship[mKey].indexes.includes(field);
378
- if (!exists) {
379
- relationship[mKey].indexes.push(field);
380
- }
379
+ // create indexes for all other keys
380
+ const idxName = indexNameFromKeys(fields);
381
+ const idxExists = relationship[mKey].indexes.find(
382
+ ([index]) => index === idxName
383
+ );
384
+
385
+ if (!idxExists) {
386
+ relationship[mKey].indexes.push([idxName, fields]);
381
387
  }
382
388
  }
389
+ }
383
390
 
384
- if (!keys[mKey].primaryKey) {
385
- keys[mKey].primaryKey = [ID];
386
- }
391
+ // set 'id' as the PK for models without a custom PK explicitly defined
392
+ if (!keys[mKey].primaryKey) {
393
+ keys[mKey].primaryKey = [ID];
387
394
  }
395
+
396
+ // create primary index
397
+ relationship[mKey].indexes.push([
398
+ 'byPk',
399
+ keys[mKey].primaryKey as string[],
400
+ { unique: true },
401
+ ]);
388
402
  });
389
403
 
390
404
  return [relationship, keys];
@@ -572,7 +586,7 @@ export const traverseModel = <T extends PersistentModel>(
572
586
  return result;
573
587
  };
574
588
 
575
- export const getIndex = (rel: RelationType[], src: any): string => {
589
+ export const getIndex = (rel: RelationType[], src: any): string | undefined => {
576
590
  let index = '';
577
591
  rel.some((relItem: RelationType) => {
578
592
  if (relItem.modelName === src) {
@@ -582,9 +596,20 @@ export const getIndex = (rel: RelationType[], src: any): string => {
582
596
  return index;
583
597
  };
584
598
 
585
- export const getIndexFromAssociation = (indexes: string[], src: any): any => {
586
- const index = indexes.find(idx => idx === src);
587
- return index;
599
+ export const getIndexFromAssociation = (
600
+ indexes: IndexesType,
601
+ src: string | string[]
602
+ ): string | undefined => {
603
+ let indexName: string;
604
+
605
+ if (Array.isArray(src)) {
606
+ indexName = indexNameFromKeys(src);
607
+ } else {
608
+ indexName = src;
609
+ }
610
+
611
+ const associationIndex = indexes.find(([idxName]) => idxName === indexName);
612
+ return associationIndex && associationIndex[0];
588
613
  };
589
614
 
590
615
  let privateModeCheckResult;
@@ -950,3 +975,14 @@ export function extractTargetNamesFromSrc(src: any): string[] | undefined {
950
975
  return undefined;
951
976
  }
952
977
  }
978
+
979
+ // Generates snake-cased index name from an aray of key field names
980
+ // E.g. for keys `[id, title]` => 'id-title'
981
+ export function indexNameFromKeys(keys: string[]): string {
982
+ return keys.reduce((prev: string, cur: string, idx: number) => {
983
+ if (idx === 0) {
984
+ return cur;
985
+ }
986
+ return `${prev}-${cur}`;
987
+ }, '');
988
+ }