@loopback/repository 2.2.1 → 2.5.1

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 (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/common-types.d.ts +1 -0
  3. package/dist/common-types.js +2 -0
  4. package/dist/common-types.js.map +1 -1
  5. package/dist/connectors/index.js +4 -0
  6. package/dist/connectors/index.js.map +1 -1
  7. package/dist/decorators/metadata.js +1 -0
  8. package/dist/decorators/metadata.js.map +1 -1
  9. package/dist/decorators/model.decorator.d.ts +1 -1
  10. package/dist/decorators/model.decorator.js +1 -0
  11. package/dist/decorators/model.decorator.js.map +1 -1
  12. package/dist/decorators/repository.decorator.js +1 -0
  13. package/dist/decorators/repository.decorator.js.map +1 -1
  14. package/dist/define-model-class.js +1 -0
  15. package/dist/define-model-class.js.map +1 -1
  16. package/dist/define-repository-class.d.ts +119 -0
  17. package/dist/define-repository-class.js +98 -0
  18. package/dist/define-repository-class.js.map +1 -0
  19. package/dist/errors/entity-not-found.error.js +1 -0
  20. package/dist/errors/entity-not-found.error.js.map +1 -1
  21. package/dist/errors/invalid-relation.error.js +1 -0
  22. package/dist/errors/invalid-relation.error.js.map +1 -1
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +4 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/keys.d.ts +34 -0
  27. package/dist/keys.js +44 -0
  28. package/dist/keys.js.map +1 -0
  29. package/dist/mixins/repository.mixin.d.ts +91 -9
  30. package/dist/mixins/repository.mixin.js +62 -20
  31. package/dist/mixins/repository.mixin.js.map +1 -1
  32. package/dist/model.d.ts +16 -1
  33. package/dist/model.js +61 -9
  34. package/dist/model.js.map +1 -1
  35. package/dist/query.js +1 -0
  36. package/dist/query.js.map +1 -1
  37. package/dist/relations/belongs-to/belongs-to-accessor.js +1 -0
  38. package/dist/relations/belongs-to/belongs-to-accessor.js.map +1 -1
  39. package/dist/relations/belongs-to/belongs-to.decorator.js +1 -0
  40. package/dist/relations/belongs-to/belongs-to.decorator.js.map +1 -1
  41. package/dist/relations/belongs-to/belongs-to.helpers.js +1 -0
  42. package/dist/relations/belongs-to/belongs-to.helpers.js.map +1 -1
  43. package/dist/relations/belongs-to/belongs-to.inclusion-resolver.js +1 -0
  44. package/dist/relations/belongs-to/belongs-to.inclusion-resolver.js.map +1 -1
  45. package/dist/relations/belongs-to/belongs-to.repository.js +1 -0
  46. package/dist/relations/belongs-to/belongs-to.repository.js.map +1 -1
  47. package/dist/relations/has-many/has-many-repository.factory.js +1 -0
  48. package/dist/relations/has-many/has-many-repository.factory.js.map +1 -1
  49. package/dist/relations/has-many/has-many-through.helpers.d.ts +74 -0
  50. package/dist/relations/has-many/has-many-through.helpers.js +145 -0
  51. package/dist/relations/has-many/has-many-through.helpers.js.map +1 -0
  52. package/dist/relations/has-many/has-many.decorator.js +1 -0
  53. package/dist/relations/has-many/has-many.decorator.js.map +1 -1
  54. package/dist/relations/has-many/has-many.helpers.d.ts +9 -0
  55. package/dist/relations/has-many/has-many.helpers.js +33 -21
  56. package/dist/relations/has-many/has-many.helpers.js.map +1 -1
  57. package/dist/relations/has-many/has-many.inclusion-resolver.js +1 -0
  58. package/dist/relations/has-many/has-many.inclusion-resolver.js.map +1 -1
  59. package/dist/relations/has-many/has-many.repository.js +1 -0
  60. package/dist/relations/has-many/has-many.repository.js.map +1 -1
  61. package/dist/relations/has-one/has-one-repository.factory.js +1 -0
  62. package/dist/relations/has-one/has-one-repository.factory.js.map +1 -1
  63. package/dist/relations/has-one/has-one.decorator.js +1 -0
  64. package/dist/relations/has-one/has-one.decorator.js.map +1 -1
  65. package/dist/relations/has-one/has-one.helpers.js +1 -0
  66. package/dist/relations/has-one/has-one.helpers.js.map +1 -1
  67. package/dist/relations/has-one/has-one.inclusion-resolver.js +1 -0
  68. package/dist/relations/has-one/has-one.inclusion-resolver.js.map +1 -1
  69. package/dist/relations/has-one/has-one.repository.js +1 -0
  70. package/dist/relations/has-one/has-one.repository.js.map +1 -1
  71. package/dist/relations/relation.decorator.js +1 -0
  72. package/dist/relations/relation.decorator.js.map +1 -1
  73. package/dist/relations/relation.helpers.js +1 -0
  74. package/dist/relations/relation.helpers.js.map +1 -1
  75. package/dist/relations/relation.types.d.ts +25 -27
  76. package/dist/relations/relation.types.js +2 -1
  77. package/dist/relations/relation.types.js.map +1 -1
  78. package/dist/repositories/constraint-utils.js +1 -0
  79. package/dist/repositories/constraint-utils.js.map +1 -1
  80. package/dist/repositories/index.js +1 -0
  81. package/dist/repositories/index.js.map +1 -1
  82. package/dist/repositories/kv.repository.bridge.js +1 -0
  83. package/dist/repositories/kv.repository.bridge.js.map +1 -1
  84. package/dist/repositories/legacy-juggler-bridge.d.ts +3 -3
  85. package/dist/repositories/legacy-juggler-bridge.js +8 -18
  86. package/dist/repositories/legacy-juggler-bridge.js.map +1 -1
  87. package/dist/repositories/repository.js +1 -0
  88. package/dist/repositories/repository.js.map +1 -1
  89. package/dist/transaction.js +1 -0
  90. package/dist/transaction.js.map +1 -1
  91. package/dist/type-resolver.d.ts +4 -0
  92. package/dist/type-resolver.js +9 -0
  93. package/dist/type-resolver.js.map +1 -1
  94. package/dist/types/any.js +1 -0
  95. package/dist/types/any.js.map +1 -1
  96. package/dist/types/array.js +1 -0
  97. package/dist/types/array.js.map +1 -1
  98. package/dist/types/boolean.js +1 -0
  99. package/dist/types/boolean.js.map +1 -1
  100. package/dist/types/buffer.js +1 -0
  101. package/dist/types/buffer.js.map +1 -1
  102. package/dist/types/date.js +1 -0
  103. package/dist/types/date.js.map +1 -1
  104. package/dist/types/index.d.ts +11 -9
  105. package/dist/types/index.js +33 -17
  106. package/dist/types/index.js.map +1 -1
  107. package/dist/types/model.js +1 -0
  108. package/dist/types/model.js.map +1 -1
  109. package/dist/types/null.d.ts +12 -0
  110. package/dist/types/null.js +33 -0
  111. package/dist/types/null.js.map +1 -0
  112. package/dist/types/number.js +1 -0
  113. package/dist/types/number.js.map +1 -1
  114. package/dist/types/object.js +1 -0
  115. package/dist/types/object.js.map +1 -1
  116. package/dist/types/string.js +1 -0
  117. package/dist/types/string.js.map +1 -1
  118. package/dist/types/union.js +1 -0
  119. package/dist/types/union.js.map +1 -1
  120. package/package.json +13 -14
  121. package/src/common-types.ts +1 -0
  122. package/src/define-repository-class.ts +170 -0
  123. package/src/index.ts +2 -0
  124. package/src/keys.ts +40 -0
  125. package/src/mixins/repository.mixin.ts +120 -25
  126. package/src/model.ts +74 -11
  127. package/src/relations/has-many/has-many-through.helpers.ts +193 -0
  128. package/src/relations/has-many/has-many.helpers.ts +41 -27
  129. package/src/relations/relation.types.ts +24 -30
  130. package/src/repositories/legacy-juggler-bridge.ts +16 -24
  131. package/src/type-resolver.ts +8 -0
  132. package/src/types/index.ts +11 -8
  133. package/src/types/null.ts +35 -0
  134. package/index.d.ts +0 -6
  135. package/index.js +0 -6
@@ -3,15 +3,60 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {Binding, BindingScope, createBindingFromClass} from '@loopback/context';
7
- import {Application} from '@loopback/core';
6
+ import {
7
+ Binding,
8
+ BindingFromClassOptions,
9
+ BindingScope,
10
+ createBindingFromClass,
11
+ } from '@loopback/context';
12
+ import {
13
+ Application,
14
+ Component,
15
+ Constructor,
16
+ CoreBindings,
17
+ MixinTarget,
18
+ } from '@loopback/core';
8
19
  import debugFactory from 'debug';
9
20
  import {Class} from '../common-types';
10
21
  import {SchemaMigrationOptions} from '../datasource';
22
+ import {RepositoryBindings, RepositoryTags} from '../keys';
23
+ import {Model} from '../model';
11
24
  import {juggler, Repository} from '../repositories';
12
25
 
13
26
  const debug = debugFactory('loopback:repository:mixin');
14
27
 
28
+ // FIXME(rfeng): Workaround for https://github.com/microsoft/rushstack/pull/1867
29
+ /* eslint-disable @typescript-eslint/no-unused-vars */
30
+ import {
31
+ BindingAddress,
32
+ BindingFilter,
33
+ JSONObject,
34
+ Provider,
35
+ Context,
36
+ ContextSubscriptionManager,
37
+ ContextEvent,
38
+ Interceptor,
39
+ InterceptorBindingOptions,
40
+ ResolutionOptions,
41
+ BindingKey,
42
+ ValueOrPromise,
43
+ ContextEventObserver,
44
+ ContextObserver,
45
+ Subscription,
46
+ BindingComparator,
47
+ ContextView,
48
+ ResolutionSession,
49
+ BindingCreationPolicy,
50
+ ContextInspectOptions,
51
+ } from '@loopback/context';
52
+ import {
53
+ Server,
54
+ ApplicationConfig,
55
+ ApplicationMetadata,
56
+ LifeCycleObserver,
57
+ ServiceOptions,
58
+ } from '@loopback/core';
59
+
15
60
  /**
16
61
  * A mixin class for Application that creates a .repository()
17
62
  * function to register a repository automatically. Also overrides
@@ -26,13 +71,15 @@ const debug = debugFactory('loopback:repository:mixin');
26
71
  * called <a href="#RepositoryMixinDoc">RepositoryMixinDoc</a>
27
72
  *
28
73
  */
29
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
- export function RepositoryMixin<T extends Class<any>>(superClass: T) {
74
+ export function RepositoryMixin<T extends MixinTarget<Application>>(
75
+ superClass: T,
76
+ ) {
31
77
  return class extends superClass {
32
78
  /**
33
79
  * Add a repository to this application.
34
80
  *
35
81
  * @param repoClass - The repository to add.
82
+ * @param nameOrOptions - Name or options for the binding
36
83
  *
37
84
  * @example
38
85
  * ```ts
@@ -60,14 +107,14 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
60
107
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
108
  repository<R extends Repository<any>>(
62
109
  repoClass: Class<R>,
63
- name?: string,
110
+ nameOrOptions?: string | BindingFromClassOptions,
64
111
  ): Binding<R> {
65
112
  const binding = createBindingFromClass(repoClass, {
66
- name,
67
- namespace: 'repositories',
68
- type: 'repository',
113
+ namespace: RepositoryBindings.REPOSITORIES,
114
+ type: RepositoryTags.REPOSITORY,
69
115
  defaultScope: BindingScope.TRANSIENT,
70
- });
116
+ ...toOptions(nameOrOptions),
117
+ }).tag(RepositoryTags.REPOSITORY);
71
118
  this.add(binding);
72
119
  return binding;
73
120
  }
@@ -86,7 +133,8 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
86
133
  * Add the dataSource to this application.
87
134
  *
88
135
  * @param dataSource - The dataSource to add.
89
- * @param name - The binding name of the datasource; defaults to dataSource.name
136
+ * @param nameOrOptions - The binding name or options of the datasource;
137
+ * defaults to dataSource.name
90
138
  *
91
139
  * @example
92
140
  * ```ts
@@ -106,22 +154,24 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
106
154
  */
107
155
  dataSource<D extends juggler.DataSource>(
108
156
  dataSource: Class<D> | D,
109
- name?: string,
157
+ nameOrOptions?: string | BindingFromClassOptions,
110
158
  ): Binding<D> {
159
+ const options = toOptions(nameOrOptions);
111
160
  // We have an instance of
112
161
  if (dataSource instanceof juggler.DataSource) {
113
162
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
114
- name = name || dataSource.name;
115
- const key = `datasources.${name}`;
116
- return this.bind(key).to(dataSource).tag('datasource');
163
+ const name = options.name || dataSource.name;
164
+ const namespace = options.namespace ?? RepositoryBindings.DATASOURCES;
165
+ const key = `${namespace}.${name}`;
166
+ return this.bind(key).to(dataSource).tag(RepositoryTags.DATASOURCE);
117
167
  } else if (typeof dataSource === 'function') {
118
168
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
119
- name = name || dataSource.dataSourceName;
169
+ options.name = options.name || dataSource.dataSourceName;
120
170
  const binding = createBindingFromClass(dataSource, {
121
- name,
122
- namespace: 'datasources',
123
- type: 'datasource',
171
+ namespace: RepositoryBindings.DATASOURCES,
172
+ type: RepositoryTags.DATASOURCE,
124
173
  defaultScope: BindingScope.SINGLETON,
174
+ ...options,
125
175
  });
126
176
  this.add(binding);
127
177
  return binding;
@@ -130,11 +180,22 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
130
180
  }
131
181
  }
132
182
 
183
+ /**
184
+ * Register a model class as a binding in the target context
185
+ * @param modelClass - Model class
186
+ */
187
+ model<M extends Class<unknown>>(modelClass: M) {
188
+ const binding = createModelClassBinding(modelClass);
189
+ this.add(binding);
190
+ return binding;
191
+ }
192
+
133
193
  /**
134
194
  * Add a component to this application. Also mounts
135
195
  * all the components repositories.
136
196
  *
137
197
  * @param component - The component to add.
198
+ * @param nameOrOptions - Name or options for the binding.
138
199
  *
139
200
  * @example
140
201
  * ```ts
@@ -151,9 +212,17 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
151
212
  * app.component(ProductComponent);
152
213
  * ```
153
214
  */
154
- public component(component: Class<unknown>, name?: string) {
155
- super.component(component, name);
156
- this.mountComponentRepositories(component);
215
+ // Unfortunately, TypeScript does not allow overriding methods inherited
216
+ // from mapped types. https://github.com/microsoft/TypeScript/issues/38496
217
+ // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
218
+ // @ts-ignore
219
+ public component<C extends Component = Component>(
220
+ componentCtor: Constructor<C>,
221
+ nameOrOptions?: string | BindingFromClassOptions,
222
+ ) {
223
+ const binding = super.component(componentCtor, nameOrOptions);
224
+ this.mountComponentRepositories(componentCtor);
225
+ return binding;
157
226
  }
158
227
 
159
228
  /**
@@ -164,8 +233,10 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
164
233
  * @param component - The component to mount repositories of
165
234
  */
166
235
  mountComponentRepositories(component: Class<unknown>) {
167
- const componentKey = `components.${component.name}`;
168
- const compInstance = this.getSync(componentKey);
236
+ const componentKey = `${CoreBindings.COMPONENTS}.${component.name}`;
237
+ const compInstance = this.getSync<{
238
+ repositories?: Class<Repository<Model>>[];
239
+ }>(componentKey);
169
240
 
170
241
  if (compInstance.repositories) {
171
242
  for (const repo of compInstance.repositories) {
@@ -200,10 +271,10 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
200
271
 
201
272
  // Look up all datasources and update/migrate schemas one by one
202
273
  const dsBindings: Readonly<Binding<object>>[] = this.findByTag(
203
- 'datasource',
274
+ RepositoryTags.DATASOURCE,
204
275
  );
205
276
  for (const b of dsBindings) {
206
- const ds = await this.get(b.key);
277
+ const ds = await this.get<juggler.DataSource>(b.key);
207
278
 
208
279
  if (operation in ds && typeof ds[operation] === 'function') {
209
280
  debug('Migrating dataSource %s', b.key);
@@ -216,6 +287,17 @@ export function RepositoryMixin<T extends Class<any>>(superClass: T) {
216
287
  };
217
288
  }
218
289
 
290
+ /**
291
+ * Normalize name or options to `BindingFromClassOptions`
292
+ * @param nameOrOptions - Name or options for binding from class
293
+ */
294
+ function toOptions(nameOrOptions?: string | BindingFromClassOptions) {
295
+ if (typeof nameOrOptions === 'string') {
296
+ return {name: nameOrOptions};
297
+ }
298
+ return nameOrOptions ?? {};
299
+ }
300
+
219
301
  /**
220
302
  * Interface for an Application mixed in with RepositoryMixin
221
303
  */
@@ -231,6 +313,7 @@ export interface ApplicationWithRepositories extends Application {
231
313
  dataSource: Class<D> | D,
232
314
  name?: string,
233
315
  ): Binding<D>;
316
+ model<M extends Class<unknown>>(modelClass: M): Binding<M>;
234
317
  component(component: Class<unknown>, name?: string): Binding;
235
318
  mountComponentRepositories(component: Class<unknown>): void;
236
319
  migrateSchema(options?: SchemaMigrationOptions): Promise<void>;
@@ -372,3 +455,15 @@ export class RepositoryMixinDoc {
372
455
  */
373
456
  async migrateSchema(options?: SchemaMigrationOptions): Promise<void> {}
374
457
  }
458
+
459
+ /**
460
+ * Create a binding for the given model class
461
+ * @param modelClass - Model class
462
+ */
463
+ export function createModelClassBinding<M extends Class<unknown>>(
464
+ modelClass: M,
465
+ ) {
466
+ return Binding.bind<M>(`${RepositoryBindings.MODELS}.${modelClass.name}`)
467
+ .to(modelClass)
468
+ .tag(RepositoryTags.MODEL);
469
+ }
package/src/model.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import {AnyObject, DataObject, Options} from './common-types';
6
+ import {AnyObject, DataObject, Options, PrototypeOf} from './common-types';
7
7
  import {JsonSchema} from './index';
8
8
  import {RelationMetadata} from './relations';
9
9
  import {TypeResolver} from './type-resolver';
@@ -189,14 +189,22 @@ function asJSON(value: any): any {
189
189
  return value;
190
190
  }
191
191
 
192
+ /**
193
+ * Convert a value to a plain object as DTO.
194
+ *
195
+ * - The prototype of the value in primitive types are preserved,
196
+ * like `Date`, `ObjectId`.
197
+ * - If the value is an instance of custom model, call `toObject` to convert.
198
+ * - If the value is an array, convert each element recursively.
199
+ *
200
+ * @param value the value to convert
201
+ * @param options the options
202
+ */
192
203
  function asObject(value: any, options?: Options): any {
193
204
  if (value == null) return value;
194
205
  if (typeof value.toObject === 'function') {
195
206
  return value.toObject(options);
196
207
  }
197
- if (typeof value.toJSON === 'function') {
198
- return value.toJSON();
199
- }
200
208
  if (Array.isArray(value)) {
201
209
  return value.map(item => asObject(item, options));
202
210
  }
@@ -217,7 +225,7 @@ export abstract class Model {
217
225
  * Serialize into a plain JSON object
218
226
  */
219
227
  toJSON(): Object {
220
- const def = (<typeof Model>this.constructor).definition;
228
+ const def = (this.constructor as typeof Model).definition;
221
229
  if (def == null || def.settings.strict === false) {
222
230
  return this.toObject({ignoreUnknownProperties: false});
223
231
  }
@@ -249,18 +257,39 @@ export abstract class Model {
249
257
 
250
258
  /**
251
259
  * Convert to a plain object as DTO
260
+ *
261
+ * If `ignoreUnknownProperty` is set to false, convert all properties in the
262
+ * model instance, otherwise only convert the ones defined in the model
263
+ * definitions.
264
+ *
265
+ * See function `asObject` for each property's conversion rules.
252
266
  */
253
267
  toObject(options?: Options): Object {
254
- let obj: AnyObject;
268
+ const def = (this.constructor as typeof Model).definition;
269
+ const obj: AnyObject = {};
270
+
255
271
  if (options && options.ignoreUnknownProperties === false) {
256
- obj = {};
272
+ const hiddenProperties: string[] = def?.settings.hiddenProperties || [];
257
273
  for (const p in this) {
258
- const val = (this as AnyObject)[p];
259
- obj[p] = asObject(val, options);
274
+ if (!hiddenProperties.includes(p)) {
275
+ const val = (this as AnyObject)[p];
276
+ obj[p] = asObject(val, options);
277
+ }
260
278
  }
261
- } else {
262
- obj = this.toJSON();
279
+ return obj;
263
280
  }
281
+
282
+ const props = def.properties;
283
+ const keys = Object.keys(props);
284
+
285
+ for (const i in keys) {
286
+ const propertyName = keys[i];
287
+ const val = (this as AnyObject)[propertyName];
288
+
289
+ if (val === undefined) continue;
290
+ obj[propertyName] = asObject(val, options);
291
+ }
292
+
264
293
  return obj;
265
294
  }
266
295
 
@@ -367,3 +396,37 @@ export class Event {
367
396
  export type EntityData = DataObject<Entity>;
368
397
 
369
398
  export type EntityResolver<T extends Entity> = TypeResolver<T, typeof Entity>;
399
+
400
+ /**
401
+ * Check model data for navigational properties linking to related models.
402
+ * Throw a descriptive error if any such property is found.
403
+ *
404
+ * @param modelClass Model constructor, e.g. `Product`.
405
+ * @param entityData Model instance or a plain-data object,
406
+ * e.g. `{name: 'pen'}`.
407
+ */
408
+ export function rejectNavigationalPropertiesInData<M extends typeof Entity>(
409
+ modelClass: M,
410
+ data: DataObject<PrototypeOf<M>>,
411
+ ) {
412
+ const def = modelClass.definition;
413
+ const props = def.properties;
414
+
415
+ for (const r in def.relations) {
416
+ const relName = def.relations[r].name;
417
+ if (!(relName in data)) continue;
418
+
419
+ let msg =
420
+ 'Navigational properties are not allowed in model data ' +
421
+ `(model "${modelClass.modelName}" property "${relName}"), ` +
422
+ 'please remove it.';
423
+
424
+ if (relName in props) {
425
+ msg +=
426
+ ' The error might be invoked by belongsTo relations, please make' +
427
+ ' sure the relation name is not the same as the property name.';
428
+ }
429
+
430
+ throw new Error(msg);
431
+ }
432
+ }
@@ -0,0 +1,193 @@
1
+ import debugFactory from 'debug';
2
+ import {camelCase} from 'lodash';
3
+ import {
4
+ DataObject,
5
+ Entity,
6
+ HasManyDefinition,
7
+ InvalidRelationError,
8
+ isTypeResolver,
9
+ } from '../..';
10
+ import {resolveHasManyMetaHelper} from './has-many.helpers';
11
+
12
+ const debug = debugFactory('loopback:repository:has-many-through-helpers');
13
+
14
+ export type HasManyThroughResolvedDefinition = HasManyDefinition & {
15
+ keyTo: string;
16
+ keyFrom: string;
17
+ through: {
18
+ keyTo: string;
19
+ keyFrom: string;
20
+ };
21
+ };
22
+
23
+ /**
24
+ * Creates constraint used to query target
25
+ * @param relationMeta - hasManyThrough metadata to resolve
26
+ * @param throughInstances - Instances of through entities used to constrain the target
27
+ * @internal
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const resolvedMetadata = {
32
+ * // .. other props
33
+ * keyFrom: 'id',
34
+ * keyTo: 'id',
35
+ * through: {
36
+ * model: () => CategoryProductLink,
37
+ * keyFrom: 'categoryId',
38
+ * keyTo: 'productId',
39
+ * },
40
+ * };
41
+
42
+ * createTargetConstraint(resolvedMetadata, [
43
+ {
44
+ id: 2,
45
+ categoryId: 2,
46
+ productId: 8,
47
+ }, {
48
+ id: 2,
49
+ categoryId: 2,
50
+ productId: 9,
51
+ }
52
+ ]);
53
+ * ```
54
+ */
55
+ export function createTargetConstraint<
56
+ Target extends Entity,
57
+ Through extends Entity
58
+ >(
59
+ relationMeta: HasManyThroughResolvedDefinition,
60
+ throughInstances: Through[],
61
+ ): DataObject<Target> {
62
+ const targetPrimaryKey = relationMeta.keyTo;
63
+ const targetFkName = relationMeta.through.keyTo;
64
+ const fkValues = throughInstances.map(
65
+ (throughInstance: Through) =>
66
+ throughInstance[targetFkName as keyof Through],
67
+ );
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const constraint: any = {
70
+ [targetPrimaryKey]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues},
71
+ };
72
+ return constraint;
73
+ }
74
+
75
+ /**
76
+ * Creates constraint used to query through model
77
+ *
78
+ * @param relationMeta - hasManyThrough metadata to resolve
79
+ * @param fkValue - Value of the foreign key of the source model used to constrain through
80
+ * @param targetInstance - Instance of target entity used to constrain through
81
+ * @internal
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * const resolvedMetadata = {
86
+ * // .. other props
87
+ * keyFrom: 'id',
88
+ * keyTo: 'id',
89
+ * through: {
90
+ * model: () => CategoryProductLink,
91
+ * keyFrom: 'categoryId',
92
+ * keyTo: 'productId',
93
+ * },
94
+ * };
95
+ * createThroughConstraint(resolvedMetadata, 1);
96
+ * ```
97
+ */
98
+ export function createThroughConstraint<Through extends Entity, ForeignKeyType>(
99
+ relationMeta: HasManyThroughResolvedDefinition,
100
+ fkValue: ForeignKeyType,
101
+ ): DataObject<Through> {
102
+ const sourceFkName = relationMeta.through.keyFrom;
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const constraint: any = {[sourceFkName]: fkValue};
105
+ return constraint;
106
+ }
107
+
108
+ /**
109
+ * Resolves given hasMany metadata if target is specified to be a resolver.
110
+ * Mainly used to infer what the `keyTo` property should be from the target's
111
+ * belongsTo metadata
112
+ * @param relationMeta - hasManyThrough metadata to resolve
113
+ * @internal
114
+ */
115
+ export function resolveHasManyThroughMetadata(
116
+ relationMeta: HasManyDefinition,
117
+ ): HasManyThroughResolvedDefinition {
118
+ // some checks and relationMeta.keyFrom are handled in here
119
+ relationMeta = resolveHasManyMetaHelper(relationMeta);
120
+
121
+ if (!relationMeta.through) {
122
+ const reason = 'through must be specified';
123
+ throw new InvalidRelationError(reason, relationMeta);
124
+ }
125
+ if (!isTypeResolver(relationMeta.through?.model)) {
126
+ const reason = 'through.model must be a type resolver';
127
+ throw new InvalidRelationError(reason, relationMeta);
128
+ }
129
+
130
+ const throughModel = relationMeta.through.model();
131
+ const throughModelProperties = throughModel.definition?.properties;
132
+
133
+ const targetModel = relationMeta.target();
134
+ const targetModelProperties = targetModel.definition?.properties;
135
+
136
+ // check if metadata is already complete
137
+ if (
138
+ relationMeta.through.keyTo &&
139
+ throughModelProperties[relationMeta.through.keyTo] &&
140
+ relationMeta.through.keyFrom &&
141
+ throughModelProperties[relationMeta.through.keyFrom] &&
142
+ relationMeta.keyTo &&
143
+ targetModelProperties[relationMeta.keyTo]
144
+ ) {
145
+ // The explict cast is needed because of a limitation of type inference
146
+ return relationMeta as HasManyThroughResolvedDefinition;
147
+ }
148
+
149
+ const sourceModel = relationMeta.source;
150
+
151
+ debug(
152
+ 'Resolved model %s from given metadata: %o',
153
+ targetModel.modelName,
154
+ targetModel,
155
+ );
156
+
157
+ debug(
158
+ 'Resolved model %s from given metadata: %o',
159
+ throughModel.modelName,
160
+ throughModel,
161
+ );
162
+
163
+ const sourceFkName =
164
+ relationMeta.through.keyFrom ?? camelCase(sourceModel.modelName + '_id');
165
+ if (!throughModelProperties[sourceFkName]) {
166
+ const reason = `through model ${throughModel.name} is missing definition of source foreign key`;
167
+ throw new InvalidRelationError(reason, relationMeta);
168
+ }
169
+
170
+ const targetFkName =
171
+ relationMeta.through.keyTo ?? camelCase(targetModel.modelName + '_id');
172
+ if (!throughModelProperties[targetFkName]) {
173
+ const reason = `through model ${throughModel.name} is missing definition of target foreign key`;
174
+ throw new InvalidRelationError(reason, relationMeta);
175
+ }
176
+
177
+ const targetPrimaryKey =
178
+ relationMeta.keyTo ?? targetModel.definition.idProperties()[0];
179
+ if (!targetPrimaryKey || !targetModelProperties[targetPrimaryKey]) {
180
+ const reason = `target model ${targetModel.modelName} does not have any primary key (id property)`;
181
+ throw new InvalidRelationError(reason, relationMeta);
182
+ }
183
+
184
+ return Object.assign(relationMeta, {
185
+ keyTo: targetPrimaryKey,
186
+ keyFrom: relationMeta.keyFrom!,
187
+ through: {
188
+ ...relationMeta.through,
189
+ keyTo: targetFkName,
190
+ keyFrom: sourceFkName,
191
+ },
192
+ });
193
+ }
@@ -30,41 +30,18 @@ export type HasManyResolvedDefinition = HasManyDefinition & {
30
30
  export function resolveHasManyMetadata(
31
31
  relationMeta: HasManyDefinition,
32
32
  ): HasManyResolvedDefinition {
33
- if ((relationMeta.type as RelationType) !== RelationType.hasMany) {
34
- const reason = 'relation type must be HasMany';
35
- throw new InvalidRelationError(reason, relationMeta);
36
- }
37
-
38
- if (!isTypeResolver(relationMeta.target)) {
39
- const reason = 'target must be a type resolver';
40
- throw new InvalidRelationError(reason, relationMeta);
41
- }
33
+ // some checks and relationMeta.keyFrom are handled in here
34
+ relationMeta = resolveHasManyMetaHelper(relationMeta);
42
35
 
43
36
  const targetModel = relationMeta.target();
44
37
  const targetModelProperties =
45
38
  targetModel.definition && targetModel.definition.properties;
46
39
 
47
40
  const sourceModel = relationMeta.source;
48
- if (!sourceModel || !sourceModel.modelName) {
49
- const reason = 'source model must be defined';
50
- throw new InvalidRelationError(reason, relationMeta);
51
- }
52
41
 
53
- // keyFrom defaults to id property
54
- let keyFrom;
55
- if (
56
- relationMeta.keyFrom &&
57
- relationMeta.source.definition.properties[relationMeta.keyFrom]
58
- ) {
59
- keyFrom = relationMeta.keyFrom;
60
- } else {
61
- keyFrom = sourceModel.getIdProperties()[0];
62
- }
63
- // Make sure that if it already keys to the foreign key property,
64
- // the key exists in the target model
65
42
  if (relationMeta.keyTo && targetModelProperties[relationMeta.keyTo]) {
66
43
  // The explicit cast is needed because of a limitation of type inference
67
- return Object.assign(relationMeta, {keyFrom}) as HasManyResolvedDefinition;
44
+ return relationMeta as HasManyResolvedDefinition;
68
45
  }
69
46
 
70
47
  debug(
@@ -81,7 +58,44 @@ export function resolveHasManyMetadata(
81
58
  }
82
59
 
83
60
  return Object.assign(relationMeta, {
84
- keyFrom,
85
61
  keyTo: defaultFkName,
86
62
  } as HasManyResolvedDefinition);
87
63
  }
64
+
65
+ /**
66
+ * A helper to check relation type and the existence of the source/target models
67
+ * and set up keyFrom
68
+ * for HasMany(Through) relations
69
+ * @param relationMeta
70
+ *
71
+ * @returns relationMeta that has set up keyFrom
72
+ */
73
+ export function resolveHasManyMetaHelper(
74
+ relationMeta: HasManyDefinition,
75
+ ): HasManyDefinition {
76
+ if ((relationMeta.type as RelationType) !== RelationType.hasMany) {
77
+ const reason = 'relation type must be HasMany';
78
+ throw new InvalidRelationError(reason, relationMeta);
79
+ }
80
+
81
+ if (!isTypeResolver(relationMeta.target)) {
82
+ const reason = 'target must be a type resolver';
83
+ throw new InvalidRelationError(reason, relationMeta);
84
+ }
85
+
86
+ const sourceModel = relationMeta.source;
87
+ if (!sourceModel || !sourceModel.modelName) {
88
+ const reason = 'source model must be defined';
89
+ throw new InvalidRelationError(reason, relationMeta);
90
+ }
91
+ let keyFrom;
92
+ if (
93
+ relationMeta.keyFrom &&
94
+ relationMeta.source.definition.properties[relationMeta.keyFrom]
95
+ ) {
96
+ keyFrom = relationMeta.keyFrom;
97
+ } else {
98
+ keyFrom = sourceModel.getIdProperties()[0];
99
+ }
100
+ return Object.assign(relationMeta, {keyFrom}) as HasManyDefinition;
101
+ }