feat: add field and recipient endpoints

This commit is contained in:
Mythie
2024-01-29 22:53:15 +11:00
parent 5a28eaa4ff
commit 98df273ebc
11 changed files with 720 additions and 8 deletions

View File

@ -4,14 +4,20 @@ import {
ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema, ZSendDocumentForSigningMutationSchema as SendDocumentMutationSchema,
ZAuthorizationHeadersSchema, ZAuthorizationHeadersSchema,
ZCreateDocumentMutationSchema, ZCreateDocumentMutationSchema,
ZCreateFieldMutationSchema,
ZCreateRecipientMutationSchema, ZCreateRecipientMutationSchema,
ZDeleteDocumentMutationSchema, ZDeleteDocumentMutationSchema,
ZDeleteFieldMutationSchema,
ZDeleteRecipientMutationSchema,
ZGetDocumentsQuerySchema, ZGetDocumentsQuerySchema,
ZSuccessfulDocumentResponseSchema, ZSuccessfulDocumentResponseSchema,
ZSuccessfulFieldResponseSchema,
ZSuccessfulRecipientResponseSchema, ZSuccessfulRecipientResponseSchema,
ZSuccessfulResponseSchema, ZSuccessfulResponseSchema,
ZSuccessfulSigningResponseSchema, ZSuccessfulSigningResponseSchema,
ZUnsuccessfulResponseSchema, ZUnsuccessfulResponseSchema,
ZUpdateFieldMutationSchema,
ZUpdateRecipientMutationSchema,
ZUploadDocumentSuccessfulSchema, ZUploadDocumentSuccessfulSchema,
} from './schema'; } from './schema';
@ -93,6 +99,76 @@ export const ApiContractV1 = c.router(
}, },
summary: 'Create a recipient for a document', summary: 'Create a recipient for a document',
}, },
updateRecipient: {
method: 'PATCH',
path: '/api/v1/documents/:id/recipients/:recipientId',
body: ZUpdateRecipientMutationSchema,
responses: {
200: ZSuccessfulRecipientResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Update a recipient for a document',
},
deleteRecipient: {
method: 'DELETE',
path: '/api/v1/documents/:id/recipients/:recipientId',
body: ZDeleteRecipientMutationSchema,
responses: {
200: ZSuccessfulRecipientResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a recipient from a document',
},
createField: {
method: 'POST',
path: '/api/v1/documents/:id/fields',
body: ZCreateFieldMutationSchema,
responses: {
200: ZSuccessfulFieldResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Create a field for a document',
},
updateField: {
method: 'PATCH',
path: '/api/v1/documents/:id/fields/:fieldId',
body: ZUpdateFieldMutationSchema,
responses: {
200: ZSuccessfulFieldResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Update a field for a document',
},
deleteField: {
method: 'DELETE',
path: '/api/v1/documents/:id/fields/:fieldId',
body: ZDeleteFieldMutationSchema,
responses: {
200: ZSuccessfulFieldResponseSchema,
400: ZUnsuccessfulResponseSchema,
401: ZUnsuccessfulResponseSchema,
404: ZUnsuccessfulResponseSchema,
500: ZUnsuccessfulResponseSchema,
},
summary: 'Delete a field from a document',
},
}, },
{ {
baseHeaders: ZAuthorizationHeadersSchema, baseHeaders: ZAuthorizationHeadersSchema,

View File

@ -4,10 +4,17 @@ import { deleteDocument } from '@documenso/lib/server-only/document/delete-docum
import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { sendDocument } from '@documenso/lib/server-only/document/send-document'; import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { createField } from '@documenso/lib/server-only/field/create-field';
import { deleteField } from '@documenso/lib/server-only/field/delete-field';
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { updateField } from '@documenso/lib/server-only/field/update-field';
import { deleteRecipient } from '@documenso/lib/server-only/recipient/delete-recipient';
import { getRecipientById } from '@documenso/lib/server-only/recipient/get-recipient-by-id';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document';
import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient';
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { ApiContractV1 } from './contract'; import { ApiContractV1 } from './contract';
import { authenticatedMiddleware } from './middleware/authenticated'; import { authenticatedMiddleware } from './middleware/authenticated';
@ -250,4 +257,347 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
}; };
} }
}), }),
updateRecipient: authenticatedMiddleware(async (args, user) => {
const { id: documentId, recipientId } = args.params;
const { name, email } = args.body;
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const updatedRecipient = await updateRecipient({
documentId: Number(documentId),
recipientId: Number(recipientId),
email,
name,
}).catch(() => null);
if (!updatedRecipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
return {
status: 200,
body: updatedRecipient,
};
}),
deleteRecipient: authenticatedMiddleware(async (args, user) => {
const { id: documentId, recipientId } = args.params;
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const deletedRecipient = await deleteRecipient({
documentId: Number(documentId),
recipientId: Number(recipientId),
}).catch(() => null);
if (!deletedRecipient) {
return {
status: 400,
body: {
message: 'Unable to delete recipient',
},
};
}
return {
status: 200,
body: deletedRecipient,
};
}),
createField: authenticatedMiddleware(async (args, user) => {
const { id: documentId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const recipient = await getRecipientById({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
const field = await createField({
documentId: Number(documentId),
recipientId: Number(recipientId),
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
});
const remappedField = {
documentId: field.documentId,
recipientId: field.recipientId ?? -1,
type: field.type,
pageNumber: field.page,
pageX: Number(field.positionX),
pageY: Number(field.positionY),
pageWidth: Number(field.width),
pageHeight: Number(field.height),
customText: field.customText,
inserted: field.inserted,
};
return {
status: 200,
body: remappedField,
};
}),
updateField: authenticatedMiddleware(async (args, user) => {
const { id: documentId, fieldId } = args.params;
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body;
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const recipient = await getRecipientById({
id: Number(recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (!recipient) {
return {
status: 404,
body: {
message: 'Recipient not found',
},
};
}
if (recipient.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
const updatedField = await updateField({
fieldId: Number(fieldId),
documentId: Number(documentId),
recipientId: recipientId ? Number(recipientId) : undefined,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
});
const remappedField = {
documentId: updatedField.documentId,
recipientId: updatedField.recipientId ?? -1,
type: updatedField.type,
pageNumber: updatedField.page,
pageX: Number(updatedField.positionX),
pageY: Number(updatedField.positionY),
pageWidth: Number(updatedField.width),
pageHeight: Number(updatedField.height),
customText: updatedField.customText,
inserted: updatedField.inserted,
};
return {
status: 200,
body: remappedField,
};
}),
deleteField: authenticatedMiddleware(async (args, user) => {
const { id: documentId, fieldId } = args.params;
const document = await getDocumentById({
id: Number(documentId),
userId: user.id,
});
if (!document) {
return {
status: 404,
body: {
message: 'Document not found',
},
};
}
if (document.status === DocumentStatus.COMPLETED) {
return {
status: 400,
body: {
message: 'Document is already completed',
},
};
}
const field = await getFieldById({
fieldId: Number(fieldId),
documentId: Number(documentId),
}).catch(() => null);
if (!field) {
return {
status: 404,
body: {
message: 'Field not found',
},
};
}
const recipient = await getRecipientById({
id: Number(field.recipientId),
documentId: Number(documentId),
}).catch(() => null);
if (recipient?.signingStatus === SigningStatus.SIGNED) {
return {
status: 400,
body: {
message: 'Recipient has already signed the document',
},
};
}
const deletedField = await deleteField({
documentId: Number(documentId),
fieldId: Number(fieldId),
}).catch(() => null);
if (!deletedField) {
return {
status: 400,
body: {
message: 'Unable to delete field',
},
};
}
const remappedField = {
documentId: deletedField.documentId,
recipientId: deletedField.recipientId ?? -1,
type: deletedField.type,
pageNumber: deletedField.page,
pageX: Number(deletedField.positionX),
pageY: Number(deletedField.positionY),
pageWidth: Number(deletedField.width),
pageHeight: Number(deletedField.height),
customText: deletedField.customText,
inserted: deletedField.inserted,
};
return {
status: 200,
body: remappedField,
};
}),
}); });

View File

@ -1,7 +1,10 @@
import { z } from 'zod'; import { z } from 'zod';
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client'; import { FieldType, ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
/**
* Documents
*/
export const ZGetDocumentsQuerySchema = z.object({ export const ZGetDocumentsQuerySchema = z.object({
page: z.string().optional(), page: z.string().optional(),
perPage: z.string().optional(), perPage: z.string().optional(),
@ -49,8 +52,19 @@ export const ZCreateRecipientMutationSchema = z.object({
email: z.string().email().min(1), email: z.string().email().min(1),
}); });
/**
* Recipients
*/
export type TCreateRecipientMutationSchema = z.infer<typeof ZCreateRecipientMutationSchema>; export type TCreateRecipientMutationSchema = z.infer<typeof ZCreateRecipientMutationSchema>;
export const ZUpdateRecipientMutationSchema = ZCreateRecipientMutationSchema.partial();
export type TUpdateRecipientMutationSchema = z.infer<typeof ZUpdateRecipientMutationSchema>;
export const ZDeleteRecipientMutationSchema = null;
export type TDeleteRecipientMutationSchema = typeof ZDeleteRecipientMutationSchema;
export const ZSuccessfulRecipientResponseSchema = z.object({ export const ZSuccessfulRecipientResponseSchema = z.object({
id: z.number(), id: z.number(),
documentId: z.number(), documentId: z.number(),
@ -67,6 +81,44 @@ export const ZSuccessfulRecipientResponseSchema = z.object({
export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecipientResponseSchema>; export type TSuccessfulRecipientResponseSchema = z.infer<typeof ZSuccessfulRecipientResponseSchema>;
/**
* Fields
*/
export const ZCreateFieldMutationSchema = z.object({
recipientId: z.number(),
type: z.nativeEnum(FieldType),
pageNumber: z.number(),
pageX: z.number(),
pageY: z.number(),
pageWidth: z.number(),
pageHeight: z.number(),
});
export type TCreateFieldMutationSchema = z.infer<typeof ZCreateFieldMutationSchema>;
export const ZUpdateFieldMutationSchema = ZCreateFieldMutationSchema.partial();
export type TUpdateFieldMutationSchema = z.infer<typeof ZUpdateFieldMutationSchema>;
export const ZDeleteFieldMutationSchema = null;
export type TDeleteFieldMutationSchema = typeof ZDeleteFieldMutationSchema;
export const ZSuccessfulFieldResponseSchema = z.object({
documentId: z.number(),
recipientId: z.number(),
type: z.nativeEnum(FieldType),
pageNumber: z.number(),
pageX: z.number(),
pageY: z.number(),
pageWidth: z.number(),
pageHeight: z.number(),
customText: z.string(),
inserted: z.boolean(),
});
export type TSuccessfulFieldResponseSchema = z.infer<typeof ZSuccessfulFieldResponseSchema>;
export const ZSuccessfulResponseSchema = z.object({ export const ZSuccessfulResponseSchema = z.object({
documents: ZSuccessfulDocumentResponseSchema.array(), documents: ZSuccessfulDocumentResponseSchema.array(),
totalPages: z.number(), totalPages: z.number(),
@ -80,14 +132,17 @@ export const ZSuccessfulSigningResponseSchema = z.object({
export type TSuccessfulSigningResponseSchema = z.infer<typeof ZSuccessfulSigningResponseSchema>; export type TSuccessfulSigningResponseSchema = z.infer<typeof ZSuccessfulSigningResponseSchema>;
export const ZUnsuccessfulResponseSchema = z.object({ /**
message: z.string(), * General
}); */
export type TUnsuccessfulResponseSchema = z.infer<typeof ZUnsuccessfulResponseSchema>;
export const ZAuthorizationHeadersSchema = z.object({ export const ZAuthorizationHeadersSchema = z.object({
authorization: z.string(), authorization: z.string(),
}); });
export type TAuthorizationHeadersSchema = z.infer<typeof ZAuthorizationHeadersSchema>; export type TAuthorizationHeadersSchema = z.infer<typeof ZAuthorizationHeadersSchema>;
export const ZUnsuccessfulResponseSchema = z.object({
message: z.string(),
});
export type TUnsuccessfulResponseSchema = z.infer<typeof ZUnsuccessfulResponseSchema>;

View File

@ -0,0 +1,41 @@
import { prisma } from '@documenso/prisma';
import type { FieldType } from '@documenso/prisma/client';
export type CreateFieldOptions = {
documentId: number;
recipientId: number;
type: FieldType;
pageNumber: number;
pageX: number;
pageY: number;
pageWidth: number;
pageHeight: number;
};
export const createField = async ({
documentId,
recipientId,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
}: CreateFieldOptions) => {
const field = await prisma.field.create({
data: {
documentId,
recipientId,
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
customText: '',
inserted: false,
},
});
return field;
};

View File

@ -0,0 +1,17 @@
import { prisma } from '@documenso/prisma';
export type DeleteFieldOptions = {
fieldId: number;
documentId: number;
};
export const deleteField = async ({ fieldId, documentId }: DeleteFieldOptions) => {
const field = await prisma.field.delete({
where: {
id: fieldId,
documentId,
},
});
return field;
};

View File

@ -0,0 +1,17 @@
import { prisma } from '@documenso/prisma';
export type GetFieldByIdOptions = {
fieldId: number;
documentId: number;
};
export const getFieldById = async ({ fieldId, documentId }: GetFieldByIdOptions) => {
const field = await prisma.field.findFirst({
where: {
id: fieldId,
documentId,
},
});
return field;
};

View File

@ -0,0 +1,44 @@
import { prisma } from '@documenso/prisma';
import type { FieldType } from '@documenso/prisma/client';
export type UpdateFieldOptions = {
fieldId: number;
documentId: number;
recipientId?: number;
type?: FieldType;
pageNumber?: number;
pageX?: number;
pageY?: number;
pageWidth?: number;
pageHeight?: number;
};
export const updateField = async ({
fieldId,
documentId,
recipientId,
type,
pageNumber,
pageX,
pageY,
pageWidth,
pageHeight,
}: UpdateFieldOptions) => {
const field = await prisma.field.update({
where: {
id: fieldId,
documentId,
},
data: {
recipientId,
type,
page: pageNumber,
positionX: pageX,
positionY: pageY,
width: pageWidth,
height: pageHeight,
},
});
return field;
};

View File

@ -0,0 +1,32 @@
import { prisma } from '@documenso/prisma';
import { SendStatus } from '@documenso/prisma/client';
export type DeleteRecipientOptions = {
documentId: number;
recipientId: number;
};
export const deleteRecipient = async ({ documentId, recipientId }: DeleteRecipientOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
id: recipientId,
documentId,
},
});
if (!recipient) {
throw new Error('Recipient not found');
}
if (recipient.sendStatus !== SendStatus.NOT_SENT) {
throw new Error('Can not delete a recipient that has already been sent a document');
}
const deletedRecipient = await prisma.recipient.delete({
where: {
id: recipient.id,
},
});
return deletedRecipient;
};

View File

@ -0,0 +1,21 @@
import { prisma } from '@documenso/prisma';
export type GetRecipientByEmailOptions = {
documentId: number;
email: string;
};
export const getRecipientByEmail = async ({ documentId, email }: GetRecipientByEmailOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
documentId,
email: email.toLowerCase(),
},
});
if (!recipient) {
throw new Error('Recipient not found');
}
return recipient;
};

View File

@ -0,0 +1,21 @@
import { prisma } from '@documenso/prisma';
export type GetRecipientByIdOptions = {
id: number;
documentId: number;
};
export const getRecipientById = async ({ documentId, id }: GetRecipientByIdOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
documentId,
id,
},
});
if (!recipient) {
throw new Error('Recipient not found');
}
return recipient;
};

View File

@ -0,0 +1,38 @@
import { prisma } from '@documenso/prisma';
export type UpdateRecipientOptions = {
documentId: number;
recipientId: number;
email?: string;
name?: string;
};
export const updateRecipient = async ({
documentId,
recipientId,
email,
name,
}: UpdateRecipientOptions) => {
const recipient = await prisma.recipient.findFirst({
where: {
id: recipientId,
documentId,
},
});
if (!recipient) {
throw new Error('Recipient not found');
}
const updatedRecipient = await prisma.recipient.update({
where: {
id: recipient.id,
},
data: {
email: email?.toLowerCase() ?? recipient.email,
name: name ?? recipient.name,
},
});
return updatedRecipient;
};