@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.
- package/CHANGELOG.md +57 -0
- package/dist/common-types.d.ts +1 -0
- package/dist/common-types.js +2 -0
- package/dist/common-types.js.map +1 -1
- package/dist/connectors/index.js +4 -0
- package/dist/connectors/index.js.map +1 -1
- package/dist/decorators/metadata.js +1 -0
- package/dist/decorators/metadata.js.map +1 -1
- package/dist/decorators/model.decorator.d.ts +1 -1
- package/dist/decorators/model.decorator.js +1 -0
- package/dist/decorators/model.decorator.js.map +1 -1
- package/dist/decorators/repository.decorator.js +1 -0
- package/dist/decorators/repository.decorator.js.map +1 -1
- package/dist/define-model-class.js +1 -0
- package/dist/define-model-class.js.map +1 -1
- package/dist/define-repository-class.d.ts +119 -0
- package/dist/define-repository-class.js +98 -0
- package/dist/define-repository-class.js.map +1 -0
- package/dist/errors/entity-not-found.error.js +1 -0
- package/dist/errors/entity-not-found.error.js.map +1 -1
- package/dist/errors/invalid-relation.error.js +1 -0
- package/dist/errors/invalid-relation.error.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/keys.d.ts +34 -0
- package/dist/keys.js +44 -0
- package/dist/keys.js.map +1 -0
- package/dist/mixins/repository.mixin.d.ts +91 -9
- package/dist/mixins/repository.mixin.js +62 -20
- package/dist/mixins/repository.mixin.js.map +1 -1
- package/dist/model.d.ts +16 -1
- package/dist/model.js +61 -9
- package/dist/model.js.map +1 -1
- package/dist/query.js +1 -0
- package/dist/query.js.map +1 -1
- package/dist/relations/belongs-to/belongs-to-accessor.js +1 -0
- package/dist/relations/belongs-to/belongs-to-accessor.js.map +1 -1
- package/dist/relations/belongs-to/belongs-to.decorator.js +1 -0
- package/dist/relations/belongs-to/belongs-to.decorator.js.map +1 -1
- package/dist/relations/belongs-to/belongs-to.helpers.js +1 -0
- package/dist/relations/belongs-to/belongs-to.helpers.js.map +1 -1
- package/dist/relations/belongs-to/belongs-to.inclusion-resolver.js +1 -0
- package/dist/relations/belongs-to/belongs-to.inclusion-resolver.js.map +1 -1
- package/dist/relations/belongs-to/belongs-to.repository.js +1 -0
- package/dist/relations/belongs-to/belongs-to.repository.js.map +1 -1
- package/dist/relations/has-many/has-many-repository.factory.js +1 -0
- package/dist/relations/has-many/has-many-repository.factory.js.map +1 -1
- package/dist/relations/has-many/has-many-through.helpers.d.ts +74 -0
- package/dist/relations/has-many/has-many-through.helpers.js +145 -0
- package/dist/relations/has-many/has-many-through.helpers.js.map +1 -0
- package/dist/relations/has-many/has-many.decorator.js +1 -0
- package/dist/relations/has-many/has-many.decorator.js.map +1 -1
- package/dist/relations/has-many/has-many.helpers.d.ts +9 -0
- package/dist/relations/has-many/has-many.helpers.js +33 -21
- package/dist/relations/has-many/has-many.helpers.js.map +1 -1
- package/dist/relations/has-many/has-many.inclusion-resolver.js +1 -0
- package/dist/relations/has-many/has-many.inclusion-resolver.js.map +1 -1
- package/dist/relations/has-many/has-many.repository.js +1 -0
- package/dist/relations/has-many/has-many.repository.js.map +1 -1
- package/dist/relations/has-one/has-one-repository.factory.js +1 -0
- package/dist/relations/has-one/has-one-repository.factory.js.map +1 -1
- package/dist/relations/has-one/has-one.decorator.js +1 -0
- package/dist/relations/has-one/has-one.decorator.js.map +1 -1
- package/dist/relations/has-one/has-one.helpers.js +1 -0
- package/dist/relations/has-one/has-one.helpers.js.map +1 -1
- package/dist/relations/has-one/has-one.inclusion-resolver.js +1 -0
- package/dist/relations/has-one/has-one.inclusion-resolver.js.map +1 -1
- package/dist/relations/has-one/has-one.repository.js +1 -0
- package/dist/relations/has-one/has-one.repository.js.map +1 -1
- package/dist/relations/relation.decorator.js +1 -0
- package/dist/relations/relation.decorator.js.map +1 -1
- package/dist/relations/relation.helpers.js +1 -0
- package/dist/relations/relation.helpers.js.map +1 -1
- package/dist/relations/relation.types.d.ts +25 -27
- package/dist/relations/relation.types.js +2 -1
- package/dist/relations/relation.types.js.map +1 -1
- package/dist/repositories/constraint-utils.js +1 -0
- package/dist/repositories/constraint-utils.js.map +1 -1
- package/dist/repositories/index.js +1 -0
- package/dist/repositories/index.js.map +1 -1
- package/dist/repositories/kv.repository.bridge.js +1 -0
- package/dist/repositories/kv.repository.bridge.js.map +1 -1
- package/dist/repositories/legacy-juggler-bridge.d.ts +3 -3
- package/dist/repositories/legacy-juggler-bridge.js +8 -18
- package/dist/repositories/legacy-juggler-bridge.js.map +1 -1
- package/dist/repositories/repository.js +1 -0
- package/dist/repositories/repository.js.map +1 -1
- package/dist/transaction.js +1 -0
- package/dist/transaction.js.map +1 -1
- package/dist/type-resolver.d.ts +4 -0
- package/dist/type-resolver.js +9 -0
- package/dist/type-resolver.js.map +1 -1
- package/dist/types/any.js +1 -0
- package/dist/types/any.js.map +1 -1
- package/dist/types/array.js +1 -0
- package/dist/types/array.js.map +1 -1
- package/dist/types/boolean.js +1 -0
- package/dist/types/boolean.js.map +1 -1
- package/dist/types/buffer.js +1 -0
- package/dist/types/buffer.js.map +1 -1
- package/dist/types/date.js +1 -0
- package/dist/types/date.js.map +1 -1
- package/dist/types/index.d.ts +11 -9
- package/dist/types/index.js +33 -17
- package/dist/types/index.js.map +1 -1
- package/dist/types/model.js +1 -0
- package/dist/types/model.js.map +1 -1
- package/dist/types/null.d.ts +12 -0
- package/dist/types/null.js +33 -0
- package/dist/types/null.js.map +1 -0
- package/dist/types/number.js +1 -0
- package/dist/types/number.js.map +1 -1
- package/dist/types/object.js +1 -0
- package/dist/types/object.js.map +1 -1
- package/dist/types/string.js +1 -0
- package/dist/types/string.js.map +1 -1
- package/dist/types/union.js +1 -0
- package/dist/types/union.js.map +1 -1
- package/package.json +13 -14
- package/src/common-types.ts +1 -0
- package/src/define-repository-class.ts +170 -0
- package/src/index.ts +2 -0
- package/src/keys.ts +40 -0
- package/src/mixins/repository.mixin.ts +120 -25
- package/src/model.ts +74 -11
- package/src/relations/has-many/has-many-through.helpers.ts +193 -0
- package/src/relations/has-many/has-many.helpers.ts +41 -27
- package/src/relations/relation.types.ts +24 -30
- package/src/repositories/legacy-juggler-bridge.ts +16 -24
- package/src/type-resolver.ts +8 -0
- package/src/types/index.ts +11 -8
- package/src/types/null.ts +35 -0
- package/index.d.ts +0 -6
- 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 {
|
|
7
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
110
|
+
nameOrOptions?: string | BindingFromClassOptions,
|
|
64
111
|
): Binding<R> {
|
|
65
112
|
const binding = createBindingFromClass(repoClass, {
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
116
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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 =
|
|
168
|
-
const compInstance = this.getSync
|
|
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
|
-
|
|
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 = (
|
|
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
|
-
|
|
268
|
+
const def = (this.constructor as typeof Model).definition;
|
|
269
|
+
const obj: AnyObject = {};
|
|
270
|
+
|
|
255
271
|
if (options && options.ignoreUnknownProperties === false) {
|
|
256
|
-
|
|
272
|
+
const hiddenProperties: string[] = def?.settings.hiddenProperties || [];
|
|
257
273
|
for (const p in this) {
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
if (!hiddenProperties.includes(p)) {
|
|
275
|
+
const val = (this as AnyObject)[p];
|
|
276
|
+
obj[p] = asObject(val, options);
|
|
277
|
+
}
|
|
260
278
|
}
|
|
261
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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
|
+
}
|