Skip to content

Instantly share code, notes, and snippets.

@eqqe
Last active October 10, 2024 09:40
Show Gist options
  • Save eqqe/30b142b1a6c7c7dfdb252c14beedf59c to your computer and use it in GitHub Desktop.
Save eqqe/30b142b1a6c7c7dfdb252c14beedf59c to your computer and use it in GitHub Desktop.
abstract model IdCreatedUpdated {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
deletedAtCascadeKey String?
@@deny('update', future().id != id || future().createdAt != createdAt )
}
import { type Model, isArrayExpr, isReferenceExpr } from '@zenstackhq/sdk/ast';
import {
createProject,
getDataModels,
getAttribute,
getAttributeArg,
isEnumFieldReference,
saveProject,
} from '@zenstackhq/sdk';
import { VariableDeclarationKind } from 'ts-morph';
export const name = 'Form';
export default async function run(model: Model) {
const project = createProject();
const sf = project.createSourceFile(`dist/cascadeDelete.tsx`, undefined, { overwrite: true });
const cascadeDelete: Record<string, Record<string, string[]>> = {};
getDataModels(model).forEach((model) => {
function getDeleteCascades() {
const allModels = getDataModels(model.$container);
return allModels.reduce((acc, m) => {
if (m === model) {
return acc;
}
const relationFields = m.fields.filter((f) => {
if (f.type.reference?.ref !== model) {
return false;
}
const relationAttr = getAttribute(f, '@relation');
if (relationAttr) {
const onDelete = getAttributeArg(relationAttr, 'onDelete');
if (onDelete && isEnumFieldReference(onDelete) && onDelete.target.ref?.name === 'Cascade') {
return true;
}
}
return false;
});
if (relationFields.length > 0) {
acc[m.name] = relationFields.flatMap((relationField) => {
const relation = relationField.attributes.find((attr) => attr.decl.ref?.name === '@relation');
if (!relation) {
return [];
}
return [relationField.name];
});
}
return acc;
}, {} as Record<string, string[]>);
}
const cascades = getDeleteCascades();
if (Object.keys(cascades).length) {
cascadeDelete[model.name] = cascades;
}
});
sf.addVariableStatement({
isExported: true,
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: 'cascadeDelete',
type: 'Record<string, Record<string, string[]>>',
initializer: JSON.stringify(cascadeDelete),
},
],
});
await saveProject(project);
}
import { enhance } from '@zenstackhq/runtime';
import { getPrismaClient } from '@/lib/getPrismaClient';
import { Prisma } from '@prisma/client';
import { pascalCase } from 'change-case';
import { camelCaseType } from '@/lib/components/Form/Read/camelCaseType';
import _modelMetas from '.zenstack/model-meta';
// Type = keyof (typeof _modelMetas)['models']
import { Type } from '@/lib/types';
// Generated by my custom cascadeDelete.ts Zenstack plugin
import { cascadeDelete } from 'dist/cascadeDelete';
import { v4 as uuid } from 'uuid';
type DeletedRecordCount = Partial<Record<Type, number>>;
const deletedAtField = 'deletedAt';
const deletedAtCascadeKey = 'deletedAtCascadeKey';
const tablesWithSoftDelete = Object.values(_modelMetas.models)
.filter(
(model) =>
Object.values(model.fields).find((field) => field.name === deletedAtField) &&
Object.values(model.fields).find((field) => field.name === deletedAtCascadeKey)
)
.map((model) => model.name);
export function enhancePrisma({ userId }: { userId?: string }) {
const enhanced = enhance(getPrismaClient(), userId ? { user: { id: userId } } : {});
const softDelete = async <T extends Type>(type: T, args: Prisma.Args<T, 'delete'>) => {
const hasDeletedField = tablesWithSoftDelete.includes(pascalCase(type));
if (!hasDeletedField) {
throw new Error(`Table ${type} does not have a ${deletedAtField} column`);
}
const deletedRecordCount: DeletedRecordCount = { [type]: 0 };
const deletedAt = new Date();
const cascadeKey = uuid();
await enhanced.$transaction(async (tx) => {
async function softDeleteModel<T extends Type>({
type,
where,
}: {
type: T;
where: Prisma.Args<T, 'deleteMany'>;
}): Promise<void> {
const model = _modelMetas.models[type];
// @ts-ignore
const { count } = await tx[type].updateMany({
where,
data: { [deletedAtField]: deletedAt, [deletedAtCascadeKey]: cascadeKey },
});
deletedRecordCount[type] = count;
if (!count) {
return;
}
const relatedTables = cascadeDelete[pascalCase(type) as keyof typeof cascadeDelete] || [];
for (const [relatedTable, relationNames] of Object.entries(relatedTables)) {
const relatedType = camelCaseType(relatedTable);
const columnExists = tablesWithSoftDelete.includes(relatedTable);
if (!columnExists) {
continue;
}
for (const relationName of relationNames) {
const deletedData = {
[deletedAtField]: deletedAt,
[deletedAtCascadeKey]: cascadeKey,
};
// @ts-ignore
const inheritedFrom: string | undefined = model.fields.deletedAt.inheritedFrom;
await softDeleteModel({
type: relatedType,
where: {
[relationName]: inheritedFrom
? { [`delegate_aux_${camelCaseType(inheritedFrom)}`]: deletedData }
: deletedData,
[deletedAtField]: null,
[deletedAtCascadeKey]: null,
},
});
}
}
}
await softDeleteModel({ type, where: args.where });
});
return deletedRecordCount;
};
return { ...enhanced, softDelete };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment