@loopback/repository
Version:
Define and implement a common set of interfaces for interacting with databases
596 lines (567 loc) • 20.6 kB
text/typescript
// Copyright IBM Corp. and LoopBack contributors 2019,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 {
constrainDataObject,
constrainFilter,
constrainWhere,
constrainWhereOr,
Count,
DataObject,
Entity,
EntityCrudRepository,
Filter,
Getter,
InvalidPolymorphismError,
Options,
StringKeyOf,
TypeResolver,
Where,
} from '../..';
/**
* CRUD operations for a target repository of a HasManyThrough relation
*
* EXPERIMENTAL: This interface is not stable and may change in the near future.
* Backwards-incompatible changes may be introduced in semver-minor versions.
*/
export interface HasManyThroughRepository<
Target extends Entity,
TargetID,
Through extends Entity,
> {
/**
* Create a target model instance
* @param targetModelData - The target model data
* @param options - Options for the operation
* options.polymorphicType a string or a string array of polymorphic type names
* specify of which concrete model the created instance should be
* @returns A promise which resolves to the newly created target model instance
*/
create(
targetModelData: DataObject<Target>,
options?: Options & {
throughData?: DataObject<Through>;
throughOptions?: Options;
} & {polymorphicType?: string},
): Promise<Target>;
/**
* Find target model instance(s)
* @param filter - A filter object for where, order, limit, etc.
* @param options - Options for the operation
* options.throughOptions.discriminator - target discriminator field on through
* options.polymorphicType a string or a string array of polymorphic type names
* to specify which repositories should are expected to be searched
* It is highly recommended to contain this param especially for
* datasources using deplicated ids across tables
* @returns A promise which resolves with the found target instance(s)
*/
find(
filter?: Filter<Target>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {polymorphicType?: string | string[]},
): Promise<Target[]>;
/**
* Delete multiple target model instances
* @param where - Instances within the where scope are deleted
* @param options
* options.throughOptions.discriminator - target discriminator field on through
* options.polymorphicType a string or a string array of polymorphic type names
* to specify which repositories should are expected to be searched
* It is highly recommended to contain this param especially for
* datasources using deplicated ids across tables
* @returns A promise which resolves the deleted target model instances
*/
delete(
where?: Where<Target>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {polymorphicType?: string | string[]},
): Promise<Count>;
/**
* Patch multiple target model instances
* @param dataObject - The fields and their new values to patch
* @param where - Instances within the where scope are patched
* @param options
* options.throughOptions.discriminator - target discriminator field on through
* options.isPolymorphic - whether dataObject is a dictionary
* @returns A promise which resolves the patched target model instances
*/
patch(
dataObject:
| DataObject<Target>
| {[polymorphicType: string]: DataObject<Target>},
where?: Where<Target>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {isPolymorphic?: boolean},
): Promise<Count>;
/**
* Creates a new many-to-many association to an existing target model instance
* @param targetModelId - The target model ID to link
* @param options
* @returns A promise which resolves to the linked target model instance
*/
link(
targetModelId: TargetID,
options?: Options & {
throughData?: DataObject<Through>;
throughOptions?: Options;
},
): Promise<void>;
/**
* Removes an association to an existing target model instance
* @param targetModelId - The target model to unlink
* @param options
* @returns A promise which resolves to null
*/
unlink(
targetModelId: TargetID,
options?: Options & {
throughOptions?: Options;
},
): Promise<void>;
/**
* Remove all association to an existing target model instance
* @param options
* @return A promise which resolves to void
*/
unlinkAll(
options?: Options & {
throughOptions?: Options;
},
): Promise<void>;
}
/**
* a class for CRUD operations for hasManyThrough relation.
*
* Warning: The hasManyThrough interface is experimental and is subject to change.
* If backwards-incompatible changes are made, a new major version may not be
* released.
*/
export class DefaultHasManyThroughRepository<
TargetEntity extends Entity,
TargetID,
TargetRepository extends EntityCrudRepository<TargetEntity, TargetID>,
ThroughEntity extends Entity,
ThroughID,
ThroughRepository extends EntityCrudRepository<ThroughEntity, ThroughID>,
> implements HasManyThroughRepository<TargetEntity, TargetID, ThroughEntity>
{
constructor(
public getTargetRepository:
| Getter<TargetRepository>
| {
[repoType: string]: Getter<TargetRepository>;
},
public getThroughRepository: Getter<ThroughRepository>,
public getTargetConstraintFromThroughModels: (
throughInstances: ThroughEntity[],
) => DataObject<TargetEntity>,
public getTargetKeys: (throughInstances: ThroughEntity[]) => TargetID[],
public getThroughConstraintFromSource: () => DataObject<ThroughEntity>,
public getTargetIds: (targetInstances: TargetEntity[]) => TargetID[],
public getThroughConstraintFromTarget: (
targetID: TargetID[],
) => DataObject<ThroughEntity>,
public targetResolver: TypeResolver<Entity, typeof Entity>,
public throughResolver: TypeResolver<Entity, typeof Entity>,
) {
if (typeof getTargetRepository === 'function') {
this.getTargetRepositoryDict = {
[targetResolver().name]:
getTargetRepository as Getter<TargetRepository>,
};
} else {
this.getTargetRepositoryDict = getTargetRepository as {
[repoType: string]: Getter<TargetRepository>;
};
}
}
public getTargetRepositoryDict: {
[repoType: string]: Getter<TargetRepository>;
};
async create(
targetModelData: DataObject<TargetEntity>,
options?: Options & {
throughData?: DataObject<ThroughEntity>;
throughOptions?: Options;
} & {polymorphicType?: string},
): Promise<TargetEntity> {
let targetPolymorphicTypeName = options?.polymorphicType;
if (targetPolymorphicTypeName) {
if (!this.getTargetRepositoryDict[targetPolymorphicTypeName]) {
throw new InvalidPolymorphismError(targetPolymorphicTypeName);
}
} else {
if (Object.keys(this.getTargetRepositoryDict).length > 1) {
console.warn(
'It is highly recommended to specify the polymorphicTypes param when using polymorphic types.',
);
}
targetPolymorphicTypeName = this.targetResolver().name;
if (!this.getTargetRepositoryDict[targetPolymorphicTypeName]) {
throw new InvalidPolymorphismError(targetPolymorphicTypeName);
}
}
const targetRepository =
await this.getTargetRepositoryDict[targetPolymorphicTypeName]();
const targetInstance = await targetRepository.create(
targetModelData,
options,
);
await this.link(targetInstance.getId(), options);
return targetInstance;
}
async find(
filter?: Filter<TargetEntity>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {polymorphicType?: string | string[]},
): Promise<TargetEntity[]> {
const targetDiscriminatorOnThrough = options?.throughOptions?.discriminator;
let targetPolymorphicTypes = options?.polymorphicType;
let allKeys: string[];
if (Object.keys(this.getTargetRepositoryDict).length <= 1) {
allKeys = Object.keys(this.getTargetRepositoryDict);
} else {
if (!targetDiscriminatorOnThrough) {
console.warn(
'It is highly recommended to specify the targetDiscriminatorOnThrough param when using polymorphic types.',
);
}
if (!targetPolymorphicTypes || targetPolymorphicTypes.length === 0) {
console.warn(
'It is highly recommended to specify the polymorphicTypes param when using polymorphic types.',
);
allKeys = Object.keys(this.getTargetRepositoryDict);
} else {
if (typeof targetPolymorphicTypes === 'string') {
targetPolymorphicTypes = [targetPolymorphicTypes];
}
allKeys = [];
new Set(targetPolymorphicTypes!).forEach(element => {
if (Object.keys(this.getTargetRepositoryDict).includes(element)) {
allKeys.push(element);
}
});
}
}
const sourceConstraint = this.getThroughConstraintFromSource();
const throughCategorized: {[concreteType: string]: (ThroughEntity & {})[]} =
{};
const throughRepository = await this.getThroughRepository();
(
await throughRepository.find(
constrainFilter(undefined, sourceConstraint),
options?.throughOptions,
)
).forEach(element => {
let concreteTargetType;
if (!targetDiscriminatorOnThrough) {
concreteTargetType = this.targetResolver().name;
} else {
concreteTargetType = String(
element[targetDiscriminatorOnThrough as StringKeyOf<ThroughEntity>],
);
}
if (!allKeys.includes(concreteTargetType)) {
return;
}
if (!this.getTargetRepositoryDict[concreteTargetType]) {
throw new InvalidPolymorphismError(
concreteTargetType,
targetDiscriminatorOnThrough,
);
}
if (!throughCategorized[concreteTargetType]) {
throughCategorized[concreteTargetType] = [];
}
throughCategorized[concreteTargetType].push(element);
});
let allTargets: TargetEntity[] = [];
for (const key of Object.keys(throughCategorized)) {
const targetRepository = await this.getTargetRepositoryDict[key]();
const targetConstraint = this.getTargetConstraintFromThroughModels(
throughCategorized[key],
);
allTargets = allTargets.concat(
await targetRepository.find(constrainFilter(filter, targetConstraint), {
...options,
polymorphicType: key,
}),
);
}
return allTargets;
}
async delete(
where?: Where<TargetEntity>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {polymorphicType?: string | string[]},
): Promise<Count> {
const targetDiscriminatorOnThrough = options?.throughOptions?.discriminator;
let targetPolymorphicTypes = options?.polymorphicType;
let allKeys: string[];
if (Object.keys(this.getTargetRepositoryDict).length <= 1) {
allKeys = Object.keys(this.getTargetRepositoryDict);
} else {
if (!targetDiscriminatorOnThrough) {
console.warn(
'It is highly recommended to specify the targetDiscriminatorOnThrough param when using polymorphic types.',
);
}
if (!targetPolymorphicTypes || targetPolymorphicTypes.length === 0) {
console.warn(
'It is highly recommended to specify the polymorphicTypes param when using polymorphic types.',
);
allKeys = Object.keys(this.getTargetRepositoryDict);
} else {
if (typeof targetPolymorphicTypes === 'string') {
targetPolymorphicTypes = [targetPolymorphicTypes];
}
allKeys = [];
new Set(targetPolymorphicTypes!).forEach(element => {
if (Object.keys(this.getTargetRepositoryDict).includes(element)) {
allKeys.push(element);
}
});
}
}
const sourceConstraint = this.getThroughConstraintFromSource();
let totalCount = 0;
const throughCategorized: {[concreteType: string]: (ThroughEntity & {})[]} =
{};
const throughRepository = await this.getThroughRepository();
(
await throughRepository.find(
constrainFilter(undefined, sourceConstraint),
options?.throughOptions,
)
).forEach(element => {
let concreteTargetType;
if (!targetDiscriminatorOnThrough) {
concreteTargetType = this.targetResolver().name;
} else {
concreteTargetType = String(
element[targetDiscriminatorOnThrough as StringKeyOf<ThroughEntity>],
);
}
if (!allKeys.includes(concreteTargetType)) {
return;
}
if (!this.getTargetRepositoryDict[concreteTargetType]) {
throw new InvalidPolymorphismError(
concreteTargetType,
targetDiscriminatorOnThrough,
);
}
if (!throughCategorized[concreteTargetType]) {
throughCategorized[concreteTargetType] = [];
}
throughCategorized[concreteTargetType].push(element);
});
for (const targetKey of Object.keys(throughCategorized)) {
const targetRepository = await this.getTargetRepositoryDict[targetKey]();
if (where) {
// only delete related through models
// TODO(Agnes): this performance can be improved by only fetching related data
// TODO: add target ids to the `where` constraint
const targets = await targetRepository.find({where});
const targetIds = this.getTargetIds(targets);
if (targetIds.length > 0) {
const targetConstraint =
this.getThroughConstraintFromTarget(targetIds);
const constraints = {...targetConstraint, ...sourceConstraint};
await throughRepository.deleteAll(
constrainDataObject(
{},
constraints as DataObject<ThroughEntity>,
) as Where<ThroughEntity>,
options?.throughOptions,
);
}
} else {
// otherwise, delete through models that relate to the sourceId
const targetFkValues = this.getTargetKeys(
throughCategorized[targetKey],
);
// delete through instances that have the targets that are going to be deleted
const throughFkConstraint =
this.getThroughConstraintFromTarget(targetFkValues);
await throughRepository.deleteAll(
constrainWhereOr({}, [
sourceConstraint as Where<ThroughEntity>,
throughFkConstraint as Where<ThroughEntity>,
]),
);
}
// delete target(s)
const targetConstraint = this.getTargetConstraintFromThroughModels(
throughCategorized[targetKey],
);
totalCount +=
(
await targetRepository.deleteAll(
constrainWhere(where, targetConstraint as Where<TargetEntity>),
options,
)
)?.count ?? 0;
}
return {count: totalCount};
}
// only allows patch target instances for now
async patch(
dataObject:
| DataObject<TargetEntity>
| {[polymorphicType: string]: DataObject<TargetEntity>},
where?: Where<TargetEntity>,
options?: Options & {
throughOptions?: Options & {discriminator?: string};
} & {isPolymorphic?: boolean},
): Promise<Count> {
const targetDiscriminatorOnThrough = options?.throughOptions?.discriminator;
const isMultipleTypes = options?.isPolymorphic;
let allKeys: string[];
if (!targetDiscriminatorOnThrough) {
if (Object.keys(this.getTargetRepositoryDict).length > 1) {
console.warn(
'It is highly recommended to specify the targetDiscriminatorOnThrough param when using polymorphic types.',
);
}
}
if (!isMultipleTypes) {
if (Object.keys(this.getTargetRepositoryDict).length > 1) {
console.warn(
'It is highly recommended to specify the isMultipleTypes param and pass in a dictionary of dataobjects when using polymorphic types.',
);
}
allKeys = Object.keys(this.getTargetRepositoryDict);
} else {
allKeys = [];
new Set(Object.keys(dataObject)).forEach(element => {
if (Object.keys(this.getTargetRepositoryDict).includes(element)) {
allKeys.push(element);
}
});
}
const sourceConstraint = this.getThroughConstraintFromSource();
const throughCategorized: {[concreteType: string]: (ThroughEntity & {})[]} =
{};
const throughRepository = await this.getThroughRepository();
(
await throughRepository.find(
constrainFilter(undefined, sourceConstraint),
options?.throughOptions,
)
).forEach(element => {
let concreteTargetType;
if (!targetDiscriminatorOnThrough) {
concreteTargetType = this.targetResolver().name;
} else {
concreteTargetType = String(
element[targetDiscriminatorOnThrough as StringKeyOf<ThroughEntity>],
);
}
if (!allKeys.includes(concreteTargetType)) {
return;
}
if (!this.getTargetRepositoryDict[concreteTargetType]) {
throw new InvalidPolymorphismError(
concreteTargetType,
targetDiscriminatorOnThrough,
);
}
if (!throughCategorized[concreteTargetType]) {
throughCategorized[concreteTargetType] = [];
}
throughCategorized[concreteTargetType].push(element);
});
let updatedCount = 0;
for (const key of Object.keys(throughCategorized)) {
const targetRepository = await this.getTargetRepositoryDict[key]();
const targetConstraint = this.getTargetConstraintFromThroughModels(
throughCategorized[key],
);
updatedCount +=
(
await targetRepository.updateAll(
constrainDataObject(
isMultipleTypes
? (
dataObject as {
[polymorphicType: string]: DataObject<TargetEntity>;
}
)[key]
: (dataObject as DataObject<TargetEntity>),
targetConstraint,
),
constrainWhere(where, targetConstraint as Where<TargetEntity>),
options,
)
)?.count ?? 0;
}
return {count: updatedCount};
}
async link(
targetId: TargetID,
options?: Options & {
throughData?: DataObject<ThroughEntity>;
throughOptions?: Options;
},
): Promise<void> {
const throughRepository = await this.getThroughRepository();
const sourceConstraint = this.getThroughConstraintFromSource();
const targetConstraint = this.getThroughConstraintFromTarget([targetId]);
const constraints = {...targetConstraint, ...sourceConstraint};
await throughRepository.create(
constrainDataObject(
options?.throughData ?? {},
constraints as DataObject<ThroughEntity>,
),
options?.throughOptions,
);
}
async unlink(
targetId: TargetID,
options?: Options & {
throughOptions?: Options;
},
): Promise<void> {
const throughRepository = await this.getThroughRepository();
const sourceConstraint = this.getThroughConstraintFromSource();
const targetConstraint = this.getThroughConstraintFromTarget([targetId]);
const constraints = {...targetConstraint, ...sourceConstraint};
await throughRepository.deleteAll(
constrainDataObject(
{},
constraints as DataObject<ThroughEntity>,
) as Where<ThroughEntity>,
options?.throughOptions,
);
}
async unlinkAll(
options?: Options & {
throughOptions?: Options;
},
): Promise<void> {
const throughRepository = await this.getThroughRepository();
const sourceConstraint = this.getThroughConstraintFromSource();
const throughInstances = await throughRepository.find(
constrainFilter(undefined, sourceConstraint),
options?.throughOptions,
);
const targetFkValues = this.getTargetKeys(throughInstances);
const targetConstraint =
this.getThroughConstraintFromTarget(targetFkValues);
const constraints = {...targetConstraint, ...sourceConstraint};
await throughRepository.deleteAll(
constrainDataObject(
{},
constraints as DataObject<ThroughEntity>,
) as Where<ThroughEntity>,
options?.throughOptions,
);
}
}