Skip to content
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
135 changes: 120 additions & 15 deletions packages/runtime/src/client/crud/dialects/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { match, P } from 'ts-pattern';
import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, SchemaDef } from '../../../schema';
import { enumerate } from '../../../utils/enumerate';
import type { OrArray } from '../../../utils/type-utils';
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
import type {
BooleanFilter,
BytesFilter,
Expand All @@ -20,13 +21,17 @@ import {
buildFieldRef,
buildJoinPairs,
flattenCompoundUniqueFilters,
getDelegateDescendantModels,
getField,
getIdFields,
getManyToManyRelation,
getRelationForeignKeyFieldPairs,
isEnum,
isInheritedField,
isRelationField,
makeDefaultOrderBy,
requireField,
requireModel,
} from '../../query-utils';

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

abstract get provider(): DataSourceProviderType;

transformPrimitive(value: unknown, _type: BuiltinType, _forArrayField: boolean) {
return value;
}

abstract buildRelationSelection(
query: SelectQueryBuilder<any, any, any>,
model: string,
relationField: string,
parentAlias: string,
payload: true | FindArgs<Schema, GetModels<Schema>, true>,
): SelectQueryBuilder<any, any, any>;

abstract buildSkipTake(
query: SelectQueryBuilder<any, any, any>,
skip: number | undefined,
take: number | undefined,
): SelectQueryBuilder<any, any, any>;
// #region common query builders

buildFilter(
eb: ExpressionBuilder<any, any>,
Expand Down Expand Up @@ -788,6 +779,92 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
return result;
}

buildSelectAllFields(
model: string,
query: SelectQueryBuilder<any, any, any>,
omit?: Record<string, boolean | undefined>,
joinedBases: string[] = [],
) {
const modelDef = requireModel(this.schema, model);
let result = query;

for (const field of Object.keys(modelDef.fields)) {
if (isRelationField(this.schema, model, field)) {
continue;
}
if (omit?.[field] === true) {
continue;
}
result = this.buildSelectField(result, model, model, field, joinedBases);
}

// select all fields from delegate descendants and pack into a JSON field `$delegate$Model`
const descendants = getDelegateDescendantModels(this.schema, model);
for (const subModel of descendants) {
if (!joinedBases.includes(subModel.name)) {
joinedBases.push(subModel.name);
result = this.buildDelegateJoin(model, subModel.name, result);
}
result = result.select((eb) => {
const jsonObject: Record<string, Expression<any>> = {};
for (const field of Object.keys(subModel.fields)) {
if (
isRelationField(this.schema, subModel.name, field) ||
isInheritedField(this.schema, subModel.name, field)
) {
continue;
}
jsonObject[field] = eb.ref(`${subModel.name}.${field}`);
}
return this.buildJsonObject(eb, jsonObject).as(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`);
});
}

return result;
}

buildSelectField(
query: SelectQueryBuilder<any, any, any>,
model: string,
modelAlias: string,
field: string,
joinedBases: string[],
) {
const fieldDef = requireField(this.schema, model, field);

if (fieldDef.computed) {
// TODO: computed field from delegate base?
return query.select((eb) => buildFieldRef(this.schema, model, field, this.options, eb).as(field));
} else if (!fieldDef.originModel) {
// regular field
return query.select(sql.ref(`${modelAlias}.${field}`).as(field));
} else {
// field from delegate base, build a join
let result = query;
if (!joinedBases.includes(fieldDef.originModel)) {
joinedBases.push(fieldDef.originModel);
result = this.buildDelegateJoin(model, fieldDef.originModel, result);
}
result = this.buildSelectField(result, fieldDef.originModel, fieldDef.originModel, field, joinedBases);
return result;
}
}

buildDelegateJoin(thisModel: string, otherModel: string, query: SelectQueryBuilder<any, any, any>) {
const idFields = getIdFields(this.schema, thisModel);
query = query.leftJoin(otherModel, (qb) => {
for (const idField of idFields) {
qb = qb.onRef(`${thisModel}.${idField}`, '=', `${otherModel}.${idField}`);
}
return qb;
});
return query;
}

// #endregion

// #region utils

private negateSort(sort: SortOrder, negated: boolean) {
return negated ? (sort === 'asc' ? 'desc' : 'asc') : sort;
}
Expand Down Expand Up @@ -842,6 +919,32 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
return eb.not(this.and(eb, ...args));
}

// #endregion

// #region abstract methods

abstract get provider(): DataSourceProviderType;

/**
* Builds selection for a relation field.
*/
abstract buildRelationSelection(
query: SelectQueryBuilder<any, any, any>,
model: string,
relationField: string,
parentAlias: string,
payload: true | FindArgs<Schema, GetModels<Schema>, true>,
): SelectQueryBuilder<any, any, any>;

/**
* Builds skip and take clauses.
*/
abstract buildSkipTake(
query: SelectQueryBuilder<any, any, any>,
skip: number | undefined,
take: number | undefined,
): SelectQueryBuilder<any, any, any>;

/**
* Builds an Kysely expression that returns a JSON object for the given key-value pairs.
*/
Expand Down Expand Up @@ -877,4 +980,6 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
* Whether the dialect supports DISTINCT ON.
*/
abstract get supportsDistinctOn(): boolean;

// #endregion
}
26 changes: 25 additions & 1 deletion packages/runtime/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import {
} from 'kysely';
import { match } from 'ts-pattern';
import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema';
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
import type { FindArgs } from '../../crud-types';
import {
buildFieldRef,
buildJoinPairs,
getDelegateDescendantModels,
getIdFields,
getManyToManyRelation,
isRelationField,
Expand Down Expand Up @@ -79,10 +81,18 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
// simple select by default
let result = eb.selectFrom(`${relationModel} as ${joinTableName}`);

const joinBases: string[] = [];

// however if there're filter/orderBy/take/skip,
// we need to build a subquery to handle them before aggregation
result = eb.selectFrom(() => {
let subQuery = eb.selectFrom(`${relationModel}`).selectAll();
let subQuery = eb.selectFrom(relationModel);
subQuery = this.buildSelectAllFields(
relationModel,
subQuery,
typeof payload === 'object' ? payload?.omit : undefined,
joinBases,
);

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

// TODO: descendant JSON shouldn't be joined and selected if none of its fields are selected
const descendantModels = getDelegateDescendantModels(this.schema, relationModel);
if (descendantModels.length > 0) {
// select all JSONs built from delegate descendants
objArgs.push(
...descendantModels
.map((subModel) => [
sql.lit(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
eb.ref(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
])
.flatMap((v) => v),
);
}

if (payload === true || !payload.select) {
// select all scalar fields
objArgs.push(
Expand Down
26 changes: 25 additions & 1 deletion packages/runtime/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import {
} from 'kysely';
import { match } from 'ts-pattern';
import type { BuiltinType, GetModels, SchemaDef } from '../../../schema';
import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants';
import type { FindArgs } from '../../crud-types';
import {
buildFieldRef,
getDelegateDescendantModels,
getIdFields,
getManyToManyRelation,
getRelationForeignKeyFieldPairs,
Expand Down Expand Up @@ -75,7 +77,15 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
const subQueryName = `${parentName}$${relationField}`;

let tbl = eb.selectFrom(() => {
let subQuery = eb.selectFrom(relationModel).selectAll();
let subQuery = eb.selectFrom(relationModel);

const joinBases: string[] = [];
subQuery = this.buildSelectAllFields(
relationModel,
subQuery,
typeof payload === 'object' ? payload?.omit : undefined,
joinBases,
);

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

// TODO: descendant JSON shouldn't be joined and selected if none of its fields are selected
const descendantModels = getDelegateDescendantModels(this.schema, relationModel);
if (descendantModels.length > 0) {
// select all JSONs built from delegate descendants
objArgs.push(
...descendantModels
.map((subModel) => [
sql.lit(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
eb.ref(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`),
])
.flatMap((v) => v),
);
}

if (payload === true || !payload.select) {
// select all scalar fields
objArgs.push(
Expand Down
Loading