UNPKG

@loopback/repository

Version:

Define and implement a common set of interfaces for interacting with databases

388 lines (362 loc) 10.7 kB
// 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, }, }); }