Last active
October 10, 2024 09:40
-
-
Save eqqe/30b142b1a6c7c7dfdb252c14beedf59c to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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