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
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
- [x] Counting relation
- [x] Pagination
- [x] Skip and limit
- [ ] Cursor
- [x] Cursor
- [x] Filtering
- [x] Unique fields
- [x] Scalar fields
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/client/client-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ function createModelCrudHandler<
}
let result: unknown;
if (r && postProcess) {
result = resultProcessor.processResult(r, model);
result = resultProcessor.processResult(r, model, args);
} else {
result = r ?? null;
}
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ type Distinct<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
distinct?: OrArray<NonRelationFields<Schema, Model>>;
};

type Cursor<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
cursor?: WhereUnique<Schema, Model>;
};

type Select<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Expand Down Expand Up @@ -570,7 +574,8 @@ export type FindArgs<
}
: {}) &
SelectIncludeOmit<Schema, Model, Collection> &
Distinct<Schema, Model>;
Distinct<Schema, Model> &
Cursor<Schema, Model>;

export type FindUniqueArgs<
Schema extends SchemaDef,
Expand Down
59 changes: 30 additions & 29 deletions packages/runtime/src/client/crud/dialects/postgresql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class PostgresCrudDialect<
);

return joinedQuery.select(
`${parentAlias}$${relationField}.data as ${relationField}`
`${parentAlias}$${relationField}.$j as ${relationField}`
);
}

Expand Down Expand Up @@ -82,12 +82,12 @@ export class PostgresCrudDialect<

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

if (payload && typeof payload === 'object') {
if (payload.where) {
subQuery = subQuery.where((eb) =>
this.buildFilter(
Expand Down Expand Up @@ -118,9 +118,27 @@ export class PostgresCrudDialect<
skip !== undefined || take !== undefined,
negateOrderBy
);
return subQuery.as(joinTableName);
});
}
}

// add join conditions
const joinPairs = buildJoinPairs(
this.schema,
model,
parentName,
relationField,
relationModel
);
subQuery = subQuery.where((eb) =>
this.and(
eb,
...joinPairs.map(([left, right]) =>
eb(sql.ref(left), '=', sql.ref(right))
)
)
);

return subQuery.as(joinTableName);
});

result = this.buildRelationObjectSelect(
relationModel,
Expand All @@ -131,23 +149,6 @@ export class PostgresCrudDialect<
parentName
);

// add join conditions
const joinPairs = buildJoinPairs(
this.schema,
model,
parentName,
relationField,
joinTableName
);
result = result.where((eb) =>
this.and(
eb,
...joinPairs.map(([left, right]) =>
eb(sql.ref(left), '=', sql.ref(right))
)
)
);

// add nested joins for each relation
result = this.buildRelationJoins(
relationModel,
Expand Down Expand Up @@ -189,9 +190,9 @@ export class PostgresCrudDialect<
)}))`,
sql`'[]'::jsonb`
)
.as('data');
.as('$j');
} else {
return sql`jsonb_build_object(${sql.join(objArgs)})`.as('data');
return sql`jsonb_build_object(${sql.join(objArgs)})`.as('$j');
}
});

Expand Down Expand Up @@ -267,7 +268,7 @@ export class PostgresCrudDialect<
.filter(([, value]) => value)
.map(([field]) => [
sql.lit(field),
eb.ref(`${parentName}$${relationField}$${field}.data`),
eb.ref(`${parentName}$${relationField}$${field}.$j`),
])
.flatMap((v) => v)
);
Expand Down
66 changes: 29 additions & 37 deletions packages/runtime/src/client/crud/dialects/sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,12 @@ export class SqliteCrudDialect<
const relationModel = relationFieldDef.type as GetModels<Schema>;
const relationModelDef = requireModel(this.schema, relationModel);

const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(
this.schema,
model,
relationField
);

const subQueryName = `${parentName}$${relationField}`;

// simple select by default
let tbl: SelectQueryBuilder<any, any, any> = eb.selectFrom(
`${relationModel} as ${subQueryName}`
);

// however if there're filter/orderBy/take/skip,
// we need to build a subquery to handle them before aggregation
if (payload && typeof payload === 'object') {
tbl = eb.selectFrom(() => {
let subQuery = eb.selectFrom(relationModel).selectAll();
let tbl = eb.selectFrom(() => {
let subQuery = eb.selectFrom(relationModel).selectAll();

if (payload && typeof payload === 'object') {
if (payload.where) {
subQuery = subQuery.where((eb) =>
this.buildFilter(
Expand Down Expand Up @@ -123,10 +110,33 @@ export class SqliteCrudDialect<
skip !== undefined || take !== undefined,
negateOrderBy
);
}

return subQuery.as(subQueryName);
// join conditions
const { keyPairs, ownedByModel } = getRelationForeignKeyFieldPairs(
this.schema,
model,
relationField
);
keyPairs.forEach(({ fk, pk }) => {
if (ownedByModel) {
// the parent model owns the fk
subQuery = subQuery.whereRef(
`${relationModel}.${pk}`,
'=',
`${parentName}.${fk}`
);
} else {
// the relation side owns the fk
subQuery = subQuery.whereRef(
`${relationModel}.${fk}`,
'=',
`${parentName}.${pk}`
);
}
});
}
return subQuery.as(subQueryName);
});

tbl = tbl.select(() => {
type ArgsType =
Expand Down Expand Up @@ -227,30 +237,12 @@ export class SqliteCrudDialect<
)}))`,
sql`json_array()`
)
.as('data');
.as('$j');
} else {
return sql`json_object(${sql.join(objArgs)})`.as('data');
}
});

