Skip to content

Commit 04959ad

Browse files
authored
fix(delegate): relation selection (#111)
1 parent cbf1ce3 commit 04959ad

File tree

6 files changed

+390
-258
lines changed

6 files changed

+390
-258
lines changed

packages/runtime/src/client/crud/dialects/base.ts

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { match, P } from 'ts-pattern';
55
import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, SchemaDef } from '../../../schema';
66
import { enumerate } from '../../../utils/enumerate';
77
import type { OrArray } from '../../../utils/type-utils';
8+
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
89
import type {
910
BooleanFilter,
1011
BytesFilter,
@@ -20,13 +21,17 @@ import {
2021
buildFieldRef,
2122
buildJoinPairs,
2223
flattenCompoundUniqueFilters,
24+
getDelegateDescendantModels,
2325
getField,
2426
getIdFields,
2527
getManyToManyRelation,
2628
getRelationForeignKeyFieldPairs,
2729
isEnum,
30+
isInheritedField,
31+
isRelationField,
2832
makeDefaultOrderBy,
2933
requireField,
34+
requireModel,
3035
} from '../../query-utils';
3136

3237
export abstract class BaseCrudDialect<Schema extends SchemaDef> {
@@ -35,25 +40,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
3540
protected readonly options: ClientOptions<Schema>,
3641
) {}
3742

38-
abstract get provider(): DataSourceProviderType;
39-
4043
transformPrimitive(value: unknown, _type: BuiltinType, _forArrayField: boolean) {
4144
return value;
4245
}
4346

44-
abstract buildRelationSelection(
45-
query: SelectQueryBuilder<any, any, any>,
46-
model: string,
47-
relationField: string,
48-
parentAlias: string,
49-
payload: true | FindArgs<Schema, GetModels<Schema>, true>,
50-
): SelectQueryBuilder<any, any, any>;
51-
52-
abstract buildSkipTake(
53-
query: SelectQueryBuilder<any, any, any>,
54-
skip: number | undefined,
55-
take: number | undefined,
56-
): SelectQueryBuilder<any, any, any>;
47+
// #region common query builders
5748

5849
buildFilter(
5950
eb: ExpressionBuilder<any, any>,
@@ -788,6 +779,92 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
788779
return result;
789780
}
790781

782+
buildSelectAllFields(
783+
model: string,
784+
query: SelectQueryBuilder<any, any, any>,
785+
omit?: Record<string, boolean | undefined>,
786+
joinedBases: string[] = [],
787+
) {
788+
const modelDef = requireModel(this.schema, model);
789+
let result = query;
790+
791+
for (const field of Object.keys(modelDef.fields)) {
792+
if (isRelationField(this.schema, model, field)) {
793+
continue;
794+
}
795+
if (omit?.[field] === true) {
796+
continue;
797+
}
798+
result = this.buildSelectField(result, model, model, field, joinedBases);
799+
}
800+
801+
// select all fields from delegate descendants and pack into a JSON field `$delegate$Model`
802+
const descendants = getDelegateDescendantModels(this.schema, model);
803+
for (const subModel of descendants) {
804+
if (!joinedBases.includes(subModel.name)) {
805+
joinedBases.push(subModel.name);
806+
result = this.buildDelegateJoin(model, subModel.name, result);
807+
}
808+
result = result.select((eb) => {
809+
const jsonObject: Record<string, Expression<any>> = {};
810+
for (const field of Object.keys(subModel.fields)) {
811+
if (
812+
isRelationField(this.schema, subModel.name, field) ||
813+
isInheritedField(this.schema, subModel.name, field)
814+
) {
815+
continue;
816+
}
817+
jsonObject[field] = eb.ref(`${subModel.name}.${field}`);
818+
}
819+
return this.buildJsonObject(eb, jsonObject).as(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`);
820+
});
821+
}
822+
823+
return result;
824+
}
825+
826+
buildSelectField(
827+
query: SelectQueryBuilder<any, any, any>,
828+
model: string,
829+
modelAlias: string,
830+
field: string,
831+
joinedBases: string[],
832+
) {
833+
const fieldDef = requireField(this.schema, model, field);
834+
835+
if (fieldDef.computed) {
836+
// TODO: computed field from delegate base?
837+
return query.select((eb) => buildFieldRef(this.schema, model, field, this.options, eb).as(field));
838+
} else if (!fieldDef.originModel) {
839+
// regular field
840+
return query.select(sql.ref(`${modelAlias}.${field}`).as(field));
841+
} else {
842+
// field from delegate base, build a join
843+
let result = query;
844+
if (!joinedBases.includes(fieldDef.originModel)) {
845+
joinedBases.push(fieldDef.originModel);
846+
result = this.buildDelegateJoin(model, fieldDef.originModel, result);
847+
}
848+
result = this.buildSelectField(result, fieldDef.originModel, fieldDef.originModel, field, joinedBases);
849+
return result;
850+
}
851+
}
852+
853+
buildDelegateJoin(thisModel: string, otherModel: string, query: SelectQueryBuilder<any, any, any>) {
854+
const idFields = getIdFields(this.schema, thisModel);
855+
query = query.leftJoin(otherModel, (qb) => {
856+
for (const idField of idFields) {
857+
qb = qb.onRef(`${thisModel}.${idField}`, '=', `${otherModel}.${idField}`);
858+
}
859+
return qb;
860+
});
861+
return query;
862+
}
863+
864+
// #endregion
865+
866+
// #region utils
867+
791868
private negateSort(sort: SortOrder, negated: boolean) {
792869
return negated ? (sort === 'asc' ? 'desc' : 'asc') : sort;
793870
}
@@ -842,6 +919,32 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
842919
return eb.not(this.and(eb, ...args));
843920
}
844921

922+
// #endregion
923+
924+
// #region abstract methods
925+
926+
abstract get provider(): DataSourceProviderType;
927+
928+
/**
929+
* Builds selection for a relation field.
930+
*/
931+
abstract buildRelationSelection(
932+
query: SelectQueryBuilder<any, any, any>,
933+
model: string,
934+
relationField: string,
935+
parentAlias: string,
936+
payload: true | FindArgs<Schema, GetModels<Schema>, true>,
937+
): SelectQueryBuilder<any, any, any>;
938+
939+
/**
940+
* Builds skip and take clauses.
941+
*/
942+
abstract buildSkipTake(
943+
query: SelectQueryBuilder<any, any, any>,
944+
skip: number | undefined,
945+
take: number | undefined,
946+
): SelectQueryBuilder<any, any, any>;
947+
845948
/**
846949
* Builds an Kysely expression that returns a JSON object for the given key-value pairs.
847950
*/
@@ -877,4 +980,6 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
877980
* Whether the dialect supports DISTINCT ON.
878981
*/
879982
abstract get supportsDistinctOn(): boolean;
983+
984+
// #endregion
880985
}

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import {
99
} from 'kysely';
1010
import { match } from 'ts-pattern';
1111
import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema';
12+
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
1213
import type { FindArgs } from '../../crud-types';
1314
import {
1415
buildFieldRef,
1516
buildJoinPairs,
17+
getDelegateDescendantModels,
1618
getIdFields,
1719
getManyToManyRelation,
1820
isRelationField,
@@ -79,10 +81,18 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
7981
// simple select by default
8082
let result = eb.selectFrom(`${relationModel} as ${joinTableName}`);
8183

84+
const joinBases: string[] = [];
85+
8286
// however if there're filter/orderBy/take/skip,
8387
// we need to build a subquery to handle them before aggregation
8488
result = eb.selectFrom(() => {
85-
let subQuery = eb.selectFrom(`${relationModel}`).selectAll();
89+
let subQuery = eb.selectFrom(relationModel);
90+
subQuery = this.buildSelectAllFields(
91+
relationModel,
92+
subQuery,
93+
typeof payload === 'object' ? payload?.omit : undefined,
94+
joinBases,
95+
);
8696

8797
if (payload && typeof payload === 'object') {
8898
if (payload.where) {
@@ -200,6 +210,20 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
200210
string | ExpressionWrapper<any, any, any> | SelectQueryBuilder<any, any, any> | RawBuilder<any>
201211
> = [];
202212

213+
// TODO: descendant JSON shouldn't be joined and selected if none of its fields are selected
214+
const descendantModels = getDelegateDescendantModels(this.schema, relationModel);
215+
if (descendantModels.length > 0) {
216+
// select all JSONs built from delegate descendants
217+
objArgs.push(
218+
...descendantModels
219+
.map((subModel) => [
220+
sql.lit(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
221+
eb.ref(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
222+
])
223+
.flatMap((v) => v),
224+
);
225+
}
226+
203227
if (payload === true || !payload.select) {
204228
// select all scalar fields
205229
objArgs.push(

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import {
1010
} from 'kysely';
1111
import { match } from 'ts-pattern';
1212
import type { BuiltinType, GetModels, SchemaDef } from '../../../schema';
13+
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
1314
import type { FindArgs } from '../../crud-types';
1415
import {
1516
buildFieldRef,
17+
getDelegateDescendantModels,
1618
getIdFields,
1719
getManyToManyRelation,
1820
getRelationForeignKeyFieldPairs,
@@ -75,7 +77,15 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
7577
const subQueryName = `${parentName}$${relationField}`;
7678

7779
let tbl = eb.selectFrom(() => {
78-
let subQuery = eb.selectFrom(relationModel).selectAll();
80+
let subQuery = eb.selectFrom(relationModel);
81+
82+
const joinBases: string[] = [];
83+
subQuery = this.buildSelectAllFields(
84+
relationModel,
85+
subQuery,
86+
typeof payload === 'object' ? payload?.omit : undefined,
87+
joinBases,
88+
);
7989

8090
if (payload && typeof payload === 'object') {
8191
if (payload.where) {
@@ -143,6 +153,20 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
143153
type ArgsType = Expression<any> | RawBuilder<any> | SelectQueryBuilder<any, any, any>;
144154
const objArgs: ArgsType[] = [];
145155

156+
// TODO: descendant JSON shouldn't be joined and selected if none of its fields are selected
157+
const descendantModels = getDelegateDescendantModels(this.schema, relationModel);
158+
if (descendantModels.length > 0) {
159+
// select all JSONs built from delegate descendants
160+
objArgs.push(
161+
...descendantModels
162+
.map((subModel) => [
163+
sql.lit(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
164+
eb.ref(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
165+
])
166+
.flatMap((v) => v),
167+
);
168+
}
169+
146170
if (payload === true || !payload.select) {
147171
// select all scalar fields
148172
objArgs.push(

0 commit comments

Comments
 (0)