Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add edit and delete middlewares #529

Merged
merged 4 commits into from
Feb 3, 2025
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
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 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 @@
} else {
try {
setIsPending(true);
await runDeletion([id!] as string[] | number[]);
await runSingleDeletion(id!);
router.replace({
pathname: `${basePath}/${slugify(resource)}`,
query: {
Expand Down Expand Up @@ -203,7 +203,7 @@
</div>
);
},
[isPending, id]

Check warning on line 206 in packages/next-admin/src/components/Form.tsx

View workflow job for this annotation

GitHub Actions / start

React Hook useMemo has missing dependencies: 'basePath', 'canCreate', 'canDelete', 'canEdit', 'edit', 'resource', 'router', 'runSingleDeletion', 'showMessage', and 't'. Either include them or remove the dependency array
);

const extraErrors: ErrorSchema | undefined = validation?.reduce(
Expand Down Expand Up @@ -303,7 +303,7 @@
setIsPending(false);
}
},
[apiBasePath, id]

Check warning on line 306 in packages/next-admin/src/components/Form.tsx

View workflow job for this annotation

GitHub Actions / start

React Hook useCallback has missing dependencies: 'basePath', 'cleanAll', 'resource', 'router', 'setFormData', 'showMessage', and 't'. Either include them or remove the dependency array
);

const fields: RjsfForm["props"]["fields"] = {
Expand Down Expand Up @@ -569,7 +569,7 @@
/>
);
}),
[submitButton]

Check warning on line 572 in packages/next-admin/src/components/Form.tsx

View workflow job for this annotation

GitHub Actions / start

React Hook useMemo has missing dependencies: 'CustomForm', 'allDisabled', 'extraErrors', 'fields', 'isPending', 'schema', 'schemas', 'setFormData', and 'templates'. Either include them or remove the dependency array
);

return (
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
Loading