// join conditions
keyPairs.forEach(({ fk, pk }) => {
if (ownedByModel) {
// the parent model owns the fk
tbl = tbl.whereRef(
`${parentName}$${relationField}.${pk}`,
'=',
`${parentName}.${fk}`
);
} else {
// the relation side owns the fk
tbl = tbl.whereRef(
`${parentName}$${relationField}.${fk}`,
'=',
`${parentName}.${pk}`
);
}
});
return tbl;
}

Expand Down
107 changes: 90 additions & 17 deletions packages/runtime/src/client/crud/operations/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createId } from '@paralleldrive/cuid2';
import {
DeleteResult,
expressionBuilder,
ExpressionWrapper,
sql,
UpdateResult,
type ExpressionBuilder,
Expand Down Expand Up @@ -29,7 +30,12 @@ import {
} from '../../../utils/object-utils';
import { CONTEXT_COMMENT_PREFIX } from '../../constants';
import type { CRUD } from '../../contract';
import type { FindArgs, SelectIncludeOmit, WhereInput } from '../../crud-types';
import type {
FindArgs,
SelectIncludeOmit,
SortOrder,
WhereInput,
} from '../../crud-types';
import { InternalError, NotFoundError, QueryError } from '../../errors';
import type { ToKysely } from '../../query-builder';
import {
Expand All @@ -44,8 +50,10 @@ import {
isForeignKeyField,
isRelationField,
isScalarField,
makeDefaultOrderBy,
requireField,
requireModel,
safeJSONStringify,
} from '../../query-utils';
import { getCrudDialect } from '../dialects';
import type { BaseCrudDialect } from '../dialects/base';
Expand Down Expand Up @@ -205,33 +213,46 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
);
}

if (args?.cursor) {
query = this.buildCursorFilter(
model,
query,
args.cursor,
args.orderBy,
negateOrderBy
);
}

query = query.modifyEnd(
this.makeContextComment({ model, operation: 'read' })
);

let result: any[] = [];
try {
let result = await query.execute();
if (inMemoryDistinct) {
const distinctResult: Record<string, unknown>[] = [];
const seen = new Set<string>();
for (const r of result as any[]) {
const key = JSON.stringify(
inMemoryDistinct.map((f) => r[f])
)!;
if (!seen.has(key)) {
distinctResult.push(r);
seen.add(key);
}
}
result = distinctResult;
}
return result;
result = await query.execute();
} catch (err) {
const { sql, parameters } = query.compile();
throw new QueryError(
`Failed to execute query: ${err}, sql: ${sql}, parameters: ${parameters}`
);
}

if (inMemoryDistinct) {
const distinctResult: Record<string, unknown>[] = [];
const seen = new Set<string>();
for (const r of result as any[]) {
const key = safeJSONStringify(
inMemoryDistinct.map((f) => r[f])
)!;
if (!seen.has(key)) {
distinctResult.push(r);
seen.add(key);
}
}
result = distinctResult;
}

return result;
}

protected async readUnique(
Expand Down Expand Up @@ -408,6 +429,58 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
}
}

private buildCursorFilter(
model: string,
query: SelectQueryBuilder<any, any, {}>,
cursor: FindArgs<Schema, GetModels<Schema>, true>['cursor'],
orderBy: FindArgs<Schema, GetModels<Schema>, true>['orderBy'],
negateOrderBy: boolean
) {
if (!orderBy) {
orderBy = makeDefaultOrderBy(this.schema, model);
}

const orderByItems = ensureArray(orderBy).flatMap((obj) =>
Object.entries<SortOrder>(obj)
);

const eb = expressionBuilder<any, any>();
const cursorFilter = this.dialect.buildFilter(eb, model, model, cursor);

let result = query;
let filters: ExpressionWrapper<any, any, any>[] = [];

for (let i = orderByItems.length - 1; i >= 0; i--) {
const andFilters: ExpressionWrapper<any, any, any>[] = [];

for (let j = 0; j <= i; j++) {
const [field, order] = orderByItems[j]!;
const _order = negateOrderBy
? order === 'asc'
? 'desc'
: 'asc'
: order;
const op = j === i ? (_order === 'asc' ? '>=' : '<=') : '=';
andFilters.push(
eb(
eb.ref(`${model}.${field}`),
op,
eb
.selectFrom(model)
.select(`${model}.${field}`)
.where(cursorFilter)
)
);
}

filters.push(eb.and(andFilters));
}

result = result.where((eb) => eb.or(filters));

return result;
}

protected async create(
kysely: ToKysely<Schema>,
model: GetModels<Schema>,
Expand Down
Loading