@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
388 lines (362 loc) • 10.7 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2020. All Rights Reserved.
// Node module: @loopback/repository
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
import debugFactory from 'debug';
import {camelCase} from 'lodash';
import {
DataObject,
deduplicate,
Entity,
HasManyDefinition,
InvalidRelationError,
isTypeResolver,
} from '../..';
import {resolveHasManyMetaHelper} from './has-many.helpers';
const debug = debugFactory(
'loopback:repository:relations:has-many-through:helpers',
);
export type HasManyThroughResolvedDefinition = HasManyDefinition & {
keyTo: string;
keyFrom: string;
through: {
keyTo: string;
keyFrom: string;
polymorphic: false | {discriminator: string};
};
};
/**
* Creates target constraint based on through models
* @param relationMeta - resolved hasManyThrough metadata
* @param throughInstances - an array of through instances
*
* @example
* ```ts
* const resolvedMetadata = {
* // .. other props
* keyFrom: 'id',
* keyTo: 'id',
* through: {
* model: () => CategoryProductLink,
* keyFrom: 'categoryId',
* keyTo: 'productId',
* },
* };
* createTargetConstraintFromThrough(resolvedMetadata,[{
id: 2,
categoryId: 2,
productId: 8,
}]);
* >>> {id: 8}
* createTargetConstraintFromThrough(resolvedMetadata, [
{
id: 2,
categoryId: 2,
productId: 8,
}, {
id: 1,
categoryId: 2,
productId: 9,
}
]);
>>> {id: {inq: [9, 8]}}
* ```
*/
export function createTargetConstraintFromThrough<
Target extends Entity,
Through extends Entity,
>(
relationMeta: HasManyThroughResolvedDefinition,
throughInstances: Through[],
): DataObject<Target> {
const fkValues = getTargetKeysFromThroughModels(
relationMeta,
throughInstances,
);
const targetPrimaryKey = relationMeta.keyTo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const constraint: any = {
[targetPrimaryKey]: fkValues.length === 1 ? fkValues[0] : {inq: fkValues},
};
return constraint;
}
/**
* Returns an array of target fks of the given throughInstances.
*
* @param relationMeta - resolved hasManyThrough metadata
* @param throughInstances - an array of through instances
*
* @example
* ```ts
* const resolvedMetadata = {
* // .. other props
* keyFrom: 'id',
* keyTo: 'id',
* through: {
* model: () => CategoryProductLink,
* keyFrom: 'categoryId',
* keyTo: 'productId',
* },
* };
* getTargetKeysFromThroughModels(resolvedMetadata,[{
id: 2,
categoryId: 2,
productId: 8,
}]);
* >>> [8]
* getTargetKeysFromThroughModels(resolvedMetadata, [
{
id: 2,
categoryId: 2,
productId: 8,
}, {
id: 1,
categoryId: 2,
productId: 9,
}
]);
>>> [8, 9]
*/
export function getTargetKeysFromThroughModels<
Through extends Entity,
TargetID,
>(
relationMeta: HasManyThroughResolvedDefinition,
throughInstances: Through[],
): TargetID[] {
const targetFkName = relationMeta.through.keyTo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let fkValues: any = throughInstances.map(
(throughInstance: Through) =>
throughInstance[targetFkName as keyof Through],
);
fkValues = deduplicate(fkValues);
return fkValues as TargetID[];
}
/**
* Creates through constraint based on the source key
*
* @param relationMeta - resolved hasManyThrough metadata
* @param fkValue - foreign key of the source instance
* @internal
*
* @example
* ```ts
* const resolvedMetadata = {
* // .. other props
* keyFrom: 'id',
* keyTo: 'id',
* through: {
* model: () => CategoryProductLink,
* keyFrom: 'categoryId',
* keyTo: 'productId',
* },
* };
* createThroughConstraintFromSource(resolvedMetadata, 1);
*
* >>> {categoryId: 1}
* ```
*/
export function createThroughConstraintFromSource<
Through extends Entity,
SourceID,
>(
relationMeta: HasManyThroughResolvedDefinition,
fkValue: SourceID,
): DataObject<Through> {
const sourceFkName = relationMeta.through.keyFrom;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const constraint: any = {[sourceFkName]: fkValue};
return constraint;
}
/**
* Returns an array of target ids of the given target instances.
*
* @param relationMeta - resolved hasManyThrough metadata
* @param targetInstances - an array of target instances
*
* @example
* ```ts
* const resolvedMetadata = {
* // .. other props
* keyFrom: 'id',
* keyTo: 'id',
* through: {
* model: () => CategoryProductLink,
* keyFrom: 'categoryId',
* keyTo: 'productId',
* },
* };
* getTargetKeysFromTargetModels(resolvedMetadata,[{
id: 2,
des: 'a target',
}]);
* >>> [2]
* getTargetKeysFromTargetModels(resolvedMetadata, [
{
id: 2,
des: 'a target',
}, {
id: 1,
des: 'a target',
}
]);
>>> [2, 1]
*/
export function getTargetIdsFromTargetModels<Target extends Entity, TargetID>(
relationMeta: HasManyThroughResolvedDefinition,
targetInstances: Target[],
): TargetID[] {
const targetId = relationMeta.keyTo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let ids = [] as any;
ids = targetInstances.map(
(targetInstance: Target) => targetInstance[targetId as keyof Target],
);
ids = deduplicate(ids);
return ids as TargetID[];
}
/**
* Creates through constraint based on the target foreign key
*
* @param relationMeta - resolved hasManyThrough metadata
* @param fkValue an array of the target instance foreign keys
* @internal
*
* @example
* ```ts
* const resolvedMetadata = {
* // .. other props
* keyFrom: 'id',
* keyTo: 'id',
* through: {
* model: () => CategoryProductLink,
* keyFrom: 'categoryId',
* keyTo: 'productId',
* },
* };
* createThroughConstraintFromTarget(resolvedMetadata, [3]);
*
* >>> {productId: 3}
*
* createThroughConstraintFromTarget(resolvedMetadata, [3,4]);
*
* >>> {productId: {inq:[3,4]}}
*/
export function createThroughConstraintFromTarget<
Through extends Entity,
TargetID,
>(
relationMeta: HasManyThroughResolvedDefinition,
fkValues: TargetID[],
): DataObject<Through> {
if (fkValues === undefined || fkValues.length === 0) {
throw new Error('"fkValue" must be provided');
}
const targetFkName = relationMeta.through.keyTo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const constraint: any =
fkValues.length === 1
? {[targetFkName]: fkValues[0]}
: {[targetFkName]: {inq: fkValues}};
return constraint as DataObject<Through>;
}
/**
* Resolves given hasMany metadata if target is specified to be a resolver.
* Mainly used to infer what the `keyTo` property should be from the target's
* belongsTo metadata
* @param relationMeta - hasManyThrough metadata to resolve
* @internal
*/
export function resolveHasManyThroughMetadata(
relationMeta: HasManyDefinition,
): HasManyThroughResolvedDefinition {
// some checks and relationMeta.keyFrom are handled in here
relationMeta = resolveHasManyMetaHelper(relationMeta);
if (!relationMeta.through) {
const reason = 'through must be specified';
throw new InvalidRelationError(reason, relationMeta);
}
if (!isTypeResolver(relationMeta.through?.model)) {
const reason = 'through.model must be a type resolver';
throw new InvalidRelationError(reason, relationMeta);
}
const throughModel = relationMeta.through.model();
const throughModelProperties = throughModel.definition?.properties;
const targetModel = relationMeta.target();
const targetModelProperties = targetModel.definition?.properties;
// check if metadata is already complete
if (
relationMeta.through.keyTo &&
throughModelProperties[relationMeta.through.keyTo] &&
relationMeta.through.keyFrom &&
throughModelProperties[relationMeta.through.keyFrom] &&
relationMeta.keyTo &&
targetModelProperties[relationMeta.keyTo] &&
(relationMeta.through.polymorphic === false ||
(typeof relationMeta.through.polymorphic === 'object' &&
relationMeta.through.polymorphic.discriminator.length > 0))
) {
// The explicit cast is needed because of a limitation of type inference
return relationMeta as HasManyThroughResolvedDefinition;
}
const sourceModel = relationMeta.source;
debug(
'Resolved model %s from given metadata: %o',
targetModel.modelName,
targetModel,
);
debug(
'Resolved model %s from given metadata: %o',
throughModel.modelName,
throughModel,
);
const sourceFkName =
relationMeta.through.keyFrom ?? camelCase(sourceModel.modelName + '_id');
if (!throughModelProperties[sourceFkName]) {
const reason = `through model ${throughModel.name} is missing definition of source foreign key`;
throw new InvalidRelationError(reason, relationMeta);
}
const targetFkName =
relationMeta.through.keyTo ?? camelCase(targetModel.modelName + '_id');
if (!throughModelProperties[targetFkName]) {
const reason = `through model ${throughModel.name} is missing definition of target foreign key`;
throw new InvalidRelationError(reason, relationMeta);
}
const targetPrimaryKey =
relationMeta.keyTo ?? targetModel.definition.idProperties()[0];
if (!targetPrimaryKey || !targetModelProperties[targetPrimaryKey]) {
const reason = `target model ${targetModel.modelName} does not have any primary key (id property)`;
throw new InvalidRelationError(reason, relationMeta);
}
let throughPolymorphic: false | {discriminator: string};
if (
relationMeta.through.polymorphic === undefined ||
relationMeta.through.polymorphic === false ||
!relationMeta.through.polymorphic
) {
const polymorphicFalse = false as const;
throughPolymorphic = polymorphicFalse;
} else {
if (relationMeta.through.polymorphic === true) {
const polymorphicObject: {discriminator: string} = {
discriminator: camelCase(relationMeta.target().name + '_type'),
};
throughPolymorphic = polymorphicObject;
} else {
const polymorphicObject: {discriminator: string} = relationMeta.through
.polymorphic as {discriminator: string};
throughPolymorphic = polymorphicObject;
}
}
return Object.assign(relationMeta, {
keyTo: targetPrimaryKey,
keyFrom: relationMeta.keyFrom!,
through: {
...relationMeta.through,
keyTo: targetFkName,
keyFrom: sourceFkName,
polymorphic: throughPolymorphic,
},
});
}