Skip to content

Commit

Permalink
feat: add edit and delete middlewares (#529)
Browse files Browse the repository at this point in the history
* feat: add edit and delete middlewares

* Update packages/next-admin/src/appHandler.ts

Co-authored-by: Regourd Colin <[email protected]>

* Update packages/next-admin/src/handlers/resources.ts

Co-authored-by: Regourd Colin <[email protected]>

* Update packages/next-admin/src/pageHandler.ts

Co-authored-by: Regourd Colin <[email protected]>

---------

Co-authored-by: Regourd Colin <[email protected]>
  • Loading branch information
foyarash and cregourd authored Feb 3, 2025
1 parent 5854cce commit f4d1d95
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 20 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-bobcats-cough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@premieroctet/next-admin": minor
---

feat: add edit and delete middlewares (#527 #528)
31 changes: 24 additions & 7 deletions packages/next-admin/src/appHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,25 @@ export const createHandler = <P extends string = "nextadmin">({
);
}

await deleteResource({
body: [params[paramKey][1]],
prisma,
resource,
});
try {
const deleted = await deleteResource({
body: [params[paramKey][1]],
prisma,
resource,
modelOptions: options?.model?.[resource],
});

return NextResponse.json({ ok: true });
if (!deleted) {
throw new Error('Deletion failed')
}

return NextResponse.json({ ok: true });
} catch (e) {
return NextResponse.json(
{ error: (e as Error).message },
{ status: 500 }
);
}
})
.delete(`${apiBasePath}/:model`, async (req, ctx) => {
const params = await ctx.params;
Expand All @@ -241,7 +253,12 @@ export const createHandler = <P extends string = "nextadmin">({
try {
const body = await req.json();

await deleteResource({ body, prisma, resource });
await deleteResource({
body,
prisma,
resource,
modelOptions: options?.model?.[resource],
});

return NextResponse.json({ ok: true });
} catch (e) {
Expand Down
4 changes: 2 additions & 2 deletions packages/next-admin/src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const Form = ({
const formRef = useRef<RjsfForm>(null);
const [isPending, setIsPending] = useState(false);
const allDisabled = edit && !canEdit;
const { runDeletion } = useDeleteAction(resource);
const { runSingleDeletion } = useDeleteAction(resource);
const { showMessage } = useMessage();
const { cleanAll } = useFormState();
const { setFormData } = useFormData();
Expand Down Expand Up @@ -173,7 +173,7 @@ const Form = ({
} else {
try {
setIsPending(true);
await runDeletion([id!] as string[] | number[]);
await runSingleDeletion(id!);
router.replace({
pathname: `${basePath}/${slugify(resource)}`,
query: {
Expand Down
75 changes: 67 additions & 8 deletions packages/next-admin/src/handlers/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
} from "@prisma/client/runtime/library";
import {
EditFieldsOptions,
Model,
ModelName,
ModelOptions,
NextAdminOptions,
Permission,
Schema,
Expand All @@ -15,6 +17,7 @@ import {
import { hasPermission } from "../utils/permissions";
import { getDataItem } from "../utils/prisma";
import {
formatId,
formattedFormData,
getModelIdProperty,
parseFormData,
Expand All @@ -26,20 +29,53 @@ type DeleteResourceParams = {
prisma: PrismaClient;
resource: ModelName;
body: string[] | number[];
modelOptions?: ModelOptions<ModelName>[ModelName];
};

export const deleteResource = ({
export const deleteResource = async ({
prisma,
resource,
body,
modelOptions,
}: DeleteResourceParams) => {
const modelIdProperty = getModelIdProperty(resource);


if (modelOptions?.middlewares?.delete) {
// @ts-expect-error
const resources = await prisma[uncapitalize(resource)].findMany({
where: {
[modelIdProperty]: {
in: body.map((id) => formatId(resource, id.toString())),
},
},
});

const middlewareExec: PromiseSettledResult<boolean>[] =
await Promise.allSettled(
// @ts-expect-error
resources.map(async (res) => {
const isSuccessDelete =
await modelOptions?.middlewares?.delete?.(res);

return isSuccessDelete;
})
);

if (
middlewareExec.some(
(exec) => exec.status === "rejected" || exec.value === false
)
) {
return false;
}
}

// @ts-expect-error
return prisma[uncapitalize(resource)].deleteMany({
where: {
[modelIdProperty]: {
in: body,
in: body.map((id) => formatId(resource, id.toString())),
},
},
});
Expand Down Expand Up @@ -98,12 +134,35 @@ export const submitResource = async ({
};
}

// @ts-expect-error
await prisma[resource].update({
where: {
[resourceIdField]: id,
},
data: formattedData,
await prisma.$transaction(async (client) => {
let canEdit = true;
if (options?.model?.[resource]?.middlewares?.edit) {
const currentData = await prisma[
uncapitalize(resource)
// @ts-expect-error
].findUniqueOrThrow({
where: {
[resourceIdField]: formatId(resource, id.toString()),
},
});

canEdit = await options?.model?.[resource]?.middlewares?.edit(
formattedData,
currentData
);
}

if (!canEdit) {
throw new Error("Unable to edit this item");
}

// @ts-expect-error
await prisma[resource].update({
where: {
[resourceIdField]: id,
},
data: formattedData,
});
});

const data = await getDataItem({
Expand Down
13 changes: 12 additions & 1 deletion packages/next-admin/src/hooks/useDeleteAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ export const useDeleteAction = (resource: ModelName) => {
}
};

const runSingleDeletion = async (id: string | number) => {
const response = await fetch(`${apiBasePath}/${slugify(resource)}/${id}`, {
method: "DELETE",
});

if (!response.ok) {
const result = await response.json();
throw new Error(result.error);
}
};

const deleteItems = async (ids: string[] | number[]) => {
if (
window.confirm(t("list.row.actions.delete.alert", { count: ids.length }))
Expand All @@ -46,5 +57,5 @@ export const useDeleteAction = (resource: ModelName) => {
}
};

return { deleteItems, runDeletion };
return { deleteItems, runDeletion, runSingleDeletion };
};
14 changes: 12 additions & 2 deletions packages/next-admin/src/pageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,17 @@ export const createHandler = <P extends string = "nextadmin">({
}

try {
await deleteResource({
const deleted = await deleteResource({
body: [req.query[paramKey]![1]],
prisma,
resource,
modelOptions: options?.model?.[resource],
});

if (!deleted) {
throw new Error("Deletion failed")
}

return res.json({ ok: true });
} catch (e) {
return res.status(500).json({ error: (e as Error).message });
Expand Down Expand Up @@ -273,7 +278,12 @@ export const createHandler = <P extends string = "nextadmin">({
}

try {
await deleteResource({ body, prisma, resource });
await deleteResource({
body,
prisma,
resource,
modelOptions: options?.model?.[resource],
});

return res.json({ ok: true });
} catch (e) {
Expand Down
26 changes: 26 additions & 0 deletions packages/next-admin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ export type Handler<
* an optional string displayed in the input field as an error message in case of a failure during the upload handler.
*/
uploadErrorMessage?: string;
/**
* an async function that takes the resource value as parameter and returns a boolean. If false is returned, the deletion will not happen.
* @param input
* @returns boolean
*/
delete?: (input: T) => Promise<boolean> | boolean;
};

export type UploadParameters = Parameters<
Expand Down Expand Up @@ -613,6 +619,25 @@ export enum Permission {

export type PermissionType = "create" | "edit" | "delete";

export type ModelMiddleware<T extends ModelName> = {
/**
* a function that is called before the form data is sent to the database.
* @param data - the form data as a record
* @param currentData - the current data in the database
* @returns boolean - if false is returned, the update will not happen.
*/
edit?: (
updatedData: Model<T>,
currentData: Model<T>
) => Promise<boolean> | boolean;
/**
* a function that is called before resource is deleted from the database.
* @param data - the current data in the database
* @returns boolean - if false is returned, the deletion will not happen.
*/
delete?: (data: Model<T>) => Promise<boolean> | boolean;
};

export type ModelOptions<T extends ModelName> = {
[P in T]?: {
/**
Expand Down Expand Up @@ -644,6 +669,7 @@ export type ModelOptions<T extends ModelName> = {
*/
icon?: ModelIcon;
permissions?: PermissionType[];
middlewares?: ModelMiddleware<P>;
};
};

Expand Down

0 comments on commit f4d1d95

Please sign in to comment.