Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dataloader can now resolve to null for nullable fields #4773

Merged
merged 3 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/core/dataloader/creators/base/data.loader.creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { ILoader } from '../../loader.interface';
import { DataLoaderCreatorOptions } from './data.loader.creator.options';
import { EntityNotFoundException } from '@common/exceptions';
hero101 marked this conversation as resolved.
Show resolved Hide resolved

export interface DataLoaderCreator<TReturn> {
create(options?: DataLoaderCreatorOptions<TReturn>): ILoader<TReturn>;
create(
options?: DataLoaderCreatorOptions<TReturn>
): ILoader<TReturn | null | EntityNotFoundException>;
}
1 change: 1 addition & 0 deletions src/core/dataloader/creators/base/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './data.loader.creator';

export * from './data.loader.creator.base.options';
export * from './data.loader.creator.options';
export * from './data.loader.creator.limit.options';
export * from './data.loader.creator.pagination.options';
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import {
DataLoaderCreator,
DataLoaderCreatorBaseOptions,
} from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { Callout, ICallout } from '@domain/collaboration/callout';
import { EntityNotFoundException } from '@common/exceptions';

@Injectable()
export class CalloutLoaderCreator implements DataLoaderCreator<ICallout> {
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<ICallout> {
return createBatchLoader(
this.constructor.name,
Callout.name,
this.calloutInBatch
);
public create(
options?: DataLoaderCreatorBaseOptions<any, any>
): ILoader<ICallout | null | EntityNotFoundException> {
return createBatchLoader(this.calloutInBatch, {
name: this.constructor.name,
loadedTypeName: Callout.name,
resolveToNull: options?.resolveToNull,
});
}

private calloutInBatch = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { EntityManager } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { CommunityContributorType } from '@common/enums/community.contributor.type';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import {
DataLoaderCreator,
DataLoaderCreatorBaseOptions,
} from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { User } from '@domain/community/user/user.entity';
import { Organization } from '@domain/community/organization';
Expand All @@ -14,12 +17,12 @@ export class CommunityTypeLoaderCreator
{
constructor(@InjectEntityManager() private manager: EntityManager) {}

create() {
return createBatchLoader(
this.constructor.name,
'CommunityContributorType',
this.communityTypeInBatch
);
create(options?: DataLoaderCreatorBaseOptions<any, any>) {
return createBatchLoader(this.communityTypeInBatch, {
name: this.constructor.name,
loadedTypeName: 'CommunityContributorType',
resolveToNull: options?.resolveToNull,
});
}

private async communityTypeInBatch(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import {
DataLoaderCreator,
DataLoaderCreatorBaseOptions,
} from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { IContributor } from '@domain/community/contributor/contributor.interface';
import { User } from '@domain/community/user/user.entity';
import { Organization } from '@domain/community/organization';
import { VirtualContributor } from '@domain/community/virtual-contributor/virtual.contributor.entity';
import { IContributorBase } from '@domain/community/contributor';
import { EntityNotFoundException } from '@common/exceptions';

@Injectable()
export class ContributorLoaderCreator
implements DataLoaderCreator<IContributorBase>
{
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<IContributor> {
return createBatchLoader(
this.constructor.name,
'Contributor',
this.contributorsInBatch
);
public create(
options?: DataLoaderCreatorBaseOptions<any, any>
): ILoader<IContributor | null | EntityNotFoundException> {
return createBatchLoader(this.contributorsInBatch, {
name: this.constructor.name,
loadedTypeName: 'Contributor',
resolveToNull: options?.resolveToNull,
});
}

private contributorsInBatch = async (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { EntityManager, In } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { DataLoaderCreator } from '@core/dataloader/creators/base';
import {
DataLoaderCreator,
DataLoaderCreatorBaseOptions,
} from '@core/dataloader/creators/base';
import { createBatchLoader } from '@core/dataloader/utils';
import { ILoader } from '@core/dataloader/loader.interface';
import { ISpace } from '@domain/space/space/space.interface';
import { Space } from '@domain/space/space/space.entity';
import { EntityNotFoundException } from '@common/exceptions';

@Injectable()
export class SpaceLoaderCreator implements DataLoaderCreator<ISpace> {
constructor(@InjectEntityManager() private manager: EntityManager) {}

public create(): ILoader<ISpace> {
return createBatchLoader(
this.constructor.name,
Space.name,
this.spaceInBatch
);
public create(
options?: DataLoaderCreatorBaseOptions<any, any>
): ILoader<ISpace | null | EntityNotFoundException> {
return createBatchLoader(this.spaceInBatch, {
name: this.constructor.name,
loadedTypeName: Space.name,
resolveToNull: options?.resolveToNull,
});
}

private spaceInBatch = (keys: ReadonlyArray<string>): Promise<Space[]> => {
Expand Down
34 changes: 21 additions & 13 deletions src/core/dataloader/utils/createTypedBatchLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ import { EntityNotFoundException } from '@common/exceptions';
import { LogContext } from '@common/enums';
import { ILoader } from '../loader.interface';
import { sorOutputByKeys } from '@core/dataloader/utils/sort.output.by.keys';
import { DataLoaderCreatorBaseOptions } from '@core/dataloader/creators/base/data.loader.creator.base.options';

export const createBatchLoader = <TResult extends { id: string }>(
name: string, // for debugging purposes
loadedTypeName: string, // for debugging purposes
batchLoadFn: (keys: ReadonlyArray<string>) => Promise<TResult[]>
): ILoader<TResult> => {
// the data loader returns an array the MUST match the input length
batchLoadFn: (keys: ReadonlyArray<string>) => Promise<TResult[]>,
options?: {
name: string; // for debugging purposes
loadedTypeName: string; // for debugging purposes
} & Pick<DataLoaderCreatorBaseOptions<any, any>, 'resolveToNull'>
): ILoader<TResult | null | EntityNotFoundException> => {
// the data loader returns an array the MUST match the input length AND input key order
// the provided batch function does not necessarily complete this requirement
// so we create a wrapper function that executes the batch function and ensure the output length
// so we create a wrapper function that executes the batch function and ensure the output length and order
// by either returning the original output (if the length matches) or filling the missing values with errors
const loadAndEnsureOutputLengthAndOrder = async (keys: readonly string[]) => {
const loadAndEnsureOutputLengthAndOrder = async (
keys: readonly string[]
): Promise<(TResult | null | Error)[]> => {
const unsortedOutput = await batchLoadFn(keys);
const sortedOutput = sorOutputByKeys(unsortedOutput, keys);
if (sortedOutput.length == keys.length) {
Expand All @@ -29,16 +34,19 @@ export const createBatchLoader = <TResult extends { id: string }>(
key => resultsById.get(key) ?? resolveUnresolvedForKey(key)
);
};
const { name, loadedTypeName, resolveToNull } = options ?? {};
// a function to resolve an unresolved entity for a given key (e.g. if not found, etc.)
const resolveUnresolvedForKey = (key: string) => {
return new EntityNotFoundException(
`Could not find ${loadedTypeName} for the given key`,
LogContext.DATA_LOADER,
{ id: key }
);
return resolveToNull
? null
: new EntityNotFoundException(
`Could not find ${loadedTypeName} for the given key`,
LogContext.DATA_LOADER,
{ id: key }
);
};

return new DataLoader<string, TResult>(
return new DataLoader<string, TResult | null>(
keys => loadAndEnsureOutputLengthAndOrder(keys),
{
cache: true,
Expand Down
5 changes: 3 additions & 2 deletions src/core/dataloader/utils/createTypedRelationLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ import {
FindOptionsSelect,
} from 'typeorm';
import { Type } from '@nestjs/common';
import { EntityNotFoundException } from '@common/exceptions';
import { DataLoaderCreatorOptions } from '../creators/base';
import { ILoader } from '../loader.interface';
import { findByBatchIds } from './findByBatchIds';
import { selectOptionsFromFields } from './selectOptionsFromFields';

export const createTypedRelationDataLoader = <
TParent extends { id: string } & { [key: string]: any }, // todo better type,
TResult
TResult,
>(
manager: EntityManager,
parentClassRef: Type<TParent>,
relations: FindOptionsRelations<TParent>,
name: string,
options?: DataLoaderCreatorOptions<TResult, TParent>
): ILoader<TResult> => {
): ILoader<TResult | null | EntityNotFoundException> => {
const { fields, ...restOptions } = options ?? {};

const topRelation = <keyof TResult>Object.keys(relations)[0];
Expand Down
3 changes: 2 additions & 1 deletion src/core/dataloader/utils/createTypedSimpleLoader.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import DataLoader from 'dataloader';
import { EntityManager, FindOptionsSelect } from 'typeorm';
import { Type } from '@nestjs/common';
import { EntityNotFoundException } from '@common/exceptions';
import { DataLoaderCreatorOptions } from '../creators/base';
import { ILoader } from '../loader.interface';
import { selectOptionsFromFields } from './selectOptionsFromFields';
Expand All @@ -11,7 +12,7 @@ export const createTypedSimpleDataLoader = <TResult extends { id: string }>(
classRef: Type<TResult>,
name: string,
options?: DataLoaderCreatorOptions<TResult, TResult>
): ILoader<TResult> => {
): ILoader<TResult | null | EntityNotFoundException> => {
const { fields, ...restOptions } = options ?? {};
// if fields ia specified, select specific fields, otherwise select all fields
const selectOptions = fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export abstract class InAppNotificationCalloutPublished extends InAppNotificatio
type!: NotificationEventType.COLLABORATION_CALLOUT_PUBLISHED;
payload!: InAppNotificationCalloutPublishedPayload;
// fields resolved by a concrete resolver
callout!: ICallout;
space!: ISpace;
callout?: ICallout;
space?: ISpace;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class InAppNotificationCommunityNewMember extends InAppNotificationBase()
type!: NotificationEventType.COMMUNITY_NEW_MEMBER;
payload!: InAppNotificationCommunityNewMemberPayload;
// fields resolved by a concrete resolver
contributorType?: CommunityContributorType;
contributorType!: CommunityContributorType;
actor?: IContributor;
space?: ISpace;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class InAppNotificationUserMentioned extends InAppNotificationBase() {
type!: NotificationEventType.COMMUNICATION_USER_MENTION;
payload!: InAppNotificationContributorMentionedPayload;
// fields resolved by a concrete resolver
contributorType?: CommunityContributorType;
comment?: string;
commentUrl?: string;
contributorType!: CommunityContributorType;
comment!: string;
commentUrl!: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,25 @@ import { ILoader } from '@core/dataloader/loader.interface';
@Resolver(() => InAppNotificationCalloutPublished)
export class InAppNotificationCalloutPublishedResolverFields {
@ResolveField(() => ICallout, {
nullable: false,
nullable: true,
description: 'The Callout that was published.',
})
public callout(
@Parent() { payload }: InAppNotificationCalloutPublished,
@Loader(CalloutLoaderCreator) loader: ILoader<ICallout>
@Loader(CalloutLoaderCreator, { resolveToNull: true })
loader: ILoader<ICallout>
) {
return loader.load(payload.calloutID);
}

@ResolveField(() => ISpace, {
nullable: false,
nullable: true,
description: 'Where the callout is located.',
})
public space(
@Parent() { payload }: InAppNotificationCalloutPublished,
@Loader(SpaceLoaderCreator) loader: ILoader<ISpace>
@Loader(SpaceLoaderCreator, { resolveToNull: true })
loader: ILoader<ISpace>
) {
return loader.load(payload.spaceID);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,26 @@ export class InAppNotificationCommunityNewMemberResolverFields {
}

@ResolveField(() => IContributor, {
nullable: false,
nullable: true,
description: 'The Contributor that joined.',
})
// todo: rename?
public actor(
@Parent() { payload }: InAppNotificationCommunityNewMember,
@Loader(ContributorLoaderCreator) loader: ILoader<IContributor>
@Loader(ContributorLoaderCreator, { resolveToNull: true })
loader: ILoader<IContributor>
) {
return loader.load(payload.newMemberID);
}

@ResolveField(() => ISpace, {
nullable: false,
nullable: true,
description: 'The Space that was joined.',
})
public space(
@Parent() { payload }: InAppNotificationCommunityNewMember,
@Loader(SpaceLoaderCreator) loader: ILoader<ISpace>
@Loader(SpaceLoaderCreator, { resolveToNull: true })
loader: ILoader<ISpace>
) {
return loader.load(payload.spaceID);
}
Expand Down