Skip to content

Commit

Permalink
feat: add lazy join decorator (#1240)
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaud-moncel authored Jan 23, 2025
1 parent 036d7e4 commit 1b6b248
Show file tree
Hide file tree
Showing 3 changed files with 462 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ComputedCollectionDecorator from './computed/collection';
import DecoratorsStackBase, { Options } from './decorators-stack-base';
import EmptyCollectionDecorator from './empty/collection';
import HookCollectionDecorator from './hook/collection';
import LazyJoinDecorator from './lazy-join/collection';
import OperatorsEmulateCollectionDecorator from './operators-emulate/collection';
import OperatorsEquivalenceCollectionDecorator from './operators-equivalence/collection';
import OverrideCollectionDecorator from './override/collection';
Expand Down Expand Up @@ -39,6 +40,7 @@ export default class DecoratorsStack extends DecoratorsStackBase {
last = this.earlyOpEmulate = new DataSourceDecorator(last, OperatorsEmulateCollectionDecorator);
last = new DataSourceDecorator(last, OperatorsEquivalenceCollectionDecorator);
last = this.relation = new DataSourceDecorator(last, RelationCollectionDecorator);
last = new DataSourceDecorator(last, LazyJoinDecorator);
last = this.lateComputed = new DataSourceDecorator(last, ComputedCollectionDecorator);
last = this.lateOpEmulate = new DataSourceDecorator(last, OperatorsEmulateCollectionDecorator);
last = new DataSourceDecorator(last, OperatorsEquivalenceCollectionDecorator);
Expand Down
110 changes: 110 additions & 0 deletions packages/datasource-customizer/src/decorators/lazy-join/collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
AggregateResult,
Aggregation,
Caller,
CollectionDecorator,
FieldSchema,
Filter,
ManyToOneSchema,
PaginatedFilter,
Projection,
RecordData,
} from '@forestadmin/datasource-toolkit';

export default class LazyJoinDecorator extends CollectionDecorator {
override async list(
caller: Caller,
filter: PaginatedFilter,
projection: Projection,
): Promise<RecordData[]> {
const refinedProjection = projection.replace(field => this.refineField(field, projection));
const refinedFilter = await this.refineFilter(caller, filter);

const records = await this.childCollection.list(caller, refinedFilter, refinedProjection);

this.refineResults(projection, (relationName, foreignKey, foreignKeyTarget) => {
records.forEach(record => {
if (record[foreignKey]) {
record[relationName] = { [foreignKeyTarget]: record[foreignKey] };
}

delete record[foreignKey];
});
});

return records;
}

override async aggregate(
caller: Caller,
filter: Filter,
aggregation: Aggregation,
limit?: number,
): Promise<AggregateResult[]> {
const refinedAggregation = aggregation.replaceFields(field =>
this.refineField(field, aggregation.projection),
);
const refinedFilter = await this.refineFilter(caller, filter);

const results = await this.childCollection.aggregate(
caller,
refinedFilter,
refinedAggregation,
limit,
);

this.refineResults(aggregation.projection, (relationName, foreignKey, foreignKeyTarget) => {
results.forEach(result => {
if (result.group[foreignKey]) {
result.group[`${relationName}:${foreignKeyTarget}`] = result.group[foreignKey];
}

delete result.group[foreignKey];
});
});

return results;
}

private isLazyRelationProjection(relation: FieldSchema, relationProjection: Projection) {
return (
relation.type === 'ManyToOne' &&
relationProjection.length === 1 &&
relationProjection[0] === relation.foreignKeyTarget
);
}

private refineField(field: string, projection: Projection): string {
const relationName = field.split(':')[0];
const relation = this.schema.fields[relationName] as ManyToOneSchema;
const relationProjection = projection.relations[relationName];

return this.isLazyRelationProjection(relation, relationProjection)
? relation.foreignKey
: field;
}

override async refineFilter(caller: Caller, filter: PaginatedFilter): Promise<PaginatedFilter> {
if (filter.conditionTree) {
filter.conditionTree = filter.conditionTree.replaceFields(field =>
this.refineField(field, filter.conditionTree.projection),
);
}

return filter;
}

private refineResults(
projection: Projection,
handler: (relationName: string, foreignKey: string, foreignKeyTarget: string) => void,
) {
Object.entries(projection.relations).forEach(([relationName, relationProjection]) => {
const relation = this.schema.fields[relationName] as ManyToOneSchema;

if (this.isLazyRelationProjection(relation, relationProjection)) {
const { foreignKeyTarget, foreignKey } = relation;
handler(relationName, foreignKey, foreignKeyTarget);
}
});
}
}
Loading

0 comments on commit 1b6b248

Please sign in to comment.