Skip to content

Commit cefe223

Browse files
authored
feat: implement delegate delete (#114)
1 parent 05b0c51 commit cefe223

File tree

3 files changed

+310
-96
lines changed

3 files changed

+310
-96
lines changed

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

Lines changed: 123 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,6 +1325,11 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
13251325
return (returnData ? [] : { count: 0 }) as Result;
13261326
}
13271327

1328+
const modelDef = this.requireModel(model);
1329+
if (modelDef.baseModel && limit !== undefined) {
1330+
throw new QueryError('Updating with a limit is not supported for polymorphic models');
1331+
}
1332+
13281333
filterModel ??= model;
13291334
let updateFields: any = {};
13301335

@@ -1335,7 +1340,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
13351340
updateFields[field] = this.processScalarFieldUpdateData(model, field, data);
13361341
}
13371342

1338-
const modelDef = this.requireModel(model);
13391343
let shouldFallbackToIdFilter = false;
13401344

13411345
if (limit !== undefined && !this.dialect.supportsUpdateWithLimit) {
@@ -1358,7 +1362,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
13581362
modelDef.baseModel,
13591363
where,
13601364
updateFields,
1361-
limit,
13621365
filterModel,
13631366
);
13641367
updateFields = baseResult.remainingFields;
@@ -1412,7 +1415,6 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
14121415
model: string,
14131416
where: any,
14141417
updateFields: any,
1415-
limit: number | undefined,
14161418
filterModel: GetModels<Schema>,
14171419
) {
14181420
const thisUpdateFields: any = {};
@@ -1433,7 +1435,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
14331435
model as GetModels<Schema>,
14341436
where,
14351437
thisUpdateFields,
1436-
limit,
1438+
undefined,
14371439
false,
14381440
filterModel,
14391441
);
@@ -1983,24 +1985,18 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
19831985
const fieldDef = this.requireField(fromRelation.model, fromRelation.field);
19841986
invariant(fieldDef.relation?.opposite);
19851987

1986-
deleteResult = await this.delete(
1987-
kysely,
1988-
model,
1989-
{
1990-
AND: [
1991-
{
1992-
[fieldDef.relation.opposite]: {
1993-
some: fromRelation.ids,
1994-
},
1995-
},
1996-
{
1997-
OR: deleteConditions,
1988+
deleteResult = await this.delete(kysely, model, {
1989+
AND: [
1990+
{
1991+
[fieldDef.relation.opposite]: {
1992+
some: fromRelation.ids,
19981993
},
1999-
],
2000-
},
2001-
undefined,
2002-
false,
2003-
);
1994+
},
1995+
{
1996+
OR: deleteConditions,
1997+
},
1998+
],
1999+
});
20042000
} else {
20052001
const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs(
20062002
this.schema,
@@ -2018,36 +2014,24 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
20182014

20192015
const fieldDef = this.requireField(fromRelation.model, fromRelation.field);
20202016
invariant(fieldDef.relation?.opposite);
2021-
deleteResult = await this.delete(
2022-
kysely,
2023-
model,
2024-
{
2025-
AND: [
2026-
// filter for parent
2027-
Object.fromEntries(keyPairs.map(({ fk, pk }) => [pk, fromEntity[fk]])),
2028-
{
2029-
OR: deleteConditions,
2030-
},
2031-
],
2032-
},
2033-
undefined,
2034-
false,
2035-
);
2017+
deleteResult = await this.delete(kysely, model, {
2018+
AND: [
2019+
// filter for parent
2020+
Object.fromEntries(keyPairs.map(({ fk, pk }) => [pk, fromEntity[fk]])),
2021+
{
2022+
OR: deleteConditions,
2023+
},
2024+
],
2025+
});
20362026
} else {
2037-
deleteResult = await this.delete(
2038-
kysely,
2039-
model,
2040-
{
2041-
AND: [
2042-
Object.fromEntries(keyPairs.map(({ fk, pk }) => [fk, fromRelation.ids[pk]])),
2043-
{
2044-
OR: deleteConditions,
2045-
},
2046-
],
2047-
},
2048-
undefined,
2049-
false,
2050-
);
2027+
deleteResult = await this.delete(kysely, model, {
2028+
AND: [
2029+
Object.fromEntries(keyPairs.map(({ fk, pk }) => [fk, fromRelation.ids[pk]])),
2030+
{
2031+
OR: deleteConditions,
2032+
},
2033+
],
2034+
});
20512035
}
20522036
}
20532037

