mirror of
https://github.com/documenso/documenso.git
synced 2025-11-09 20:12:31 +10:00
1735 lines
47 KiB
TypeScript
1735 lines
47 KiB
TypeScript
import { DocumentDataType, EnvelopeType, SigningStatus } from '@prisma/client';
|
|
import { tsr } from '@ts-rest/serverless/fetch';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
|
import '@documenso/lib/constants/time-zones';
|
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
|
import { AppError } from '@documenso/lib/errors/app-error';
|
|
import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data';
|
|
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
|
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
|
|
import { findDocuments } from '@documenso/lib/server-only/document/find-documents';
|
|
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
|
|
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
|
|
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
|
import {
|
|
getEnvelopeById,
|
|
getEnvelopeWhereInput,
|
|
} from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
|
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
|
|
import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields';
|
|
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
|
|
import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient';
|
|
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
|
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
|
|
import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients';
|
|
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
|
|
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
|
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
|
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
|
|
import { extractDerivedDocumentEmailSettings } from '@documenso/lib/types/document-email';
|
|
import {
|
|
ZCheckboxFieldMeta,
|
|
ZDropdownFieldMeta,
|
|
ZFieldMetaSchema,
|
|
ZNumberFieldMeta,
|
|
ZRadioFieldMeta,
|
|
ZTextFieldMeta,
|
|
} from '@documenso/lib/types/field-meta';
|
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
|
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
|
import {
|
|
getPresignGetUrl,
|
|
getPresignPostUrl,
|
|
} from '@documenso/lib/universal/upload/server-actions';
|
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
|
import {
|
|
mapSecondaryIdToDocumentId,
|
|
mapSecondaryIdToTemplateId,
|
|
} from '@documenso/lib/utils/envelope';
|
|
import { prisma } from '@documenso/prisma';
|
|
|
|
import { ApiContractV1 } from './contract';
|
|
import { authenticatedMiddleware } from './middleware/authenticated';
|
|
|
|
export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
|
const page = Number(args.query.page) || 1;
|
|
const perPage = Number(args.query.perPage) || 10;
|
|
|
|
const { data: documents, totalPages } = await findDocuments({
|
|
page,
|
|
perPage,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
documents: documents.map((document) => ({
|
|
id: mapSecondaryIdToDocumentId(document.secondaryId),
|
|
externalId: document.externalId,
|
|
userId: document.userId,
|
|
teamId: document.teamId,
|
|
title: document.title,
|
|
status: document.status,
|
|
createdAt: document.createdAt,
|
|
updatedAt: document.updatedAt,
|
|
completedAt: document.completedAt,
|
|
})),
|
|
totalPages,
|
|
},
|
|
};
|
|
}),
|
|
|
|
getDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
|
|
const { id: documentId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
|
where: envelopeWhereInput,
|
|
include: {
|
|
recipients: {
|
|
orderBy: {
|
|
id: 'asc',
|
|
},
|
|
},
|
|
fields: {
|
|
include: {
|
|
signature: true,
|
|
recipient: {
|
|
select: {
|
|
name: true,
|
|
email: true,
|
|
signingStatus: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: {
|
|
id: 'asc',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const { fields, recipients } = envelope;
|
|
|
|
const parsedMetaFields = fields.map((field) => {
|
|
let parsedMetaOrNull = null;
|
|
|
|
if (field.fieldMeta) {
|
|
const result = ZFieldMetaSchema.safeParse(field.fieldMeta);
|
|
|
|
if (!result.success) {
|
|
throw new Error('Field meta parsing failed for field ' + field.id);
|
|
}
|
|
|
|
parsedMetaOrNull = result.data;
|
|
}
|
|
|
|
return {
|
|
...field,
|
|
fieldMeta: parsedMetaOrNull,
|
|
};
|
|
});
|
|
|
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
id: legacyDocumentId,
|
|
externalId: envelope.externalId,
|
|
userId: envelope.userId,
|
|
teamId: envelope.teamId,
|
|
title: envelope.title,
|
|
status: envelope.status,
|
|
createdAt: envelope.createdAt,
|
|
updatedAt: envelope.updatedAt,
|
|
completedAt: envelope.completedAt,
|
|
recipients: recipients.map((recipient) => ({
|
|
id: recipient.id,
|
|
documentId: legacyDocumentId,
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
token: recipient.token,
|
|
signedAt: recipient.signedAt,
|
|
readStatus: recipient.readStatus,
|
|
signingStatus: recipient.signingStatus,
|
|
sendStatus: recipient.sendStatus,
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
})),
|
|
fields: parsedMetaFields,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
downloadSignedDocument: authenticatedMiddleware(async (args, user, team, { logger }) => {
|
|
const { id: documentId } = args.params;
|
|
const { downloadOriginalDocument } = args.query;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
}).catch(() => null);
|
|
|
|
const firstDocumentData = envelope?.envelopeItems[0]?.documentData;
|
|
|
|
if (!envelope || !firstDocumentData) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
// This error is done AFTER the get envelope so we can test access controls without S3.
|
|
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'Document downloads are only available when S3 storage is configured.',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (DocumentDataType.S3_PATH !== firstDocumentData.type) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Invalid document data type',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (!downloadOriginalDocument && !isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Document is not completed yet.',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (envelope.envelopeItems.length !== 1) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'API V1 does not support items',
|
|
},
|
|
};
|
|
}
|
|
|
|
const { url } = await getPresignGetUrl(
|
|
downloadOriginalDocument ? firstDocumentData.initialData : firstDocumentData.data,
|
|
);
|
|
|
|
return {
|
|
status: 200,
|
|
body: { downloadUrl: url },
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'Error downloading the document. Please try again.',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
deleteDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const legacyDocumentId = Number(documentId);
|
|
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
const deletedDocument = await deleteDocument({
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
id: legacyDocumentId,
|
|
externalId: deletedDocument.externalId,
|
|
userId: deletedDocument.userId,
|
|
teamId: deletedDocument.teamId,
|
|
title: deletedDocument.title,
|
|
status: deletedDocument.status,
|
|
createdAt: deletedDocument.createdAt,
|
|
updatedAt: deletedDocument.updatedAt,
|
|
completedAt: deletedDocument.completedAt,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
createDocument: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
|
const { body } = args;
|
|
|
|
try {
|
|
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'Create document is not available without S3 transport.',
|
|
},
|
|
};
|
|
}
|
|
|
|
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
|
|
|
|
if (remaining.documents <= 0) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'You have reached the maximum number of documents allowed for this month',
|
|
},
|
|
};
|
|
}
|
|
|
|
const dateFormat = body.meta.dateFormat
|
|
? DATE_FORMATS.find((format) => format.value === body.meta.dateFormat)
|
|
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
|
|
|
if (body.meta.dateFormat && !dateFormat) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Invalid date format. Please provide a valid date format',
|
|
},
|
|
};
|
|
}
|
|
|
|
const timezone = body.meta.timezone
|
|
? TIME_ZONES.find((tz) => tz === body.meta.timezone)
|
|
: DEFAULT_DOCUMENT_TIME_ZONE;
|
|
|
|
const isTimeZoneValid = body.meta.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
|
|
|
if (!isTimeZoneValid) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Invalid timezone. Please provide a valid timezone',
|
|
},
|
|
};
|
|
}
|
|
|
|
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
|
|
|
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
|
|
|
const documentData = await createDocumentData({
|
|
data: key,
|
|
type: DocumentDataType.S3_PATH,
|
|
});
|
|
|
|
const envelope = await createEnvelope({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
internalVersion: 1,
|
|
data: {
|
|
title: body.title,
|
|
type: EnvelopeType.DOCUMENT,
|
|
externalId: body.externalId || undefined,
|
|
formValues: body.formValues,
|
|
folderId: body.folderId,
|
|
envelopeItems: [
|
|
{
|
|
documentDataId: documentData.id,
|
|
},
|
|
],
|
|
globalAccessAuth: body.authOptions?.globalAccessAuth,
|
|
globalActionAuth: body.authOptions?.globalActionAuth,
|
|
},
|
|
attachments: body.attachments,
|
|
meta: {
|
|
subject: body.meta.subject,
|
|
message: body.meta.message,
|
|
timezone,
|
|
dateFormat: dateFormat?.value,
|
|
redirectUrl: body.meta.redirectUrl,
|
|
signingOrder: body.meta.signingOrder,
|
|
allowDictateNextSigner: body.meta.allowDictateNextSigner,
|
|
language: body.meta.language,
|
|
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
|
uploadSignatureEnabled: body.meta.uploadSignatureEnabled,
|
|
drawSignatureEnabled: body.meta.drawSignatureEnabled,
|
|
distributionMethod: body.meta.distributionMethod,
|
|
emailSettings: body.meta.emailSettings,
|
|
},
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
|
|
|
const { recipients } = await setDocumentRecipients({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
recipients: body.recipients,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
uploadUrl: url,
|
|
documentId: legacyDocumentId,
|
|
recipients: recipients.map((recipient) => ({
|
|
recipientId: recipient.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
token: recipient.token,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
})),
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'An error has occured while uploading the file',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
createTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => {
|
|
const { body } = args;
|
|
const {
|
|
title,
|
|
folderId,
|
|
externalId,
|
|
visibility,
|
|
globalAccessAuth,
|
|
globalActionAuth,
|
|
publicTitle,
|
|
publicDescription,
|
|
type,
|
|
meta,
|
|
attachments,
|
|
} = body;
|
|
|
|
try {
|
|
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'Create template is not available without S3 transport.',
|
|
},
|
|
};
|
|
}
|
|
|
|
const dateFormat = meta?.dateFormat
|
|
? DATE_FORMATS.find((format) => format.value === meta?.dateFormat)
|
|
: DATE_FORMATS.find((format) => format.value === DEFAULT_DOCUMENT_DATE_FORMAT);
|
|
|
|
if (meta?.dateFormat && !dateFormat) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Invalid date format. Please provide a valid date format',
|
|
},
|
|
};
|
|
}
|
|
|
|
const timezone = meta?.timezone
|
|
? TIME_ZONES.find((tz) => tz === meta?.timezone)
|
|
: DEFAULT_DOCUMENT_TIME_ZONE;
|
|
|
|
const isTimeZoneValid = meta?.timezone ? TIME_ZONES.includes(String(timezone)) : true;
|
|
|
|
if (!isTimeZoneValid) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Invalid timezone. Please provide a valid timezone',
|
|
},
|
|
};
|
|
}
|
|
|
|
const fileName = title?.endsWith('.pdf') ? title : `${title}.pdf`;
|
|
|
|
const { url, key } = await getPresignPostUrl(fileName, 'application/pdf');
|
|
|
|
const templateDocumentData = await createDocumentData({
|
|
data: key,
|
|
type: DocumentDataType.S3_PATH,
|
|
});
|
|
|
|
const createdTemplate = await createEnvelope({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
internalVersion: 1,
|
|
data: {
|
|
type: EnvelopeType.TEMPLATE,
|
|
envelopeItems: [
|
|
{
|
|
documentDataId: templateDocumentData.id,
|
|
},
|
|
],
|
|
templateType: type,
|
|
title,
|
|
folderId,
|
|
externalId: externalId ?? undefined,
|
|
visibility,
|
|
globalAccessAuth,
|
|
globalActionAuth,
|
|
publicTitle,
|
|
publicDescription,
|
|
},
|
|
meta,
|
|
attachments,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
const fullTemplate = await getTemplateById({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: createdTemplate.id,
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
uploadUrl: url,
|
|
template: fullTemplate,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'An error has occured while creating the template',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
deleteTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
|
|
const { id: templateId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: templateId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const deletedTemplate = await deleteTemplate({
|
|
id: {
|
|
type: 'templateId',
|
|
id: Number(templateId),
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const legacyTemplateId = mapSecondaryIdToTemplateId(deletedTemplate.secondaryId);
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
id: legacyTemplateId,
|
|
externalId: deletedTemplate.externalId,
|
|
type: deletedTemplate.templateType,
|
|
title: deletedTemplate.title,
|
|
userId: deletedTemplate.userId,
|
|
teamId: deletedTemplate.teamId,
|
|
createdAt: deletedTemplate.createdAt,
|
|
updatedAt: deletedTemplate.updatedAt,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Template not found',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
getTemplate: authenticatedMiddleware(async (args, user, team, { logger }) => {
|
|
const { id: templateId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: templateId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const template = await getTemplateById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: Number(templateId),
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
...template,
|
|
templateMeta: template.templateMeta
|
|
? {
|
|
...template.templateMeta,
|
|
templateId: template.id,
|
|
}
|
|
: null,
|
|
Field: template.fields.map((field) => ({
|
|
...field,
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
|
|
})),
|
|
Recipient: template.recipients,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return AppError.toRestAPIError(err);
|
|
}
|
|
}),
|
|
|
|
getTemplates: authenticatedMiddleware(async (args, user, team) => {
|
|
const page = Number(args.query.page) || 1;
|
|
const perPage = Number(args.query.perPage) || 10;
|
|
|
|
try {
|
|
const { data: templates, totalPages } = await findTemplates({
|
|
page,
|
|
perPage,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
templates: templates.map((template) => ({
|
|
id: mapSecondaryIdToTemplateId(template.secondaryId),
|
|
externalId: template.externalId,
|
|
type: template.templateType,
|
|
title: template.title,
|
|
userId: template.userId,
|
|
teamId: template.teamId,
|
|
createdAt: template.createdAt,
|
|
updatedAt: template.updatedAt,
|
|
directLink: template.directLink,
|
|
Field: template.fields.map((field) => ({
|
|
...field,
|
|
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,
|
|
})),
|
|
Recipient: template.recipients,
|
|
})),
|
|
totalPages,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return AppError.toRestAPIError(err);
|
|
}
|
|
}),
|
|
|
|
createDocumentFromTemplate: authenticatedMiddleware(
|
|
async (args, user, team, { logger, metadata }) => {
|
|
const { body, params } = args;
|
|
|
|
logger.info({
|
|
input: {
|
|
templateId: params.templateId,
|
|
},
|
|
});
|
|
|
|
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
|
|
|
|
if (remaining.documents <= 0) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'You have reached the maximum number of documents allowed for this month',
|
|
},
|
|
};
|
|
}
|
|
|
|
const templateId = Number(params.templateId);
|
|
|
|
const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`;
|
|
|
|
const template = await getEnvelopeById({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
type: EnvelopeType.TEMPLATE,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (template.envelopeItems.length !== 1) {
|
|
throw new Error('API V1 does not support templates with multiple documents');
|
|
}
|
|
|
|
// V1 API request schema uses indices for recipients
|
|
// So we remap the recipients to attach the IDs
|
|
const mappedRecipients = body.recipients.map((recipient, index) => {
|
|
const existingRecipient = template.recipients.at(index);
|
|
|
|
if (!existingRecipient) {
|
|
throw new Error('Recipient not found.');
|
|
}
|
|
|
|
return {
|
|
id: existingRecipient.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
signingOrder: recipient.signingOrder,
|
|
role: recipient.role, // You probably shouldn't be able to change the role.
|
|
};
|
|
});
|
|
|
|
const createdEnvelope = await createDocumentFromTemplate({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
externalId: body.externalId || null,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
recipients: mappedRecipients,
|
|
override: {
|
|
...body.meta,
|
|
title: body.title,
|
|
},
|
|
attachments: body.attachments,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
const envelopeItems = await prisma.envelopeItem.findMany({
|
|
where: {
|
|
envelopeId: createdEnvelope.id,
|
|
},
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
});
|
|
|
|
const firstEnvelopeItemData = envelopeItems[0].documentData;
|
|
|
|
if (!firstEnvelopeItemData) {
|
|
throw new Error('Document data not found.');
|
|
}
|
|
|
|
if (body.formValues) {
|
|
const pdf = await getFileServerSide(firstEnvelopeItemData);
|
|
|
|
const prefilled = await insertFormValuesInPdf({
|
|
pdf: Buffer.from(pdf),
|
|
formValues: body.formValues,
|
|
});
|
|
|
|
const newDocumentData = await putPdfFileServerSide({
|
|
name: fileName,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
|
});
|
|
|
|
await prisma.envelopeItem.update({
|
|
where: {
|
|
id: firstEnvelopeItemData.id,
|
|
},
|
|
data: {
|
|
title: body.title || fileName,
|
|
documentDataId: newDocumentData.id,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (body.authOptions || body.formValues) {
|
|
await prisma.envelope.update({
|
|
where: {
|
|
id: createdEnvelope.id,
|
|
},
|
|
data: {
|
|
formValues: body.formValues,
|
|
authOptions: body.authOptions,
|
|
},
|
|
});
|
|
}
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
documentId: mapSecondaryIdToDocumentId(createdEnvelope.secondaryId),
|
|
recipients: createdEnvelope.recipients.map((recipient) => ({
|
|
recipientId: recipient.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
token: recipient.token,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
})),
|
|
},
|
|
};
|
|
},
|
|
),
|
|
|
|
generateDocumentFromTemplate: authenticatedMiddleware(
|
|
async (args, user, team, { logger, metadata }) => {
|
|
const { body, params } = args;
|
|
|
|
logger.info({
|
|
input: {
|
|
templateId: params.templateId,
|
|
},
|
|
});
|
|
|
|
const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id });
|
|
|
|
if (remaining.documents <= 0) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'You have reached the maximum number of documents allowed for this month',
|
|
},
|
|
};
|
|
}
|
|
|
|
const templateId = Number(params.templateId);
|
|
|
|
let envelope: Awaited<ReturnType<typeof createDocumentFromTemplate>> | null = null;
|
|
|
|
try {
|
|
envelope = await createDocumentFromTemplate({
|
|
id: {
|
|
type: 'templateId',
|
|
id: templateId,
|
|
},
|
|
externalId: body.externalId || null,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
recipients: body.recipients,
|
|
prefillFields: body.prefillFields,
|
|
folderId: body.folderId,
|
|
override: {
|
|
title: body.title,
|
|
...body.meta,
|
|
},
|
|
requestMetadata: metadata,
|
|
});
|
|
} catch (err) {
|
|
return AppError.toRestAPIError(err);
|
|
}
|
|
|
|
if (envelope.envelopeItems.length !== 1) {
|
|
throw new Error('API V1 does not support envelopes');
|
|
}
|
|
|
|
const firstEnvelopeDocumentData = await prisma.envelopeItem.findFirstOrThrow({
|
|
where: {
|
|
envelopeId: envelope.id,
|
|
},
|
|
include: {
|
|
documentData: true,
|
|
},
|
|
});
|
|
|
|
if (body.formValues) {
|
|
const fileName = envelope.title.endsWith('.pdf') ? envelope.title : `${envelope.title}.pdf`;
|
|
|
|
const pdf = await getFileServerSide(firstEnvelopeDocumentData.documentData);
|
|
|
|
const prefilled = await insertFormValuesInPdf({
|
|
pdf: Buffer.from(pdf),
|
|
formValues: body.formValues,
|
|
});
|
|
|
|
const newDocumentData = await putPdfFileServerSide({
|
|
name: fileName,
|
|
type: 'application/pdf',
|
|
arrayBuffer: async () => Promise.resolve(prefilled),
|
|
});
|
|
|
|
await prisma.envelope.update({
|
|
where: {
|
|
id: envelope.id,
|
|
},
|
|
data: {
|
|
formValues: body.formValues,
|
|
envelopeItems: {
|
|
update: {
|
|
where: {
|
|
id: firstEnvelopeDocumentData.id,
|
|
},
|
|
data: {
|
|
documentDataId: newDocumentData.id,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
if (body.authOptions) {
|
|
await prisma.envelope.update({
|
|
where: {
|
|
id: envelope.id,
|
|
},
|
|
data: {
|
|
authOptions: body.authOptions,
|
|
},
|
|
});
|
|
}
|
|
|
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
documentId: legacyDocumentId,
|
|
recipients: envelope.recipients.map((recipient) => ({
|
|
recipientId: recipient.id,
|
|
name: recipient.name,
|
|
email: recipient.email,
|
|
token: recipient.token,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
})),
|
|
},
|
|
};
|
|
},
|
|
),
|
|
|
|
sendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId } = args.params;
|
|
const { sendEmail, sendCompletionEmails } = args.body;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const legacyDocumentId = Number(documentId);
|
|
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Document is already complete',
|
|
},
|
|
};
|
|
}
|
|
|
|
const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta);
|
|
|
|
// Update document email settings if sendCompletionEmails is provided
|
|
if (typeof sendCompletionEmails === 'boolean') {
|
|
await updateDocumentMeta({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: envelope.id,
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
emailSettings: {
|
|
...emailSettings,
|
|
documentCompleted: sendCompletionEmails,
|
|
ownerDocumentCompleted: sendCompletionEmails,
|
|
},
|
|
requestMetadata: metadata,
|
|
});
|
|
}
|
|
|
|
const { recipients, ...sentDocument } = await sendDocument({
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: envelope.id,
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
sendEmail,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
message: 'Document sent for signing successfully',
|
|
id: mapSecondaryIdToDocumentId(sentDocument.secondaryId),
|
|
externalId: sentDocument.externalId,
|
|
userId: sentDocument.userId,
|
|
teamId: sentDocument.teamId,
|
|
title: sentDocument.title,
|
|
status: sentDocument.status,
|
|
createdAt: sentDocument.createdAt,
|
|
updatedAt: sentDocument.updatedAt,
|
|
completedAt: sentDocument.completedAt,
|
|
recipients: recipients.map((recipient) => ({
|
|
...recipient,
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`,
|
|
})),
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'An error has occured while sending the document for signing',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
resendDocument: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId } = args.params;
|
|
const { recipients } = args.body;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
try {
|
|
await resendDocument({
|
|
userId: user.id,
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
recipients,
|
|
teamId: team.id,
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
message: 'Document resend successfully initiated',
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'An error has occured while resending the document',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
createRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId } = args.params;
|
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
const legacyDocumentId = Number(documentId);
|
|
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Document is already completed',
|
|
},
|
|
};
|
|
}
|
|
|
|
const recipients = await getRecipientsForDocument({
|
|
documentId: Number(documentId),
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email);
|
|
|
|
if (recipientAlreadyExists) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Recipient already exists',
|
|
},
|
|
};
|
|
}
|
|
|
|
try {
|
|
const { recipients: newRecipients } = await setDocumentRecipients({
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
recipients: [
|
|
...recipients.map((recipient) => ({
|
|
email: recipient.email,
|
|
name: recipient.name,
|
|
role: recipient.role,
|
|
signingOrder: recipient.signingOrder,
|
|
actionAuth: ZRecipientAuthOptionsSchema.parse(recipient.authOptions)?.actionAuth ?? [],
|
|
})),
|
|
{
|
|
email,
|
|
name,
|
|
role,
|
|
signingOrder,
|
|
actionAuth: authOptions?.actionAuth ?? [],
|
|
},
|
|
],
|
|
requestMetadata: metadata,
|
|
});
|
|
|
|
const newRecipient = newRecipients.find((recipient) => recipient.email === email);
|
|
|
|
if (!newRecipient) {
|
|
throw new Error('Recipient not found');
|
|
}
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
...newRecipient,
|
|
documentId: Number(documentId),
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${newRecipient.token}`,
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: 500,
|
|
body: {
|
|
message: 'An error has occured while creating the recipient',
|
|
},
|
|
};
|
|
}
|
|
}),
|
|
|
|
updateRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId, recipientId } = args.params;
|
|
const { name, email, role, authOptions, signingOrder } = args.body;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
recipientId,
|
|
},
|
|
});
|
|
|
|
const legacyDocumentId = Number(documentId);
|
|
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Document is already completed',
|
|
},
|
|
};
|
|
}
|
|
|
|
const updatedRecipient = await updateEnvelopeRecipients({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
id: {
|
|
type: 'envelopeId',
|
|
id: envelope.id,
|
|
},
|
|
recipients: [
|
|
{
|
|
id: Number(recipientId),
|
|
email,
|
|
name,
|
|
role,
|
|
signingOrder,
|
|
actionAuth: authOptions?.actionAuth ?? [],
|
|
},
|
|
],
|
|
requestMetadata: metadata,
|
|
})
|
|
.then(({ recipients }) => recipients[0])
|
|
.catch(null);
|
|
|
|
if (!updatedRecipient) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Recipient not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
...updatedRecipient,
|
|
documentId: Number(documentId),
|
|
signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${updatedRecipient.token}`,
|
|
},
|
|
};
|
|
}),
|
|
|
|
deleteRecipient: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId, recipientId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
recipientId,
|
|
},
|
|
});
|
|
|
|
const deletedRecipient = await deleteEnvelopeRecipient({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
recipientId: Number(recipientId),
|
|
requestMetadata: {
|
|
requestMetadata: metadata.requestMetadata,
|
|
source: 'apiV1',
|
|
auth: 'api',
|
|
auditUser: {
|
|
id: team.id,
|
|
email: team.name,
|
|
name: team.name,
|
|
},
|
|
},
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
...deletedRecipient,
|
|
documentId: Number(documentId),
|
|
signingUrl: '',
|
|
},
|
|
};
|
|
}),
|
|
|
|
createField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
},
|
|
});
|
|
|
|
const fields = Array.isArray(args.body) ? args.body : [args.body];
|
|
|
|
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
teamId: team.id,
|
|
userId: user.id,
|
|
});
|
|
|
|
const envelope = await prisma.envelope.findFirst({
|
|
where: envelopeWhereInput,
|
|
select: {
|
|
id: true,
|
|
secondaryId: true,
|
|
status: true,
|
|
envelopeItems: {
|
|
select: { id: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: { message: 'Document not found' },
|
|
};
|
|
}
|
|
|
|
const firstEnvelopeItemId = envelope.envelopeItems[0].id;
|
|
|
|
if (!firstEnvelopeItemId) {
|
|
throw new Error('Missing envelope item ID');
|
|
}
|
|
|
|
if (envelope.envelopeItems.length !== 1) {
|
|
throw new Error('API V1 does not support multiple documents');
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: { message: 'Document is already completed' },
|
|
};
|
|
}
|
|
|
|
try {
|
|
const createdFields = await prisma.$transaction(async (tx) => {
|
|
return Promise.all(
|
|
fields.map(async (fieldData) => {
|
|
const {
|
|
recipientId,
|
|
type,
|
|
pageNumber,
|
|
pageWidth,
|
|
pageHeight,
|
|
pageX,
|
|
pageY,
|
|
fieldMeta,
|
|
} = fieldData;
|
|
|
|
if (pageNumber <= 0) {
|
|
throw new Error('Invalid page number');
|
|
}
|
|
|
|
const recipient = await prisma.recipient.findFirst({
|
|
where: {
|
|
id: Number(recipientId),
|
|
envelopeId: envelope.id,
|
|
},
|
|
});
|
|
|
|
if (!recipient) {
|
|
throw new Error('Recipient not found');
|
|
}
|
|
|
|
if (recipient.signingStatus === SigningStatus.SIGNED) {
|
|
throw new Error('Recipient has already signed the document');
|
|
}
|
|
|
|
const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(
|
|
type,
|
|
);
|
|
|
|
if (advancedField && !fieldMeta) {
|
|
throw new Error(
|
|
'Field meta is required for this type of field. Please provide the appropriate field meta object.',
|
|
);
|
|
}
|
|
|
|
if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) {
|
|
throw new Error('Field meta type does not match the field type');
|
|
}
|
|
|
|
const result = match(type)
|
|
.with('RADIO', () => ZRadioFieldMeta.safeParse(fieldMeta))
|
|
.with('CHECKBOX', () => ZCheckboxFieldMeta.safeParse(fieldMeta))
|
|
.with('DROPDOWN', () => ZDropdownFieldMeta.safeParse(fieldMeta))
|
|
.with('NUMBER', () => ZNumberFieldMeta.safeParse(fieldMeta))
|
|
.with('TEXT', () => ZTextFieldMeta.safeParse(fieldMeta))
|
|
.with('SIGNATURE', 'INITIALS', 'DATE', 'EMAIL', 'NAME', () => ({
|
|
success: true,
|
|
data: undefined,
|
|
}))
|
|
.with('FREE_SIGNATURE', () => ({
|
|
success: false,
|
|
error: 'FREE_SIGNATURE is not supported',
|
|
data: undefined,
|
|
}))
|
|
.exhaustive();
|
|
|
|
if (!result.success) {
|
|
throw new Error('Field meta parsing failed');
|
|
}
|
|
|
|
const field = await tx.field.create({
|
|
data: {
|
|
envelopeId: envelope.id,
|
|
envelopeItemId: firstEnvelopeItemId,
|
|
recipientId: Number(recipientId),
|
|
type,
|
|
page: pageNumber,
|
|
positionX: pageX,
|
|
positionY: pageY,
|
|
width: pageWidth,
|
|
height: pageHeight,
|
|
customText: '',
|
|
inserted: false,
|
|
fieldMeta: result.data,
|
|
},
|
|
include: {
|
|
recipient: true,
|
|
},
|
|
});
|
|
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: 'FIELD_CREATED',
|
|
envelopeId: envelope.id,
|
|
user: {
|
|
id: team.id ?? user.id,
|
|
email: team?.name ?? user.email,
|
|
name: team ? '' : user.name,
|
|
},
|
|
data: {
|
|
fieldId: field.secondaryId,
|
|
fieldRecipientEmail: field.recipient?.email ?? '',
|
|
fieldRecipientId: recipientId,
|
|
fieldType: field.type,
|
|
},
|
|
requestMetadata: metadata.requestMetadata,
|
|
}),
|
|
});
|
|
|
|
return {
|
|
id: field.id,
|
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
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,
|
|
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
|
|
inserted: field.inserted,
|
|
};
|
|
}),
|
|
);
|
|
});
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
fields: createdFields,
|
|
documentId: Number(documentId),
|
|
},
|
|
};
|
|
} catch (err) {
|
|
return AppError.toRestAPIError(err);
|
|
}
|
|
}),
|
|
|
|
updateField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
const { id: documentId, fieldId } = args.params;
|
|
const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } =
|
|
args.body;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: documentId,
|
|
fieldId,
|
|
},
|
|
});
|
|
|
|
const envelope = await getEnvelopeById({
|
|
id: {
|
|
type: 'documentId',
|
|
id: Number(documentId),
|
|
},
|
|
type: EnvelopeType.DOCUMENT,
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
});
|
|
|
|
if (!envelope) {
|
|
return {
|
|
status: 404,
|
|
body: {
|
|
message: 'Document not found',
|
|
},
|
|
};
|
|
}
|
|
|
|
const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId);
|
|
|
|
const firstEnvelopeItemId = envelope.envelopeItems[0].id;
|
|
|
|
if (!firstEnvelopeItemId) {
|
|
throw new Error('Missing document data');
|
|
}
|
|
|
|
if (envelope.envelopeItems.length > 1) {
|
|
throw new Error('API V1 does not support multiple documents');
|
|
}
|
|
|
|
if (isDocumentCompleted(envelope.status)) {
|
|
return {
|
|
status: 400,
|
|
body: {
|
|
message: 'Document is already completed',
|
|
},
|
|
};
|
|
}
|
|
|
|
const recipient = await prisma.recipient.findFirst({
|
|
where: {
|
|
id: Number(recipientId),
|
|
envelopeId: envelope.id,
|
|
},
|
|
});
|
|
|
|
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 { fields } = await updateEnvelopeFields({
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
id: {
|
|
type: 'documentId',
|
|
id: legacyDocumentId,
|
|
},
|
|
fields: [
|
|
{
|
|
id: Number(fieldId),
|
|
type,
|
|
pageNumber,
|
|
pageX,
|
|
pageY,
|
|
width: pageWidth,
|
|
height: pageHeight,
|
|
fieldMeta: fieldMeta ? ZFieldMetaSchema.parse(fieldMeta) : undefined,
|
|
},
|
|
],
|
|
requestMetadata: {
|
|
requestMetadata: metadata.requestMetadata,
|
|
source: 'apiV1',
|
|
auth: 'api',
|
|
auditUser: {
|
|
id: team.id,
|
|
email: team.name,
|
|
name: team.name,
|
|
},
|
|
},
|
|
});
|
|
|
|
const updatedField = fields[0];
|
|
|
|
return {
|
|
status: 200,
|
|
body: {
|
|
id: updatedField.id,
|
|
documentId: legacyDocumentId,
|
|
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,
|
|
},
|
|
};
|
|
}),
|
|
|
|
deleteField: authenticatedMiddleware(async (args, user, team, { logger, metadata }) => {
|
|
// Note: documentId isn't actually used anywhere, so we just return it.
|
|
const { id: unverifiedDocumentId, fieldId } = args.params;
|
|
|
|
logger.info({
|
|
input: {
|
|
id: unverifiedDocumentId,
|
|
fieldId,
|
|
},
|
|
});
|
|
|
|
const deletedField = await deleteDocumentField({
|
|
fieldId: Number(fieldId),
|
|
userId: user.id,
|
|
teamId: team.id,
|
|
requestMetadata: {
|
|
requestMetadata: metadata.requestMetadata,
|
|
source: 'apiV1',
|
|
auth: 'api',
|
|
auditUser: {
|
|
id: team.id,
|
|
email: team.name,
|
|
name: team.name,
|
|
},
|
|
},
|
|
});
|
|
|
|
const remappedField = {
|
|
id: deletedField.id,
|
|
documentId: Number(unverifiedDocumentId),
|
|
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,
|
|
};
|
|
}),
|
|
});
|