@@ -2064,54 +2048,109 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
20642048

20652049
// #endregion
20662050

2067-
protected async delete<
2068-
ReturnData extends boolean,
2069-
Result = ReturnData extends true ? unknown[] : { count: number },
2070-
>(
2051+
protected async delete(
20712052
kysely: ToKysely<Schema>,
20722053
model: GetModels<Schema>,
20732054
where: any,
2074-
limit: number | undefined,
2075-
returnData: ReturnData,
2076-
): Promise<Result> {
2055+
limit?: number,
2056+
filterModel?: GetModels<Schema>,
2057+
): Promise<{ count: number }> {
2058+
filterModel ??= model;
2059+
2060+
const modelDef = this.requireModel(model);
2061+
2062+
if (modelDef.baseModel) {
2063+
if (limit !== undefined) {
2064+
throw new QueryError('Deleting with a limit is not supported for polymorphic models');
2065+
}
2066+
// just delete base and it'll cascade back to this model
2067+
return this.processBaseModelDelete(kysely, modelDef.baseModel, where, limit, filterModel);
2068+
}
2069+
20772070
let query = kysely.deleteFrom(model);
2071+
let needIdFilter = false;
2072+
2073+
if (limit !== undefined && !this.dialect.supportsDeleteWithLimit) {
2074+
// if the dialect doesn't support delete with limit natively, we'll
2075+
// simulate it by filtering by id with a limit
2076+
needIdFilter = true;
2077+
}
20782078

2079-
if (limit === undefined) {
2079+
if (modelDef.isDelegate || modelDef.baseModel) {
2080+
// if the model is in a delegate hierarchy, we'll need to filter by
2081+
// id because the filter may involve fields in different models in
2082+
// the hierarchy
2083+
needIdFilter = true;
2084+
}
2085+
2086+
if (!needIdFilter) {
20802087
query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where));
20812088
} else {
2082-
if (this.dialect.supportsDeleteWithLimit) {
2083-
query = query.where((eb) => this.dialect.buildFilter(eb, model, model, where)).limit(limit!);
2084-
} else {
2085-
query = query.where((eb) =>
2086-
eb(
2087-
eb.refTuple(
2088-
// @ts-expect-error
2089-
...this.buildIdFieldRefs(kysely, model),
2090-
),
2091-
'in',
2092-
kysely
2093-
.selectFrom(model)
2094-
.where((eb) => this.dialect.buildFilter(eb, model, model, where))
2095-
.select(this.buildIdFieldRefs(kysely, model))
2096-
.limit(limit!),
2089+
query = query.where((eb) =>
2090+
eb(
2091+
eb.refTuple(
2092+
// @ts-expect-error
2093+
...this.buildIdFieldRefs(kysely, model),
20972094
),
2098-
);
2099-
}
2095+
'in',
2096+
this.dialect
2097+
.buildSelectModel(eb, filterModel)
2098+
.where((eb) => this.dialect.buildFilter(eb, filterModel, filterModel, where))
2099+
.select(this.buildIdFieldRefs(kysely, filterModel))
2100+
.$if(limit !== undefined, (qb) => qb.limit(limit!)),
2101+
),
2102+
);
21002103
}
21012104

2105+
// if the model being deleted has a relation to a model that extends a delegate model, and if that
2106+
// relation is set to trigger a cascade delete from this model, the deletion will not automatically
2107+
// clean up the base hierarchy of the relation side (because polymorphic model's cascade deletion
2108+
// works downward not upward). We need to take care of the base deletions manually here.
2109+
await this.processDelegateRelationDelete(kysely, modelDef, where, limit);
2110+
21022111
query = query.modifyEnd(this.makeContextComment({ model, operation: 'delete' }));
2112+
const result = await query.executeTakeFirstOrThrow();
2113+
return { count: Number(result.numDeletedRows) };
2114+
}
21032115

2104-
if (returnData) {
2105-
const result = await query.execute();
2106-
return result as Result;
2107-
} else {
2108-
const result = (await query.executeTakeFirstOrThrow()) as DeleteResult;
2109-
return {
2110-
count: Number(result.numDeletedRows),
2111-
} as Result;
2116+
private async processDelegateRelationDelete(
2117+
kysely: ToKysely<Schema>,
2118+
modelDef: ModelDef,
2119+
where: any,
2120+
limit: number | undefined,
2121+
) {
2122+
for (const fieldDef of Object.values(modelDef.fields)) {
2123+
if (fieldDef.relation && fieldDef.relation.opposite) {
2124+
const oppositeModelDef = this.requireModel(fieldDef.type);
2125+
const oppositeRelation = this.requireField(fieldDef.type, fieldDef.relation.opposite);
2126+
if (oppositeModelDef.baseModel && oppositeRelation.relation?.onDelete === 'Cascade') {
2127+
if (limit !== undefined) {
2128+
throw new QueryError('Deleting with a limit is not supported for polymorphic models');
2129+
}
2130+
// the deletion will propagate upward to the base model chain
2131+
await this.delete(
2132+
kysely,
2133+
fieldDef.type as GetModels<Schema>,
2134+
{
2135+
[fieldDef.relation.opposite]: where,
2136+
},
2137+
undefined,
2138+
);
2139+
}
2140+
}
21122141
}
21132142
}
21142143

2144+
private async processBaseModelDelete(
2145+
kysely: ToKysely<Schema>,
2146+
model: string,
2147+
where: any,
2148+
limit: number | undefined,
2149+
filterModel: GetModels<Schema>,
2150+
) {
2151+
return this.delete(kysely, model as GetModels<Schema>, where, limit, filterModel);
2152+
}
2153+
21152154
protected makeIdSelect(model: string) {
21162155
const modelDef = this.requireModel(model);
21172156
return modelDef.idFields.reduce((acc, f) => {
@@ -2154,9 +2193,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
21542193
} else {
21552194
// otherwise, create a new transaction and execute the callback
21562195
let txBuilder = this.kysely.transaction();
2157-
if (isolationLevel) {
2158-
txBuilder = txBuilder.setIsolationLevel(isolationLevel);
2159-
}
2196+
txBuilder = txBuilder.setIsolationLevel(isolationLevel ?? 'repeatable read');
21602197
return txBuilder.execute(callback);
21612198
}
21622199
}

packages/runtime/src/client/crud/operations/delete.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,22 @@ export class DeleteOperationHandler<Schema extends SchemaDef> extends BaseOperat
2727
if (!existing) {
2828
throw new NotFoundError(this.model);
2929
}
30-
const result = await this.delete(this.kysely, this.model, args.where, undefined, false);
31-
if (result.count === 0) {
32-
throw new NotFoundError(this.model);
33-
}
30+
31+
// TODO: avoid using transaction for simple delete
32+
await this.safeTransaction(async (tx) => {
33+
const result = await this.delete(tx, this.model, args.where, undefined);
34+
if (result.count === 0) {
35+
throw new NotFoundError(this.model);
36+
}
37+
});
38+
3439
return existing;
3540
}
3641

3742
async runDeleteMany(args: DeleteManyArgs<Schema, Extract<keyof Schema['models'], string>> | undefined) {
38-
const result = await this.delete(this.kysely, this.model, args?.where, args?.limit, false);
39-
return result;
43+
return await this.safeTransaction(async (tx) => {
44+
const result = await this.delete(tx, this.model, args?.where, args?.limit);
45+
return result;
46+
});
4047
}
4148
}

0 commit comments

Comments
 (0)