From bf89bc781bee30a7e0422c5d220992f6cb7763a5 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 11 Sep 2025 18:23:38 +1000 Subject: [PATCH] feat: migrate templates and documents to envelope model --- .../growth/get-monthly-completed-document.ts | 11 +- .../dialogs/admin-document-delete-dialog.tsx | 7 +- .../document-move-to-folder-dialog.tsx | 8 +- .../dialogs/document-resend-dialog.tsx | 10 +- .../public-profile-template-manage-dialog.tsx | 4 +- .../dialogs/template-create-dialog.tsx | 2 +- .../template-direct-link-dialog-wrapper.tsx | 3 +- .../dialogs/template-direct-link-dialog.tsx | 8 +- .../template-move-to-folder-dialog.tsx | 8 +- .../embed/authoring/configure-fields-view.tsx | 3 + .../embed/embed-document-signing-page.tsx | 6 +- .../document-signing-auth-provider.tsx | 8 +- .../document-signing-reject-dialog.tsx | 2 +- .../document/document-certificate-qr-view.tsx | 19 +- .../document/document-drop-zone-wrapper.tsx | 2 +- .../document/document-page-view-button.tsx | 3 +- .../document/document-page-view-dropdown.tsx | 5 +- .../document-page-view-information.tsx | 3 +- .../document-page-view-recipients.tsx | 3 +- .../general/document/document-upload.tsx | 2 +- .../template/template-drop-zone-wrapper.tsx | 2 +- .../template-page-view-information.tsx | 3 +- .../template-page-view-recipients.tsx | 3 +- .../tables/admin-dashboard-users-table.tsx | 11 +- ...ettings-public-profile-templates-table.tsx | 3 +- .../templates-table-action-dropdown.tsx | 3 +- .../_authenticated+/admin+/documents.$id.tsx | 36 +- .../t.$teamUrl+/documents.$id._index.tsx | 12 +- .../t.$teamUrl+/documents.$id.edit.tsx | 5 +- .../t.$teamUrl+/documents.$id.logs.tsx | 31 +- .../t.$teamUrl+/settings.public-profile.tsx | 3 +- .../_internal+/[__htmltopdf]+/audit-log.tsx | 36 +- .../_internal+/[__htmltopdf]+/certificate.tsx | 38 +- .../_recipient+/sign.$token+/complete.tsx | 3 +- .../_recipient+/sign.$token+/waiting.tsx | 14 +- apps/remix/app/routes/_share+/share.$slug.tsx | 6 +- .../v1+/authoring+/document.edit.$id.tsx | 5 +- apps/remix/server/redirects.ts | 19 +- packages/api/v1/implementation.ts | 912 +++-- packages/api/v1/schema.ts | 4 +- .../e2e/api/v1/document-sending.spec.ts | 19 +- .../e2e/api/v1/template-field-prefill.spec.ts | 79 +- .../v1/test-unauthorized-api-access.spec.ts | 1509 ++++--- .../e2e/api/v2/template-field-prefill.spec.ts | 79 +- .../v2/test-unauthorized-api-access.spec.ts | 3535 ++++++++++++----- .../e2e/document-auth/access-auth.spec.ts | 4 +- .../next-recipient-dictation.spec.ts | 8 +- .../e2e/document-flow/settings-step.spec.ts | 9 +- .../e2e/document-flow/signers-step.spec.ts | 3 +- .../document-flow/stepper-component.spec.ts | 55 +- .../test-unauthorized-document-access.spec.ts | 29 +- .../include-document-certificate.spec.ts | 54 +- .../e2e/folders/team-account-folders.spec.ts | 6 +- .../organisation-team-preferences.spec.ts | 6 +- .../e2e/teams/team-documents.spec.ts | 11 +- .../e2e/teams/team-signature-settings.spec.ts | 15 +- .../template-settings-step.spec.ts | 22 +- .../template-signers-step.spec.ts | 5 +- .../create-document-from-template.spec.ts | 107 +- .../e2e/templates/direct-templates.spec.ts | 3 +- .../test-unauthorized-template-access.spec.ts | 13 +- packages/ee/server-only/limits/server.ts | 8 +- .../client-only/hooks/use-copy-share-link.ts | 6 +- .../hooks/use-field-page-coords.ts | 4 +- packages/lib/constants/teams.ts | 12 +- .../send-document-cancelled-emails.handler.ts | 27 +- .../send-recipient-signed-email.handler.ts | 30 +- .../emails/send-rejection-emails.handler.ts | 39 +- .../emails/send-signing-email.handler.ts | 38 +- .../internal/bulk-send-template.handler.ts | 12 +- .../internal/seal-document.handler.ts | 87 +- ...l-documents.ts => admin-find-documents.ts} | 24 +- .../admin-super-delete-document.ts} | 35 +- .../server-only/admin/get-documents-stats.ts | 7 +- .../server-only/admin/get-entire-document.ts | 30 +- .../server-only/admin/get-signing-volume.ts | 13 +- .../document-meta/upsert-document-meta.ts | 61 +- .../document/complete-document-with-token.ts | 88 +- .../server-only/document/create-document.ts | 171 - .../server-only/document/delete-document.ts | 79 +- .../document/duplicate-document-by-id.ts | 184 +- .../document/find-document-audit-logs.ts | 20 +- .../server-only/document/find-documents.ts | 37 +- .../document/get-document-by-access-token.ts | 48 +- .../document/get-document-by-id.ts | 156 - .../document/get-document-by-token.ts | 86 +- .../get-document-certificate-audit-logs.ts | 6 +- .../get-document-with-details-by-id.ts | 81 +- ...-recipient-or-sender-by-share-link-slug.ts | 6 +- .../lib/server-only/document/get-stats.ts | 41 +- .../document/is-recipient-authorized.ts | 7 +- .../document/reject-document-with-token.ts | 26 +- .../server-only/document/resend-document.ts | 69 +- .../lib/server-only/document/seal-document.ts | 130 +- .../document/search-documents-with-keyword.ts | 42 +- .../document/send-completed-email.ts | 97 +- .../server-only/document/send-delete-email.ts | 21 +- .../{send-document.tsx => send-document.ts} | 99 +- .../document/send-pending-email.ts | 25 +- .../server-only/document/update-document.ts | 164 +- .../document/validate-field-auth.ts | 6 +- .../server-only/document/viewed-document.ts | 43 +- .../create-envelope.ts} | 221 +- .../envelope/get-envelope-by-id.ts | 168 + .../lib/server-only/envelope/increment-id.ts | 39 + .../field/create-document-fields.ts | 126 - .../field/create-envelope-fields.ts | 167 + .../lib/server-only/field/create-field.ts | 136 - .../field/create-template-fields.ts | 101 - .../field/delete-document-field.ts | 40 +- .../lib/server-only/field/delete-field.ts | 78 - .../field/delete-template-field.ts | 22 +- .../field/get-completed-fields-for-token.ts | 6 +- .../lib/server-only/field/get-field-by-id.ts | 49 +- .../field/get-fields-for-document.ts | 41 - .../server-only/field/get-fields-for-token.ts | 8 +- .../field/remove-signed-field-with-token.ts | 12 +- .../field/set-fields-for-document.ts | 67 +- .../field/set-fields-for-template.ts | 57 +- .../field/sign-field-with-token.ts | 22 +- .../field/update-document-fields.ts | 28 +- .../lib/server-only/field/update-field.ts | 119 - .../field/update-template-fields.ts | 25 +- .../lib/server-only/folder/create-folder.ts | 7 +- .../lib/server-only/folder/delete-folder.ts | 16 +- .../lib/server-only/folder/find-folders.ts | 32 +- .../folder/move-document-to-folder.ts | 92 - .../folder/move-template-to-folder.ts | 63 - .../profile/get-public-profile-by-url.ts | 41 +- .../recipient/create-document-recipients.ts | 28 +- .../recipient/create-template-recipients.ts | 19 +- .../recipient/delete-document-recipient.ts | 41 +- .../server-only/recipient/delete-recipient.ts | 88 - .../recipient/delete-template-recipient.ts | 36 +- .../recipient/get-is-recipient-turn.ts | 9 +- .../recipient/get-next-pending-recipient.ts | 9 +- .../recipient/get-recipient-by-email.ts | 21 - .../recipient/get-recipient-by-id-v1-api.ts | 21 - .../recipient/get-recipient-by-id.ts | 35 +- .../recipient/get-recipient-suggestions.ts | 11 +- .../recipient/get-recipients-for-assistant.ts | 4 +- .../recipient/get-recipients-for-document.ts | 14 +- .../recipient/set-document-recipients.ts | 51 +- .../recipient/set-template-recipients.ts | 48 +- .../recipient/update-document-recipients.ts | 34 +- .../recipient/update-template-recipients.ts | 27 +- .../share/create-or-get-share-link.ts | 31 +- .../share/get-share-link-by-slug.ts | 13 - .../lib/server-only/team/get-member-roles.ts | 9 +- .../create-document-from-direct-template.ts | 159 +- .../create-document-from-template-legacy.ts | 183 - .../template/create-document-from-template.ts | 142 +- .../template/create-template-direct-link.ts | 48 +- .../server-only/template/create-template.ts | 112 - .../template/delete-template-direct-link.ts | 27 +- .../server-only/template/delete-template.ts | 17 +- .../template/duplicate-template.ts | 175 +- .../server-only/template/find-templates.ts | 77 +- .../get-template-by-direct-link-token.ts | 50 +- .../template/get-template-by-id.ts | 63 +- .../template/toggle-template-direct-link.ts | 35 +- .../server-only/template/update-template.ts | 101 +- packages/lib/server-only/user/delete-user.ts | 5 +- .../lib/server-only/user/get-all-users.ts | 21 +- .../webhooks/trigger/generate-sample-data.ts | 1 - .../webhooks/zapier/list-documents.ts | 103 +- packages/lib/types/document-audit-logs.ts | 2 +- packages/lib/types/document.ts | 30 +- packages/lib/types/field.ts | 8 +- packages/lib/types/recipient.ts | 23 +- packages/lib/types/template.ts | 12 +- packages/lib/types/webhook-payload.ts | 57 +- packages/lib/universal/id.ts | 4 + packages/lib/utils/document-audit-logs.ts | 15 +- packages/lib/utils/document-auth.ts | 4 +- packages/lib/utils/document-visibility.ts | 20 - packages/lib/utils/document.ts | 92 +- packages/lib/utils/envelope.ts | 141 + .../mask-recipient-tokens-for-document.ts | 8 +- packages/lib/utils/teams.ts | 21 +- packages/lib/utils/templates.ts | 26 +- packages/prisma/schema.prisma | 220 +- packages/prisma/seed/documents.ts | 240 +- packages/prisma/seed/initial-seed.ts | 45 +- packages/prisma/seed/templates.ts | 122 +- .../prisma/types/document-legacy-schema.ts | 53 + .../prisma/types/document-with-recipient.ts | 6 +- .../prisma/types/template-legacy-schema.ts | 35 + .../server/admin-router/delete-document.ts | 8 +- .../admin-router/delete-document.types.ts | 2 +- .../server/admin-router/find-documents.ts | 10 +- .../server/admin-router/reseal-document.ts | 22 +- .../admin-router/reseal-document.types.ts | 2 +- .../create-document-temporary.ts | 44 +- .../server/document-router/create-document.ts | 26 +- .../document-router/create-document.types.ts | 2 +- .../document-router/distribute-document.ts | 19 +- .../download-document-audit-logs.ts | 16 +- .../download-document-certificate.ts | 16 +- .../document-router/download-document.ts | 43 +- .../document-router/duplicate-document.ts | 5 +- .../find-documents-internal.ts | 2 + .../server/document-router/find-documents.ts | 6 +- .../trpc/server/document-router/find-inbox.ts | 25 +- .../document-router/get-document-by-token.ts | 26 +- .../server/document-router/get-document.ts | 5 +- .../server/document-router/get-inbox-count.ts | 5 +- .../trpc/server/document-router/router.ts | 2 + .../server/document-router/share-document.ts | 28 + .../document-router/share-document.types.ts | 16 + .../server/document-router/update-document.ts | 23 +- .../document-router/update-document.types.ts | 1 + .../apply-multi-sign-signature.ts | 2 +- .../create-embedding-document.ts | 20 +- .../create-embedding-template.ts | 65 +- .../get-multi-sign-document.types.ts | 2 - .../update-embedding-document.ts | 9 +- .../update-embedding-template.ts | 5 +- packages/trpc/server/field-router/router.ts | 37 +- packages/trpc/server/folder-router/router.ts | 94 - packages/trpc/server/folder-router/schema.ts | 12 - .../trpc/server/recipient-router/router.ts | 41 +- packages/trpc/server/router.ts | 2 - .../get-document-internal-url-for-qr-code.ts | 50 - ...document-internal-url-for-qr-code.types.ts | 15 - .../trpc/server/share-link-router/router.ts | 33 - .../trpc/server/share-link-router/schema.ts | 10 - .../trpc/server/template-router/router.ts | 118 +- .../trpc/server/template-router/schema.ts | 9 +- .../document/document-read-only-fields.tsx | 5 +- .../document/document-share-button.tsx | 2 +- .../ui/components/field/field-tooltip.tsx | 5 +- .../primitives/document-flow/add-fields.tsx | 2 +- .../template-flow/add-template-fields.tsx | 2 +- 234 files changed, 8677 insertions(+), 6054 deletions(-) rename packages/lib/server-only/admin/{get-all-documents.ts => admin-find-documents.ts} (64%) rename packages/lib/server-only/{document/super-delete-document.ts => admin/admin-super-delete-document.ts} (81%) delete mode 100644 packages/lib/server-only/document/create-document.ts delete mode 100644 packages/lib/server-only/document/get-document-by-id.ts rename packages/lib/server-only/document/{send-document.tsx => send-document.ts} (69%) rename packages/lib/server-only/{document/create-document-v2.ts => envelope/create-envelope.ts} (53%) create mode 100644 packages/lib/server-only/envelope/get-envelope-by-id.ts create mode 100644 packages/lib/server-only/envelope/increment-id.ts delete mode 100644 packages/lib/server-only/field/create-document-fields.ts create mode 100644 packages/lib/server-only/field/create-envelope-fields.ts delete mode 100644 packages/lib/server-only/field/create-field.ts delete mode 100644 packages/lib/server-only/field/create-template-fields.ts delete mode 100644 packages/lib/server-only/field/delete-field.ts delete mode 100644 packages/lib/server-only/field/get-fields-for-document.ts delete mode 100644 packages/lib/server-only/field/update-field.ts delete mode 100644 packages/lib/server-only/folder/move-document-to-folder.ts delete mode 100644 packages/lib/server-only/folder/move-template-to-folder.ts delete mode 100644 packages/lib/server-only/recipient/delete-recipient.ts delete mode 100644 packages/lib/server-only/recipient/get-recipient-by-email.ts delete mode 100644 packages/lib/server-only/recipient/get-recipient-by-id-v1-api.ts delete mode 100644 packages/lib/server-only/share/get-share-link-by-slug.ts delete mode 100644 packages/lib/server-only/template/create-document-from-template-legacy.ts delete mode 100644 packages/lib/server-only/template/create-template.ts delete mode 100644 packages/lib/utils/document-visibility.ts create mode 100644 packages/lib/utils/envelope.ts create mode 100644 packages/prisma/types/document-legacy-schema.ts create mode 100644 packages/prisma/types/template-legacy-schema.ts create mode 100644 packages/trpc/server/document-router/share-document.ts create mode 100644 packages/trpc/server/document-router/share-document.types.ts delete mode 100644 packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts delete mode 100644 packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts delete mode 100644 packages/trpc/server/share-link-router/router.ts delete mode 100644 packages/trpc/server/share-link-router/schema.ts diff --git a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts index f429b0a54..808d7259d 100644 --- a/apps/openpage-api/lib/growth/get-monthly-completed-document.ts +++ b/apps/openpage-api/lib/growth/get-monthly-completed-document.ts @@ -1,4 +1,4 @@ -import { DocumentStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { DateTime } from 'luxon'; import { kyselyPrisma, sql } from '@documenso/prisma'; @@ -7,18 +7,19 @@ import { addZeroMonth } from '../add-zero-month'; export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => { const qb = kyselyPrisma.$kysely - .selectFrom('Document') + .selectFrom('Envelope') .select(({ fn }) => [ - fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']).as('month'), + fn('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']).as('month'), fn.count('id').as('count'), fn .sum(fn.count('id')) // Feels like a bug in the Kysely extension but I just can not do this orderBy in a type-safe manner // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any - .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Document.updatedAt']) as any)) + .over((ob) => ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'Envelope.updatedAt']) as any)) .as('cume_count'), ]) - .where(() => sql`"Document"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`) + .where(() => sql`"Envelope"."status" = ${DocumentStatus.COMPLETED}::"DocumentStatus"`) + .where(() => sql`"Envelope"."type" = ${EnvelopeType.DOCUMENT}::"EnvelopeType"`) .groupBy('month') .orderBy('month', 'desc') .limit(12); diff --git a/apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx b/apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx index 9f82d8551..aee9167cc 100644 --- a/apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx @@ -3,7 +3,6 @@ import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document } from '@prisma/client'; import { useNavigate } from 'react-router'; import { trpc } from '@documenso/trpc/react'; @@ -22,10 +21,10 @@ import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; export type AdminDocumentDeleteDialogProps = { - document: Document; + envelopeId: string; }; -export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialogProps) => { +export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDialogProps) => { const { _ } = useLingui(); const { toast } = useToast(); @@ -42,7 +41,7 @@ export const AdminDocumentDeleteDialog = ({ document }: AdminDocumentDeleteDialo return; } - await deleteDocument({ id: document.id, reason }); + await deleteDocument({ id: envelopeId, reason }); toast({ title: _(msg`Document deleted`), diff --git a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx index 401fb3529..c4c85c051 100644 --- a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx @@ -81,7 +81,7 @@ export const DocumentMoveToFolderDialog = ({ }, ); - const { mutateAsync: moveDocumentToFolder } = trpc.folder.moveDocumentToFolder.useMutation(); + const { mutateAsync: updateDocument } = trpc.document.update.useMutation(); useEffect(() => { if (!open) { @@ -94,9 +94,11 @@ export const DocumentMoveToFolderDialog = ({ const onSubmit = async (data: TMoveDocumentFormSchema) => { try { - await moveDocumentToFolder({ + await updateDocument({ documentId, - folderId: data.folderId ?? null, + data: { + folderId: data.folderId ?? null, + }, }); const documentsPath = formatDocumentsPath(team.url); diff --git a/apps/remix/app/components/dialogs/document-resend-dialog.tsx b/apps/remix/app/components/dialogs/document-resend-dialog.tsx index d93f29e84..d8c0a73ee 100644 --- a/apps/remix/app/components/dialogs/document-resend-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-resend-dialog.tsx @@ -4,15 +4,15 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { type Recipient, SigningStatus } from '@prisma/client'; +import { type Recipient, SigningStatus, type Team, type User } from '@prisma/client'; import { History } from 'lucide-react'; import { useForm, useWatch } from 'react-hook-form'; import * as z from 'zod'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; -import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document'; import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; @@ -43,7 +43,11 @@ import { StackAvatar } from '../general/stack-avatar'; const FORM_ID = 'resend-email'; export type DocumentResendDialogProps = { - document: TDocumentRow; + document: Pick & { + user: Pick; + recipients: Recipient[]; + team: Pick | null; + }; recipients: Recipient[]; }; diff --git a/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx b/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx index bcd624290..64b2cb7c8 100644 --- a/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx +++ b/apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx @@ -4,14 +4,14 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Plural, Trans } from '@lingui/react/macro'; -import type { Template, TemplateDirectLink } from '@prisma/client'; -import { TemplateType } from '@prisma/client'; +import { type TemplateDirectLink, TemplateType } from '@prisma/client'; import type * as DialogPrimitive from '@radix-ui/react-dialog'; import { CheckCircle2Icon, CircleIcon } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { P, match } from 'ts-pattern'; import { z } from 'zod'; +import { type Template } from '@documenso/prisma/types/template-legacy-schema'; import { trpc } from '@documenso/trpc/react'; import { MAX_TEMPLATE_PUBLIC_DESCRIPTION_LENGTH, diff --git a/apps/remix/app/components/dialogs/template-create-dialog.tsx b/apps/remix/app/components/dialogs/template-create-dialog.tsx index 1e05dadba..fb89c1d2e 100644 --- a/apps/remix/app/components/dialogs/template-create-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-create-dialog.tsx @@ -54,7 +54,7 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) => try { const response = await putPdfFile(file); - const { id } = await createTemplate({ + const { legacyTemplateId: id } = await createTemplate({ title: file.name, templateDocumentDataId: response.id, folderId: folderId, diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx index 60ff06715..7538daf48 100644 --- a/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx +++ b/apps/remix/app/components/dialogs/template-direct-link-dialog-wrapper.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import type { Recipient, Template, TemplateDirectLink } from '@prisma/client'; +import type { Recipient, TemplateDirectLink } from '@prisma/client'; import { LinkIcon } from 'lucide-react'; +import type { Template } from '@documenso/prisma/types/template-legacy-schema'; import { Button } from '@documenso/ui/primitives/button'; import { TemplateDirectLinkDialog } from '~/components/dialogs/template-direct-link-dialog'; diff --git a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx index d7e60b512..3b178b36d 100644 --- a/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-direct-link-dialog.tsx @@ -3,12 +3,7 @@ import { useEffect, useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { - type Recipient, - RecipientRole, - type Template, - type TemplateDirectLink, -} from '@prisma/client'; +import { type Recipient, RecipientRole, type TemplateDirectLink } from '@prisma/client'; import { CircleDotIcon, CircleIcon, ClipboardCopyIcon, InfoIcon, LoaderIcon } from 'lucide-react'; import { Link, useRevalidator } from 'react-router'; import { P, match } from 'ts-pattern'; @@ -20,6 +15,7 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { DIRECT_TEMPLATE_DOCUMENTATION } from '@documenso/lib/constants/template'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; +import type { Template } from '@documenso/prisma/types/template-legacy-schema'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; diff --git a/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx index 406b56035..d6ccfa07d 100644 --- a/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +++ b/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx @@ -83,7 +83,7 @@ export function TemplateMoveToFolderDialog({ }, ); - const { mutateAsync: moveTemplateToFolder } = trpc.folder.moveTemplateToFolder.useMutation(); + const { mutateAsync: updateTemplate } = trpc.template.updateTemplate.useMutation(); useEffect(() => { if (!isOpen) { @@ -96,9 +96,11 @@ export function TemplateMoveToFolderDialog({ const onSubmit = async (data: TMoveTemplateFormSchema) => { try { - await moveTemplateToFolder({ + await updateTemplate({ templateId, - folderId: data.folderId ?? null, + data: { + folderId: data.folderId ?? null, + }, }); toast({ diff --git a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx index 21faee750..42fbc0516 100644 --- a/apps/remix/app/components/embed/authoring/configure-fields-view.tsx +++ b/apps/remix/app/components/embed/authoring/configure-fields-view.tsx @@ -118,6 +118,9 @@ export const ConfigureFieldsView = ({ sendStatus: signer.disabled ? SendStatus.SENT : SendStatus.NOT_SENT, readStatus: signer.disabled ? ReadStatus.OPENED : ReadStatus.NOT_OPENED, signingStatus: signer.disabled ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED, + + // Todo: Envelopes - Dummy data + envelopeId: '', })); }, [configData.signers]); diff --git a/apps/remix/app/components/embed/embed-document-signing-page.tsx b/apps/remix/app/components/embed/embed-document-signing-page.tsx index bffa0b457..f6c5f4cf8 100644 --- a/apps/remix/app/components/embed/embed-document-signing-page.tsx +++ b/apps/remix/app/components/embed/embed-document-signing-page.tsx @@ -15,12 +15,14 @@ import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; -import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import { trpc } from '@documenso/trpc/react'; -import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; +import { + type DocumentField, + DocumentReadOnlyFields, +} from '@documenso/ui/components/document/document-read-only-fields'; import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip'; import { Button } from '@documenso/ui/primitives/button'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; diff --git a/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx b/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx index c3b1be53e..113ace1a0 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-auth-provider.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; -import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client'; +import { type Envelope, FieldType, type Passkey, type Recipient } from '@prisma/client'; import type { SessionUser } from '@documenso/auth/server/lib/session/session'; import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth'; @@ -26,9 +26,9 @@ type PasskeyData = { export type DocumentSigningAuthContextValue = { executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise; - documentAuthOptions: Document['authOptions']; + documentAuthOptions: Envelope['authOptions']; documentAuthOption: TDocumentAuthOptions; - setDocumentAuthOptions: (_value: Document['authOptions']) => void; + setDocumentAuthOptions: (_value: Envelope['authOptions']) => void; recipient: Recipient; recipientAuthOption: TRecipientAuthOptions; setRecipient: (_value: Recipient) => void; @@ -61,7 +61,7 @@ export const useRequiredDocumentSigningAuthContext = () => { }; export interface DocumentSigningAuthProviderProps { - documentAuthOptions: Document['authOptions']; + documentAuthOptions: Envelope['authOptions']; recipient: Recipient; user?: SessionUser | null; children: React.ReactNode; diff --git a/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx b/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx index baed7a7c5..4583d2430 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx @@ -3,12 +3,12 @@ import { useEffect, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { msg } from '@lingui/core/macro'; import { Trans } from '@lingui/react/macro'; -import type { Document } from '@prisma/client'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router'; import { useSearchParams } from 'react-router'; import { z } from 'zod'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { diff --git a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx index 20d13a007..aa17f75d8 100644 --- a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx +++ b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx @@ -4,6 +4,7 @@ import { Trans } from '@lingui/react/macro'; import type { DocumentData } from '@prisma/client'; import { DateTime } from 'luxon'; +import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -23,6 +24,7 @@ export type DocumentCertificateQRViewProps = { title: string; documentData: DocumentData; password?: string | null; + documentTeamUrl: string; recipientCount?: number; completedDate?: Date; }; @@ -32,29 +34,30 @@ export const DocumentCertificateQRView = ({ title, documentData, password, + documentTeamUrl, recipientCount = 0, completedDate, }: DocumentCertificateQRViewProps) => { - const { data: documentUrl } = trpc.shareLink.getDocumentInternalUrlForQRCode.useQuery({ + const { data: documentViaUser } = trpc.document.get.useQuery({ documentId, }); - const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentUrl); + const [isDialogOpen, setIsDialogOpen] = useState(() => !!documentViaUser); const formattedDate = completedDate ? DateTime.fromJSDate(completedDate).toLocaleString(DateTime.DATETIME_MED) : ''; useEffect(() => { - if (documentUrl) { + if (documentViaUser) { setIsDialogOpen(true); } - }, [documentUrl]); + }, [documentViaUser]); return (
{/* Dialog for internal document link */} - {documentUrl && ( + {documentViaUser && ( @@ -72,7 +75,11 @@ export const DocumentCertificateQRView = ({ diff --git a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx index 84b1dfc4a..117e86873 100644 --- a/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx +++ b/apps/remix/app/components/general/document/document-drop-zone-wrapper.tsx @@ -64,7 +64,7 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon const response = await putPdfFile(file); - const { id } = await createDocument({ + const { legacyDocumentId: id } = await createDocument({ title: file.name, documentDataId: response.id, timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field. diff --git a/apps/remix/app/components/general/document/document-page-view-button.tsx b/apps/remix/app/components/general/document/document-page-view-button.tsx index e5fea4d2b..15be51b4f 100644 --- a/apps/remix/app/components/general/document/document-page-view-button.tsx +++ b/apps/remix/app/components/general/document/document-page-view-button.tsx @@ -1,7 +1,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document, Recipient, Team, User } from '@prisma/client'; +import type { Recipient, Team, User } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react'; import { Link } from 'react-router'; @@ -11,6 +11,7 @@ import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import { trpc as trpcClient } from '@documenso/trpc/client'; import { Button } from '@documenso/ui/primitives/button'; import { useToast } from '@documenso/ui/primitives/use-toast'; diff --git a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx index 326c7553c..542dd6b86 100644 --- a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx +++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document, Recipient, Team, User } from '@prisma/client'; +import type { Recipient, Team, User } from '@prisma/client'; import { DocumentStatus } from '@prisma/client'; import { Copy, @@ -19,6 +19,7 @@ import { Link, useNavigate } from 'react-router'; import { downloadPDF } from '@documenso/lib/client-only/download-pdf'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import type { TDocument } from '@documenso/lib/types/document'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { trpc as trpcClient } from '@documenso/trpc/client'; @@ -39,7 +40,7 @@ import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/d import { useCurrentTeam } from '~/providers/team'; export type DocumentPageViewDropdownProps = { - document: Document & { + document: TDocument & { user: Pick; recipients: Recipient[]; team: Pick | null; diff --git a/apps/remix/app/components/general/document/document-page-view-information.tsx b/apps/remix/app/components/general/document/document-page-view-information.tsx index 6ca4d784c..05918688b 100644 --- a/apps/remix/app/components/general/document/document-page-view-information.tsx +++ b/apps/remix/app/components/general/document/document-page-view-information.tsx @@ -3,10 +3,11 @@ import { useMemo } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Document, Recipient, User } from '@prisma/client'; +import type { Recipient, User } from '@prisma/client'; import { DateTime } from 'luxon'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; export type DocumentPageViewInformationProps = { userId: number; diff --git a/apps/remix/app/components/general/document/document-page-view-recipients.tsx b/apps/remix/app/components/general/document/document-page-view-recipients.tsx index 2e413a8ad..6df50a21e 100644 --- a/apps/remix/app/components/general/document/document-page-view-recipients.tsx +++ b/apps/remix/app/components/general/document/document-page-view-recipients.tsx @@ -2,7 +2,7 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; -import type { Document, Recipient } from '@prisma/client'; +import type { Recipient } from '@prisma/client'; import { AlertTriangle, CheckIcon, @@ -19,6 +19,7 @@ import { match } from 'ts-pattern'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { formatSigningLink } from '@documenso/lib/utils/recipients'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button'; import { SignatureIcon } from '@documenso/ui/icons/signature'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; diff --git a/apps/remix/app/components/general/document/document-upload.tsx b/apps/remix/app/components/general/document/document-upload.tsx index b86a12ecc..2f5b05d7c 100644 --- a/apps/remix/app/components/general/document/document-upload.tsx +++ b/apps/remix/app/components/general/document/document-upload.tsx @@ -75,7 +75,7 @@ export const DocumentUploadDropzone = ({ className }: DocumentUploadDropzoneProp const response = await putPdfFile(file); - const { id } = await createDocument({ + const { legacyDocumentId: id } = await createDocument({ title: file.name, documentDataId: response.id, timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field. diff --git a/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx index dbca5a8ea..c0942f693 100644 --- a/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx +++ b/apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx @@ -42,7 +42,7 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon const documentData = await putPdfFile(file); - const { id } = await createTemplate({ + const { legacyTemplateId: id } = await createTemplate({ title: file.name, templateDocumentDataId: documentData.id, folderId: folderId ?? undefined, diff --git a/apps/remix/app/components/general/template/template-page-view-information.tsx b/apps/remix/app/components/general/template/template-page-view-information.tsx index 7d0d662c5..097c39989 100644 --- a/apps/remix/app/components/general/template/template-page-view-information.tsx +++ b/apps/remix/app/components/general/template/template-page-view-information.tsx @@ -3,10 +3,11 @@ import { useMemo } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Template, User } from '@prisma/client'; +import type { User } from '@prisma/client'; import { DateTime } from 'luxon'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; +import type { Template } from '@documenso/prisma/types/template-legacy-schema'; export type TemplatePageViewInformationProps = { userId: number; diff --git a/apps/remix/app/components/general/template/template-page-view-recipients.tsx b/apps/remix/app/components/general/template/template-page-view-recipients.tsx index 3896baf11..97331cd74 100644 --- a/apps/remix/app/components/general/template/template-page-view-recipients.tsx +++ b/apps/remix/app/components/general/template/template-page-view-recipients.tsx @@ -1,13 +1,14 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Recipient, Template } from '@prisma/client'; +import type { Recipient } from '@prisma/client'; import { PenIcon, PlusIcon } from 'lucide-react'; import { Link } from 'react-router'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; +import type { Template } from '@documenso/prisma/types/template-legacy-schema'; import { AvatarWithText } from '@documenso/ui/primitives/avatar'; export type TemplatePageViewRecipientsProps = { diff --git a/apps/remix/app/components/tables/admin-dashboard-users-table.tsx b/apps/remix/app/components/tables/admin-dashboard-users-table.tsx index 6c3919b4c..c409f7d2c 100644 --- a/apps/remix/app/components/tables/admin-dashboard-users-table.tsx +++ b/apps/remix/app/components/tables/admin-dashboard-users-table.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; -import type { Document, Role, Subscription } from '@prisma/client'; +import type { Role, Subscription } from '@prisma/client'; import { Edit, Loader } from 'lucide-react'; import { Link } from 'react-router'; @@ -20,7 +20,7 @@ type UserData = { email: string; roles: Role[]; subscriptions?: SubscriptionLite[] | null; - documents: DocumentLite[]; + documentCount: number; }; type SubscriptionLite = Pick< @@ -28,8 +28,6 @@ type SubscriptionLite = Pick< 'id' | 'status' | 'planId' | 'priceId' | 'createdAt' | 'periodEnd' >; -type DocumentLite = Pick; - type AdminDashboardUsersTableProps = { users: UserData[]; totalPages: number; @@ -74,10 +72,7 @@ export const AdminDashboardUsersTable = ({ }, { header: _(msg`Documents`), - accessorKey: 'documents', - cell: ({ row }) => { - return
{row.original.documents?.length}
; - }, + accessorKey: 'documentCount', }, { header: '', diff --git a/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx b/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx index d3c89cdab..499753133 100644 --- a/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx +++ b/apps/remix/app/components/tables/settings-public-profile-templates-table.tsx @@ -3,8 +3,7 @@ import { useMemo, useState } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { TemplateDirectLink } from '@prisma/client'; -import { TemplateType } from '@prisma/client'; +import { type TemplateDirectLink, TemplateType } from '@prisma/client'; import { EditIcon, FileIcon, LinkIcon, MoreHorizontalIcon, Trash2Icon } from 'lucide-react'; import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard'; diff --git a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx index 91f8ec9b2..a03495e91 100644 --- a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx +++ b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx @@ -1,11 +1,12 @@ import { useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import type { Recipient, Template, TemplateDirectLink } from '@prisma/client'; +import type { Recipient, TemplateDirectLink } from '@prisma/client'; import { Copy, Edit, FolderIcon, MoreHorizontal, Share2Icon, Trash2, Upload } from 'lucide-react'; import { Link } from 'react-router'; import { useSession } from '@documenso/lib/client-only/providers/session'; +import type { Template } from '@documenso/prisma/types/template-legacy-schema'; import { DropdownMenu, DropdownMenuContent, diff --git a/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx index 623ac0938..1812016ef 100644 --- a/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx +++ b/apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx @@ -1,11 +1,11 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { SigningStatus } from '@prisma/client'; +import { EnvelopeType, SigningStatus } from '@prisma/client'; import { DateTime } from 'luxon'; import { Link, redirect } from 'react-router'; -import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; +import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document'; import { trpc } from '@documenso/trpc/react'; import { Accordion, @@ -36,13 +36,19 @@ export async function loader({ params }: Route.LoaderArgs) { throw redirect('/admin/documents'); } - const document = await getEntireDocument({ id }); + const envelope = await unsafeGetEntireEnvelope({ + id: { + type: 'documentId', + id, + }, + type: EnvelopeType.DOCUMENT, + }); - return { document }; + return { envelope }; } export default function AdminDocumentDetailsPage({ loaderData }: Route.ComponentProps) { - const { document } = loaderData; + const { envelope } = loaderData; const { _, i18n } = useLingui(); const { toast } = useToast(); @@ -68,11 +74,11 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
-

{document.title}

- +

{envelope.title}

+
- {document.deletedAt && ( + {envelope.deletedAt && ( Deleted @@ -81,11 +87,11 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
- Created on: {i18n.date(document.createdAt, DateTime.DATETIME_MED)} + Created on: {i18n.date(envelope.createdAt, DateTime.DATETIME_MED)}
- Last updated at: {i18n.date(document.updatedAt, DateTime.DATETIME_MED)} + Last updated at: {i18n.date(envelope.updatedAt, DateTime.DATETIME_MED)}
@@ -102,12 +108,12 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component @@ -123,7 +129,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component @@ -136,7 +142,7 @@ export default function AdminDocumentDetailsPage({ loaderData }: Route.Component
- {document.recipients.map((recipient) => ( + {envelope.recipients.map((recipient) => ( - {document && } + {envelope && }
); } diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx index 84c9c6883..01d34eef5 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id._index.tsx @@ -12,7 +12,10 @@ import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { logDocumentAccess } from '@documenso/lib/utils/logger'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; -import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields'; +import { + DocumentReadOnlyFields, + mapFieldsWithRecipients, +} from '@documenso/ui/components/document/document-read-only-fields'; import { Badge } from '@documenso/ui/primitives/badge'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; @@ -54,7 +57,10 @@ export async function loader({ params, request }: Route.LoaderArgs) { } const document = await getDocumentWithDetailsById({ - documentId, + id: { + type: 'documentId', + id: documentId, + }, userId: user.id, teamId: team.id, }).catch(() => null); @@ -171,7 +177,7 @@ export default function DocumentPage() { {document.status !== DocumentStatus.COMPLETED && ( null); diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx index b27ded2bb..4f582bde0 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/documents.$id.logs.tsx @@ -2,15 +2,16 @@ import type { MessageDescriptor } from '@lingui/core'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import type { Recipient } from '@prisma/client'; +import { EnvelopeType, type Recipient } from '@prisma/client'; import { ChevronLeft } from 'lucide-react'; import { DateTime } from 'luxon'; import { Link, redirect } from 'react-router'; import { getSession } from '@documenso/auth/server/lib/utils/get-session'; -import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { logDocumentAccess } from '@documenso/lib/utils/logger'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { Card } from '@documenso/ui/primitives/card'; @@ -40,13 +41,17 @@ export async function loader({ params, request }: Route.LoaderArgs) { throw redirect(documentRootPath); } - const document = await getDocumentById({ - documentId, + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }).catch(() => null); - if (!document || !document.documentData) { + if (!envelope) { throw redirect(documentRootPath); } @@ -63,7 +68,19 @@ export async function loader({ params, request }: Route.LoaderArgs) { }); return { - document, + // Only return necessary data + document: { + id: mapSecondaryIdToDocumentId(envelope.secondaryId), + title: envelope.title, + status: envelope.status, + user: { + name: envelope.user.name, + email: envelope.user.email, + }, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + documentMeta: envelope.documentMeta, + }, recipients, documentRootPath, }; diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx index f6c6d36e4..e4453d960 100644 --- a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx +++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.public-profile.tsx @@ -2,8 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; import { useLingui } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro'; -import type { TemplateDirectLink } from '@prisma/client'; -import { TemplateType } from '@prisma/client'; +import { type TemplateDirectLink, TemplateType } from '@prisma/client'; import { getSession } from '@documenso/auth/server/lib/utils/get-session'; import { useSession } from '@documenso/lib/client-only/providers/session'; diff --git a/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx b/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx index d2f625bbc..d8ac22a77 100644 --- a/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx +++ b/apps/remix/app/routes/_internal+/[__htmltopdf]+/audit-log.tsx @@ -1,14 +1,16 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; +import { EnvelopeType } from '@prisma/client'; import { DateTime } from 'luxon'; import { redirect } from 'react-router'; import { DOCUMENT_STATUS } from '@documenso/lib/constants/document'; import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; -import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; +import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { getTranslations } from '@documenso/lib/utils/i18n'; import { Card, CardContent } from '@documenso/ui/primitives/card'; @@ -39,20 +41,24 @@ export async function loader({ request }: Route.LoaderArgs) { const documentId = Number(rawDocumentId); - const document = await getEntireDocument({ - id: documentId, + const envelope = await unsafeGetEntireEnvelope({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, }).catch(() => null); - if (!document) { + if (!envelope) { throw redirect('/'); } - const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language); + const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language); const { data: auditLogs } = await findDocumentAuditLogs({ documentId: documentId, - userId: document.userId, - teamId: document.teamId, + userId: envelope.userId, + teamId: envelope.teamId, perPage: 100_000, }); @@ -60,7 +66,20 @@ export async function loader({ request }: Route.LoaderArgs) { return { auditLogs, - document, + document: { + id: mapSecondaryIdToDocumentId(envelope.secondaryId), + title: envelope.title, + status: envelope.status, + user: { + name: envelope.user.name, + email: envelope.user.email, + }, + recipients: envelope.recipients, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + deletedAt: envelope.deletedAt, + documentMeta: envelope.documentMeta, + }, documentLanguage, messages, }; @@ -90,6 +109,7 @@ export default function AuditLog({ loaderData }: Route.ComponentProps) {

+ {/* Todo: Envelopes - Should we should envelope ID instead here? */} {_(msg`Document ID`)} {document.id} diff --git a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx index ff14ccd32..34af24655 100644 --- a/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx +++ b/apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; -import { FieldType, SigningStatus } from '@prisma/client'; +import { EnvelopeType, FieldType, SigningStatus } from '@prisma/client'; import { DateTime } from 'luxon'; import { redirect } from 'react-router'; import { prop, sortBy } from 'remeda'; @@ -14,12 +14,13 @@ import { RECIPIENT_ROLES_DESCRIPTION, RECIPIENT_ROLE_SIGNING_REASONS, } from '@documenso/lib/constants/recipient-roles'; -import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; +import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document'; import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt'; import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs'; import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { getTranslations } from '@documenso/lib/utils/i18n'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { @@ -55,26 +56,45 @@ export async function loader({ request }: Route.LoaderArgs) { const documentId = Number(rawDocumentId); - const document = await getEntireDocument({ - id: documentId, + const envelope = await unsafeGetEntireEnvelope({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, }).catch(() => null); - if (!document) { + if (!envelope) { throw redirect('/'); } - const organisationClaim = await getOrganisationClaimByTeamId({ teamId: document.teamId }); + const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId }); - const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language); + const documentLanguage = ZSupportedLanguageCodeSchema.parse(envelope.documentMeta?.language); const auditLogs = await getDocumentCertificateAuditLogs({ - id: documentId, + envelopeId: envelope.id, }); const messages = await getTranslations(documentLanguage); return { - document, + document: { + id: mapSecondaryIdToDocumentId(envelope.secondaryId), + title: envelope.title, + status: envelope.status, + user: { + name: envelope.user.name, + email: envelope.user.email, + }, + qrToken: envelope.qrToken, + authOptions: envelope.authOptions, + recipients: envelope.recipients, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + deletedAt: envelope.deletedAt, + documentMeta: envelope.documentMeta, + }, hidePoweredBy: organisationClaim.flags.hidePoweredBy, documentLanguage, auditLogs, diff --git a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx index 5376f2af6..6fa5897cd 100644 --- a/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx +++ b/apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; -import { type Document, DocumentStatus, FieldType, RecipientRole } from '@prisma/client'; +import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client'; import { CheckCircle2, Clock8, FileSearch } from 'lucide-react'; import { Link, useRevalidator } from 'react-router'; import { match } from 'ts-pattern'; @@ -19,6 +19,7 @@ import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; import { env } from '@documenso/lib/utils/env'; +import type { Document } from '@documenso/prisma/types/document-legacy-schema'; import DocumentDialog from '@documenso/ui/components/document/document-dialog'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; diff --git a/apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx b/apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx index fbbc02ce6..bf7b97bf7 100644 --- a/apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx +++ b/apps/remix/app/routes/_recipient+/sign.$token+/waiting.tsx @@ -1,11 +1,11 @@ import { Trans } from '@lingui/react/macro'; import type { Team } from '@prisma/client'; -import { DocumentStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { Link, redirect } from 'react-router'; import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session'; -import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getTeamById } from '@documenso/lib/server-only/team/get-team'; import { formatDocumentsPath } from '@documenso/lib/utils/teams'; @@ -40,12 +40,16 @@ export async function loader({ params, request }: Route.LoaderArgs) { let team: Team | null = null; if (user) { - isOwnerOrTeamMember = await getDocumentById({ - documentId: document.id, + isOwnerOrTeamMember = await getEnvelopeById({ + id: { + type: 'documentId', + id: document.id, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, teamId: document.teamId ?? undefined, }) - .then((document) => !!document) + .then((envelope) => !!envelope) .catch(() => false); if (document.teamId) { diff --git a/apps/remix/app/routes/_share+/share.$slug.tsx b/apps/remix/app/routes/_share+/share.$slug.tsx index c0a3f75b2..c78da55eb 100644 --- a/apps/remix/app/routes/_share+/share.$slug.tsx +++ b/apps/remix/app/routes/_share+/share.$slug.tsx @@ -81,9 +81,9 @@ export default function SharePage() { ); diff --git a/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx index 9bb820ec3..611f27506 100644 --- a/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx +++ b/apps/remix/app/routes/embed+/v1+/authoring+/document.edit.$id.tsx @@ -54,7 +54,10 @@ export const loader = async ({ request, params }: Route.LoaderArgs) => { } const document = await getDocumentWithDetailsById({ - documentId, + id: { + type: 'documentId', + id: documentId, + }, userId: result?.userId, teamId: result?.teamId ?? undefined, }).catch(() => null); diff --git a/apps/remix/server/redirects.ts b/apps/remix/server/redirects.ts index 45570bcc4..4367e234b 100644 --- a/apps/remix/server/redirects.ts +++ b/apps/remix/server/redirects.ts @@ -1,6 +1,11 @@ +import { EnvelopeType } from '@prisma/client'; import type { Context } from 'hono'; import { getSession } from '@documenso/auth/server/lib/utils/get-session'; +import { + mapDocumentIdToSecondaryId, + mapTemplateIdToSecondaryId, +} from '@documenso/lib/utils/envelope'; import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; @@ -104,9 +109,10 @@ async function hasAccessToDocument(c: Context, documentId: number): Promise { @@ -154,9 +160,10 @@ async function hasAccessToTemplate(c: Context, templateId: number): Promise ({ + 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, }, }; @@ -89,23 +98,43 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }); try { - const document = await getDocumentById({ - documentId: Number(documentId), + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: Number(documentId), + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - const recipients = await getRecipientsForDocument({ - documentId: Number(documentId), - 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 = await getFieldsForDocument({ - documentId: Number(documentId), - userId: user.id, - teamId: team?.id, - }); + const { fields, recipients } = envelope; const parsedMetaFields = fields.map((field) => { let parsedMetaOrNull = null; @@ -126,12 +155,32 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; }); + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + return { status: 200, body: { - ...document, + 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) => ({ - ...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, @@ -158,22 +207,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }); try { - if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') { - return { - status: 500, - body: { - message: 'Please make sure the storage transport is set to S3.', - }, - }; - } - - const document = await getDocumentById({ - documentId: Number(documentId), + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: Number(documentId), + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, - }); + teamId: team.id, + }).catch(() => null); - if (!document || !document.documentDataId) { + const firstDocumentData = envelope?.envelopeItems[0]?.documentData; + + if (!envelope || !firstDocumentData) { return { status: 404, body: { @@ -182,7 +228,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (DocumentDataType.S3_PATH !== document.documentData.type) { + // 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: { @@ -191,7 +247,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (!downloadOriginalDocument && !isDocumentCompleted(document.status)) { + if (!downloadOriginalDocument && !isDocumentCompleted(envelope.status)) { return { status: 400, body: { @@ -200,8 +256,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } + if (envelope.envelopeItems.length !== 1) { + return { + status: 400, + body: { + message: 'API V1 does not support items', + }, + }; + } + const { url } = await getPresignGetUrl( - downloadOriginalDocument ? document.documentData.initialData : document.documentData.data, + downloadOriginalDocument ? firstDocumentData.initialData : firstDocumentData.data, ); return { @@ -228,13 +293,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }); try { - const document = await getDocumentById({ - documentId: Number(documentId), + const legacyDocumentId = Number(documentId); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: legacyDocumentId, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - if (!document) { + if (!envelope) { return { status: 404, body: { @@ -243,16 +314,26 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - const deletedDocument = await deleteDocument({ - id: document.id, + const deletedEnvelope = await deleteDocument({ + id: legacyDocumentId, userId: user.id, - teamId: team?.id, + teamId: team.id, requestMetadata: metadata, }); return { status: 200, - body: deletedDocument, + body: { + id: legacyDocumentId, + externalId: deletedEnvelope.externalId, + userId: deletedEnvelope.userId, + teamId: deletedEnvelope.teamId, + title: deletedEnvelope.title, + status: deletedEnvelope.status, + createdAt: deletedEnvelope.createdAt, + updatedAt: deletedEnvelope.updatedAt, + completedAt: deletedEnvelope.completedAt, + }, }; } catch (err) { return { @@ -325,53 +406,47 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { type: DocumentDataType.S3_PATH, }); - const document = await createDocument({ - title: body.title, - externalId: body.externalId || null, + const envelope = await createEnvelope({ userId: user.id, - teamId: team?.id, - formValues: body.formValues, - folderId: body.folderId, - documentDataId: documentData.id, + teamId: team.id, + 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, + }, + 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, }); - await upsertDocumentMeta({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - 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, - }); - - if (body.authOptions) { - await updateDocumentSettings({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - data: { - ...body.authOptions, - }, - requestMetadata: metadata, - }); - } + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); const { recipients } = await setDocumentRecipients({ userId: user.id, - teamId: team?.id, - documentId: document.id, + teamId: team.id, + documentId: legacyDocumentId, recipients: body.recipients, requestMetadata: metadata, }); @@ -380,7 +455,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { status: 200, body: { uploadUrl: url, - documentId: document.id, + documentId: legacyDocumentId, recipients: recipients.map((recipient) => ({ recipientId: recipient.id, name: recipient.name, @@ -388,7 +463,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { token: recipient.token, role: recipient.role, signingOrder: recipient.signingOrder, - signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, })), }, @@ -403,7 +477,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { } }), - createTemplate: authenticatedMiddleware(async (args, user, team) => { + createTemplate: authenticatedMiddleware(async (args, user, team, { metadata }) => { const { body } = args; const { title, @@ -465,26 +539,32 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { type: DocumentDataType.S3_PATH, }); - const createdTemplate = await createTemplate({ + const createdTemplate = await createEnvelope({ userId: user.id, teamId: team.id, - templateDocumentDataId: templateDocumentData.id, data: { + type: EnvelopeType.TEMPLATE, + envelopeItems: [ + { + documentDataId: templateDocumentData.id, + }, + ], + templateType: type, title, folderId, - externalId, + externalId: externalId ?? undefined, visibility, globalAccessAuth, globalActionAuth, publicTitle, publicDescription, - type, }, meta, + requestMetadata: metadata, }); const fullTemplate = await getTemplateById({ - id: createdTemplate.id, + id: mapSecondaryIdToTemplateId(createdTemplate.secondaryId), userId: user.id, teamId: team.id, }); @@ -519,12 +599,23 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const deletedTemplate = await deleteTemplate({ id: Number(templateId), userId: user.id, - teamId: team?.id, + teamId: team.id, }); + const legacyTemplateId = mapSecondaryIdToTemplateId(deletedTemplate.secondaryId); + return { status: 200, - body: deletedTemplate, + 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 { @@ -549,7 +640,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const template = await getTemplateById({ id: Number(templateId), userId: user.id, - teamId: team?.id, + teamId: team.id, }); return { @@ -583,16 +674,25 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { page, perPage, userId: user.id, - teamId: team?.id, + teamId: team.id, }); return { status: 200, body: { templates: templates.map((template) => ({ - ...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, @@ -615,7 +715,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id }); + const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id }); if (remaining.documents <= 0) { return { @@ -630,17 +730,71 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const fileName = body.title.endsWith('.pdf') ? body.title : `${body.title}.pdf`; - const document = await createDocumentFromTemplateLegacy({ - templateId, + const template = await getEnvelopeById({ + id: { + type: 'templateId', + id: templateId, + }, + type: EnvelopeType.TEMPLATE, userId: user.id, - teamId: team?.id, - recipients: body.recipients, + teamId: team.id, }); - let documentDataId = document.documentDataId; + 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, // Todo: Migration - Should you actually 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, + }, + 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(document.documentData); + const pdf = await getFileServerSide(firstEnvelopeItemData); const prefilled = await insertFormValuesInPdf({ pdf: Buffer.from(pdf), @@ -653,50 +807,34 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { arrayBuffer: async () => Promise.resolve(prefilled), }); - documentDataId = newDocumentData.id; - } - - await updateDocument({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - data: { - title: fileName, - externalId: body.externalId || null, - formValues: body.formValues, - documentData: { - connect: { - id: documentDataId, - }, + await prisma.envelopeItem.update({ + where: { + id: firstEnvelopeItemData.id, + }, + data: { + title: body.title || fileName, + documentDataId: newDocumentData.id, }, - }, - }); - - if (body.meta) { - await upsertDocumentMeta({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - ...body.meta, - requestMetadata: metadata, }); } - if (body.authOptions) { - await updateDocumentSettings({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - data: body.authOptions, - requestMetadata: metadata, + 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: document.id, - recipients: document.recipients.map((recipient) => ({ + documentId: mapSecondaryIdToDocumentId(createdEnvelope.secondaryId), + recipients: createdEnvelope.recipients.map((recipient) => ({ recipientId: recipient.id, name: recipient.name, email: recipient.email, @@ -721,7 +859,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const { remaining } = await getServerLimits({ userId: user.id, teamId: team?.id }); + const { remaining } = await getServerLimits({ userId: user.id, teamId: team.id }); if (remaining.documents <= 0) { return { @@ -734,14 +872,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const templateId = Number(params.templateId); - let document: Awaited> | null = null; + let envelope: Awaited> | null = null; try { - document = await createDocumentFromTemplate({ - templateId, + envelope = await createDocumentFromTemplate({ + id: { + type: 'templateId', + id: templateId, + }, externalId: body.externalId || null, userId: user.id, - teamId: team?.id, + teamId: team.id, recipients: body.recipients, prefillFields: body.prefillFields, folderId: body.folderId, @@ -755,10 +896,23 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { return AppError.toRestAPIError(err); } - if (body.formValues) { - const fileName = document.title.endsWith('.pdf') ? document.title : `${document.title}.pdf`; + if (envelope.envelopeItems.length !== 1) { + throw new Error('API V1 does not support envelopes'); + } - const pdf = await getFileServerSide(document.documentData); + 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), @@ -771,15 +925,20 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { arrayBuffer: async () => Promise.resolve(prefilled), }); - await updateDocument({ - documentId: document.id, - userId: user.id, - teamId: team?.id, + await prisma.envelope.update({ + where: { + id: envelope.id, + }, data: { formValues: body.formValues, - documentData: { - connect: { - id: newDocumentData.id, + envelopeItems: { + update: { + where: { + id: firstEnvelopeDocumentData.id, + }, + data: { + documentDataId: newDocumentData.id, + }, }, }, }, @@ -787,20 +946,23 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { } if (body.authOptions) { - await updateDocumentSettings({ - documentId: document.id, - userId: user.id, - teamId: team?.id, - data: body.authOptions, - requestMetadata: metadata, + await prisma.envelope.update({ + where: { + id: envelope.id, + }, + data: { + authOptions: body.authOptions, + }, }); } + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + return { status: 200, body: { - documentId: document.id, - recipients: document.recipients.map((recipient) => ({ + documentId: legacyDocumentId, + recipients: envelope.recipients.map((recipient) => ({ recipientId: recipient.id, name: recipient.name, email: recipient.email, @@ -825,13 +987,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }); try { - const document = await getDocumentById({ - documentId: Number(documentId), + const legacyDocumentId = Number(documentId); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: legacyDocumentId, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - if (!document) { + if (!envelope) { return { status: 404, body: { @@ -840,7 +1008,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { return { status: 400, body: { @@ -849,14 +1017,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta); + const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta); // Update document email settings if sendCompletionEmails is provided if (typeof sendCompletionEmails === 'boolean') { - await upsertDocumentMeta({ - documentId: document.id, + await updateDocumentMeta({ + id: { + type: 'envelopeId', + id: envelope.id, + }, userId: user.id, - teamId: team?.id, + teamId: team.id, emailSettings: { ...emailSettings, documentCompleted: sendCompletionEmails, @@ -867,9 +1038,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { } const { recipients, ...sentDocument } = await sendDocument({ - documentId: document.id, + id: { + type: 'envelopeId', + id: envelope.id, + }, userId: user.id, - teamId: team?.id, + teamId: team.id, sendEmail, requestMetadata: metadata, }); @@ -878,7 +1052,15 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { status: 200, body: { message: 'Document sent for signing successfully', - ...sentDocument, + 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}`, @@ -910,7 +1092,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { userId: user.id, documentId: Number(documentId), recipients, - teamId: team?.id, + teamId: team.id, requestMetadata: metadata, }); @@ -940,13 +1122,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const document = await getDocumentById({ - documentId: Number(documentId), + const legacyDocumentId = Number(documentId); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: legacyDocumentId, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - if (!document) { + if (!envelope) { return { status: 404, body: { @@ -955,7 +1143,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { return { status: 400, body: { @@ -967,7 +1155,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const recipients = await getRecipientsForDocument({ documentId: Number(documentId), userId: user.id, - teamId: team?.id, + teamId: team.id, }); const recipientAlreadyExists = recipients.some((recipient) => recipient.email === email); @@ -985,7 +1173,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const { recipients: newRecipients } = await setDocumentRecipients({ documentId: Number(documentId), userId: user.id, - teamId: team?.id, + teamId: team.id, recipients: [ ...recipients.map((recipient) => ({ email: recipient.email, @@ -1040,13 +1228,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const document = await getDocumentById({ - documentId: Number(documentId), + const legacyDocumentId = Number(documentId); + + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: legacyDocumentId, + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - if (!document) { + if (!envelope) { return { status: 404, body: { @@ -1055,7 +1249,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { return { status: 400, body: { @@ -1067,7 +1261,10 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const updatedRecipient = await updateDocumentRecipients({ userId: user.id, teamId: team.id, - documentId: Number(documentId), + id: { + type: 'envelopeId', + id: envelope.id, + }, recipients: [ { id: Number(recipientId), @@ -1112,46 +1309,21 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const document = await getDocumentById({ - documentId: Number(documentId), + const deletedRecipient = await deleteDocumentRecipient({ userId: user.id, - teamId: team?.id, - }); - - if (!document) { - return { - status: 404, - body: { - message: 'Document not found', - }, - }; - } - - if (isDocumentCompleted(document.status)) { - return { - status: 400, - body: { - message: 'Document is already completed', - }, - }; - } - - const deletedRecipient = await deleteRecipient({ - documentId: Number(documentId), + teamId: team.id, recipientId: Number(recipientId), - userId: user.id, - teamId: team?.id, - requestMetadata: metadata.requestMetadata, - }).catch(() => null); - - if (!deletedRecipient) { - return { - status: 400, - body: { - message: 'Unable to delete recipient', + requestMetadata: { + requestMetadata: metadata.requestMetadata, + source: 'apiV1', + auth: 'api', + auditUser: { + id: team.id, + email: team.name, + name: team.name, }, - }; - } + }, + }); return { status: 200, @@ -1174,22 +1346,46 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const fields = Array.isArray(args.body) ? args.body : [args.body]; - const document = await prisma.document.findFirst({ - select: { id: true, status: true }, - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', id: Number(documentId), - team: buildTeamWhereQuery({ teamId: team.id, userId: user.id }), + }, + 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 (!document) { + if (!envelope) { return { status: 404, body: { message: 'Document not found' }, }; } - if (isDocumentCompleted(document.status)) { + 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' }, @@ -1215,10 +1411,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { throw new Error('Invalid page number'); } - const recipient = await getRecipientByIdV1Api({ - id: Number(recipientId), - documentId: Number(documentId), - }).catch(() => null); + const recipient = await prisma.recipient.findFirst({ + where: { + id: Number(recipientId), + envelopeId: envelope.id, + }, + }); if (!recipient) { throw new Error('Recipient not found'); @@ -1265,7 +1463,8 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { const field = await tx.field.create({ data: { - documentId: Number(documentId), + envelopeId: envelope.id, + envelopeItemId: firstEnvelopeItemId, recipientId: Number(recipientId), type, page: pageNumber, @@ -1285,9 +1484,9 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: 'FIELD_CREATED', - documentId: Number(documentId), + envelopeId: envelope.id, user: { - id: team?.id ?? user.id, + id: team.id ?? user.id, email: team?.name ?? user.email, name: team ? '' : user.name, }, @@ -1303,7 +1502,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { return { id: field.id, - documentId: Number(field.documentId), + documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), recipientId: field.recipientId ?? -1, type: field.type, pageNumber: field.page, @@ -1343,13 +1542,17 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }, }); - const document = await getDocumentById({ - documentId: Number(documentId), + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: Number(documentId), + }, + type: EnvelopeType.DOCUMENT, userId: user.id, - teamId: team?.id, + teamId: team.id, }); - if (!document) { + if (!envelope) { return { status: 404, body: { @@ -1358,7 +1561,19 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - if (isDocumentCompleted(document.status)) { + 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: { @@ -1367,10 +1582,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - const recipient = await getRecipientByIdV1Api({ - id: Number(recipientId), - documentId: Number(documentId), - }).catch(() => null); + const recipient = await prisma.recipient.findFirst({ + where: { + id: Number(recipientId), + envelopeId: envelope.id, + }, + }); if (!recipient) { return { @@ -1390,128 +1607,84 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { }; } - const updatedField = await updateField({ - fieldId: Number(fieldId), + const { fields } = await updateDocumentFields({ userId: user.id, - teamId: team?.id, - documentId: Number(documentId), - recipientId: recipientId ? Number(recipientId) : undefined, - type, - pageNumber, - pageX, - pageY, - pageWidth, - pageHeight, - requestMetadata: metadata.requestMetadata, - fieldMeta: fieldMeta ? ZFieldMetaSchema.parse(fieldMeta) : undefined, + teamId: team.id, + documentId: 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 remappedField = { - id: updatedField.id, - 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, - }; + const updatedField = fields[0]; return { status: 200, body: { - ...remappedField, - documentId: Number(documentId), + 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 }) => { - const { id: documentId, fieldId } = args.params; + // Note: documentId isn't actually used anywhere, so we just return it. + const { id: unverifiedDocumentId, fieldId } = args.params; logger.info({ input: { - id: documentId, + id: unverifiedDocumentId, fieldId, }, }); - const document = await getDocumentById({ - documentId: Number(documentId), + 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, + }, + }, }); - if (!document) { - return { - status: 404, - body: { - message: 'Document not found', - }, - }; - } - - if (isDocumentCompleted(document.status)) { - return { - status: 400, - body: { - message: 'Document is already completed', - }, - }; - } - - const field = await getFieldById({ - userId: user.id, - teamId: team?.id, - fieldId: Number(fieldId), - }).catch(() => null); - - if (!field) { - return { - status: 404, - body: { - message: 'Field not found', - }, - }; - } - - const recipient = await getRecipientByIdV1Api({ - 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), - userId: user.id, - teamId: team?.id, - requestMetadata: metadata.requestMetadata, - }).catch(() => null); - - if (!deletedField) { - return { - status: 400, - body: { - message: 'Unable to delete field', - }, - }; - } - const remappedField = { id: deletedField.id, - documentId: deletedField.documentId, + documentId: Number(unverifiedDocumentId), recipientId: deletedField.recipientId ?? -1, type: deletedField.type, pageNumber: deletedField.page, @@ -1525,32 +1698,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, { return { status: 200, - body: { - ...remappedField, - documentId: Number(documentId), - }, + body: remappedField, }; }), }); - -const updateDocument = async ({ - documentId, - userId, - teamId, - data, -}: { - documentId: number; - data: Prisma.DocumentUpdateInput; - userId: number; - teamId: number; -}) => { - return await prisma.document.update({ - where: { - id: documentId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - data: { - ...data, - }, - }); -}; diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index 5b4b33f7b..7155c0ad3 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -8,8 +8,8 @@ import { RecipientRole, SendStatus, SigningStatus, - TemplateType, } from '@prisma/client'; +import { TemplateType } from '@prisma/client'; import { z } from 'zod'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; @@ -49,7 +49,6 @@ export const ZSuccessfulDocumentResponseSchema = z.object({ teamId: z.number().nullish(), title: z.string(), status: z.string(), - documentDataId: z.string(), createdAt: z.date(), updatedAt: z.date(), completedAt: z.date().nullable(), @@ -545,7 +544,6 @@ export const ZTemplateSchema = z.object({ title: z.string(), userId: z.number(), teamId: z.number().nullish(), - templateDocumentDataId: z.string(), createdAt: z.date(), updatedAt: z.date(), }); diff --git a/packages/app-tests/e2e/api/v1/document-sending.spec.ts b/packages/app-tests/e2e/api/v1/document-sending.spec.ts index 503c7335a..4723b4690 100644 --- a/packages/app-tests/e2e/api/v1/document-sending.spec.ts +++ b/packages/app-tests/e2e/api/v1/document-sending.spec.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents'; import { seedUser } from '@documenso/prisma/seed/users'; @@ -25,7 +26,7 @@ test.describe('Document API', () => { // Test with sendCompletionEmails: false const response = await request.post( - `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`, + `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`, { headers: { Authorization: `Bearer ${token}`, @@ -41,7 +42,7 @@ test.describe('Document API', () => { expect(response.status()).toBe(200); // Verify email settings were updated - const updatedDocument = await prisma.document.findUnique({ + const updatedDocument = await prisma.envelope.findUnique({ where: { id: document.id }, include: { documentMeta: true }, }); @@ -53,7 +54,7 @@ test.describe('Document API', () => { // Test with sendCompletionEmails: true const response2 = await request.post( - `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`, + `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`, { headers: { Authorization: `Bearer ${token}`, @@ -69,7 +70,7 @@ test.describe('Document API', () => { expect(response2.status()).toBe(200); // Verify email settings were updated - const updatedDocument2 = await prisma.document.findUnique({ + const updatedDocument2 = await prisma.envelope.findUnique({ where: { id: document.id }, include: { documentMeta: true }, }); @@ -93,16 +94,16 @@ test.describe('Document API', () => { // Set initial email settings await prisma.documentMeta.upsert({ - where: { documentId: document.id }, + where: { id: document.documentMetaId }, create: { - documentId: document.id, + id: document.documentMetaId, emailSettings: { documentCompleted: true, ownerDocumentCompleted: false, }, }, update: { - documentId: document.id, + id: document.documentMetaId, emailSettings: { documentCompleted: true, ownerDocumentCompleted: false, @@ -118,7 +119,7 @@ test.describe('Document API', () => { }); const response = await request.post( - `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${document.id}/send`, + `${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`, { headers: { Authorization: `Bearer ${token}`, @@ -134,7 +135,7 @@ test.describe('Document API', () => { expect(response.status()).toBe(200); // Verify email settings were not modified - const updatedDocument = await prisma.document.findUnique({ + const updatedDocument = await prisma.envelope.findUnique({ where: { id: document.id }, include: { documentMeta: true }, }); diff --git a/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts b/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts index b67645820..799e08c7d 100644 --- a/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts +++ b/packages/app-tests/e2e/api/v1/template-field-prefill.spec.ts @@ -3,6 +3,11 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta'; +import { + mapDocumentIdToSecondaryId, + mapSecondaryIdToDocumentId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { FieldType, RecipientRole } from '@documenso/prisma/client'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; @@ -35,10 +40,12 @@ test.describe('Template Field Prefill API v1', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -53,7 +60,8 @@ test.describe('Template Field Prefill API v1', () => { // Add TEXT field const textField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.TEXT, page: 1, @@ -73,7 +81,8 @@ test.describe('Template Field Prefill API v1', () => { // Add NUMBER field const numberField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.NUMBER, page: 1, @@ -93,7 +102,8 @@ test.describe('Template Field Prefill API v1', () => { // Add RADIO field const radioField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.RADIO, page: 1, @@ -117,7 +127,8 @@ test.describe('Template Field Prefill API v1', () => { // Add CHECKBOX field const checkboxField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.CHECKBOX, page: 1, @@ -141,7 +152,8 @@ test.describe('Template Field Prefill API v1', () => { // Add DROPDOWN field const dropdownField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.DROPDOWN, page: 1, @@ -166,11 +178,13 @@ test.describe('Template Field Prefill API v1', () => { }); // 7. Navigate to the template - await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`); + await page.goto( + `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + ); // 8. Create a document from the template with prefilled fields const response = await request.post( - `${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`, + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/generate-document`, { headers: { Authorization: `Bearer ${token}`, @@ -229,9 +243,9 @@ test.describe('Template Field Prefill API v1', () => { expect(responseData.documentId).toBeDefined(); // 9. Verify the document was created with prefilled fields - const document = await prisma.document.findUnique({ + const document = await prisma.envelope.findUnique({ where: { - id: responseData.documentId, + secondaryId: mapDocumentIdToSecondaryId(responseData.documentId), }, include: { fields: true, @@ -240,6 +254,10 @@ test.describe('Template Field Prefill API v1', () => { expect(document).not.toBeNull(); + if (!document) { + throw new Error('Document not found'); + } + // 10. Verify each field has the correct prefilled values const documentTextField = document?.fields.find( (field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text', @@ -297,14 +315,14 @@ test.describe('Template Field Prefill API v1', () => { // 11. Sign in as the recipient and verify the prefilled fields are visible const documentRecipient = await prisma.recipient.findFirst({ where: { - documentId: document?.id, + envelopeId: document?.id, email: 'recipient@example.com', }, }); // Send the document to the recipient const sendResponse = await request.post( - `${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`, + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`, { headers: { Authorization: `Bearer ${token}`, @@ -367,10 +385,12 @@ test.describe('Template Field Prefill API v1', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -385,7 +405,8 @@ test.describe('Template Field Prefill API v1', () => { // Add TEXT field await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.TEXT, page: 1, @@ -405,7 +426,8 @@ test.describe('Template Field Prefill API v1', () => { // Add NUMBER field await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.NUMBER, page: 1, @@ -429,11 +451,13 @@ test.describe('Template Field Prefill API v1', () => { }); // 7. Navigate to the template - await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`); + await page.goto( + `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + ); // 8. Create a document from the template without prefilled fields const response = await request.post( - `${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`, + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/generate-document`, { headers: { Authorization: `Bearer ${token}`, @@ -461,9 +485,9 @@ test.describe('Template Field Prefill API v1', () => { expect(responseData.documentId).toBeDefined(); // 9. Verify the document was created with default fields - const document = await prisma.document.findUnique({ + const document = await prisma.envelope.findUnique({ where: { - id: responseData.documentId, + secondaryId: mapDocumentIdToSecondaryId(responseData.documentId), }, include: { fields: true, @@ -472,6 +496,10 @@ test.describe('Template Field Prefill API v1', () => { expect(document).not.toBeNull(); + if (!document) { + throw new Error('Document not found'); + } + // 10. Verify fields have their default values const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT); expect(documentTextField?.fieldMeta).toMatchObject({ @@ -488,7 +516,7 @@ test.describe('Template Field Prefill API v1', () => { // 11. Sign in as the recipient and verify the default fields are visible const documentRecipient = await prisma.recipient.findFirst({ where: { - documentId: document?.id, + envelopeId: document?.id, email: 'recipient@example.com', }, }); @@ -496,7 +524,7 @@ test.describe('Template Field Prefill API v1', () => { expect(documentRecipient).not.toBeNull(); const sendResponse = await request.post( - `${WEBAPP_BASE_URL}/api/v1/documents/${document?.id}/send`, + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/send`, { headers: { Authorization: `Bearer ${token}`, @@ -539,10 +567,12 @@ test.describe('Template Field Prefill API v1', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -556,7 +586,8 @@ test.describe('Template Field Prefill API v1', () => { // 5. Add a field to the template const field = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.RADIO, page: 1, @@ -579,7 +610,7 @@ test.describe('Template Field Prefill API v1', () => { // 6. Try to create a document with invalid prefill value const response = await request.post( - `${WEBAPP_BASE_URL}/api/v1/templates/${template.id}/generate-document`, + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/generate-document`, { headers: { Authorization: `Bearer ${token}`, diff --git a/packages/app-tests/e2e/api/v1/test-unauthorized-api-access.spec.ts b/packages/app-tests/e2e/api/v1/test-unauthorized-api-access.spec.ts index 875c89d4b..2c42d15eb 100644 --- a/packages/app-tests/e2e/api/v1/test-unauthorized-api-access.spec.ts +++ b/packages/app-tests/e2e/api/v1/test-unauthorized-api-access.spec.ts @@ -2,6 +2,10 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; +import { + mapSecondaryIdToDocumentId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { FieldType } from '@documenso/prisma/client'; import { @@ -10,7 +14,7 @@ import { seedDraftDocument, seedPendingDocumentWithFullFields, } from '@documenso/prisma/seed/documents'; -import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; +import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates'; import { seedUser } from '@documenso/prisma/seed/users'; const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL(); @@ -20,521 +24,1116 @@ test.describe.configure({ }); test.describe('Document Access API V1', () => { - test('should block unauthorized access to documents not owned by the user', async ({ - request, - }) => { - const { user: userA, team: teamA } = await seedUser(); + test.describe('Document GET endpoint', () => { + test('should block unauthorized access to documents not owned by the user', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const documentA = await seedBlankDocument(userA, teamA.id); + + // User B cannot access User A's document + const resB = await request.get( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); }); - const documentA = await seedBlankDocument(userA, teamA.id); + test('should allow authorized access to documents owned by the user', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - // User B cannot access User A's document - const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, + const documentA = await seedBlankDocument(userA, teamA.id); + + // User A can access their own document + const resA = await request.get( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(404); }); - test('should block unauthorized access to document download endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); + test.describe('Document download endpoint', () => { + test('should block unauthorized access to document download endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const documentA = await seedCompletedDocument(userA, teamA.id, ['test@example.com'], { + createDocumentOptions: { title: 'Document 1 - Completed' }, + }); + + const resB = await request.get( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/download`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + const res = await resB.json(); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); }); - const documentA = await seedCompletedDocument(userA, teamA.id, ['test@example.com'], { - createDocumentOptions: { title: 'Document 1 - Completed' }, - }); + test('should allow authorized access to document download endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/download`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); + const documentA = await seedCompletedDocument(userA, teamA.id, ['test@example.com'], { + createDocumentOptions: { title: 'Document 1 - Completed' }, + }); - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(500); + const resA = await request.get( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/download`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + const res = await resA.json(); + + if (res.message === 'Document downloads are only available when S3 storage is configured.') { + expect(resA.ok()).toBeFalsy(); + expect(resA.status()).toBe(500); + } else { + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + } + }); }); - test('should block unauthorized access to document delete endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); + test.describe('Document delete endpoint', () => { + test('should block unauthorized access to document delete endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const documentA = await seedBlankDocument(userA, teamA.id); + + const resB = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); }); - const documentA = await seedBlankDocument(userA, teamA.id); + test('should allow authorized access to document delete endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - const resB = await request.delete(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, + const documentA = await seedBlankDocument(userA, teamA.id); + + const resA = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(404); }); - test('should block unauthorized access to document send endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); + test.describe('Document send endpoint', () => { + test('should block unauthorized access to document send endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { document: documentA } = await seedPendingDocumentWithFullFields({ + owner: userA, + recipients: ['test@example.com'], + teamId: teamA.id, + }); + + const resB = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/send`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: {}, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(500); }); - const { document: documentA } = await seedPendingDocumentWithFullFields({ - owner: userA, - recipients: ['test@example.com'], - teamId: teamA.id, - }); + test('should allow authorized access to document send endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/send`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: {}, - }); + const { document: documentA } = await seedPendingDocumentWithFullFields({ + owner: userA, + recipients: ['test@example.com'], + teamId: teamA.id, + }); - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(500); + const resA = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/send`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: {}, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); }); - test('should block unauthorized access to document resend endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); + test.describe('Document resend endpoint', () => { + test('should block unauthorized access to document resend endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); - const { user: recipientUser } = await seedUser(); + const { user: recipientUser } = await seedUser(); - const { document: documentA, recipients } = await seedPendingDocumentWithFullFields({ - owner: userA, - recipients: [recipientUser.email], - teamId: teamA.id, - }); + const { document: documentA, recipients } = await seedPendingDocumentWithFullFields({ + owner: userA, + recipients: [recipientUser.email], + teamId: teamA.id, + }); - const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/resend`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - recipients: [recipients[0].id], - }, - }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(500); - }); - - test('should block unauthorized access to document recipients endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const documentA = await seedBlankDocument(userA, teamA.id); - - const resB = await request.post( - `${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { name: 'Test', email: 'test@example.com' }, - }, - ); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(401); - }); - - test('should block unauthorized access to PATCH on recipients endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const { user: userRecipient } = await seedUser(); - - const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: documentA.id, - email: userRecipient.email, - }, - }); - - const patchRes = await request.patch( - `${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients/${recipient!.id}`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - name: 'New Name', - email: 'new@example.com', - role: 'SIGNER', - signingOrder: null, - authOptions: { - accessAuth: [], - actionAuth: [], + const resB = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/resend`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + recipients: [recipients[0].id], }, }, - }, - ); + ); - expect(patchRes.ok()).toBeFalsy(); - expect(patchRes.status()).toBe(401); - }); - - test('should block unauthorized access to DELETE on recipients endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(500); }); - const { user: userRecipient } = await seedUser(); + test('should allow authorized access to document resend endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + const { user: recipientUser } = await seedUser(); - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: documentA.id, - email: userRecipient.email, - }, - }); + const { document: documentA, recipients } = await seedPendingDocumentWithFullFields({ + owner: userA, + recipients: [recipientUser.email], + teamId: teamA.id, + }); - const deleteRes = await request.delete( - `${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/recipients/${recipient!.id}`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: {}, - }, - ); - - expect(deleteRes.ok()).toBeFalsy(); - expect(deleteRes.status()).toBe(401); - }); - - test('should block unauthorized access to document fields endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const { user: recipientUser } = await seedUser(); - - const documentA = await seedDraftDocument(userA, teamA.id, [recipientUser.email]); - - const documentRecipient = await prisma.recipient.findFirst({ - where: { - documentId: documentA.id, - email: recipientUser.email, - }, - }); - - const resB = await request.post(`${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - recipientId: documentRecipient!.id, - type: 'SIGNATURE', - pageNumber: 1, - pageX: 1, - pageY: 1, - pageWidth: 1, - pageHeight: 1, - }, - }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(404); - }); - - test('should block unauthorized access to template get endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const templateA = await seedBlankTemplate(userA, teamA.id); - - const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(404); - }); - - test('should block unauthorized access to template delete endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const templateA = await seedBlankTemplate(userA, teamA.id); - - const resB = await request.delete(`${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(404); - }); - - test('should block unauthorized access to PATCH on fields endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const { user: userRecipient } = await seedUser(); - const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: documentA.id, - email: userRecipient.email, - }, - }); - - const field = await prisma.field.create({ - data: { - documentId: documentA.id, - recipientId: recipient!.id, - type: FieldType.TEXT, - page: 1, - positionX: 5, - positionY: 5, - width: 10, - height: 5, - customText: '', - inserted: false, - fieldMeta: { - type: 'text', - label: 'Default Text Field', + const resA = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/resend`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + recipients: [recipients[0].id], + }, }, - }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); + }); + + test.describe('Document recipients POST endpoint', () => { + test('should block unauthorized access to document recipients endpoint', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const documentA = await seedBlankDocument(userA, teamA.id); + + const resB = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { name: 'Test', email: 'test@example.com' }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(401); }); - const patchRes = await request.patch( - `${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields/${field.id}`, - { - headers: { Authorization: `Bearer ${tokenB}` }, + test('should allow authorized access to document recipients endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const documentA = await seedBlankDocument(userA, teamA.id); + + const resA = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { name: 'Test', email: 'test@example.com' }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); + }); + + test.describe('Document recipients PATCH endpoint', () => { + test('should block unauthorized access to PATCH on recipients endpoint', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const patchRes = await request.patch( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + name: 'New Name', + email: 'new@example.com', + role: 'SIGNER', + signingOrder: null, + authOptions: { + accessAuth: [], + actionAuth: [], + }, + }, + }, + ); + + expect(patchRes.ok()).toBeFalsy(); + expect(patchRes.status()).toBe(401); + }); + + test('should allow authorized access to PATCH on recipients endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const patchRes = await request.patch( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + name: 'New Name', + email: 'new@example.com', + role: 'SIGNER', + signingOrder: null, + authOptions: { + accessAuth: [], + actionAuth: [], + }, + }, + }, + ); + + expect(patchRes.ok()).toBeTruthy(); + expect(patchRes.status()).toBe(200); + }); + }); + + test.describe('Document recipients DELETE endpoint', () => { + test('should block unauthorized access to DELETE on recipients endpoint', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const deleteRes = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: {}, + }, + ); + + expect(deleteRes.ok()).toBeFalsy(); + expect(deleteRes.status()).toBe(401); + }); + + test('should allow authorized access to DELETE on recipients endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const deleteRes = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/recipients/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: {}, + }, + ); + + expect(deleteRes.ok()).toBeTruthy(); + expect(deleteRes.status()).toBe(200); + }); + }); + + test.describe('Document fields POST endpoint', () => { + test('should block unauthorized access to document fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { user: recipientUser } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [recipientUser.email]); + + const documentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: recipientUser.email, + }, + }); + + const resB = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + recipientId: documentRecipient!.id, + type: 'SIGNATURE', + pageNumber: 1, + pageX: 1, + pageY: 1, + pageWidth: 1, + pageHeight: 1, + }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); + }); + + test('should allow authorized access to document fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const { user: recipientUser } = await seedUser(); + + const documentA = await seedDraftDocument(userA, teamA.id, [recipientUser.email]); + + const documentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: recipientUser.email, + }, + }); + + const resA = await request.post( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + recipientId: documentRecipient!.id, + type: 'SIGNATURE', + pageNumber: 1, + pageX: 1, + pageY: 1, + pageWidth: 1, + pageHeight: 1, + }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); + }); + + test.describe('Template GET endpoint', () => { + test('should block unauthorized access to template get endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const templateA = await seedBlankTemplate(userA, teamA.id); + + const resB = await request.get( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); + }); + + test('should allow authorized access to template get endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const templateA = await seedBlankTemplate(userA, teamA.id); + + const resA = await request.get( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); + }); + + test.describe('Template DELETE endpoint', () => { + test('should block unauthorized access to template delete endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const templateA = await seedBlankTemplate(userA, teamA.id); + + const resB = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(404); + }); + + test('should allow authorized access to template delete endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const templateA = await seedBlankTemplate(userA, teamA.id); + + const resA = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); + }); + + test.describe('Document fields PATCH endpoint', () => { + test('should block unauthorized access to PATCH on fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const firstEnvelopeItem = documentA.envelopeItems[0]; + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const field = await prisma.field.create({ data: { + envelopeId: documentA.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient!.id, type: FieldType.TEXT, - pageNumber: 1, - pageX: 99, - pageY: 99, - pageWidth: 99, - pageHeight: 99, + page: 1, + positionX: 5, + positionY: 5, + width: 10, + height: 5, + customText: '', + inserted: false, fieldMeta: { type: 'text', - label: 'My new field', + label: 'Default Text Field', }, }, - }, - ); - expect(patchRes.ok()).toBeFalsy(); - expect(patchRes.status()).toBe(401); - }); + }); - test('should block unauthorized access to DELETE on fields endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const { user: userRecipient } = await seedUser(); - const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: documentA.id, - email: userRecipient.email, - }, - }); - - const field = await prisma.field.create({ - data: { - documentId: documentA.id, - recipientId: recipient!.id, - type: FieldType.NUMBER, - page: 1, - positionX: 5, - positionY: 5, - width: 10, - height: 5, - customText: '', - inserted: false, - fieldMeta: { - type: 'number', - label: 'Default Number Field', + const patchRes = await request.patch( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields/${field.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + recipientId: recipient!.id, + type: FieldType.TEXT, + pageNumber: 1, + pageX: 99, + pageY: 99, + pageWidth: 99, + pageHeight: 99, + fieldMeta: { + type: 'text', + label: 'My new field', + }, + }, }, - }, + ); + expect(patchRes.ok()).toBeFalsy(); + expect(patchRes.status()).toBe(401); }); - const deleteRes = await request.delete( - `${WEBAPP_BASE_URL}/api/v1/documents/${documentA.id}/fields/${field.id}`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: {}, - }, - ); + test('should allow authorized access to PATCH on fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); - expect(deleteRes.ok()).toBeFalsy(); - expect(deleteRes.status()).toBe(401); - }); + const { user: userRecipient } = await seedUser(); + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - test('should block unauthorized access to documents list endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); + const firstEnvelopeItem = documentA.envelopeItems[0]; - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - await seedBlankDocument(userA, teamA.id); - - const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - const reqData = await resB.json(); - - expect(resB.ok()).toBeTruthy(); - expect(resB.status()).toBe(200); - expect(reqData.documents.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe( - true, - ); - expect(reqData.documents.length).toBe(0); - expect(reqData.totalPages).toBe(0); - }); - - test('should block unauthorized access to templates list endpoint', async ({ request }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - await seedBlankTemplate(userA, teamA.id); - - const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - const reqData = await resB.json(); - - expect(resB.ok()).toBeTruthy(); - expect(resB.status()).toBe(200); - expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId !== userA.id)).toBe( - true, - ); - expect(reqData.templates.length).toBe(0); - expect(reqData.totalPages).toBe(0); - }); - - test('should block unauthorized access to create-document-from-template endpoint', async ({ - request, - }) => { - const { user: userA, team: teamA } = await seedUser(); - - const { user: userB, team: teamB } = await seedUser(); - const { token: tokenB } = await createApiToken({ - userId: userB.id, - teamId: teamB.id, - tokenName: 'userB', - expiresIn: null, - }); - - const templateA = await seedBlankTemplate(userA, teamA.id); - - const resB = await request.post( - `${WEBAPP_BASE_URL}/api/v1/templates/${templateA.id}/create-document`, - { - headers: { - Authorization: `Bearer ${tokenB}`, - 'Content-Type': 'application/json', + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, }, + }); + + const field = await prisma.field.create({ data: { - title: 'Should not work', - recipients: [{ name: 'Test user', email: 'test@example.com' }], - meta: { - subject: 'Test', - message: 'Test', - timezone: 'UTC', - dateFormat: 'yyyy-MM-dd', - redirectUrl: 'https://example.com', + envelopeId: documentA.id, + envelopeItemId: firstEnvelopeItem.id, + recipientId: recipient!.id, + type: FieldType.TEXT, + page: 1, + positionX: 5, + positionY: 5, + width: 10, + height: 5, + customText: '', + inserted: false, + fieldMeta: { + type: 'text', + label: 'Default Text Field', }, }, - }, - ); + }); - expect(resB.ok()).toBeFalsy(); - expect(resB.status()).toBe(401); + const patchRes = await request.patch( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields/${field.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + recipientId: recipient!.id, + type: FieldType.TEXT, + pageNumber: 1, + pageX: 99, + pageY: 99, + pageWidth: 99, + pageHeight: 99, + fieldMeta: { + type: 'text', + label: 'My new field', + }, + }, + }, + ); + expect(patchRes.ok()).toBeTruthy(); + expect(patchRes.status()).toBe(200); + }); + }); + + test.describe('Document fields DELETE endpoint', () => { + test('should block unauthorized access to DELETE on fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const firstEnvelopeItem = documentA.envelopeItems[0]; + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: documentA.id, + envelopeItemId: firstEnvelopeItem.id, + recipientId: recipient!.id, + type: FieldType.NUMBER, + page: 1, + positionX: 5, + positionY: 5, + width: 10, + height: 5, + customText: '', + inserted: false, + fieldMeta: { + type: 'number', + label: 'Default Number Field', + }, + }, + }); + + const deleteRes = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields/${field.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: {}, + }, + ); + + expect(deleteRes.ok()).toBeFalsy(); + expect(deleteRes.status()).toBe(401); + }); + + test('should allow authorized access to DELETE on fields endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const { user: userRecipient } = await seedUser(); + const documentA = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const firstEnvelopeItem = documentA.envelopeItems[0]; + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: documentA.id, + email: userRecipient.email, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: documentA.id, + envelopeItemId: firstEnvelopeItem.id, + recipientId: recipient!.id, + type: FieldType.NUMBER, + page: 1, + positionX: 5, + positionY: 5, + width: 10, + height: 5, + customText: '', + inserted: false, + fieldMeta: { + type: 'number', + label: 'Default Number Field', + }, + }, + }); + + const deleteRes = await request.delete( + `${WEBAPP_BASE_URL}/api/v1/documents/${mapSecondaryIdToDocumentId(documentA.secondaryId)}/fields/${field.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: {}, + }, + ); + + expect(deleteRes.ok()).toBeTruthy(); + expect(deleteRes.status()).toBe(200); + }); + }); + + test.describe('Documents list endpoint', () => { + test('should block unauthorized access to documents list endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + await seedBlankDocument(userA, teamA.id); + + const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + const reqData = await resB.json(); + + expect(resB.ok()).toBeTruthy(); + expect(resB.status()).toBe(200); + expect(reqData.documents.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe( + true, + ); + expect(reqData.documents.length).toBe(0); + expect(reqData.totalPages).toBe(0); + }); + + test('should allow authorized access to documents list endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + await seedBlankDocument(userA, teamA.id); + + const resA = await request.get(`${WEBAPP_BASE_URL}/api/v1/documents`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + const reqData = await resA.json(); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + expect(reqData.documents.length).toBeGreaterThan(0); + expect(reqData.documents.every((doc: { userId: number }) => doc.userId === userA.id)).toBe( + true, + ); + }); + }); + + test.describe('Templates list endpoint', () => { + test('should block unauthorized access to templates list endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + await seedBlankTemplate(userA, teamA.id); + + const resB = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + const reqData = await resB.json(); + + expect(resB.ok()).toBeTruthy(); + expect(resB.status()).toBe(200); + expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId !== userA.id)).toBe( + true, + ); + expect(reqData.templates.length).toBe(0); + expect(reqData.totalPages).toBe(0); + }); + + test('should allow authorized access to templates list endpoint', async ({ request }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + await seedBlankTemplate(userA, teamA.id); + + const resA = await request.get(`${WEBAPP_BASE_URL}/api/v1/templates`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + const reqData = await resA.json(); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + expect(reqData.templates.length).toBeGreaterThan(0); + expect(reqData.templates.every((tpl: { userId: number }) => tpl.userId === userA.id)).toBe( + true, + ); + }); + }); + + test.describe('Create document from template endpoint', () => { + test('should block unauthorized access to create-document-from-template endpoint', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); + + const { user: userB, team: teamB } = await seedUser(); + const { token: tokenB } = await createApiToken({ + userId: userB.id, + teamId: teamB.id, + tokenName: 'userB', + expiresIn: null, + }); + + const templateA = await seedTemplate({ + teamId: teamA.id, + userId: userA.id, + }); + + const resB = await request.post( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}/create-document`, + { + headers: { + Authorization: `Bearer ${tokenB}`, + 'Content-Type': 'application/json', + }, + data: { + title: 'Should not work', + recipients: [{ name: 'Test user', email: 'test@example.com' }], + meta: { + subject: 'Test', + message: 'Test', + timezone: 'UTC', + dateFormat: 'yyyy-MM-dd', + redirectUrl: 'https://example.com', + }, + }, + }, + ); + + expect(resB.ok()).toBeFalsy(); + expect(resB.status()).toBe(401); + }); + + test('should allow authorized access to create-document-from-template endpoint', async ({ + request, + }) => { + const { user: userA, team: teamA } = await seedUser(); + const { token: tokenA } = await createApiToken({ + userId: userA.id, + teamId: teamA.id, + tokenName: 'userA', + expiresIn: null, + }); + + const templateA = await seedTemplate({ + teamId: teamA.id, + userId: userA.id, + }); + + const resA = await request.post( + `${WEBAPP_BASE_URL}/api/v1/templates/${mapSecondaryIdToTemplateId(templateA.secondaryId)}/create-document`, + { + headers: { + Authorization: `Bearer ${tokenA}`, + 'Content-Type': 'application/json', + }, + data: { + title: 'Should work', + recipients: [{ name: 'Test user', email: 'test@example.com' }], + meta: { + subject: 'Test', + message: 'Test', + timezone: 'UTC', + dateFormat: 'yyyy-MM-dd', + redirectUrl: 'https://example.com', + }, + }, + }, + ); + + expect(resA.ok()).toBeTruthy(); + expect(resA.status()).toBe(200); + }); }); }); diff --git a/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts b/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts index ec83008aa..f0835a000 100644 --- a/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts +++ b/packages/app-tests/e2e/api/v2/template-field-prefill.spec.ts @@ -3,6 +3,11 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; import type { TCheckboxFieldMeta, TRadioFieldMeta } from '@documenso/lib/types/field-meta'; +import { + mapDocumentIdToSecondaryId, + mapSecondaryIdToDocumentId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { FieldType, RecipientRole } from '@documenso/prisma/client'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; @@ -35,10 +40,12 @@ test.describe('Template Field Prefill API v2', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -53,7 +60,8 @@ test.describe('Template Field Prefill API v2', () => { // Add TEXT field const textField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.TEXT, page: 1, @@ -73,7 +81,8 @@ test.describe('Template Field Prefill API v2', () => { // Add NUMBER field const numberField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.NUMBER, page: 1, @@ -93,7 +102,8 @@ test.describe('Template Field Prefill API v2', () => { // Add RADIO field const radioField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.RADIO, page: 1, @@ -117,7 +127,8 @@ test.describe('Template Field Prefill API v2', () => { // Add CHECKBOX field const checkboxField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.CHECKBOX, page: 1, @@ -141,7 +152,8 @@ test.describe('Template Field Prefill API v2', () => { // Add DROPDOWN field const dropdownField = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.DROPDOWN, page: 1, @@ -166,7 +178,9 @@ test.describe('Template Field Prefill API v2', () => { }); // 7. Navigate to the template - await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`); + await page.goto( + `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + ); // 8. Create a document from the template with prefilled fields using v2 API const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, { @@ -175,7 +189,7 @@ test.describe('Template Field Prefill API v2', () => { 'Content-Type': 'application/json', }, data: { - templateId: template.id, + templateId: mapSecondaryIdToTemplateId(template.secondaryId), recipients: [ { id: recipient.id, @@ -226,9 +240,9 @@ test.describe('Template Field Prefill API v2', () => { expect(responseData.id).toBeDefined(); // 9. Verify the document was created with prefilled fields - const document = await prisma.document.findUnique({ + const document = await prisma.envelope.findUnique({ where: { - id: responseData.id, + secondaryId: mapDocumentIdToSecondaryId(responseData.id), }, include: { fields: true, @@ -237,6 +251,10 @@ test.describe('Template Field Prefill API v2', () => { expect(document).not.toBeNull(); + if (!document) { + throw new Error('Document not found'); + } + // 10. Verify each field has the correct prefilled values const documentTextField = document?.fields.find( (field) => field.type === FieldType.TEXT && field.fieldMeta?.type === 'text', @@ -297,7 +315,7 @@ test.describe('Template Field Prefill API v2', () => { 'Content-Type': 'application/json', }, data: { - documentId: document?.id, + documentId: mapSecondaryIdToDocumentId(document?.secondaryId), meta: { subject: 'Test Subject', message: 'Test Message', @@ -311,7 +329,7 @@ test.describe('Template Field Prefill API v2', () => { // 11. Sign in as the recipient and verify the prefilled fields are visible const documentRecipient = await prisma.recipient.findFirst({ where: { - documentId: document?.id, + envelopeId: document?.id, email: 'recipient@example.com', }, }); @@ -364,10 +382,12 @@ test.describe('Template Field Prefill API v2', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -382,7 +402,8 @@ test.describe('Template Field Prefill API v2', () => { // Add TEXT field await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.TEXT, page: 1, @@ -402,7 +423,8 @@ test.describe('Template Field Prefill API v2', () => { // Add NUMBER field await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.NUMBER, page: 1, @@ -426,7 +448,9 @@ test.describe('Template Field Prefill API v2', () => { }); // 7. Navigate to the template - await page.goto(`${WEBAPP_BASE_URL}/templates/${template.id}`); + await page.goto( + `${WEBAPP_BASE_URL}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + ); // 8. Create a document from the template without prefilled fields using v2 API const response = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, { @@ -435,7 +459,7 @@ test.describe('Template Field Prefill API v2', () => { 'Content-Type': 'application/json', }, data: { - templateId: template.id, + templateId: mapSecondaryIdToTemplateId(template.secondaryId), recipients: [ { id: recipient.id, @@ -454,9 +478,9 @@ test.describe('Template Field Prefill API v2', () => { expect(responseData.id).toBeDefined(); // 9. Verify the document was created with default fields - const document = await prisma.document.findUnique({ + const document = await prisma.envelope.findUnique({ where: { - id: responseData.id, + secondaryId: mapDocumentIdToSecondaryId(responseData.id), }, include: { fields: true, @@ -465,6 +489,10 @@ test.describe('Template Field Prefill API v2', () => { expect(document).not.toBeNull(); + if (!document) { + throw new Error('Document not found'); + } + // 10. Verify fields have their default values const documentTextField = document?.fields.find((field) => field.type === FieldType.TEXT); expect(documentTextField?.fieldMeta).toMatchObject({ @@ -484,7 +512,7 @@ test.describe('Template Field Prefill API v2', () => { 'Content-Type': 'application/json', }, data: { - documentId: document?.id, + documentId: mapSecondaryIdToDocumentId(document?.secondaryId), meta: { subject: 'Test Subject', message: 'Test Message', @@ -498,7 +526,7 @@ test.describe('Template Field Prefill API v2', () => { // 11. Sign in as the recipient and verify the default fields are visible const documentRecipient = await prisma.recipient.findFirst({ where: { - documentId: document?.id, + envelopeId: document?.id, email: 'recipient@example.com', }, }); @@ -531,10 +559,12 @@ test.describe('Template Field Prefill API v2', () => { }, }); + const firstEnvelopeItem = template.envelopeItems[0]; + // 4. Create a recipient for the template const recipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: 'recipient@example.com', name: 'Test Recipient', role: RecipientRole.SIGNER, @@ -548,7 +578,8 @@ test.describe('Template Field Prefill API v2', () => { // 5. Add a field to the template const field = await prisma.field.create({ data: { - templateId: template.id, + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: recipient.id, type: FieldType.RADIO, page: 1, @@ -576,7 +607,7 @@ test.describe('Template Field Prefill API v2', () => { 'Content-Type': 'application/json', }, data: { - templateId: template.id, + templateId: mapSecondaryIdToTemplateId(template.secondaryId), recipients: [ { id: recipient.id, diff --git a/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts b/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts index 73ef577b7..d45ea4512 100644 --- a/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts +++ b/packages/app-tests/e2e/api/v2/test-unauthorized-api-access.spec.ts @@ -4,6 +4,10 @@ import type { Team, User } from '@prisma/client'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token'; import { nanoid } from '@documenso/lib/universal/id'; +import { + mapSecondaryIdToDocumentId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { DocumentVisibility, @@ -29,7 +33,7 @@ test.describe.configure({ mode: 'parallel', }); -test.describe('Unauthorized Access - Document API V2', () => { +test.describe('Document API V2', () => { let userA: User, teamA: Team, userB: User, teamB: Team, tokenA: string, tokenB: string; test.beforeEach(async () => { @@ -50,227 +54,386 @@ test.describe('Unauthorized Access - Document API V2', () => { })); }); - test('should block unauthorized access to document list endpoint', async ({ request }) => { - await seedCompletedDocument(userA, teamA.id, ['test@example.com']); + test.describe('Document list endpoint', () => { + test('should block unauthorized access to document list endpoint', async ({ request }) => { + await seedCompletedDocument(userA, teamA.id, ['test@example.com']); - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document`, { - headers: { Authorization: `Bearer ${tokenB}` }, + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + + const data = await res.json(); + expect(data.data.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(true); }); - expect(res.ok()).toBeTruthy(); - expect(res.status()).toBe(200); + test('should allow authorized access to document list endpoint', async ({ request }) => { + await seedCompletedDocument(userA, teamA.id, ['test@example.com']); - const data = await res.json(); - expect(data.data.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(true); + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + + const data = await res.json(); + expect(data.data.length).toBeGreaterThan(0); + expect(data.data.every((doc: { userId: number }) => doc.userId === userA.id)).toBe(true); + }); }); - test('should block unauthorized access to document detail endpoint', async ({ request }) => { - const doc = await seedBlankDocument(userA, teamA.id); + test.describe('Document detail endpoint', () => { + test('should block unauthorized access to document detail endpoint', async ({ request }) => { + const doc = await seedBlankDocument(userA, teamA.id); - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document/${doc.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/document/${mapSecondaryIdToDocumentId(doc.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + test('should allow authorized access to document detail endpoint', async ({ request }) => { + const doc = await seedBlankDocument(userA, teamA.id); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/document/${mapSecondaryIdToDocumentId(doc.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document update endpoint', async ({ request }) => { - const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); + test.describe('Document update endpoint', () => { + test('should block unauthorized access to document update endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id, data: { title: 'Updated Title' } }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + data: { title: 'Updated Title' }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + test('should allow authorized access to document update endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + data: { title: 'Updated Title' }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document delete endpoint', async ({ request }) => { - const doc = await seedDraftDocument(userA, teamA.id, []); + test.describe('Document delete endpoint', () => { + test('should block unauthorized access to document delete endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(401); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(401); + test('should allow authorized access to document delete endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document move endpoint', async ({ request }) => { - const doc = await seedDraftDocument(userA, teamA.id, []); + test.describe('Document distribute endpoint', () => { + test('should block unauthorized access to document distribute endpoint', async ({ + request, + }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/move`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id, teamId: teamB.id }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(500); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); + test('should allow authorized access to document distribute endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); - test('should block unauthorized access to document distribute endpoint', async ({ request }) => { - const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/distribute`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id }, + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(500); }); - test('should block unauthorized access to document redistribute endpoint', async ({ - request, - }) => { - const doc = await seedPendingDocument(userA, teamA.id, []); + test.describe('Document redistribute endpoint', () => { + test('should block unauthorized access to document redistribute endpoint', async ({ + request, + }) => { + const doc = await seedPendingDocument(userA, teamA.id, []); - const userRecipient = await prisma.recipient.create({ - data: { - email: 'test@example.com', - name: 'Test', - token: nanoid(), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.SENT, - signingStatus: SigningStatus.NOT_SIGNED, - signedAt: null, - document: { - connect: { - id: doc.id, + const userRecipient = await prisma.recipient.create({ + data: { + email: 'test@example.com', + name: 'Test', + token: nanoid(), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.SENT, + signingStatus: SigningStatus.NOT_SIGNED, + signedAt: null, + envelopeId: doc.id, + fields: { + create: { + page: 1, + type: FieldType.NAME, + inserted: true, + customText: '', + positionX: new Prisma.Decimal(1), + positionY: new Prisma.Decimal(1), + width: new Prisma.Decimal(1), + height: new Prisma.Decimal(1), + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + }, }, }, - fields: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: '', - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: doc.id, + }); + + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: userRecipient.email, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/redistribute`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [recipient!.id], + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(500); + }); + + test('should allow authorized access to document redistribute endpoint', async ({ + request, + }) => { + const doc = await seedPendingDocument(userA, teamA.id, []); + + const userRecipient = await prisma.recipient.create({ + data: { + email: 'test@example.com', + name: 'Test', + token: nanoid(), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.SENT, + signingStatus: SigningStatus.NOT_SIGNED, + signedAt: null, + envelopeId: doc.id, + fields: { + create: { + page: 1, + type: FieldType.NAME, + inserted: true, + customText: '', + positionX: new Prisma.Decimal(1), + positionY: new Prisma.Decimal(1), + width: new Prisma.Decimal(1), + height: new Prisma.Decimal(1), + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + }, }, }, - }, - }); + }); - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: doc.id, - email: userRecipient.email, - }, - }); + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: userRecipient.email, + }, + }); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/redistribute`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id, recipients: [recipient!.id] }, - }); + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/redistribute`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [recipient!.id], + }, + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(500); + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document duplicate endpoint', async ({ request }) => { - const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); + test.describe('Document duplicate endpoint', () => { + test('should block unauthorized access to document duplicate endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/duplicate`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { documentId: doc.id }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/duplicate`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + test('should allow authorized access to document duplicate endpoint', async ({ request }) => { + const doc = await seedDraftDocument(userA, teamA.id, ['test@example.com']); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/duplicate`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { documentId: mapSecondaryIdToDocumentId(doc.secondaryId) }, + }); + + const asdf = await res.json(); + console.log({ + asdf, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document field endpoint', async ({ request }) => { - const { user: userRecipient } = await seedUser(); + test.describe('Document field GET endpoint', () => { + test('should block unauthorized access to document field endpoint', async ({ request }) => { + const { user: userRecipient } = await seedUser(); - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - const recipient = await prisma.recipient.findFirst({ - where: { - documentId: doc.id, - email: userRecipient.email, - }, - }); + const recipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: userRecipient.email, + }, + }); - const field = await prisma.field.create({ - data: { - documentId: doc.id, - recipientId: recipient!.id, - type: 'TEXT', - page: 1, - positionX: 1, - positionY: 1, - width: 1, - height: 1, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'Test' }, - }, - }); - - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/${field.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document field create endpoint', async ({ - request, - }) => { - const { user: userRecipient } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - field: { + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, recipientId: recipient!.id, type: 'TEXT', - pageNumber: 791.77, - pageX: 7845.22, - pageY: 6843.16, - width: 3932.15, - height: 8879.89, - fieldMeta: { type: 'text', label: 'Test Field' }, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, }, - }, + }); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/${field.id}`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect([404, 401, 500]).toContain(res.status()); + test('should allow authorized access to document field endpoint', async ({ request }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirstOrThrow({ + where: { + envelopeId: doc.id, + email: userRecipient.email, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient.id, + type: 'TEXT', + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, + }, + }); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/${field.id}`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + const asdf = await res.json(); + console.log({ + asdf, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document field create-many endpoint', async ({ - request, - }) => { - const { user: userRecipient } = await seedUser(); + test.describe('Document field create endpoint', () => { + test('should block unauthorized access to document field create endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create-many`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - fields: [ - { + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + field: { recipientId: recipient!.id, type: 'TEXT', pageNumber: 791.77, @@ -278,86 +441,156 @@ test.describe('Unauthorized Access - Document API V2', () => { pageY: 6843.16, width: 3932.15, height: 8879.89, - fieldMeta: { type: 'text', label: 'First test field' }, + fieldMeta: { type: 'text', label: 'Test Field' }, }, - { + }, + }); + + expect(res.ok()).toBeFalsy(); + expect([404, 401, 500]).toContain(res.status()); + }); + + test('should allow authorized access to document field create endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + field: { recipientId: recipient!.id, type: 'TEXT', pageNumber: 791.77, - pageX: 845.22, - pageY: 843.16, - width: 932.15, - height: 879.89, - fieldMeta: { type: 'text', label: 'Second test field' }, + pageX: 7845.22, + pageY: 6843.16, + width: 3932.15, + height: 8879.89, + fieldMeta: { type: 'text', label: 'Test Field' }, }, - ], - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document field update endpoint', async ({ - request, - }) => { - const { user: userRecipient } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const field = await prisma.field.create({ - data: { - documentId: doc.id, - recipientId: recipient!.id, - type: 'TEXT', - page: 1, - positionX: 1, - positionY: 1, - width: 1, - height: 1, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'A text field' }, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - field: { - id: field.id, - type: FieldType.TEXT, - fieldMeta: { type: 'text', label: 'An updated text field' }, }, - }, - }); + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to document field update-many endpoint', async ({ - request, - }) => { - const { user: userRecipient } = await seedUser(); + test.describe('Document field create-many endpoint', () => { + test('should block unauthorized access to document field create-many endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create-many`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + fields: [ + { + recipientId: recipient!.id, + type: 'TEXT', + pageNumber: 791.77, + pageX: 7845.22, + pageY: 6843.16, + width: 3932.15, + height: 8879.89, + fieldMeta: { type: 'text', label: 'First test field' }, + }, + { + recipientId: recipient!.id, + type: 'TEXT', + pageNumber: 791.77, + pageX: 845.22, + pageY: 843.16, + width: 932.15, + height: 879.89, + fieldMeta: { type: 'text', label: 'Second test field' }, + }, + ], + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - const fields = await prisma.field.createManyAndReturn({ - data: [ - { - documentId: doc.id, + test('should allow authorized access to document field create-many endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/create-many`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + fields: [ + { + recipientId: recipient!.id, + type: 'TEXT', + pageNumber: 791.77, + pageX: 7845.22, + pageY: 6843.16, + width: 3932.15, + height: 8879.89, + fieldMeta: { type: 'text', label: 'First test field' }, + }, + { + recipientId: recipient!.id, + type: 'TEXT', + pageNumber: 791.77, + pageX: 845.22, + pageY: 843.16, + width: 932.15, + height: 879.89, + fieldMeta: { type: 'text', label: 'Second test field' }, + }, + ], + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document field update endpoint', () => { + test('should block unauthorized access to document field update endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, recipientId: recipient!.id, - type: FieldType.TEXT, + type: 'TEXT', page: 1, positionX: 1, positionY: 1, @@ -365,12 +598,43 @@ test.describe('Unauthorized Access - Document API V2', () => { height: 1, customText: '', inserted: false, - fieldMeta: { type: 'text', label: 'Test' }, + fieldMeta: { type: 'text', label: 'A text field' }, }, - { - documentId: doc.id, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + field: { + id: field.id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'An updated text field' }, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document field update endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, recipientId: recipient!.id, - type: FieldType.NUMBER, + type: 'TEXT', page: 1, positionX: 1, positionY: 1, @@ -378,289 +642,562 @@ test.describe('Unauthorized Access - Document API V2', () => { height: 1, customText: '', inserted: false, - fieldMeta: { type: 'text', label: 'Test' }, + fieldMeta: { type: 'text', label: 'A text field' }, }, - ], - }); + }); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update-many`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - fields: [ - { - id: fields[0].id, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + field: { + id: field.id, type: FieldType.TEXT, - fieldMeta: { type: 'text', label: 'Updated first test field' }, + fieldMeta: { type: 'text', label: 'An updated text field' }, }, - { - id: fields[1].id, - type: FieldType.NUMBER, - fieldMeta: { type: 'number', label: 'Updated second test field' }, - }, - ], - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document field delete endpoint', async ({ - request, - }) => { - const { user: userRecipient } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const field = await prisma.field.create({ - data: { - documentId: doc.id, - recipientId: recipient!.id, - type: FieldType.TEXT, - page: 1, - positionX: 1, - positionY: 1, - width: 1, - height: 1, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'Test' }, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { fieldId: field.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template field create endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - field: { - recipientId: recipient.id, - type: FieldType.TEXT, - pageNumber: 5735.12, - pageX: 936.28, - pageY: 594.41, - width: 589.39, - height: 122.23, - fieldMeta: { type: 'text', label: 'Test' }, }, - }, - }); + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to template field get field endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); + test.describe('Document field update-many endpoint', () => { + test('should block unauthorized access to document field update-many endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); - const field = await prisma.field.create({ - data: { - templateId: template.id, - recipientId: recipient.id, - type: FieldType.TEXT, - page: 1, - positionX: 936.28, - positionY: 594.41, - width: 589.39, - height: 122.23, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'New test field' }, - }, - }); + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/${field.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template field create-many endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const secondRecipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test2@example.com', - name: 'Test 2', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create-many`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - fields: [ + const fields = await prisma.field.createManyAndReturn({ + data: [ { - recipientId: recipient.id, + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, type: FieldType.TEXT, - pageNumber: 1, - pageX: 1, - pageY: 1, + page: 1, + positionX: 1, + positionY: 1, width: 1, height: 1, + customText: '', + inserted: false, fieldMeta: { type: 'text', label: 'Test' }, }, { - recipientId: secondRecipient.id, + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, type: FieldType.NUMBER, - pageNumber: 1, - pageX: 1, - pageY: 1, + page: 1, + positionX: 1, + positionY: 1, width: 1, height: 1, - fieldMeta: { type: 'number', label: 'Test 2' }, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, }, ], - }, - }); + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template field update endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const field = await prisma.field.create({ - data: { - templateId: template.id, - recipientId: recipient.id, - type: FieldType.TEXT, - page: 1, - positionX: 1, - positionY: 1, - width: 1, - height: 1, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'Test field to update' }, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - field: { - id: field.id, - type: FieldType.TEXT, - fieldMeta: { type: 'text', label: 'Updated field' }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update-many`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + fields: [ + { + id: fields[0].id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'Updated first test field' }, + }, + { + id: fields[1].id, + type: FieldType.NUMBER, + fieldMeta: { type: 'number', label: 'Updated second test field' }, + }, + ], }, - }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + test('should allow authorized access to document field update-many endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const fields = await prisma.field.createManyAndReturn({ + data: [ + { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, + type: FieldType.TEXT, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, + }, + { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, + type: FieldType.NUMBER, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, + }, + ], + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/update-many`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + fields: [ + { + id: fields[0].id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'Updated first test field' }, + }, + { + id: fields[1].id, + type: FieldType.NUMBER, + fieldMeta: { type: 'number', label: 'Updated second test field' }, + }, + ], + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to template field update-many endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); + test.describe('Document field delete endpoint', () => { + test('should block unauthorized access to document field delete endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, + type: FieldType.TEXT, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { fieldId: field.id }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - const field = await prisma.field.createManyAndReturn({ - data: [ - { - templateId: template.id, + test('should allow authorized access to document field delete endpoint', async ({ + request, + }) => { + const { user: userRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [userRecipient.email]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: doc.id, + envelopeItemId: doc.envelopeItems[0].id, + recipientId: recipient!.id, + type: FieldType.TEXT, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test' }, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/field/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { fieldId: field.id }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field create endpoint', () => { + test('should block unauthorized access to template field create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + field: { + recipientId: recipient.id, + type: FieldType.TEXT, + pageNumber: 5735.12, + pageX: 936.28, + pageY: 594.41, + width: 589.39, + height: 122.23, + fieldMeta: { type: 'text', label: 'Test' }, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template field create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + field: { + recipientId: recipient.id, + type: FieldType.TEXT, + pageNumber: 5735.12, + pageX: 936.28, + pageY: 594.41, + width: 589.39, + height: 122.23, + fieldMeta: { type: 'text', label: 'Test' }, + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field GET endpoint', () => { + test('should block unauthorized access to template field get field endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, recipientId: recipient.id, - type: 'TEXT', + type: FieldType.TEXT, + page: 1, + positionX: 936.28, + positionY: 594.41, + width: 589.39, + height: 122.23, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'New test field' }, + }, + }); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/${field.id}`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template field get field endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: FieldType.TEXT, + page: 1, + positionX: 936.28, + positionY: 594.41, + width: 589.39, + height: 122.23, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'New test field' }, + }, + }); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/${field.id}`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field create-many endpoint', () => { + test('should block unauthorized access to template field create-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const secondRecipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test2@example.com', + name: 'Test 2', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create-many`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + fields: [ + { + recipientId: recipient.id, + type: FieldType.TEXT, + pageNumber: 1, + pageX: 1, + pageY: 1, + width: 1, + height: 1, + fieldMeta: { type: 'text', label: 'Test' }, + }, + { + recipientId: secondRecipient.id, + type: FieldType.NUMBER, + pageNumber: 1, + pageX: 1, + pageY: 1, + width: 1, + height: 1, + fieldMeta: { type: 'number', label: 'Test 2' }, + }, + ], + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template field create-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const secondRecipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test2@example.com', + name: 'Test 2', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/create-many`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + fields: [ + { + recipientId: recipient.id, + type: FieldType.TEXT, + pageNumber: 1, + pageX: 1, + pageY: 1, + width: 1, + height: 1, + fieldMeta: { type: 'text', label: 'Test' }, + }, + { + recipientId: secondRecipient.id, + type: FieldType.NUMBER, + pageNumber: 1, + pageX: 1, + pageY: 1, + width: 1, + height: 1, + fieldMeta: { type: 'number', label: 'Test 2' }, + }, + ], + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field update endpoint', () => { + test('should block unauthorized access to template field update endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: FieldType.TEXT, page: 1, positionX: 1, positionY: 1, @@ -670,10 +1207,48 @@ test.describe('Unauthorized Access - Document API V2', () => { inserted: false, fieldMeta: { type: 'text', label: 'Test field to update' }, }, - { - templateId: template.id, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + field: { + id: field.id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'Updated field' }, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template field update endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, recipientId: recipient.id, - type: FieldType.NUMBER, + type: FieldType.TEXT, page: 1, positionX: 1, positionY: 1, @@ -681,655 +1256,1463 @@ test.describe('Unauthorized Access - Document API V2', () => { height: 1, customText: '', inserted: false, - fieldMeta: { type: 'number', label: 'Test field to update' }, + fieldMeta: { type: 'text', label: 'Test field to update' }, }, - ], - }); + }); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update-many`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - fields: [ - { - id: field[0].id, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + field: { + id: field.id, type: FieldType.TEXT, - fieldMeta: { type: 'text', label: 'Updated first field - text' }, + fieldMeta: { type: 'text', label: 'Updated field' }, + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field update-many endpoint', () => { + test('should block unauthorized access to template field update-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.createManyAndReturn({ + data: [ + { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: 'TEXT', + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test field to update' }, }, { - id: field[1].id, + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, type: FieldType.NUMBER, - fieldMeta: { type: 'number', label: 'Updated second field - number' }, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'number', label: 'Test field to update' }, }, ], - }, - }); + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template field delete endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const field = await prisma.field.create({ - data: { - templateId: template.id, - recipientId: recipient.id, - type: 'TEXT', - page: 1, - positionX: 1, - positionY: 1, - width: 1, - height: 1, - customText: '', - inserted: false, - fieldMeta: { type: 'text', label: 'Test field to delete' }, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { fieldId: field.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document recipient get endpoint', async ({ - request, - }) => { - const { user: recipientUser } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const res = await request.get( - `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/${recipient!.id}`, - { + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update-many`, { headers: { Authorization: `Bearer ${tokenB}` }, - }, - ); + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + fields: [ + { + id: field[0].id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'Updated first field - text' }, + }, + { + id: field[1].id, + type: FieldType.NUMBER, + fieldMeta: { type: 'number', label: 'Updated second field - number' }, + }, + ], + }, + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); - test('should block unauthorized access to document recipient create endpoint', async ({ - request, - }) => { - const doc = await seedDraftDocument(userA, teamA.id, []); + test('should allow authorized access to template field update-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - recipient: { - name: 'Test', + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, email: 'test@example.com', + name: 'Test', role: RecipientRole.SIGNER, - }, - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document recipient create-many endpoint', async ({ - request, - }) => { - const doc = await seedDraftDocument(userA, teamA.id, []); - - const res = await request.post( - `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create-many`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - recipients: [ - { - name: 'Test', - email: 'test@example.com', - role: RecipientRole.SIGNER, - }, - { - name: 'Test 2', - email: 'test2@example.com', - role: RecipientRole.SIGNER, - }, - ], - }, - }, - ); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document recipient update endpoint', async ({ - request, - }) => { - const { user: recipientUser } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - recipient: { - id: recipient!.id, - name: 'Updated recipient', - }, - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document recipient update-many endpoint', async ({ - request, - }) => { - const { user: firstRecipient } = await seedUser(); - const { user: secondRecipient } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [firstRecipient, secondRecipient]); - - const firstDocumentRecipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id, email: firstRecipient.email }, - }); - - const secondDocumentRecipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id, email: secondRecipient.email }, - }); - - const res = await request.post( - `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update-many`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - documentId: doc.id, - recipients: [ - { - id: firstDocumentRecipient!.id, - name: 'Updated first recipient', - }, - { - id: secondDocumentRecipient!.id, - name: 'Updated second recipient', - }, - ], - }, - }, - ); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to document recipient delete endpoint', async ({ - request, - }) => { - const { user: recipientUser } = await seedUser(); - - const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); - - const recipient = await prisma.recipient.findFirst({ - where: { documentId: doc.id }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { recipientId: recipient!.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient get endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const templateRecipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test wuth', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const res = await request.get( - `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/${templateRecipient.id}`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - }, - ); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient create endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - recipient: { - name: 'Test', - email: 'test@example.com', - role: RecipientRole.SIGNER, - }, - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient create-many endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post( - `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create-many`, - { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - recipients: [ - { - name: 'Test first recipient', - email: 'test-first-recipient@example.com', - role: RecipientRole.SIGNER, - }, - { - name: 'Test second recipient', - email: 'test-second-recipient@example.com', - role: RecipientRole.SIGNER, - }, - ], - }, - }, - ); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient update endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - recipient: { - id: recipient.id, - name: 'Updated test name', - }, - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient update-many endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipients = await prisma.recipient.createManyAndReturn({ - data: [ - { - templateId: template.id, - email: 'test@example.com', - name: 'Test', token: nanoid(12), readStatus: ReadStatus.NOT_OPENED, sendStatus: SendStatus.NOT_SENT, signingStatus: SigningStatus.NOT_SIGNED, }, - { - templateId: template.id, - email: 'test2@example.com', - name: 'Test 2', + }); + + const field = await prisma.field.createManyAndReturn({ + data: [ + { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: 'TEXT', + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test field to update' }, + }, + { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: FieldType.NUMBER, + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'number', label: 'Test field to update' }, + }, + ], + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/update-many`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + fields: [ + { + id: field[0].id, + type: FieldType.TEXT, + fieldMeta: { type: 'text', label: 'Updated first field - text' }, + }, + { + id: field[1].id, + type: FieldType.NUMBER, + fieldMeta: { type: 'number', label: 'Updated second field - number' }, + }, + ], + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template field delete endpoint', () => { + test('should block unauthorized access to template field delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, token: nanoid(12), readStatus: ReadStatus.NOT_OPENED, sendStatus: SendStatus.NOT_SENT, signingStatus: SigningStatus.NOT_SIGNED, }, - ], + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: 'TEXT', + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test field to delete' }, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { fieldId: field.id }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - const res = await request.post( - `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update-many`, - { + test('should allow authorized access to template field delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const field = await prisma.field.create({ + data: { + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, + recipientId: recipient.id, + type: 'TEXT', + page: 1, + positionX: 1, + positionY: 1, + width: 1, + height: 1, + customText: '', + inserted: false, + fieldMeta: { type: 'text', label: 'Test field to delete' }, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/field/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { fieldId: field.id }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient GET endpoint', () => { + test('should block unauthorized access to document recipient get endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient get endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/${recipient!.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient create endpoint', () => { + test('should block unauthorized access to document recipient create endpoint', async ({ + request, + }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create`, { headers: { Authorization: `Bearer ${tokenB}` }, data: { - templateId: template.id, + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipient: { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient create endpoint', async ({ + request, + }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipient: { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient create-many endpoint', () => { + test('should block unauthorized access to document recipient create-many endpoint', async ({ + request, + }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create-many`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [ + { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + { + name: 'Test 2', + email: 'test2@example.com', + role: RecipientRole.SIGNER, + }, + ], + }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient create-many endpoint', async ({ + request, + }) => { + const doc = await seedDraftDocument(userA, teamA.id, []); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/create-many`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [ + { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + { + name: 'Test 2', + email: 'test2@example.com', + role: RecipientRole.SIGNER, + }, + ], + }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient update endpoint', () => { + test('should block unauthorized access to document recipient update endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipient: { + id: recipient!.id, + name: 'Updated recipient', + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient update endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipient: { + id: recipient!.id, + name: 'Updated recipient', + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient update-many endpoint', () => { + test('should block unauthorized access to document recipient update-many endpoint', async ({ + request, + }) => { + const { user: firstRecipient } = await seedUser(); + const { user: secondRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [firstRecipient, secondRecipient]); + + const firstDocumentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: firstRecipient.email, + }, + }); + + const secondDocumentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: secondRecipient.email, + }, + }); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update-many`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [ + { + id: firstDocumentRecipient!.id, + name: 'Updated first recipient', + }, + { + id: secondDocumentRecipient!.id, + name: 'Updated second recipient', + }, + ], + }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient update-many endpoint', async ({ + request, + }) => { + const { user: firstRecipient } = await seedUser(); + const { user: secondRecipient } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [firstRecipient, secondRecipient]); + + const firstDocumentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: firstRecipient.email, + }, + }); + + const secondDocumentRecipient = await prisma.recipient.findFirst({ + where: { + envelopeId: doc.id, + email: secondRecipient.email, + }, + }); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/update-many`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + documentId: mapSecondaryIdToDocumentId(doc.secondaryId), + recipients: [ + { + id: firstDocumentRecipient!.id, + name: 'Updated first recipient', + }, + { + id: secondDocumentRecipient!.id, + name: 'Updated second recipient', + }, + ], + }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Document recipient delete endpoint', () => { + test('should block unauthorized access to document recipient delete endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { recipientId: recipient!.id }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to document recipient delete endpoint', async ({ + request, + }) => { + const { user: recipientUser } = await seedUser(); + + const doc = await seedDraftDocument(userA, teamA.id, [recipientUser]); + + const recipient = await prisma.recipient.findFirst({ + where: { envelopeId: doc.id }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/document/recipient/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { recipientId: recipient!.id }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient GET endpoint', () => { + test('should block unauthorized access to template recipient get endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const templateRecipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test wuth', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/${templateRecipient.id}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient get endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const templateRecipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test wuth', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/${templateRecipient.id}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient create endpoint', () => { + test('should block unauthorized access to template recipient create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipient: { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipient: { + name: 'Test', + email: 'test@example.com', + role: RecipientRole.SIGNER, + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient create-many endpoint', () => { + test('should block unauthorized access to template recipient create-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create-many`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipients: [ + { + name: 'Test first recipient', + email: 'test-first-recipient@example.com', + role: RecipientRole.SIGNER, + }, + { + name: 'Test second recipient', + email: 'test-second-recipient@example.com', + role: RecipientRole.SIGNER, + }, + ], + }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient create-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/create-many`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipients: [ + { + name: 'Test first recipient', + email: 'test-first-recipient@example.com', + role: RecipientRole.SIGNER, + }, + { + name: 'Test second recipient', + email: 'test-second-recipient@example.com', + role: RecipientRole.SIGNER, + }, + ], + }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient update endpoint', () => { + test('should block unauthorized access to template recipient update endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipient: { + id: recipient.id, + name: 'Updated test name', + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient update endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipient: { + id: recipient.id, + name: 'Updated test name', + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient update-many endpoint', () => { + test('should block unauthorized access to template recipient update-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipients = await prisma.recipient.createManyAndReturn({ + data: [ + { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + { + envelopeId: template.id, + email: 'test2@example.com', + name: 'Test 2', + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], + }); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update-many`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipients: [ + { + id: recipients[0].id, + name: 'Updated test first recipient name', + }, + { + id: recipients[1].id, + name: 'Updated test second recipient name', + }, + ], + }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient update-many endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipients = await prisma.recipient.createManyAndReturn({ + data: [ + { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + { + envelopeId: template.id, + email: 'test2@example.com', + name: 'Test 2', + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], + }); + + const res = await request.post( + `${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/update-many`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipients: [ + { + id: recipients[0].id, + name: 'Updated test first recipient name', + }, + { + id: recipients[1].id, + name: 'Updated test second recipient name', + }, + ], + }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template recipient delete endpoint', () => { + test('should block unauthorized access to template recipient delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { recipientId: recipient.id }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template recipient delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + email: 'test@example.com', + name: 'Test', + role: RecipientRole.SIGNER, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { recipientId: recipient.id }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template list endpoint', () => { + test('should block unauthorized access to template list endpoint', async ({ request }) => { + await seedBlankTemplate(userA, teamA.id); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template`, { + headers: { Authorization: `Bearer ${tokenB}` }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + + const data = await res.json(); + expect(data.data.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(true); + }); + + test('should allow authorized access to template list endpoint', async ({ request }) => { + await seedBlankTemplate(userA, teamA.id); + + const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template`, { + headers: { Authorization: `Bearer ${tokenA}` }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + + const data = await res.json(); + expect(data.data.length).toBeGreaterThan(0); + expect(data.data.every((doc: { userId: number }) => doc.userId === userA.id)).toBe(true); + }); + }); + + test.describe('Template GET endpoint', () => { + test('should block unauthorized access to template get endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/template/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenB}` }, + }, + ); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template get endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.get( + `${WEBAPP_BASE_URL}/api/v2-beta/template/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + { + headers: { Authorization: `Bearer ${tokenA}` }, + }, + ); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template update endpoint', () => { + test('should block unauthorized access to template update endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/update`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + data: { + title: 'Updated template title', + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template update endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/update`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + data: { + title: 'Updated template title', + visibility: DocumentVisibility.MANAGER_AND_ABOVE, + }, + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template duplicate endpoint', () => { + test('should block unauthorized access to template duplicate endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/duplicate`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); + }); + + test('should allow authorized access to template duplicate endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/duplicate`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template delete endpoint', () => { + test('should block unauthorized access to template delete endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(500); + }); + + test('should allow authorized access to template delete endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); + }); + + test.describe('Template use endpoint', () => { + test('should block unauthorized access to template use endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const { user: firstRecipientUser } = await seedUser(); + const { user: secondRecipientUser } = await seedUser(); + + const updatedTemplate = await prisma.envelope.update({ + where: { id: template.id }, + data: { + recipients: { + create: [ + { + name: firstRecipientUser.name || '', + email: firstRecipientUser.email, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + { + name: secondRecipientUser.name || '', + email: secondRecipientUser.email, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], + }, + }, + include: { + recipients: true, + }, + }); + + const recipientAId = updatedTemplate.recipients.find( + (recipient) => recipient.email === firstRecipientUser.email, + )?.id; + const recipientBId = updatedTemplate.recipients.find( + (recipient) => recipient.email === secondRecipientUser.email, + )?.id; + + if (!recipientAId || !recipientBId) { + throw new Error('Recipient IDs not found'); + } + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), recipients: [ { - id: recipients[0].id, - name: 'Updated test first recipient name', - }, - { - id: recipients[1].id, - name: 'Updated test second recipient name', - }, - ], - }, - }, - ); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template recipient delete endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - email: 'test@example.com', - name: 'Test', - role: RecipientRole.SIGNER, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - }, - }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/recipient/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { recipientId: recipient.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template list endpoint', async ({ request }) => { - await seedBlankTemplate(userA, teamA.id); - - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(res.ok()).toBeTruthy(); - expect(res.status()).toBe(200); - - const data = await res.json(); - expect(data.data.every((doc: { userId: number }) => doc.userId !== userA.id)).toBe(true); - }); - - test('should block unauthorized access to template get endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.get(`${WEBAPP_BASE_URL}/api/v2-beta/template/${template.id}`, { - headers: { Authorization: `Bearer ${tokenB}` }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template update endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/update`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - data: { - title: 'Updated template title', - visibility: DocumentVisibility.MANAGER_AND_ABOVE, - }, - }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template duplicate endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/duplicate`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(500); - }); - - test('should block unauthorized access to template delete endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(500); - }); - - test('should block unauthorized access to template use endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const { user: firstRecipientUser } = await seedUser(); - const { user: secondRecipientUser } = await seedUser(); - - const updatedTemplate = await prisma.template.update({ - where: { id: template.id }, - data: { - recipients: { - create: [ - { - name: firstRecipientUser.name || '', + id: recipientAId, + name: firstRecipientUser.name, email: firstRecipientUser.email, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, + role: RecipientRole.SIGNER, }, { - name: secondRecipientUser.name || '', + id: recipientBId, + name: secondRecipientUser.name, email: secondRecipientUser.email, - token: nanoid(12), - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, + role: RecipientRole.SIGNER, }, ], }, - }, - include: { - recipients: true, - }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - const recipientAId = updatedTemplate.recipients.find( - (recipient) => recipient.email === firstRecipientUser.email, - )?.id; - const recipientBId = updatedTemplate.recipients.find( - (recipient) => recipient.email === secondRecipientUser.email, - )?.id; + test('should allow authorized access to template use endpoint', async ({ request }) => { + const template = await seedBlankTemplate(userA, teamA.id); - if (!recipientAId || !recipientBId) { - throw new Error('Recipient IDs not found'); - } + const { user: firstRecipientUser } = await seedUser(); + const { user: secondRecipientUser } = await seedUser(); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { - templateId: template.id, - recipients: [ - { - id: recipientAId, - name: firstRecipientUser.name, - email: firstRecipientUser.email, - role: RecipientRole.SIGNER, + const updatedTemplate = await prisma.envelope.update({ + where: { id: template.id }, + data: { + recipients: { + create: [ + { + name: firstRecipientUser.name || '', + email: firstRecipientUser.email, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + { + name: secondRecipientUser.name || '', + email: secondRecipientUser.email, + token: nanoid(12), + readStatus: ReadStatus.NOT_OPENED, + sendStatus: SendStatus.NOT_SENT, + signingStatus: SigningStatus.NOT_SIGNED, + }, + ], }, - { - id: recipientBId, - name: secondRecipientUser.name, - email: secondRecipientUser.email, - role: RecipientRole.SIGNER, - }, - ], - }, - }); + }, + include: { + recipients: true, + }, + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + const recipientAId = updatedTemplate.recipients.find( + (recipient) => recipient.email === firstRecipientUser.email, + )?.id; + const recipientBId = updatedTemplate.recipients.find( + (recipient) => recipient.email === secondRecipientUser.email, + )?.id; + + if (!recipientAId || !recipientBId) { + throw new Error('Recipient IDs not found'); + } + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/use`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { + templateId: mapSecondaryIdToTemplateId(template.secondaryId), + recipients: [ + { + id: recipientAId, + name: firstRecipientUser.name, + email: firstRecipientUser.email, + role: RecipientRole.SIGNER, + }, + { + id: recipientBId, + name: secondRecipientUser.name, + email: secondRecipientUser.email, + role: RecipientRole.SIGNER, + }, + ], + }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to template direct create endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); + test.describe('Template direct create endpoint', () => { + test('should block unauthorized access to template direct create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/create`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id }, + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/create`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + test('should allow authorized access to template direct create endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/create`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to template direct delete endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); + test.describe('Template direct delete endpoint', () => { + test('should block unauthorized access to template direct delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - name: 'Test', - email: 'test@example.com', - token: nanoid(12), - }, + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + name: 'Test', + email: 'test@example.com', + token: nanoid(12), + }, + }); + + await prisma.templateDirectLink.create({ + data: { + envelopeId: template.id, + enabled: true, + token: nanoid(12), + directTemplateRecipientId: recipient.id, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/delete`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - await prisma.templateDirectLink.create({ - data: { - templateId: template.id, - enabled: true, - token: nanoid(12), - directTemplateRecipientId: recipient.id, - }, - }); + test('should allow authorized access to template direct delete endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/delete`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id }, - }); + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + name: 'Test', + email: 'test@example.com', + token: nanoid(12), + }, + }); - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); + await prisma.templateDirectLink.create({ + data: { + envelopeId: template.id, + enabled: true, + token: nanoid(12), + directTemplateRecipientId: recipient.id, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/delete`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId) }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); + }); }); - test('should block unauthorized access to template direct toggle endpoint', async ({ - request, - }) => { - const template = await seedBlankTemplate(userA, teamA.id); + test.describe('Template direct toggle endpoint', () => { + test('should block unauthorized access to template direct toggle endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); - const recipient = await prisma.recipient.create({ - data: { - templateId: template.id, - name: 'Test', - email: 'test@example.com', - token: nanoid(12), - }, + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + name: 'Test', + email: 'test@example.com', + token: nanoid(12), + }, + }); + + await prisma.templateDirectLink.create({ + data: { + envelopeId: template.id, + enabled: true, + token: nanoid(12), + directTemplateRecipientId: recipient.id, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/toggle`, { + headers: { Authorization: `Bearer ${tokenB}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId), enabled: false }, + }); + + expect(res.ok()).toBeFalsy(); + expect(res.status()).toBe(404); }); - await prisma.templateDirectLink.create({ - data: { - templateId: template.id, - enabled: true, - token: nanoid(12), - directTemplateRecipientId: recipient.id, - }, + test('should allow authorized access to template direct toggle endpoint', async ({ + request, + }) => { + const template = await seedBlankTemplate(userA, teamA.id); + + const recipient = await prisma.recipient.create({ + data: { + envelopeId: template.id, + name: 'Test', + email: 'test@example.com', + token: nanoid(12), + }, + }); + + await prisma.templateDirectLink.create({ + data: { + envelopeId: template.id, + enabled: true, + token: nanoid(12), + directTemplateRecipientId: recipient.id, + }, + }); + + const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/toggle`, { + headers: { Authorization: `Bearer ${tokenA}` }, + data: { templateId: mapSecondaryIdToTemplateId(template.secondaryId), enabled: false }, + }); + + expect(res.ok()).toBeTruthy(); + expect(res.status()).toBe(200); }); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/direct/toggle`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id, enabled: false }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); - }); - - test('should block unauthorized access to template move endpoint', async ({ request }) => { - const template = await seedBlankTemplate(userA, teamA.id); - - const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/template/move`, { - headers: { Authorization: `Bearer ${tokenB}` }, - data: { templateId: template.id, teamId: teamB.id }, - }); - - expect(res.ok()).toBeFalsy(); - expect(res.status()).toBe(404); }); }); diff --git a/packages/app-tests/e2e/document-auth/access-auth.spec.ts b/packages/app-tests/e2e/document-auth/access-auth.spec.ts index 87783a1cf..c8a643e36 100644 --- a/packages/app-tests/e2e/document-auth/access-auth.spec.ts +++ b/packages/app-tests/e2e/document-auth/access-auth.spec.ts @@ -19,7 +19,7 @@ test('[DOCUMENT_AUTH]: should grant access when not required', async ({ page }) const recipients = await prisma.recipient.findMany({ where: { - documentId: document.id, + envelopeId: document.id, }, }); @@ -52,7 +52,7 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page const recipients = await prisma.recipient.findMany({ where: { - documentId: document.id, + envelopeId: document.id, }, }); diff --git a/packages/app-tests/e2e/document-auth/next-recipient-dictation.spec.ts b/packages/app-tests/e2e/document-auth/next-recipient-dictation.spec.ts index a925388df..dbf0106e6 100644 --- a/packages/app-tests/e2e/document-auth/next-recipient-dictation.spec.ts +++ b/packages/app-tests/e2e/document-auth/next-recipient-dictation.spec.ts @@ -85,7 +85,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dict await page.waitForURL(`${signUrl}/complete`); // Verify document and recipient states - const updatedDocument = await prisma.document.findUniqueOrThrow({ + const updatedDocument = await prisma.envelope.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { @@ -172,7 +172,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', a // Verify document and recipient states - const updatedDocument = await prisma.document.findUniqueOrThrow({ + const updatedDocument = await prisma.envelope.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { @@ -259,7 +259,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async // Verify final document and recipient states await expect(async () => { - const updatedDocument = await prisma.document.findUniqueOrThrow({ + const updatedDocument = await prisma.envelope.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { @@ -362,7 +362,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer' // Verify document and recipient states await expect(async () => { - const updatedDocument = await prisma.document.findUniqueOrThrow({ + const updatedDocument = await prisma.envelope.findUniqueOrThrow({ where: { id: document.id }, include: { recipients: { diff --git a/packages/app-tests/e2e/document-flow/settings-step.spec.ts b/packages/app-tests/e2e/document-flow/settings-step.spec.ts index 159f00bf1..aced16be3 100644 --- a/packages/app-tests/e2e/document-flow/settings-step.spec.ts +++ b/packages/app-tests/e2e/document-flow/settings-step.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { seedBlankDocument, seedDraftDocument, @@ -16,7 +17,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => { await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); // Set title. @@ -52,13 +53,15 @@ test('[DOCUMENT_FLOW]: title should be disabled depending on document status', a await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${pendingDocument.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(pendingDocument.secondaryId)}/edit`, }); // Should be disabled for pending documents. await expect(page.getByLabel('Title')).toBeDisabled(); // Should be enabled for draft documents. - await page.goto(`/t/${team.url}/documents/${draftDocument.id}/edit`); + await page.goto( + `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(draftDocument.secondaryId)}/edit`, + ); await expect(page.getByLabel('Title')).toBeEnabled(); }); diff --git a/packages/app-tests/e2e/document-flow/signers-step.spec.ts b/packages/app-tests/e2e/document-flow/signers-step.spec.ts index 8bde0d0b3..76b6a5ba5 100644 --- a/packages/app-tests/e2e/document-flow/signers-step.spec.ts +++ b/packages/app-tests/e2e/document-flow/signers-step.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { seedBlankDocument } from '@documenso/prisma/seed/documents'; import { seedUser } from '@documenso/prisma/seed/users'; @@ -12,7 +13,7 @@ test('[DOCUMENT_FLOW]: add signers', async ({ page }) => { await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); // Save the settings by going to the next step. diff --git a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts index 06251175e..ef5dd532d 100644 --- a/packages/app-tests/e2e/document-flow/stepper-component.spec.ts +++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts @@ -9,7 +9,10 @@ import { import { DateTime } from 'luxon'; import path from 'node:path'; -import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email'; +import { + mapDocumentIdToSecondaryId, + mapSecondaryIdToDocumentId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { seedBlankDocument, @@ -23,7 +26,7 @@ import { signSignaturePad } from '../fixtures/signature'; // Can't use the function in server-only/document due to it indirectly using // require imports. const getDocumentByToken = async (token: string) => { - return await prisma.document.findFirstOrThrow({ + return await prisma.envelope.findFirstOrThrow({ where: { recipients: { some: { @@ -69,7 +72,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) => await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); const documentTitle = `example-${Date.now()}.pdf`; @@ -130,7 +133,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); const documentTitle = `example-${Date.now()}.pdf`; @@ -215,7 +218,7 @@ test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipie await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); // Set title @@ -313,7 +316,7 @@ test('[DOCUMENT_FLOW]: should not be able to create a document without signature await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); const documentTitle = `example-${Date.now()}.pdf`; @@ -401,7 +404,7 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); const documentTitle = `example-${Date.now()}.pdf`; @@ -442,9 +445,13 @@ test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a const url = page.url().split('/'); const documentId = url[url.length - 1]; - const { token } = await getRecipientByEmail({ - email: 'user1@example.com', - documentId: Number(documentId), + const { token } = await prisma.recipient.findFirstOrThrow({ + where: { + envelope: { + secondaryId: mapDocumentIdToSecondaryId(Number(documentId)), + }, + email: 'user1@example.com', + }, }); await page.goto(`/sign/${token}`); @@ -500,7 +507,7 @@ test('[DOCUMENT_FLOW]: should be able to sign a document with custom date', asyn recipient: { email: 'user1@example.com', }, - documentId: Number(document.id), + envelopeId: document.id, }, }); @@ -525,7 +532,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); const documentTitle = `Sequential-Signing-${Date.now()}.pdf`; @@ -587,7 +594,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip await expect(page.getByRole('link', { name: documentTitle })).toBeVisible(); - const createdDocument = await prisma.document.findFirst({ + const createdDocument = await prisma.envelope.findFirst({ where: { title: documentTitle }, include: { recipients: true }, }); @@ -602,13 +609,13 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip expect(recipient).not.toBeNull(); const fields = await prisma.field.findMany({ - where: { recipientId: recipient?.id, documentId: createdDocument?.id }, + where: { recipientId: recipient?.id, envelopeId: createdDocument?.id }, }); const recipientField = fields[0]; if (i > 0) { const previousRecipient = await prisma.recipient.findFirst({ - where: { email: `user${i}@example.com`, documentId: createdDocument?.id }, + where: { email: `user${i}@example.com`, envelopeId: createdDocument?.id }, }); expect(previousRecipient?.signingStatus).toBe(SigningStatus.SIGNED); @@ -636,7 +643,7 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip // Wait for the document to be signed. await page.waitForTimeout(10000); - const finalDocument = await prisma.document.findFirst({ + const finalDocument = await prisma.envelope.findFirst({ where: { id: createdDocument?.id }, }); @@ -648,18 +655,20 @@ test('[DOCUMENT_FLOW]: should prevent out-of-order signing in sequential mode', }) => { const { user, team } = await seedUser(); - const { recipients } = await seedPendingDocumentWithFullFields({ + const { document, recipients } = await seedPendingDocumentWithFullFields({ teamId: team.id, owner: user, recipients: ['user1@example.com', 'user2@example.com', 'user3@example.com'], fields: [FieldType.SIGNATURE], recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }], - updateDocumentOptions: { - documentMeta: { - create: { - signingOrder: DocumentSigningOrder.SEQUENTIAL, - }, - }, + }); + + await prisma.documentMeta.update({ + where: { + id: document.documentMetaId, + }, + data: { + signingOrder: DocumentSigningOrder.SEQUENTIAL, }, }); diff --git a/packages/app-tests/e2e/documents/test-unauthorized-document-access.spec.ts b/packages/app-tests/e2e/documents/test-unauthorized-document-access.spec.ts index b574025b2..b2dfe0851 100644 --- a/packages/app-tests/e2e/documents/test-unauthorized-document-access.spec.ts +++ b/packages/app-tests/e2e/documents/test-unauthorized-document-access.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { seedBlankDocument, seedCompletedDocument, @@ -27,7 +28,9 @@ test.describe('Unauthorized Access to Documents', () => { redirectPath: `/t/${team.url}/documents`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); @@ -40,10 +43,12 @@ test.describe('Unauthorized Access to Documents', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}/edit`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); @@ -57,10 +62,12 @@ test.describe('Unauthorized Access to Documents', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/documents/${document.id}`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); @@ -74,10 +81,12 @@ test.describe('Unauthorized Access to Documents', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}/edit`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); @@ -91,10 +100,12 @@ test.describe('Unauthorized Access to Documents', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/documents/${document.id}`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${document.id}`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); }); diff --git a/packages/app-tests/e2e/features/include-document-certificate.spec.ts b/packages/app-tests/e2e/features/include-document-certificate.spec.ts index b447b09c2..2d1645f92 100644 --- a/packages/app-tests/e2e/features/include-document-certificate.spec.ts +++ b/packages/app-tests/e2e/features/include-document-certificate.spec.ts @@ -28,7 +28,9 @@ test.describe('Signing Certificate Tests', () => { const documentData = await prisma.documentData .findFirstOrThrow({ where: { - id: document.documentDataId, + envelopeItem: { + envelopeId: document.id, + }, }, }) .then(async (data) => getFile(data)); @@ -65,12 +67,21 @@ test.describe('Signing Certificate Tests', () => { await page.waitForTimeout(2500); // Get the completed document - const completedDocument = await prisma.document.findFirstOrThrow({ + const completedDocument = await prisma.envelope.findFirstOrThrow({ where: { id: document.id }, - include: { documentData: true }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + }, }); - const completedDocumentData = await getFile(completedDocument.documentData); + // Todo: Envelopes + const firstDocumentData = completedDocument.envelopeItems[0].documentData; + + const completedDocumentData = await getFile(firstDocumentData); // Load the PDF and check number of pages const pdfDoc = await PDFDocument.load(completedDocumentData); @@ -110,7 +121,9 @@ test.describe('Signing Certificate Tests', () => { const documentData = await prisma.documentData .findFirstOrThrow({ where: { - id: document.documentDataId, + envelopeItem: { + envelopeId: document.id, + }, }, }) .then(async (data) => getFile(data)); @@ -145,12 +158,21 @@ test.describe('Signing Certificate Tests', () => { await page.waitForTimeout(2500); // Get the completed document - const completedDocument = await prisma.document.findFirstOrThrow({ + const completedDocument = await prisma.envelope.findFirstOrThrow({ where: { id: document.id }, - include: { documentData: true }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + }, }); - const completedDocumentData = await getFile(completedDocument.documentData); + // Todo: Envelopes + const firstDocumentData = completedDocument.envelopeItems[0].documentData; + + const completedDocumentData = await getFile(firstDocumentData); // Load the PDF and check number of pages const completedPdf = await PDFDocument.load(completedDocumentData); @@ -190,7 +212,9 @@ test.describe('Signing Certificate Tests', () => { const documentData = await prisma.documentData .findFirstOrThrow({ where: { - id: document.documentDataId, + envelopeItem: { + envelopeId: document.id, + }, }, }) .then(async (data) => getFile(data)); @@ -225,12 +249,18 @@ test.describe('Signing Certificate Tests', () => { await page.waitForTimeout(2500); // Get the completed document - const completedDocument = await prisma.document.findFirstOrThrow({ + const completedDocument = await prisma.envelope.findFirstOrThrow({ where: { id: document.id }, - include: { documentData: true }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + }, }); - const completedDocumentData = await getFile(completedDocument.documentData); + const completedDocumentData = await getFile(completedDocument.envelopeItems[0].documentData); // Load the PDF and check number of pages const completedPdf = await PDFDocument.load(completedDocumentData); diff --git a/packages/app-tests/e2e/folders/team-account-folders.spec.ts b/packages/app-tests/e2e/folders/team-account-folders.spec.ts index 281848adb..2163113f0 100644 --- a/packages/app-tests/e2e/folders/team-account-folders.spec.ts +++ b/packages/app-tests/e2e/folders/team-account-folders.spec.ts @@ -383,12 +383,10 @@ test('[TEAMS]: can create a template inside a template folder', async ({ page }) await page.waitForTimeout(3000); - await page.getByRole('button', { name: 'Create' }).click(); - - await page.waitForTimeout(1000); - + // Expect redirect. await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); + // Return to folder and verify file is visible. await page.goto(`/t/${team.url}/templates/f/${folder.id}`); await expect(page.getByText('documenso-supporter-pledge.pdf')).toBeVisible(); }); diff --git a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts index f577ab3e1..795fec9ff 100644 --- a/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts +++ b/packages/app-tests/e2e/organisations/organisation-team-preferences.spec.ts @@ -96,7 +96,7 @@ test('[ORGANISATIONS]: manage document preferences', async ({ page }) => { const documentMeta = await prisma.documentMeta.findFirstOrThrow({ where: { - documentId: document.id, + id: document.documentMetaId, }, }); @@ -272,7 +272,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { const teamOverrideDocumentMeta = await prisma.documentMeta.findFirstOrThrow({ where: { - documentId: teamOverrideDocument.id, + id: teamOverrideDocument.documentMetaId, }, }); @@ -317,7 +317,7 @@ test('[ORGANISATIONS]: manage email preferences', async ({ page }) => { const documentMeta = await prisma.documentMeta.findFirstOrThrow({ where: { - documentId: document.id, + id: document.documentMetaId, }, }); diff --git a/packages/app-tests/e2e/teams/team-documents.spec.ts b/packages/app-tests/e2e/teams/team-documents.spec.ts index e547b0c40..7e173dd91 100644 --- a/packages/app-tests/e2e/teams/team-documents.spec.ts +++ b/packages/app-tests/e2e/teams/team-documents.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { seedBlankDocument, seedDocuments, @@ -750,7 +751,7 @@ test('[TEAMS]: check that ADMIN role can change document visibility', async ({ p await apiSignin({ page, email: adminUser.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); await page.getByTestId('documentVisibilitySelectValue').click(); @@ -784,7 +785,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of EVERYONE docum await apiSignin({ page, email: teamMember.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Everyone'); @@ -810,7 +811,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of MANAGER_AND_AB await apiSignin({ page, email: teamMember.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Managers and above'); @@ -836,7 +837,7 @@ test('[TEAMS]: check that MEMBER role cannot change visibility of ADMIN document await apiSignin({ page, email: teamMember.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only'); @@ -862,7 +863,7 @@ test('[TEAMS]: check that MANAGER role cannot change visibility of ADMIN documen await apiSignin({ page, email: teamManager.email, - redirectPath: `/t/${team.url}/documents/${document.id}/edit`, + redirectPath: `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, }); await expect(page.getByTestId('documentVisibilitySelectValue')).toHaveText('Admins only'); diff --git a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts index bebf91371..5af05f6e4 100644 --- a/packages/app-tests/e2e/teams/team-signature-settings.spec.ts +++ b/packages/app-tests/e2e/teams/team-signature-settings.spec.ts @@ -1,5 +1,9 @@ import { expect, test } from '@playwright/test'; +import { + mapSecondaryIdToDocumentId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { seedTeamDocumentWithMeta, @@ -21,7 +25,9 @@ test('[TEAMS]: check that default team signature settings are all enabled', asyn const document = await seedTeamDocumentWithMeta(team); // Create a document and check the settings - await page.goto(`/t/${team.url}/documents/${document.id}/edit`); + await page.goto( + `/t/${team.url}/documents/${mapSecondaryIdToDocumentId(document.secondaryId)}/edit`, + ); // Verify that the settings match await page.getByRole('button', { name: 'Advanced Options' }).click(); @@ -154,7 +160,7 @@ test('[TEAMS]: check signature modes work for templates', async ({ page }) => { const template = await seedTeamTemplateWithMeta(team); - await page.goto(`/t/${team.url}/templates/${template.id}`); + await page.goto(`/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`); await page.getByRole('button', { name: 'Use' }).click(); // Check the send document checkbox to true @@ -162,9 +168,10 @@ test('[TEAMS]: check signature modes work for templates', async ({ page }) => { await page.getByRole('button', { name: 'Create and send' }).click(); await page.waitForTimeout(1000); - const document = await prisma.document.findFirst({ + const document = await prisma.envelope.findFirst({ where: { - templateId: template.id, + // Created from template + templateId: mapSecondaryIdToTemplateId(template.secondaryId), }, include: { documentMeta: true, diff --git a/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts b/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts index f0a86347e..41d8a64ab 100644 --- a/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts +++ b/packages/app-tests/e2e/templates-flow/template-settings-step.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { TeamMemberRole } from '@prisma/client'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedUser } from '@documenso/prisma/seed/users'; @@ -14,7 +15,7 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => { await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set title. @@ -48,7 +49,7 @@ test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => { await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set document visibility. @@ -63,7 +64,9 @@ test('[TEMPLATE_FLOW] add document visibility settings', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Add Placeholders' })).toBeVisible(); // Navigate back to the edit page to check that the settings are saved correctly. - await page.goto(`/t/${team.url}/templates/${template.id}/edit`); + await page.goto( + `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, + ); await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); await expect(page.getByTestId('documentVisibilitySelectValue')).toContainText( @@ -96,7 +99,7 @@ test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => { await apiSignin({ page, email: managerUser.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Manager should be able to set visibility to managers and above @@ -115,11 +118,12 @@ test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => { await apiSignin({ page, email: memberUser.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); - // Regular member should not be able to modify visibility when set to managers and above - await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled(); + // A regular member should not be able to see the template. + // They should be redirected to the templates page. + expect(new URL(page.url()).pathname).toBe(`/t/${team.url}/templates`); // Create a new template with 'everyone' visibility const everyoneTemplate = await seedBlankTemplate(owner, team.id, { @@ -130,7 +134,9 @@ test('[TEMPLATE_FLOW] team member visibility permissions', async ({ page }) => { }); // Navigate to the new template - await page.goto(`/t/${team.url}/templates/${everyoneTemplate.id}/edit`); + await page.goto( + `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(everyoneTemplate.secondaryId)}/edit`, + ); // Regular member should be able to see but not modify visibility await expect(page.getByTestId('documentVisibilitySelectValue')).toBeDisabled(); diff --git a/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts b/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts index 2d7f118a7..4ec2f95a2 100644 --- a/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts +++ b/packages/app-tests/e2e/templates-flow/template-signers-step.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@playwright/test'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedUser } from '@documenso/prisma/seed/users'; @@ -29,7 +30,7 @@ import { apiSignin } from '../fixtures/authentication'; // await apiSignin({ // page, // email: user.email, -// redirectPath: `/templates/${template.id}/edit`, +// redirectPath: `/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, // }); // // Save the settings by going to the next step. @@ -79,7 +80,7 @@ test('[TEMPLATE_FLOW]: add placeholder', async ({ page }) => { await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Save the settings by going to the next step. diff --git a/packages/app-tests/e2e/templates/create-document-from-template.spec.ts b/packages/app-tests/e2e/templates/create-document-from-template.spec.ts index df5d1aa87..c4c52cb11 100644 --- a/packages/app-tests/e2e/templates/create-document-from-template.spec.ts +++ b/packages/app-tests/e2e/templates/create-document-from-template.spec.ts @@ -4,6 +4,10 @@ import fs from 'fs'; import path from 'path'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; +import { + mapDocumentIdToSecondaryId, + mapSecondaryIdToTemplateId, +} from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { seedTeam, seedTeamMember } from '@documenso/prisma/seed/teams'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; @@ -29,7 +33,7 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) => await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title. @@ -79,9 +83,9 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) => const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { recipients: true, @@ -132,7 +136,7 @@ test('[TEMPLATE]: should create a team document from a team template', async ({ await apiSignin({ page, email: owner.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title. @@ -182,9 +186,9 @@ test('[TEMPLATE]: should create a team document from a team template', async ({ const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { recipients: true, @@ -240,7 +244,7 @@ test('[TEMPLATE]: should create a document from a template with custom document' await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title @@ -288,30 +292,36 @@ test('[TEMPLATE]: should create a document from a template with custom document' const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { - documentData: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); + const firstDocumentData = document.envelopeItems[0].documentData; + const expectedDocumentDataType = process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT === 's3' ? DocumentDataType.S3_PATH : DocumentDataType.BYTES_64; expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC'); - expect(document.documentData.type).toEqual(expectedDocumentDataType); + expect(firstDocumentData.type).toEqual(expectedDocumentDataType); if (expectedDocumentDataType === DocumentDataType.BYTES_64) { - expect(document.documentData.data).toEqual(pdfContent); - expect(document.documentData.initialData).toEqual(pdfContent); + expect(firstDocumentData.data).toEqual(pdfContent); + expect(firstDocumentData.initialData).toEqual(pdfContent); } else { // For S3, we expect the data/initialData to be the S3 path (non-empty string) - expect(document.documentData.data).toBeTruthy(); - expect(document.documentData.initialData).toBeTruthy(); + expect(firstDocumentData.data).toBeTruthy(); + expect(firstDocumentData.initialData).toBeTruthy(); } }); @@ -333,7 +343,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu await apiSignin({ page, email: owner.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title @@ -381,12 +391,16 @@ test('[TEMPLATE]: should create a team document from a template with custom docu const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { - documentData: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); @@ -395,17 +409,19 @@ test('[TEMPLATE]: should create a team document from a template with custom docu ? DocumentDataType.S3_PATH : DocumentDataType.BYTES_64; + const firstDocumentData = document.envelopeItems[0].documentData; + expect(document.teamId).toEqual(team.id); expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC'); - expect(document.documentData.type).toEqual(expectedDocumentDataType); + expect(firstDocumentData.type).toEqual(expectedDocumentDataType); if (expectedDocumentDataType === DocumentDataType.BYTES_64) { - expect(document.documentData.data).toEqual(pdfContent); - expect(document.documentData.initialData).toEqual(pdfContent); + expect(firstDocumentData.data).toEqual(pdfContent); + expect(firstDocumentData.initialData).toEqual(pdfContent); } else { // For S3, we expect the data/initialData to be the S3 path (non-empty string) - expect(document.documentData.data).toBeTruthy(); - expect(document.documentData.initialData).toBeTruthy(); + expect(firstDocumentData.data).toBeTruthy(); + expect(firstDocumentData.initialData).toBeTruthy(); } }); @@ -422,7 +438,7 @@ test('[TEMPLATE]: should create a document from a template using template docume await apiSignin({ page, email: user.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title @@ -455,30 +471,40 @@ test('[TEMPLATE]: should create a document from a template using template docume const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { - documentData: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); - const templateWithData = await prisma.template.findFirstOrThrow({ + const firstDocumentData = document.envelopeItems[0].documentData; + + const templateWithData = await prisma.envelope.findFirstOrThrow({ where: { id: template.id, }, include: { - templateDocumentData: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC'); - expect(document.documentData.data).toEqual(templateWithData.templateDocumentData.data); - expect(document.documentData.initialData).toEqual( - templateWithData.templateDocumentData.initialData, + expect(firstDocumentData.data).toEqual(templateWithData.envelopeItems[0].documentData.data); + expect(firstDocumentData.initialData).toEqual( + templateWithData.envelopeItems[0].documentData.initialData, ); - expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type); + expect(firstDocumentData.type).toEqual(templateWithData.envelopeItems[0].documentData.type); }); test('[TEMPLATE]: should persist document visibility when creating from template', async ({ @@ -493,7 +519,7 @@ test('[TEMPLATE]: should persist document visibility when creating from template await apiSignin({ page, email: owner.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); // Set template title and visibility @@ -536,9 +562,16 @@ test('[TEMPLATE]: should persist document visibility when creating from template const documentId = Number(page.url().split('/').pop()); - const document = await prisma.document.findFirstOrThrow({ + const document = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + secondaryId: mapDocumentIdToSecondaryId(documentId), + }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); diff --git a/packages/app-tests/e2e/templates/direct-templates.spec.ts b/packages/app-tests/e2e/templates/direct-templates.spec.ts index 80adfc34d..1c5658bf1 100644 --- a/packages/app-tests/e2e/templates/direct-templates.spec.ts +++ b/packages/app-tests/e2e/templates/direct-templates.spec.ts @@ -3,6 +3,7 @@ import { customAlphabet } from 'nanoid'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates'; @@ -34,7 +35,7 @@ test('[DIRECT_TEMPLATES]: create direct link for template', async ({ page }) => redirectPath: `/t/${team.url}/templates`, }); - const url = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${teamTemplate.id}`; + const url = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(teamTemplate.secondaryId)}`; // Owner should see list of templates with no direct link badge. await page.goto(url); diff --git a/packages/app-tests/e2e/templates/test-unauthorized-template-access.spec.ts b/packages/app-tests/e2e/templates/test-unauthorized-template-access.spec.ts index 33e38bca8..3c7747883 100644 --- a/packages/app-tests/e2e/templates/test-unauthorized-template-access.spec.ts +++ b/packages/app-tests/e2e/templates/test-unauthorized-template-access.spec.ts @@ -1,6 +1,7 @@ import { expect, test } from '@playwright/test'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { seedBlankTemplate } from '@documenso/prisma/seed/templates'; import { seedUser } from '@documenso/prisma/seed/users'; @@ -20,10 +21,12 @@ test.describe('Unauthorized Access to Templates', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/templates/${template.id}`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${template.id}`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); @@ -36,10 +39,12 @@ test.describe('Unauthorized Access to Templates', () => { await apiSignin({ page, email: unauthorizedUser.email, - redirectPath: `/t/${team.url}/templates/${template.id}/edit`, + redirectPath: `/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, }); - await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${template.id}/edit`); + await page.goto( + `${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates/${mapSecondaryIdToTemplateId(template.secondaryId)}/edit`, + ); await expect(page.getByRole('heading', { name: 'Oops! Something went wrong.' })).toBeVisible(); }); }); diff --git a/packages/ee/server-only/limits/server.ts b/packages/ee/server-only/limits/server.ts index fcaa805f7..883146e0c 100644 --- a/packages/ee/server-only/limits/server.ts +++ b/packages/ee/server-only/limits/server.ts @@ -1,4 +1,4 @@ -import { DocumentSource, SubscriptionStatus } from '@prisma/client'; +import { DocumentSource, EnvelopeType, SubscriptionStatus } from '@prisma/client'; import { DateTime } from 'luxon'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; @@ -84,8 +84,9 @@ export const getServerLimits = async ({ } const [documents, directTemplates] = await Promise.all([ - prisma.document.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.DOCUMENT, team: { organisationId: organisation.id, }, @@ -97,8 +98,9 @@ export const getServerLimits = async ({ }, }, }), - prisma.template.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.TEMPLATE, team: { organisationId: organisation.id, }, diff --git a/packages/lib/client-only/hooks/use-copy-share-link.ts b/packages/lib/client-only/hooks/use-copy-share-link.ts index 4638e4e49..a8bef34af 100644 --- a/packages/lib/client-only/hooks/use-copy-share-link.ts +++ b/packages/lib/client-only/hooks/use-copy-share-link.ts @@ -1,5 +1,5 @@ import { trpc } from '@documenso/trpc/react'; -import type { TCreateOrGetShareLinkMutationSchema } from '@documenso/trpc/server/share-link-router/schema'; +import type { TShareDocumentRequest } from '@documenso/trpc/server/document-router/share-document.types'; import { useCopyToClipboard } from './use-copy-to-clipboard'; @@ -12,14 +12,14 @@ export function useCopyShareLink({ onSuccess, onError }: UseCopyShareLinkOptions const [, copyToClipboard] = useCopyToClipboard(); const { mutateAsync: createOrGetShareLink, isPending: isCreatingShareLink } = - trpc.shareLink.createOrGetShareLink.useMutation(); + trpc.document.share.useMutation(); /** * Copy a newly created, or pre-existing share link to the user's clipboard. * * @param payload The payload to create or get a share link. */ - const createAndCopyShareLink = async (payload: TCreateOrGetShareLinkMutationSchema) => { + const createAndCopyShareLink = async (payload: TShareDocumentRequest) => { const valueToCopy = createOrGetShareLink(payload).then( (result) => `${window.location.origin}/share/${result.slug}`, ); diff --git a/packages/lib/client-only/hooks/use-field-page-coords.ts b/packages/lib/client-only/hooks/use-field-page-coords.ts index f490939b8..e212e7328 100644 --- a/packages/lib/client-only/hooks/use-field-page-coords.ts +++ b/packages/lib/client-only/hooks/use-field-page-coords.ts @@ -5,7 +5,9 @@ import type { Field } from '@prisma/client'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; -export const useFieldPageCoords = (field: Field) => { +export const useFieldPageCoords = ( + field: Pick, +) => { const [coords, setCoords] = useState({ x: 0, y: 0, diff --git a/packages/lib/constants/teams.ts b/packages/lib/constants/teams.ts index 3a2fecbc1..8d7ffb768 100644 --- a/packages/lib/constants/teams.ts +++ b/packages/lib/constants/teams.ts @@ -1,4 +1,4 @@ -import { OrganisationGroupType, TeamMemberRole } from '@prisma/client'; +import { DocumentVisibility, OrganisationGroupType, TeamMemberRole } from '@prisma/client'; export const TEAM_URL_ROOT_REGEX = new RegExp('^/t/[^/]+/?$'); export const TEAM_URL_REGEX = new RegExp('^/t/[^/]+'); @@ -33,6 +33,16 @@ export const TEAM_MEMBER_ROLE_PERMISSIONS_MAP = { MANAGE_TEAM: [TeamMemberRole.ADMIN, TeamMemberRole.MANAGER], } satisfies Record; +export const TEAM_DOCUMENT_VISIBILITY_MAP = { + [TeamMemberRole.ADMIN]: [ + DocumentVisibility.ADMIN, + DocumentVisibility.MANAGER_AND_ABOVE, + DocumentVisibility.EVERYONE, + ], + [TeamMemberRole.MANAGER]: [DocumentVisibility.MANAGER_AND_ABOVE, DocumentVisibility.EVERYONE], + [TeamMemberRole.MEMBER]: [DocumentVisibility.EVERYONE], +} satisfies Record; + /** * A hierarchy of team member roles to determine which role has higher permission than another. * diff --git a/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts index c17d7fc48..35beaee67 100644 --- a/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts +++ b/packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import { ReadStatus, SendStatus, SigningStatus } from '@prisma/client'; +import { EnvelopeType, ReadStatus, SendStatus, SigningStatus } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; @@ -11,6 +11,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; import { getEmailContext } from '../../../server-only/email/get-email-context'; import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; +import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope'; import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; import type { JobRunIO } from '../../client/_internal/job'; import type { TSendDocumentCancelledEmailsJobDefinition } from './send-document-cancelled-emails'; @@ -24,10 +25,14 @@ export const run = async ({ }) => { const { documentId, cancellationReason } = payload; - const document = await prisma.document.findFirstOrThrow({ - where: { - id: documentId, - }, + const envelope = await prisma.envelope.findFirstOrThrow({ + where: unsafeBuildEnvelopeIdQuery( + { + type: 'documentId', + id: documentId, + }, + EnvelopeType.DOCUMENT, + ), include: { user: { select: { @@ -52,12 +57,12 @@ export const run = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); - const { documentMeta, user: documentOwner } = document; + const { documentMeta, user: documentOwner } = envelope; // Check if document cancellation emails are enabled const isEmailEnabled = extractDerivedDocumentEmailSettings(documentMeta).documentDeleted; @@ -69,7 +74,7 @@ export const run = async ({ const i18n = await getI18nInstance(emailLanguage); // Send cancellation emails to all recipients who have been sent the document or viewed it - const recipientsToNotify = document.recipients.filter( + const recipientsToNotify = envelope.recipients.filter( (recipient) => (recipient.sendStatus === SendStatus.SENT || recipient.readStatus === ReadStatus.OPENED) && recipient.signingStatus !== SigningStatus.REJECTED, @@ -79,7 +84,7 @@ export const run = async ({ await Promise.all( recipientsToNotify.map(async (recipient) => { const template = createElement(DocumentCancelTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: documentOwner.name || undefined, inviterEmail: documentOwner.email, assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(), @@ -102,7 +107,7 @@ export const run = async ({ }, from: senderEmail, replyTo: replyToEmail, - subject: i18n._(msg`Document "${document.title}" Cancelled`), + subject: i18n._(msg`Document "${envelope.title}" Cancelled`), html, text, }); diff --git a/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts b/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts index 7f201f0ef..66791ee29 100644 --- a/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts +++ b/packages/lib/jobs/definitions/emails/send-recipient-signed-email.handler.ts @@ -1,6 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; +import { EnvelopeType } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import { DocumentRecipientSignedEmailTemplate } from '@documenso/email/templates/document-recipient-signed'; @@ -10,6 +11,7 @@ import { getI18nInstance } from '../../../client-only/providers/i18n-server'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; import { getEmailContext } from '../../../server-only/email/get-email-context'; import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; +import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope'; import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; import type { JobRunIO } from '../../client/_internal/job'; import type { TSendRecipientSignedEmailJobDefinition } from './send-recipient-signed-email'; @@ -23,9 +25,15 @@ export const run = async ({ }) => { const { documentId, recipientId } = payload; - const document = await prisma.document.findFirst({ + const envelope = await prisma.envelope.findFirst({ where: { - id: documentId, + ...unsafeBuildEnvelopeIdQuery( + { + type: 'documentId', + id: documentId, + }, + EnvelopeType.DOCUMENT, + ), recipients: { some: { id: recipientId, @@ -49,25 +57,25 @@ export const run = async ({ }, }); - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - if (document.recipients.length === 0) { + if (envelope.recipients.length === 0) { throw new Error('Document has no recipients'); } const isRecipientSignedEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientSigned; if (!isRecipientSignedEmailEnabled) { return; } - const [recipient] = document.recipients; + const [recipient] = envelope.recipients; const { email: recipientEmail, name: recipientName } = recipient; - const { user: owner } = document; + const { user: owner } = envelope; const recipientReference = recipientName || recipientEmail; @@ -80,9 +88,9 @@ export const run = async ({ emailType: 'INTERNAL', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; @@ -90,7 +98,7 @@ export const run = async ({ const i18n = await getI18nInstance(emailLanguage); const template = createElement(DocumentRecipientSignedEmailTemplate, { - documentName: document.title, + documentName: envelope.title, recipientName, recipientEmail, assetBaseUrl, @@ -112,7 +120,7 @@ export const run = async ({ address: owner.email, }, from: senderEmail, - subject: i18n._(msg`${recipientReference} has signed "${document.title}"`), + subject: i18n._(msg`${recipientReference} has signed "${envelope.title}"`), html, text, }); diff --git a/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts b/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts index f162ca134..25ee6402e 100644 --- a/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts +++ b/packages/lib/jobs/definitions/emails/send-rejection-emails.handler.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import { SendStatus, SigningStatus } from '@prisma/client'; +import { EnvelopeType, SendStatus, SigningStatus } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import DocumentRejectedEmail from '@documenso/email/templates/document-rejected'; @@ -13,6 +13,7 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../../constants/app'; import { DOCUMENSO_INTERNAL_EMAIL } from '../../../constants/email'; import { getEmailContext } from '../../../server-only/email/get-email-context'; import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; +import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope'; import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; import { formatDocumentsPath } from '../../../utils/teams'; import type { JobRunIO } from '../../client/_internal/job'; @@ -27,11 +28,15 @@ export const run = async ({ }) => { const { documentId, recipientId } = payload; - const [document, recipient] = await Promise.all([ - prisma.document.findFirstOrThrow({ - where: { - id: documentId, - }, + const [envelope, recipient] = await Promise.all([ + prisma.envelope.findFirstOrThrow({ + where: unsafeBuildEnvelopeIdQuery( + { + type: 'documentId', + id: documentId, + }, + EnvelopeType.DOCUMENT, + ), include: { user: { select: { @@ -58,10 +63,10 @@ export const run = async ({ }), ]); - const { user: documentOwner } = document; + const { user: documentOwner } = envelope; const isEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientSigningRequest; if (!isEmailEnabled) { @@ -72,9 +77,9 @@ export const run = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); const i18n = await getI18nInstance(emailLanguage); @@ -83,8 +88,8 @@ export const run = async ({ await io.runTask('send-rejection-confirmation-email', async () => { const recipientTemplate = createElement(DocumentRejectionConfirmedEmail, { recipientName: recipient.name, - documentName: document.title, - documentOwnerName: document.user.name || document.user.email, + documentName: envelope.title, + documentOwnerName: envelope.user.name || envelope.user.email, reason: recipient.rejectionReason || '', assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(), }); @@ -105,7 +110,7 @@ export const run = async ({ }, from: senderEmail, replyTo: replyToEmail, - subject: i18n._(msg`Document "${document.title}" - Rejection Confirmed`), + subject: i18n._(msg`Document "${envelope.title}" - Rejection Confirmed`), html, text, }); @@ -115,9 +120,9 @@ export const run = async ({ await io.runTask('send-owner-notification-email', async () => { const ownerTemplate = createElement(DocumentRejectedEmail, { recipientName: recipient.name, - documentName: document.title, - documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${ - document.id + documentName: envelope.title, + documentUrl: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(envelope.team?.url)}/${ + envelope.id }`, rejectionReason: recipient.rejectionReason || '', assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL(), @@ -138,7 +143,7 @@ export const run = async ({ address: documentOwner.email, }, from: DOCUMENSO_INTERNAL_EMAIL, // Purposefully using internal email here. - subject: i18n._(msg`Document "${document.title}" - Rejected by ${recipient.name}`), + subject: i18n._(msg`Document "${envelope.title}" - Rejected by ${recipient.name}`), html, text, }); diff --git a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts index 7d62ed1d2..58b9b1f80 100644 --- a/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts +++ b/packages/lib/jobs/definitions/emails/send-signing-email.handler.ts @@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro'; import { DocumentSource, DocumentStatus, + EnvelopeType, OrganisationType, RecipientRole, SendStatus, @@ -23,6 +24,7 @@ import { getEmailContext } from '../../../server-only/email/get-email-context'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs'; import { extractDerivedDocumentEmailSettings } from '../../../types/document-email'; import { createDocumentAuditLogData } from '../../../utils/document-audit-logs'; +import { unsafeBuildEnvelopeIdQuery } from '../../../utils/envelope'; import { renderCustomEmailTemplate } from '../../../utils/render-custom-email-template'; import { renderEmailWithI18N } from '../../../utils/render-email-with-i18n'; import type { JobRunIO } from '../../client/_internal/job'; @@ -37,7 +39,7 @@ export const run = async ({ }) => { const { userId, documentId, recipientId, requestMetadata } = payload; - const [user, document, recipient] = await Promise.all([ + const [user, envelope, recipient] = await Promise.all([ prisma.user.findFirstOrThrow({ where: { id: userId, @@ -48,9 +50,15 @@ export const run = async ({ name: true, }, }), - prisma.document.findFirstOrThrow({ + prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + ...unsafeBuildEnvelopeIdQuery( + { + type: 'documentId', + id: documentId, + }, + EnvelopeType.DOCUMENT, + ), status: DocumentStatus.PENDING, }, include: { @@ -70,14 +78,14 @@ export const run = async ({ }), ]); - const { documentMeta, team } = document; + const { documentMeta, team } = envelope; if (recipient.role === RecipientRole.CC) { return; } const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientSigningRequest; if (!isRecipientSigningRequestEmailEnabled) { @@ -89,13 +97,13 @@ export const run = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); - const customEmail = document?.documentMeta; - const isDirectTemplate = document.source === DocumentSource.TEMPLATE_DIRECT_LINK; + const customEmail = envelope?.documentMeta; + const isDirectTemplate = envelope.source === DocumentSource.TEMPLATE_DIRECT_LINK; const recipientEmailType = RECIPIENT_ROLE_TO_EMAIL_TYPE[recipient.role]; @@ -113,7 +121,7 @@ export const run = async ({ if (selfSigner) { emailMessage = i18n._( - msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`, + msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`, ); emailSubject = i18n._(msg`Please ${recipientActionVerb} your document`); } @@ -136,8 +144,8 @@ export const run = async ({ emailMessage = i18n._( settings.includeSenderDetails - ? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".` - : msg`${team.name} has invited you to ${recipientActionVerb} the document "${document.title}".`, + ? msg`${inviterName} on behalf of "${team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".` + : msg`${team.name} has invited you to ${recipientActionVerb} the document "${envelope.title}".`, ); } } @@ -145,14 +153,14 @@ export const run = async ({ const customEmailTemplate = { 'signer.name': name, 'signer.email': email, - 'document.name': document.title, + 'document.name': envelope.title, }; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`; const template = createElement(DocumentInviteEmailTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: user.name || undefined, inviterEmail: organisationType === OrganisationType.ORGANISATION @@ -210,7 +218,7 @@ export const run = async ({ await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, + envelopeId: envelope.id, user, requestMetadata, data: { diff --git a/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts b/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts index 98e6daba9..c7b597b7e 100644 --- a/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts +++ b/packages/lib/jobs/definitions/internal/bulk-send-template.handler.ts @@ -99,9 +99,12 @@ export const run = async ({ } } - const document = await io.runTask(`create-document-${rowIndex}`, async () => { + const envelope = await io.runTask(`create-document-${rowIndex}`, async () => { return await createDocumentFromTemplate({ - templateId: template.id, + id: { + type: 'templateId', + id: template.id, + }, userId, teamId, recipients: recipients.map((recipient, index) => { @@ -124,7 +127,10 @@ export const run = async ({ if (sendImmediately) { await io.runTask(`send-document-${rowIndex}`, async () => { await sendDocument({ - documentId: document.id, + id: { + type: 'envelopeId', + id: envelope.id, + }, userId, teamId, requestMetadata: { diff --git a/packages/lib/jobs/definitions/internal/seal-document.handler.ts b/packages/lib/jobs/definitions/internal/seal-document.handler.ts index 8ee0382ba..78e3f56ff 100644 --- a/packages/lib/jobs/definitions/internal/seal-document.handler.ts +++ b/packages/lib/jobs/definitions/internal/seal-document.handler.ts @@ -1,4 +1,10 @@ -import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client'; +import { + DocumentStatus, + EnvelopeType, + RecipientRole, + SigningStatus, + WebhookTriggerEvents, +} from '@prisma/client'; import { nanoid } from 'nanoid'; import path from 'node:path'; import { PDFDocument } from 'pdf-lib'; @@ -22,7 +28,7 @@ import { triggerWebhook } from '../../../server-only/webhooks/trigger/trigger-we import { DOCUMENT_AUDIT_LOG_TYPE } from '../../../types/document-audit-logs'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../../types/webhook-payload'; import { prefixedId } from '../../../universal/id'; import { getFileServerSide } from '../../../universal/upload/get-file.server'; @@ -30,6 +36,7 @@ import { putPdfFileServerSide } from '../../../universal/upload/put-file.server' import { fieldsContainUnsignedRequiredField } from '../../../utils/advanced-fields-helpers'; import { isDocumentCompleted } from '../../../utils/document'; import { createDocumentAuditLogData } from '../../../utils/document-audit-logs'; +import { mapDocumentIdToSecondaryId } from '../../../utils/envelope'; import type { JobRunIO } from '../../client/_internal/job'; import type { TSealDocumentJobDefinition } from './seal-document'; @@ -42,24 +49,35 @@ export const run = async ({ }) => { const { documentId, sendEmail = true, isResealing = false, requestMetadata } = payload; - const document = await prisma.document.findFirstOrThrow({ + const envelope = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + type: EnvelopeType.DOCUMENT, + secondaryId: mapDocumentIdToSecondaryId(documentId), }, include: { documentMeta: true, recipients: true, + envelopeItems: { + select: { + documentDataId: true, + }, + }, }, }); + // Todo: Envelopes + if (envelope.envelopeItems.length !== 1) { + throw new Error('1 Envelope item required'); + } + const settings = await getTeamSettings({ - userId: document.userId, - teamId: document.teamId, + userId: envelope.userId, + teamId: envelope.teamId, }); const isComplete = - document.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) || - document.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED); + envelope.recipients.some((recipient) => recipient.signingStatus === SigningStatus.REJECTED) || + envelope.recipients.every((recipient) => recipient.signingStatus === SigningStatus.SIGNED); if (!isComplete) { throw new AppError(AppErrorCode.UNKNOWN_ERROR, { @@ -71,13 +89,13 @@ export const run = async ({ // after it has already run through the update task further below. // eslint-disable-next-line @typescript-eslint/require-await const documentStatus = await io.runTask('get-document-status', async () => { - return document.status; + return envelope.status; }); // This is the same case as above. // eslint-disable-next-line @typescript-eslint/require-await const documentDataId = await io.runTask('get-document-data-id', async () => { - return document.documentDataId; + return envelope.envelopeItems[0].documentDataId; }); const documentData = await prisma.documentData.findFirst({ @@ -87,12 +105,12 @@ export const run = async ({ }); if (!documentData) { - throw new Error(`Document ${document.id} has no document data`); + throw new Error(`Document ${envelope.id} has no document data`); } const recipients = await prisma.recipient.findMany({ where: { - documentId: document.id, + envelopeId: envelope.id, role: { not: RecipientRole.CC, }, @@ -111,7 +129,7 @@ export const run = async ({ const fields = await prisma.field.findMany({ where: { - documentId: document.id, + envelopeId: envelope.id, }, include: { signature: true, @@ -120,7 +138,7 @@ export const run = async ({ // Skip the field check if the document is rejected if (!isRejected && fieldsContainUnsignedRequiredField(fields)) { - throw new Error(`Document ${document.id} has unsigned required fields`); + throw new Error(`Document ${envelope.id} has unsigned required fields`); } if (isResealing) { @@ -129,10 +147,10 @@ export const run = async ({ documentData.data = documentData.initialData; } - if (!document.qrToken) { - await prisma.document.update({ + if (!envelope.qrToken) { + await prisma.envelope.update({ where: { - id: document.id, + id: envelope.id, }, data: { qrToken: prefixedId('qr'), @@ -145,7 +163,7 @@ export const run = async ({ const certificateData = settings.includeSigningCertificate ? await getCertificatePdf({ documentId, - language: document.documentMeta?.language, + language: envelope.documentMeta?.language, }).catch((e) => { console.log('Failed to get certificate PDF'); console.error(e); @@ -157,7 +175,7 @@ export const run = async ({ const auditLogData = settings.includeAuditLog ? await getAuditLogsPdf({ documentId, - language: document.documentMeta?.language, + language: envelope.documentMeta?.language, }).catch((e) => { console.log('Failed to get audit logs PDF'); console.error(e); @@ -204,7 +222,7 @@ export const run = async ({ for (const field of fields) { if (field.inserted) { - document.useLegacyFieldInsertion + envelope.useLegacyFieldInsertion ? await legacy_insertFieldInPDF(pdfDoc, field) : await insertFieldInPDF(pdfDoc, field); } @@ -217,7 +235,8 @@ export const run = async ({ const pdfBytes = await pdfDoc.save(); const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) }); - const { name } = path.parse(document.title); + // Todo: Envelopes - Use the envelope item title instead. + const { name } = path.parse(envelope.title); // Add suffix based on document status const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf'; @@ -238,7 +257,7 @@ export const run = async ({ distinctId: nanoid(), event: 'App: Document Sealed', properties: { - documentId: document.id, + documentId: envelope.id, isRejected, }, }); @@ -252,9 +271,9 @@ export const run = async ({ }, }); - await tx.document.update({ + await tx.envelope.update({ where: { - id: document.id, + id: envelope.id, }, data: { status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED, @@ -274,7 +293,7 @@ export const run = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, - documentId: document.id, + envelopeId: envelope.id, requestMetadata, user: null, data: { @@ -289,21 +308,23 @@ export const run = async ({ await io.runTask('send-completed-email', async () => { let shouldSendCompletedEmail = sendEmail && !isResealing && !isRejected; - if (isResealing && !isDocumentCompleted(document.status)) { + if (isResealing && !isDocumentCompleted(envelope.status)) { shouldSendCompletedEmail = sendEmail; } if (shouldSendCompletedEmail) { - await sendCompletedEmail({ documentId, requestMetadata }); + await sendCompletedEmail({ + id: { type: 'envelopeId', id: envelope.id }, + requestMetadata, + }); } }); - const updatedDocument = await prisma.document.findFirstOrThrow({ + const updatedEnvelope = await prisma.envelope.findFirstOrThrow({ where: { - id: document.id, + id: envelope.id, }, include: { - documentData: true, documentMeta: true, recipients: true, }, @@ -313,8 +334,8 @@ export const run = async ({ event: isRejected ? WebhookTriggerEvents.DOCUMENT_REJECTED : WebhookTriggerEvents.DOCUMENT_COMPLETED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)), - userId: updatedDocument.userId, - teamId: updatedDocument.teamId ?? undefined, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)), + userId: updatedEnvelope.userId, + teamId: updatedEnvelope.teamId ?? undefined, }); }; diff --git a/packages/lib/server-only/admin/get-all-documents.ts b/packages/lib/server-only/admin/admin-find-documents.ts similarity index 64% rename from packages/lib/server-only/admin/get-all-documents.ts rename to packages/lib/server-only/admin/admin-find-documents.ts index d31dd6785..1ae9901d5 100644 --- a/packages/lib/server-only/admin/get-all-documents.ts +++ b/packages/lib/server-only/admin/admin-find-documents.ts @@ -1,17 +1,21 @@ -import type { Prisma } from '@prisma/client'; +import { EnvelopeType, type Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; import type { FindResultResponse } from '../../types/search-params'; -export interface FindDocumentsOptions { +export interface AdminFindDocumentsOptions { query?: string; page?: number; perPage?: number; } -export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocumentsOptions) => { - const termFilters: Prisma.DocumentWhereInput | undefined = !query +export const adminFindDocuments = async ({ + query, + page = 1, + perPage = 10, +}: AdminFindDocumentsOptions) => { + const termFilters: Prisma.EnvelopeWhereInput | undefined = !query ? undefined : { title: { @@ -21,8 +25,9 @@ export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocum }; const [data, count] = await Promise.all([ - prisma.document.findMany({ + prisma.envelope.findMany({ where: { + type: EnvelopeType.DOCUMENT, ...termFilters, }, skip: Math.max(page - 1, 0) * perPage, @@ -39,10 +44,17 @@ export const findDocuments = async ({ query, page = 1, perPage = 10 }: FindDocum }, }, recipients: true, + team: { + select: { + id: true, + url: true, + }, + }, }, }), - prisma.document.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.DOCUMENT, ...termFilters, }, }), diff --git a/packages/lib/server-only/document/super-delete-document.ts b/packages/lib/server-only/admin/admin-super-delete-document.ts similarity index 81% rename from packages/lib/server-only/document/super-delete-document.ts rename to packages/lib/server-only/admin/admin-super-delete-document.ts index ab6321c5e..8fe77bc7a 100644 --- a/packages/lib/server-only/document/super-delete-document.ts +++ b/packages/lib/server-only/admin/admin-super-delete-document.ts @@ -17,15 +17,18 @@ import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; -export type SuperDeleteDocumentOptions = { - id: number; +export type AdminSuperDeleteDocumentOptions = { + envelopeId: string; requestMetadata?: RequestMetadata; }; -export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDocumentOptions) => { - const document = await prisma.document.findUnique({ +export const adminSuperDeleteDocument = async ({ + envelopeId, + requestMetadata, +}: AdminSuperDeleteDocumentOptions) => { + const envelope = await prisma.envelope.findUnique({ where: { - id, + id: envelopeId, }, include: { recipients: true, @@ -40,7 +43,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); @@ -50,38 +53,38 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); - const { status, user } = document; + const { status, user } = envelope; const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).documentDeleted; // if the document is pending, send cancellation emails to all recipients if ( status === DocumentStatus.PENDING && - document.recipients.length > 0 && + envelope.recipients.length > 0 && isDocumentDeletedEmailEnabled ) { await Promise.all( - document.recipients.map(async (recipient) => { + envelope.recipients.map(async (recipient) => { if (recipient.sendStatus !== SendStatus.SENT) { return; } const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(DocumentCancelTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: user.name || undefined, inviterEmail: user.email, assetBaseUrl, }); - const lang = document.documentMeta?.language ?? settings.documentLanguage; + const lang = envelope.documentMeta?.language ?? settings.documentLanguage; const [html, text] = await Promise.all([ renderEmailWithI18N(template, { lang, branding }), @@ -113,7 +116,7 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo return await prisma.$transaction(async (tx) => { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId: id, + envelopeId, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, user, requestMetadata, @@ -123,6 +126,6 @@ export const superDeleteDocument = async ({ id, requestMetadata }: SuperDeleteDo }), }); - return await tx.document.delete({ where: { id } }); + return await tx.envelope.delete({ where: { id: envelopeId } }); }); }; diff --git a/packages/lib/server-only/admin/get-documents-stats.ts b/packages/lib/server-only/admin/get-documents-stats.ts index e91344f34..08e7bf93a 100644 --- a/packages/lib/server-only/admin/get-documents-stats.ts +++ b/packages/lib/server-only/admin/get-documents-stats.ts @@ -1,8 +1,13 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; export const getDocumentStats = async () => { - const counts = await prisma.document.groupBy({ + const counts = await prisma.envelope.groupBy({ + where: { + type: EnvelopeType.DOCUMENT, + }, by: ['status'], _count: { _all: true, diff --git a/packages/lib/server-only/admin/get-entire-document.ts b/packages/lib/server-only/admin/get-entire-document.ts index c50bdb5c4..ea350a4b6 100644 --- a/packages/lib/server-only/admin/get-entire-document.ts +++ b/packages/lib/server-only/admin/get-entire-document.ts @@ -1,14 +1,22 @@ +import type { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; -export type GetEntireDocumentOptions = { - id: number; +import { AppError, AppErrorCode } from '../../errors/app-error'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; + +export type unsafeGetEntireEnvelopeOptions = { + id: EnvelopeIdOptions; + type: EnvelopeType; }; -export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => { - const document = await prisma.document.findFirstOrThrow({ - where: { - id, - }, +/** + * An unauthenticated function that returns the whole envelope + */ +export const unsafeGetEntireEnvelope = async ({ id, type }: unsafeGetEntireEnvelopeOptions) => { + const envelope = await prisma.envelope.findFirst({ + where: unsafeBuildEnvelopeIdQuery(id, type), include: { documentMeta: true, user: { @@ -30,5 +38,11 @@ export const getEntireDocument = async ({ id }: GetEntireDocumentOptions) => { }, }); - return document; + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + return envelope; }; diff --git a/packages/lib/server-only/admin/get-signing-volume.ts b/packages/lib/server-only/admin/get-signing-volume.ts index 8ed734ecc..ce9352a92 100644 --- a/packages/lib/server-only/admin/get-signing-volume.ts +++ b/packages/lib/server-only/admin/get-signing-volume.ts @@ -1,4 +1,4 @@ -import { DocumentStatus, SubscriptionStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType, SubscriptionStatus } from '@prisma/client'; import { kyselyPrisma, sql } from '@documenso/prisma'; @@ -31,22 +31,23 @@ export async function getSigningVolume({ .selectFrom('Subscription as s') .innerJoin('Organisation as o', 's.organisationId', 'o.id') .leftJoin('Team as t', 'o.id', 't.organisationId') - .leftJoin('Document as d', (join) => + .leftJoin('Envelope as e', (join) => join - .onRef('t.id', '=', 'd.teamId') - .on('d.status', '=', sql.lit(DocumentStatus.COMPLETED)) - .on('d.deletedAt', 'is', null), + .onRef('t.id', '=', 'e.teamId') + .on('e.status', '=', sql.lit(DocumentStatus.COMPLETED)) + .on('e.deletedAt', 'is', null), ) .where(sql`s.status = ${SubscriptionStatus.ACTIVE}::"SubscriptionStatus"`) .where((eb) => eb.or([eb('o.name', 'ilike', `%${search}%`), eb('t.name', 'ilike', `%${search}%`)]), ) + .where('e.type', '=', EnvelopeType.DOCUMENT) .select([ 's.id as id', 's.createdAt as createdAt', 's.planId as planId', sql`COALESCE(o.name, 'Unknown')`.as('name'), - sql`COUNT(DISTINCT d.id)`.as('signingVolume'), + sql`COUNT(DISTINCT e.id)`.as('signingVolume'), ]) .groupBy(['s.id', 'o.name']); diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts index 08ceadcba..92949a307 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -1,4 +1,8 @@ -import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client'; +import { + type DocumentDistributionMethod, + type DocumentSigningOrder, + EnvelopeType, +} from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -11,16 +15,16 @@ import { prisma } from '@documenso/prisma'; import type { SupportedLanguageCodes } from '../../constants/i18n'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentEmailSettings } from '../../types/document-email'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type CreateDocumentMetaOptions = { userId: number; teamId: number; - documentId: number; + id: EnvelopeIdOptions; subject?: string; message?: string; timezone?: string; - password?: string; dateFormat?: string; redirectUrl?: string; emailId?: string | null; @@ -36,15 +40,14 @@ export type CreateDocumentMetaOptions = { requestMetadata: ApiRequestMetadata; }; -export const upsertDocumentMeta = async ({ +export const updateDocumentMeta = async ({ + id, userId, teamId, subject, message, timezone, dateFormat, - documentId, - password, redirectUrl, signingOrder, allowDictateNextSigner, @@ -58,26 +61,27 @@ export const upsertDocumentMeta = async ({ language, requestMetadata, }: CreateDocumentMetaOptions) => { - const { documentWhereInput, team } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput, team } = await getEnvelopeWhereInput({ + id, + type: null, // Allow updating both documents and templates meta. userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { documentMeta: true, }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - const { documentMeta: originalDocumentMeta } = document; + const { documentMeta: originalDocumentMeta } = envelope; // Validate the emailId belongs to the organisation. if (emailId) { @@ -96,33 +100,13 @@ export const upsertDocumentMeta = async ({ } return await prisma.$transaction(async (tx) => { - const upsertedDocumentMeta = await tx.documentMeta.upsert({ + const upsertedDocumentMeta = await tx.documentMeta.update({ where: { - documentId, + id: envelope.documentMetaId, }, - create: { + data: { subject, message, - password, - dateFormat, - timezone, - documentId, - redirectUrl, - signingOrder, - allowDictateNextSigner, - emailId, - emailReplyTo, - emailSettings, - distributionMethod, - typedSignatureEnabled, - uploadSignatureEnabled, - drawSignatureEnabled, - language, - }, - update: { - subject, - message, - password, dateFormat, timezone, redirectUrl, @@ -141,11 +125,12 @@ export const upsertDocumentMeta = async ({ const changes = diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta); - if (changes.length > 0) { + // Create audit logs only for document type envelopes. + if (changes.length > 0 && envelope.type === EnvelopeType.DOCUMENT) { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_META_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { changes: diffDocumentMetaChanges(originalDocumentMeta ?? {}, upsertedDocumentMeta), diff --git a/packages/lib/server-only/document/complete-document-with-token.ts b/packages/lib/server-only/document/complete-document-with-token.ts index b143b2645..eab037efa 100644 --- a/packages/lib/server-only/document/complete-document-with-token.ts +++ b/packages/lib/server-only/document/complete-document-with-token.ts @@ -1,6 +1,7 @@ import { DocumentSigningOrder, DocumentStatus, + EnvelopeType, RecipientRole, SendStatus, SigningStatus, @@ -22,9 +23,11 @@ import type { TRecipientAccessAuth, TRecipientActionAuth } from '../../types/doc import { DocumentAuth } from '../../types/document-auth'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { extractDocumentAuthMethods } from '../../utils/document-auth'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { isRecipientAuthorized } from './is-recipient-authorized'; @@ -32,7 +35,7 @@ import { sendPendingEmail } from './send-pending-email'; export type CompleteDocumentWithTokenOptions = { token: string; - documentId: number; + id: EnvelopeIdOptions; userId?: number; authOptions?: TRecipientActionAuth; accessAuthOptions?: TRecipientAccessAuth; @@ -43,10 +46,17 @@ export type CompleteDocumentWithTokenOptions = { }; }; -const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => { - return await prisma.document.findFirstOrThrow({ +export const completeDocumentWithToken = async ({ + token, + id, + userId, + accessAuthOptions, + requestMetadata, + nextSigner, +}: CompleteDocumentWithTokenOptions) => { + const envelope = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + ...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), recipients: { some: { token, @@ -62,27 +72,18 @@ const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptio }, }, }); -}; -export const completeDocumentWithToken = async ({ - token, - documentId, - userId, - accessAuthOptions, - requestMetadata, - nextSigner, -}: CompleteDocumentWithTokenOptions) => { - const document = await getDocument({ token, documentId }); + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); - if (document.status !== DocumentStatus.PENDING) { - throw new Error(`Document ${document.id} must be pending`); + if (envelope.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${envelope.id} must be pending`); } - if (document.recipients.length === 0) { - throw new Error(`Document ${document.id} has no recipient with token ${token}`); + if (envelope.recipients.length === 0) { + throw new Error(`Document ${envelope.id} has no recipient with token ${token}`); } - const [recipient] = document.recipients; + const [recipient] = envelope.recipients; if (recipient.signingStatus === SigningStatus.SIGNED) { throw new Error(`Recipient ${recipient.id} has already signed`); @@ -95,7 +96,7 @@ export const completeDocumentWithToken = async ({ }); } - if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { + if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { const isRecipientsTurn = await getIsRecipientsTurnToSign({ token: recipient.token }); if (!isRecipientsTurn) { @@ -107,7 +108,7 @@ export const completeDocumentWithToken = async ({ const fields = await prisma.field.findMany({ where: { - documentId: document.id, + envelopeId: envelope.id, // Todo: Envelopes - Need to support multi docs. recipientId: recipient.id, }, }); @@ -118,7 +119,7 @@ export const completeDocumentWithToken = async ({ // Check ACCESS AUTH 2FA validation during document completion const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({ - documentAuth: document.authOptions, + documentAuth: envelope.authOptions, recipientAuth: recipient.authOptions, }); @@ -131,7 +132,7 @@ export const completeDocumentWithToken = async ({ const isValid = await isRecipientAuthorized({ type: 'ACCESS_2FA', - documentAuthOptions: document.authOptions, + documentAuthOptions: envelope.authOptions, recipient: recipient, userId, // Can be undefined for non-account recipients authOptions: accessAuthOptions, @@ -141,7 +142,7 @@ export const completeDocumentWithToken = async ({ await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_FAILED, - documentId: document.id, + envelopeId: envelope.id, data: { recipientId: recipient.id, recipientName: recipient.name, @@ -158,7 +159,7 @@ export const completeDocumentWithToken = async ({ await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_ACCESS_AUTH_2FA_VALIDATED, - documentId: document.id, + envelopeId: envelope.id, data: { recipientId: recipient.id, recipientName: recipient.name, @@ -180,14 +181,14 @@ export const completeDocumentWithToken = async ({ }); const authOptions = extractDocumentAuthMethods({ - documentAuth: document.authOptions, + documentAuth: envelope.authOptions, recipientAuth: recipient.authOptions, }); await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, - documentId: document.id, + envelopeId: envelope.id, user: { name: recipient.name, email: recipient.email, @@ -207,7 +208,7 @@ export const completeDocumentWithToken = async ({ await jobs.triggerJob({ name: 'send.recipient.signed.email', payload: { - documentId: document.id, + documentId: legacyDocumentId, recipientId: recipient.id, }, }); @@ -221,7 +222,7 @@ export const completeDocumentWithToken = async ({ role: true, }, where: { - documentId: document.id, + envelopeId: envelope.id, signingStatus: { not: SigningStatus.SIGNED, }, @@ -235,17 +236,17 @@ export const completeDocumentWithToken = async ({ }); if (pendingRecipients.length > 0) { - await sendPendingEmail({ documentId, recipientId: recipient.id }); + await sendPendingEmail({ id, recipientId: recipient.id }); - if (document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { + if (envelope.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL) { const [nextRecipient] = pendingRecipients; await prisma.$transaction(async (tx) => { - if (nextSigner && document.documentMeta?.allowDictateNextSigner) { + if (nextSigner && envelope.documentMeta?.allowDictateNextSigner) { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED, - documentId: document.id, + envelopeId: envelope.id, user: { name: recipient.name, email: recipient.email, @@ -277,7 +278,7 @@ export const completeDocumentWithToken = async ({ where: { id: nextRecipient.id }, data: { sendStatus: SendStatus.SENT, - ...(nextSigner && document.documentMeta?.allowDictateNextSigner + ...(nextSigner && envelope.documentMeta?.allowDictateNextSigner ? { name: nextSigner.name, email: nextSigner.email, @@ -289,8 +290,8 @@ export const completeDocumentWithToken = async ({ await jobs.triggerJob({ name: 'send.signing.requested.email', payload: { - userId: document.userId, - documentId: document.id, + userId: envelope.userId, + documentId: legacyDocumentId, recipientId: nextRecipient.id, requestMetadata, }, @@ -299,9 +300,9 @@ export const completeDocumentWithToken = async ({ } } - const haveAllRecipientsSigned = await prisma.document.findFirst({ + const haveAllRecipientsSigned = await prisma.envelope.findFirst({ where: { - id: document.id, + id: envelope.id, recipients: { every: { OR: [{ signingStatus: SigningStatus.SIGNED }, { role: RecipientRole.CC }], @@ -314,15 +315,16 @@ export const completeDocumentWithToken = async ({ await jobs.triggerJob({ name: 'internal.seal-document', payload: { - documentId: document.id, + documentId: legacyDocumentId, requestMetadata, }, }); } - const updatedDocument = await prisma.document.findFirstOrThrow({ + const updatedDocument = await prisma.envelope.findFirstOrThrow({ where: { - id: document.id, + id: envelope.id, + type: EnvelopeType.DOCUMENT, }, include: { documentMeta: true, @@ -332,7 +334,7 @@ export const completeDocumentWithToken = async ({ await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_SIGNED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedDocument)), userId: updatedDocument.userId, teamId: updatedDocument.teamId ?? undefined, }); diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts deleted file mode 100644 index 1f1bab522..000000000 --- a/packages/lib/server-only/document/create-document.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { DocumentSource, WebhookTriggerEvents } from '@prisma/client'; -import type { DocumentVisibility } from '@prisma/client'; - -import { normalizePdf as makeNormalizedPdf } from '@documenso/lib/server-only/pdf/normalize-pdf'; -import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; -import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; -import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { - ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, -} from '../../types/webhook-payload'; -import { prefixedId } from '../../universal/id'; -import { getFileServerSide } from '../../universal/upload/get-file.server'; -import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; -import { extractDerivedDocumentMeta } from '../../utils/document'; -import { determineDocumentVisibility } from '../../utils/document-visibility'; -import { buildTeamWhereQuery } from '../../utils/teams'; -import { getTeamById } from '../team/get-team'; -import { getTeamSettings } from '../team/get-team-settings'; -import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; - -export type CreateDocumentOptions = { - title: string; - externalId?: string | null; - userId: number; - teamId: number; - documentDataId: string; - formValues?: Record; - normalizePdf?: boolean; - timezone?: string; - userTimezone?: string; - requestMetadata: ApiRequestMetadata; - folderId?: string; -}; - -export const createDocument = async ({ - userId, - title, - externalId, - documentDataId, - teamId, - normalizePdf, - formValues, - requestMetadata, - timezone, - userTimezone, - folderId, -}: CreateDocumentOptions) => { - const team = await getTeamById({ userId, teamId }); - - const settings = await getTeamSettings({ - userId, - teamId, - }); - - let folderVisibility: DocumentVisibility | undefined; - - if (folderId) { - const folder = await prisma.folder.findFirst({ - where: { - id: folderId, - team: buildTeamWhereQuery({ - teamId, - userId, - }), - }, - select: { - visibility: true, - }, - }); - - if (!folder) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - - folderVisibility = folder.visibility; - } - - if (normalizePdf) { - const documentData = await prisma.documentData.findFirst({ - where: { - id: documentDataId, - }, - }); - - if (documentData) { - const buffer = await getFileServerSide(documentData); - - const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer)); - - const newDocumentData = await putPdfFileServerSide({ - name: title.endsWith('.pdf') ? title : `${title}.pdf`, - type: 'application/pdf', - arrayBuffer: async () => Promise.resolve(normalizedPdf), - }); - - // eslint-disable-next-line require-atomic-updates - documentDataId = newDocumentData.id; - } - } - - // userTimezone is last because it's always passed in regardless of the organisation/team settings - // for uploads from the frontend - const timezoneToUse = timezone || settings.documentTimezone || userTimezone; - - return await prisma.$transaction(async (tx) => { - const document = await tx.document.create({ - data: { - title, - qrToken: prefixedId('qr'), - externalId, - documentDataId, - userId, - teamId, - folderId, - visibility: - folderVisibility ?? - determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole), - formValues, - source: DocumentSource.DOCUMENT, - documentMeta: { - create: extractDerivedDocumentMeta(settings, { - timezone: timezoneToUse, - }), - }, - }, - }); - - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, - documentId: document.id, - metadata: requestMetadata, - data: { - title, - source: { - type: DocumentSource.DOCUMENT, - }, - }, - }), - }); - - const createdDocument = await tx.document.findFirst({ - where: { - id: document.id, - }, - include: { - documentMeta: true, - recipients: true, - }, - }); - - if (!createdDocument) { - throw new Error('Document not found'); - } - - await triggerWebhook({ - event: WebhookTriggerEvents.DOCUMENT_CREATED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)), - userId, - teamId, - }); - - return createdDocument; - }); -}; diff --git a/packages/lib/server-only/document/delete-document.ts b/packages/lib/server-only/document/delete-document.ts index 94c039311..bd995305f 100644 --- a/packages/lib/server-only/document/delete-document.ts +++ b/packages/lib/server-only/document/delete-document.ts @@ -1,8 +1,8 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import type { Document, DocumentMeta, Recipient, User } from '@prisma/client'; -import { DocumentStatus, SendStatus, WebhookTriggerEvents } from '@prisma/client'; +import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client'; +import { DocumentStatus, EnvelopeType, SendStatus, WebhookTriggerEvents } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import DocumentCancelTemplate from '@documenso/email/templates/document-cancel'; @@ -15,11 +15,12 @@ import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import { isDocumentCompleted } from '../../utils/document'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; import { getMemberRoles } from '../team/get-member-roles'; @@ -50,24 +51,23 @@ export const deleteDocument = async ({ }); } - const document = await prisma.document.findUnique({ - where: { - id, - }, + // Note: This is an unsafe request, we validate the ownership later in the function. + const envelope = await prisma.envelope.findUnique({ + where: unsafeBuildEnvelopeIdQuery({ type: 'documentId', id }, EnvelopeType.DOCUMENT), include: { recipients: true, documentMeta: true, }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } const isUserTeamMember = await getMemberRoles({ - teamId: document.teamId, + teamId: envelope.teamId, reference: { type: 'User', id: userId, @@ -76,8 +76,8 @@ export const deleteDocument = async ({ .then(() => true) .catch(() => false); - const isUserOwner = document.userId === userId; - const userRecipient = document.recipients.find((recipient) => recipient.email === user.email); + const isUserOwner = envelope.userId === userId; + const userRecipient = envelope.recipients.find((recipient) => recipient.email === user.email); if (!isUserOwner && !isUserTeamMember && !userRecipient) { throw new AppError(AppErrorCode.UNAUTHORIZED, { @@ -88,7 +88,7 @@ export const deleteDocument = async ({ // Handle hard or soft deleting the actual document if user has permission. if (isUserOwner || isUserTeamMember) { await handleDocumentOwnerDelete({ - document, + envelope, user, requestMetadata, }); @@ -113,27 +113,16 @@ export const deleteDocument = async ({ await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_CANCELLED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), userId, teamId, }); - // Return partial document for API v1 response. - return { - id: document.id, - userId: document.userId, - teamId: document.teamId, - title: document.title, - status: document.status, - documentDataId: document.documentDataId, - createdAt: document.createdAt, - updatedAt: document.updatedAt, - completedAt: document.completedAt, - }; + return envelope; }; type HandleDocumentOwnerDeleteOptions = { - document: Document & { + envelope: Envelope & { recipients: Recipient[]; documentMeta: DocumentMeta | null; }; @@ -142,11 +131,11 @@ type HandleDocumentOwnerDeleteOptions = { }; const handleDocumentOwnerDelete = async ({ - document, + envelope, user, requestMetadata, }: HandleDocumentOwnerDeleteOptions) => { - if (document.deletedAt) { + if (envelope.deletedAt) { return; } @@ -154,17 +143,17 @@ const handleDocumentOwnerDelete = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); // Soft delete completed documents. - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { return await prisma.$transaction(async (tx) => { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId: document.id, + envelopeId: envelope.id, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, metadata: requestMetadata, data: { @@ -173,9 +162,9 @@ const handleDocumentOwnerDelete = async ({ }), }); - return await tx.document.update({ + return await tx.envelope.update({ where: { - id: document.id, + id: envelope.id, }, data: { deletedAt: new Date().toISOString(), @@ -185,12 +174,12 @@ const handleDocumentOwnerDelete = async ({ } // Hard delete draft and pending documents. - const deletedDocument = await prisma.$transaction(async (tx) => { + const deletedEnvelope = await prisma.$transaction(async (tx) => { // Currently redundant since deleting a document will delete the audit logs. // However may be useful if we disassociate audit logs and documents if required. await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId: document.id, + envelopeId: envelope.id, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED, metadata: requestMetadata, data: { @@ -199,9 +188,9 @@ const handleDocumentOwnerDelete = async ({ }), }); - return await tx.document.delete({ + return await tx.envelope.delete({ where: { - id: document.id, + id: envelope.id, status: { not: DocumentStatus.COMPLETED, }, @@ -209,17 +198,17 @@ const handleDocumentOwnerDelete = async ({ }); }); - const isDocumentDeleteEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings( + envelope.documentMeta, ).documentDeleted; - if (!isDocumentDeleteEmailEnabled) { - return deletedDocument; + if (!isEnvelopeDeleteEmailEnabled) { + return deletedEnvelope; } // Send cancellation emails to recipients. await Promise.all( - document.recipients.map(async (recipient) => { + envelope.recipients.map(async (recipient) => { if (recipient.sendStatus !== SendStatus.SENT) { return; } @@ -227,7 +216,7 @@ const handleDocumentOwnerDelete = async ({ const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(DocumentCancelTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: user.name || undefined, inviterEmail: user.email, assetBaseUrl, @@ -258,5 +247,5 @@ const handleDocumentOwnerDelete = async ({ }), ); - return deletedDocument; + return deletedEnvelope; }; diff --git a/packages/lib/server-only/document/duplicate-document-by-id.ts b/packages/lib/server-only/document/duplicate-document-by-id.ts index 18d0b4ec6..717d39062 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -1,5 +1,5 @@ -import type { Prisma, Recipient } from '@prisma/client'; -import { DocumentSource, WebhookTriggerEvents } from '@prisma/client'; +import type { Recipient } from '@prisma/client'; +import { DocumentSource, EnvelopeType, WebhookTriggerEvents } from '@prisma/client'; import { omit } from 'remeda'; import { prisma } from '@documenso/prisma'; @@ -7,39 +7,42 @@ import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { nanoid, prefixedId } from '../../universal/id'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; +import { incrementDocumentId } from '../envelope/increment-id'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; -import { getDocumentWhereInput } from './get-document-by-id'; export interface DuplicateDocumentOptions { - documentId: number; + id: EnvelopeIdOptions; userId: number; teamId: number; } -export const duplicateDocument = async ({ - documentId, - userId, - teamId, -}: DuplicateDocumentOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, +export const duplicateDocument = async ({ id, userId, teamId }: DuplicateDocumentOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, select: { title: true, userId: true, - documentData: { - select: { - data: true, - initialData: true, - type: true, + envelopeItems: { + include: { + documentData: { + select: { + data: true, + initialData: true, + type: true, + }, + }, }, }, authOptions: true, @@ -54,44 +57,36 @@ export const duplicateDocument = async ({ fields: true, }, }, + teamId: true, }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - const documentData = await prisma.documentData.create({ + const { documentId, formattedDocumentId } = await incrementDocumentId(); + + const createdDocumentMeta = await prisma.documentMeta.create({ data: { - type: document.documentData.type, - data: document.documentData.initialData, - initialData: document.documentData.initialData, + ...omit(envelope.documentMeta, ['id']), + emailSettings: envelope.documentMeta.emailSettings || undefined, }, }); - let documentMeta: Prisma.DocumentCreateArgs['data']['documentMeta'] | undefined = undefined; - - if (document.documentMeta) { - documentMeta = { - create: { - ...omit(document.documentMeta, ['id', 'documentId']), - emailSettings: document.documentMeta.emailSettings || undefined, - }, - }; - } - - const createdDocument = await prisma.document.create({ + const duplicatedEnvelope = await prisma.envelope.create({ data: { - userId: document.userId, - teamId: teamId, - title: document.title, - documentDataId: documentData.id, - authOptions: document.authOptions || undefined, - visibility: document.visibility, - qrToken: prefixedId('qr'), - documentMeta, + id: prefixedId('envelope'), + secondaryId: formattedDocumentId, + type: EnvelopeType.DOCUMENT, + userId, + teamId, + title: envelope.title, + documentMetaId: createdDocumentMeta.id, + authOptions: envelope.authOptions || undefined, + visibility: envelope.visibility, source: DocumentSource.DOCUMENT, }, include: { @@ -100,53 +95,86 @@ export const duplicateDocument = async ({ }, }); - const recipientsToCreate = document.recipients.map((recipient) => ({ - documentId: createdDocument.id, - email: recipient.email, - name: recipient.name, - role: recipient.role, - signingOrder: recipient.signingOrder, - token: nanoid(), - fields: { - createMany: { - data: recipient.fields.map((field) => ({ - documentId: createdDocument.id, - type: field.type, - page: field.page, - positionX: field.positionX, - positionY: field.positionY, - width: field.width, - height: field.height, - customText: '', - inserted: false, - fieldMeta: field.fieldMeta as PrismaJson.FieldMeta, - })), - }, - }, - })); + // Key = original envelope item ID + // Value = duplicated envelope item ID. + const oldEnvelopeItemToNewEnvelopeItemIdMap: Record = {}; + + // Duplicate the envelope items. + await Promise.all( + envelope.envelopeItems.map(async (envelopeItem) => { + const duplicatedDocumentData = await prisma.documentData.create({ + data: { + type: envelopeItem.documentData.type, + data: envelopeItem.documentData.initialData, + initialData: envelopeItem.documentData.initialData, + }, + }); + + const duplicatedEnvelopeItem = await prisma.envelopeItem.create({ + data: { + id: prefixedId('envelope_item'), + title: envelopeItem.title, + envelopeId: duplicatedEnvelope.id, + documentDataId: duplicatedDocumentData.id, + }, + }); + + oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id; + }), + ); const recipients: Recipient[] = []; - for (const recipientData of recipientsToCreate) { - const newRecipient = await prisma.recipient.create({ - data: recipientData, + for (const recipient of envelope.recipients) { + const duplicatedRecipient = await prisma.recipient.create({ + data: { + envelopeId: duplicatedEnvelope.id, + email: recipient.email, + name: recipient.name, + role: recipient.role, + signingOrder: recipient.signingOrder, + token: nanoid(), + fields: { + createMany: { + data: recipient.fields.map((field) => ({ + envelopeId: duplicatedEnvelope.id, + envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId], + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: '', + inserted: false, + fieldMeta: field.fieldMeta as PrismaJson.FieldMeta, + })), + }, + }, + }, }); - recipients.push(newRecipient); + recipients.push(duplicatedRecipient); } + const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({ + where: { + id: duplicatedEnvelope.id, + }, + include: { + documentMeta: true, + recipients: true, + }, + }); + await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_CREATED, - data: ZWebhookDocumentSchema.parse({ - ...mapDocumentToWebhookDocumentPayload(createdDocument), - recipients, - documentMeta: createdDocument.documentMeta, - }), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)), userId: userId, teamId: teamId, }); return { - documentId: createdDocument.id, + documentId, }; }; diff --git a/packages/lib/server-only/document/find-document-audit-logs.ts b/packages/lib/server-only/document/find-document-audit-logs.ts index 0dc1fa305..6706efe21 100644 --- a/packages/lib/server-only/document/find-document-audit-logs.ts +++ b/packages/lib/server-only/document/find-document-audit-logs.ts @@ -1,4 +1,4 @@ -import type { DocumentAuditLog, Prisma } from '@prisma/client'; +import { type DocumentAuditLog, EnvelopeType, type Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -6,7 +6,7 @@ import { AppError, AppErrorCode } from '../../errors/app-error'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import type { FindResultResponse } from '../../types/search-params'; import { parseDocumentAuditLogData } from '../../utils/document-audit-logs'; -import { getDocumentWhereInput } from './get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface FindDocumentAuditLogsOptions { userId: number; @@ -35,22 +35,26 @@ export const findDocumentAuditLogs = async ({ const orderByColumn = orderBy?.column ?? 'createdAt'; const orderByDirection = orderBy?.direction ?? 'desc'; - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND); } const whereClause: Prisma.DocumentAuditLogWhereInput = { - documentId, + envelopeId: envelope.id, }; // Filter events down to what we consider recent activity. diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 97dd07582..d839c2f38 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,5 +1,5 @@ -import type { Document, DocumentSource, Prisma, Team, TeamEmail, User } from '@prisma/client'; -import { RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client'; +import type { DocumentSource, Envelope, Prisma, Team, TeamEmail, User } from '@prisma/client'; +import { EnvelopeType, RecipientRole, SigningStatus, TeamMemberRole } from '@prisma/client'; import { DateTime } from 'luxon'; import { match } from 'ts-pattern'; @@ -22,7 +22,7 @@ export type FindDocumentsOptions = { page?: number; perPage?: number; orderBy?: { - column: keyof Omit; + column: keyof Pick; direction: 'asc' | 'desc'; }; period?: PeriodSelectorValue; @@ -69,7 +69,7 @@ export const findDocuments = async ({ const orderByDirection = orderBy?.direction ?? 'desc'; const teamMemberRole = team?.currentTeamRole ?? null; - const searchFilter: Prisma.DocumentWhereInput = { + const searchFilter: Prisma.EnvelopeWhereInput = { OR: [ { title: { contains: query, mode: 'insensitive' } }, { externalId: { contains: query, mode: 'insensitive' } }, @@ -111,7 +111,7 @@ export const findDocuments = async ({ }, ]; - let filters: Prisma.DocumentWhereInput | null = findDocumentsFilter(status, user, folderId); + let filters: Prisma.EnvelopeWhereInput | null = findDocumentsFilter(status, user, folderId); if (team) { filters = findTeamDocumentsFilter(status, team, visibilityFilters, folderId); @@ -127,7 +127,7 @@ export const findDocuments = async ({ }; } - let deletedFilter: Prisma.DocumentWhereInput = { + let deletedFilter: Prisma.EnvelopeWhereInput = { AND: { OR: [ { @@ -180,7 +180,7 @@ export const findDocuments = async ({ }; } - const whereAndClause: Prisma.DocumentWhereInput['AND'] = [ + const whereAndClause: Prisma.EnvelopeWhereInput['AND'] = [ { ...filters }, { ...deletedFilter }, { ...searchFilter }, @@ -198,7 +198,8 @@ export const findDocuments = async ({ }); } - const whereClause: Prisma.DocumentWhereInput = { + const whereClause: Prisma.EnvelopeWhereInput = { + type: EnvelopeType.DOCUMENT, AND: whereAndClause, }; @@ -225,7 +226,7 @@ export const findDocuments = async ({ } const [data, count] = await Promise.all([ - prisma.document.findMany({ + prisma.envelope.findMany({ where: whereClause, skip: Math.max(page - 1, 0) * perPage, take: perPage, @@ -249,7 +250,7 @@ export const findDocuments = async ({ }, }, }), - prisma.document.count({ + prisma.envelope.count({ where: whereClause, }), ]); @@ -275,7 +276,7 @@ const findDocumentsFilter = ( user: Pick, folderId?: string | null, ) => { - return match(status) + return match(status) .with(ExtendedDocumentStatus.ALL, () => ({ OR: [ { @@ -414,14 +415,14 @@ const findDocumentsFilter = ( const findTeamDocumentsFilter = ( status: ExtendedDocumentStatus, team: Team & { teamEmail: TeamEmail | null }, - visibilityFilters: Prisma.DocumentWhereInput[], + visibilityFilters: Prisma.EnvelopeWhereInput[], folderId?: string, ) => { const teamEmail = team.teamEmail?.email ?? null; - return match(status) + return match(status) .with(ExtendedDocumentStatus.ALL, () => { - const filter: Prisma.DocumentWhereInput = { + const filter: Prisma.EnvelopeWhereInput = { // Filter to display all documents that belong to the team. OR: [ { @@ -483,7 +484,7 @@ const findTeamDocumentsFilter = ( }; }) .with(ExtendedDocumentStatus.DRAFT, () => { - const filter: Prisma.DocumentWhereInput = { + const filter: Prisma.EnvelopeWhereInput = { OR: [ { teamId: team.id, @@ -508,7 +509,7 @@ const findTeamDocumentsFilter = ( return filter; }) .with(ExtendedDocumentStatus.PENDING, () => { - const filter: Prisma.DocumentWhereInput = { + const filter: Prisma.EnvelopeWhereInput = { OR: [ { teamId: team.id, @@ -550,7 +551,7 @@ const findTeamDocumentsFilter = ( return filter; }) .with(ExtendedDocumentStatus.COMPLETED, () => { - const filter: Prisma.DocumentWhereInput = { + const filter: Prisma.EnvelopeWhereInput = { status: ExtendedDocumentStatus.COMPLETED, OR: [ { @@ -582,7 +583,7 @@ const findTeamDocumentsFilter = ( return filter; }) .with(ExtendedDocumentStatus.REJECTED, () => { - const filter: Prisma.DocumentWhereInput = { + const filter: Prisma.EnvelopeWhereInput = { status: ExtendedDocumentStatus.REJECTED, OR: [ { diff --git a/packages/lib/server-only/document/get-document-by-access-token.ts b/packages/lib/server-only/document/get-document-by-access-token.ts index e7ccdfbf8..836c2030e 100644 --- a/packages/lib/server-only/document/get-document-by-access-token.ts +++ b/packages/lib/server-only/document/get-document-by-access-token.ts @@ -1,5 +1,9 @@ +import { DocumentStatus, EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; +import { mapSecondaryIdToDocumentId } from '../../utils/envelope'; + export type GetDocumentByAccessTokenOptions = { token: string; }; @@ -9,30 +13,54 @@ export const getDocumentByAccessToken = async ({ token }: GetDocumentByAccessTok throw new Error('Missing token'); } - const result = await prisma.document.findFirstOrThrow({ + const result = await prisma.envelope.findFirstOrThrow({ where: { + type: EnvelopeType.DOCUMENT, + status: DocumentStatus.COMPLETED, qrToken: token, }, + // Do not provide extra information that is not needed. select: { id: true, + secondaryId: true, title: true, completedAt: true, - documentData: { + team: { select: { - id: true, - type: true, - data: true, - initialData: true, + url: true, }, }, - documentMeta: { + envelopeItems: { select: { - password: true, + documentData: { + select: { + id: true, + type: true, + data: true, + initialData: true, + }, + }, + }, + }, + _count: { + select: { + recipients: true, }, }, - recipients: true, }, }); - return result; + // Todo: Envelopes + if (!result.envelopeItems[0].documentData) { + throw new Error('Missing document data'); + } + + return { + id: mapSecondaryIdToDocumentId(result.secondaryId), + title: result.title, + completedAt: result.completedAt, + documentData: result.envelopeItems[0].documentData, + recipientCount: result._count.recipients, + documentTeamUrl: result.team.url, + }; }; diff --git a/packages/lib/server-only/document/get-document-by-id.ts b/packages/lib/server-only/document/get-document-by-id.ts deleted file mode 100644 index 542e6e3cb..000000000 --- a/packages/lib/server-only/document/get-document-by-id.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { Prisma } from '@prisma/client'; -import { DocumentStatus, TeamMemberRole } from '@prisma/client'; -import { match } from 'ts-pattern'; - -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { DocumentVisibility } from '../../types/document-visibility'; -import { getTeamById } from '../team/get-team'; - -export type GetDocumentByIdOptions = { - documentId: number; - userId: number; - teamId: number; - folderId?: string; -}; - -export const getDocumentById = async ({ - documentId, - userId, - teamId, - folderId, -}: GetDocumentByIdOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, - userId, - teamId, - }); - - const document = await prisma.document.findFirst({ - where: { - ...documentWhereInput, - folderId, - }, - include: { - documentData: true, - documentMeta: true, - user: { - select: { - id: true, - name: true, - email: true, - }, - }, - recipients: { - select: { - email: true, - }, - }, - team: { - select: { - id: true, - url: true, - }, - }, - }, - }); - - if (!document) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document could not be found', - }); - } - - return document; -}; - -export type GetDocumentWhereInputOptions = { - documentId: number; - userId: number; - teamId: number; -}; - -/** - * Generate the where input for a given Prisma document query. - * - * This will return a query that allows a user to get a document if they have valid access to it. - */ -export const getDocumentWhereInput = async ({ - documentId, - userId, - teamId, -}: GetDocumentWhereInputOptions) => { - const team = await getTeamById({ teamId, userId }); - - const user = await prisma.user.findFirstOrThrow({ - where: { - id: userId, - }, - }); - - const teamVisibilityFilters = match(team.currentTeamRole) - .with(TeamMemberRole.ADMIN, () => [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - DocumentVisibility.ADMIN, - ]) - .with(TeamMemberRole.MANAGER, () => [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - ]) - .otherwise(() => [DocumentVisibility.EVERYONE]); - - const documentOrInput: Prisma.DocumentWhereInput[] = [ - // Allow access if they own the document. - { - userId, - }, - // Or, if they belong to the team that the document is associated with. - { - visibility: { - in: teamVisibilityFilters, - }, - teamId: team.id, - }, - // Or, if they are a recipient of the document. - { - status: { - not: DocumentStatus.DRAFT, - }, - recipients: { - some: { - email: user.email, - }, - }, - }, - ]; - - // Allow access to documents sent to or from the team email. - if (team.teamEmail) { - documentOrInput.push( - { - recipients: { - some: { - email: team.teamEmail.email, - }, - }, - }, - { - user: { - email: team.teamEmail.email, - }, - }, - ); - } - - const documentWhereInput: Prisma.DocumentWhereUniqueInput = { - id: documentId, - OR: documentOrInput, - }; - - return { - documentWhereInput, - team, - }; -}; diff --git a/packages/lib/server-only/document/get-document-by-token.ts b/packages/lib/server-only/document/get-document-by-token.ts index d2f18f56e..1d531f09d 100644 --- a/packages/lib/server-only/document/get-document-by-token.ts +++ b/packages/lib/server-only/document/get-document-by-token.ts @@ -1,8 +1,10 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; -import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAuthMethods } from '../../types/document-auth'; +import { mapSecondaryIdToDocumentId } from '../../utils/envelope'; import { isRecipientAuthorized } from './is-recipient-authorized'; export interface GetDocumentAndSenderByTokenOptions { @@ -39,8 +41,9 @@ export const getDocumentByToken = async ({ token }: GetDocumentByTokenOptions) = throw new Error('Missing token'); } - const result = await prisma.document.findFirstOrThrow({ + const result = await prisma.envelope.findFirstOrThrow({ where: { + type: EnvelopeType.DOCUMENT, recipients: { some: { token, @@ -64,8 +67,9 @@ export const getDocumentAndSenderByToken = async ({ throw new Error('Missing token'); } - const result = await prisma.document.findFirstOrThrow({ + const result = await prisma.envelope.findFirstOrThrow({ where: { + type: EnvelopeType.DOCUMENT, recipients: { some: { token, @@ -80,13 +84,17 @@ export const getDocumentAndSenderByToken = async ({ name: true, }, }, - documentData: true, documentMeta: true, recipients: { where: { token, }, }, + envelopeItems: { + select: { + documentData: true, + }, + }, team: { select: { name: true, @@ -102,6 +110,13 @@ export const getDocumentAndSenderByToken = async ({ }, }); + // Todo: Envelopes + const firstDocumentData = result.envelopeItems[0].documentData; + + if (!firstDocumentData) { + throw new Error('Missing document data'); + } + const recipient = result.recipients[0]; // Sanity check, should not be possible. @@ -127,6 +142,8 @@ export const getDocumentAndSenderByToken = async ({ }); } + const legacyDocumentId = mapSecondaryIdToDocumentId(result.secondaryId); + return { ...result, user: { @@ -134,64 +151,7 @@ export const getDocumentAndSenderByToken = async ({ email: result.user.email, name: result.user.name, }, + documentData: firstDocumentData, + id: legacyDocumentId, }; }; - -/** - * Get a Document and a Recipient by the recipient token. - */ -export const getDocumentAndRecipientByToken = async ({ - token, - userId, - accessAuth, - requireAccessAuth = true, -}: GetDocumentAndRecipientByTokenOptions): Promise => { - if (!token) { - throw new Error('Missing token'); - } - - const result = await prisma.document.findFirstOrThrow({ - where: { - recipients: { - some: { - token, - }, - }, - }, - include: { - recipients: { - where: { - token, - }, - }, - documentData: true, - }, - }); - - const [recipient] = result.recipients; - - // Sanity check, should not be possible. - if (!recipient) { - throw new Error('Missing recipient'); - } - - let documentAccessValid = true; - - if (requireAccessAuth) { - documentAccessValid = await isRecipientAuthorized({ - type: 'ACCESS', - documentAuthOptions: result.authOptions, - recipient, - userId, - authOptions: accessAuth, - }); - } - - if (!documentAccessValid) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'Invalid access values', - }); - } - - return result; -}; diff --git a/packages/lib/server-only/document/get-document-certificate-audit-logs.ts b/packages/lib/server-only/document/get-document-certificate-audit-logs.ts index 3ace2687f..7723fd1d5 100644 --- a/packages/lib/server-only/document/get-document-certificate-audit-logs.ts +++ b/packages/lib/server-only/document/get-document-certificate-audit-logs.ts @@ -4,15 +4,15 @@ import { DOCUMENT_AUDIT_LOG_TYPE, DOCUMENT_EMAIL_TYPE } from '../../types/docume import { parseDocumentAuditLogData } from '../../utils/document-audit-logs'; export type GetDocumentCertificateAuditLogsOptions = { - id: number; + envelopeId: string; }; export const getDocumentCertificateAuditLogs = async ({ - id, + envelopeId, }: GetDocumentCertificateAuditLogsOptions) => { const rawAuditLogs = await prisma.documentAuditLog.findMany({ where: { - documentId: id, + envelopeId, type: { in: [ DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, diff --git a/packages/lib/server-only/document/get-document-with-details-by-id.ts b/packages/lib/server-only/document/get-document-with-details-by-id.ts index 2a93156a7..965f0af94 100644 --- a/packages/lib/server-only/document/get-document-with-details-by-id.ts +++ b/packages/lib/server-only/document/get-document-with-details-by-id.ts @@ -1,67 +1,52 @@ -import { prisma } from '@documenso/prisma'; +import { EnvelopeType } from '@prisma/client'; -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { getDocumentWhereInput } from './get-document-by-id'; +import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope'; +import { getEnvelopeById } from '../envelope/get-envelope-by-id'; export type GetDocumentWithDetailsByIdOptions = { - documentId: number; + id: EnvelopeIdOptions; userId: number; teamId: number; }; export const getDocumentWithDetailsById = async ({ - documentId, + id, userId, teamId, }: GetDocumentWithDetailsByIdOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const envelope = await getEnvelopeById({ + id, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: { - ...documentWhereInput, - }, - include: { - documentData: true, - documentMeta: true, - recipients: true, - folder: true, - fields: { - include: { - signature: true, - recipient: { - select: { - name: true, - email: true, - signingStatus: true, - }, - }, - }, - }, - team: { - select: { - id: true, - url: true, - }, - }, - user: { - select: { - id: true, - name: true, - email: true, - }, - }, - }, - }); + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); - if (!document) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', - }); + // Todo: Envelopes + const firstDocumentData = envelope.envelopeItems[0].documentData; + + if (!firstDocumentData) { + throw new Error('Document data not found'); } - return document; + return { + ...envelope, + documentData: firstDocumentData, + id: legacyDocumentId, + fields: envelope.fields.map((field) => ({ + ...field, + documentId: legacyDocumentId, + })), + user: { + id: envelope.userId, + name: envelope.user.name, + email: envelope.user.email, + }, + team: { + id: envelope.teamId, + url: envelope.team.url, + }, + recipients: envelope.recipients, + }; }; diff --git a/packages/lib/server-only/document/get-recipient-or-sender-by-share-link-slug.ts b/packages/lib/server-only/document/get-recipient-or-sender-by-share-link-slug.ts index 2beaf5d1b..b6e2a18b8 100644 --- a/packages/lib/server-only/document/get-recipient-or-sender-by-share-link-slug.ts +++ b/packages/lib/server-only/document/get-recipient-or-sender-by-share-link-slug.ts @@ -7,7 +7,7 @@ export type GetRecipientOrSenderByShareLinkSlugOptions = { export const getRecipientOrSenderByShareLinkSlug = async ({ slug, }: GetRecipientOrSenderByShareLinkSlugOptions) => { - const { documentId, email } = await prisma.documentShareLink.findFirstOrThrow({ + const { envelopeId, email } = await prisma.documentShareLink.findFirstOrThrow({ where: { slug, }, @@ -15,7 +15,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({ const sender = await prisma.user.findFirst({ where: { - documents: { some: { id: documentId } }, + envelopes: { some: { id: envelopeId } }, email, }, select: { @@ -31,7 +31,7 @@ export const getRecipientOrSenderByShareLinkSlug = async ({ const recipient = await prisma.recipient.findFirst({ where: { - documentId, + envelopeId, email, }, select: { diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index 487e7a24a..58612cf7f 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -1,4 +1,4 @@ -import { TeamMemberRole } from '@prisma/client'; +import { EnvelopeType, TeamMemberRole } from '@prisma/client'; import type { Prisma, User } from '@prisma/client'; import { SigningStatus } from '@prisma/client'; import { DocumentVisibility } from '@prisma/client'; @@ -25,7 +25,7 @@ export const getStats = async ({ folderId, ...options }: GetStatsInput) => { - let createdAt: Prisma.DocumentWhereInput['createdAt']; + let createdAt: Prisma.EnvelopeWhereInput['createdAt']; if (period) { const daysAgo = parseInt(period.replace(/d$/, ''), 10); @@ -90,13 +90,13 @@ export const getStats = async ({ type GetCountsOption = { user: Pick; - createdAt: Prisma.DocumentWhereInput['createdAt']; + createdAt: Prisma.EnvelopeWhereInput['createdAt']; search?: string; folderId?: string | null; }; const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) => { - const searchFilter: Prisma.DocumentWhereInput = { + const searchFilter: Prisma.EnvelopeWhereInput = { OR: [ { title: { contains: search, mode: 'insensitive' } }, { recipients: { some: { name: { contains: search, mode: 'insensitive' } } } }, @@ -108,12 +108,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) return Promise.all([ // Owner counts. - prisma.document.groupBy({ + prisma.envelope.groupBy({ by: ['status'], _count: { _all: true, }, where: { + type: EnvelopeType.DOCUMENT, userId: user.id, createdAt, deletedAt: null, @@ -121,12 +122,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) }, }), // Not signed counts. - prisma.document.groupBy({ + prisma.envelope.groupBy({ by: ['status'], _count: { _all: true, }, where: { + type: EnvelopeType.DOCUMENT, status: ExtendedDocumentStatus.PENDING, recipients: { some: { @@ -140,12 +142,13 @@ const getCounts = async ({ user, createdAt, search, folderId }: GetCountsOption) }, }), // Has signed counts. - prisma.document.groupBy({ + prisma.envelope.groupBy({ by: ['status'], _count: { _all: true, }, where: { + type: EnvelopeType.DOCUMENT, createdAt, user: { email: { @@ -186,7 +189,7 @@ type GetTeamCountsOption = { senderIds?: number[]; currentUserEmail: string; userId: number; - createdAt: Prisma.DocumentWhereInput['createdAt']; + createdAt: Prisma.EnvelopeWhereInput['createdAt']; currentTeamMemberRole?: TeamMemberRole; search?: string; folderId?: string | null; @@ -197,14 +200,14 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { const senderIds = options.senderIds ?? []; - const userIdWhereClause: Prisma.DocumentWhereInput['userId'] = + const userIdWhereClause: Prisma.EnvelopeWhereInput['userId'] = senderIds.length > 0 ? { in: senderIds, } : undefined; - const searchFilter: Prisma.DocumentWhereInput = { + const searchFilter: Prisma.EnvelopeWhereInput = { OR: [ { title: { contains: options.search, mode: 'insensitive' } }, { recipients: { some: { name: { contains: options.search, mode: 'insensitive' } } } }, @@ -212,7 +215,8 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { ], }; - let ownerCountsWhereInput: Prisma.DocumentWhereInput = { + let ownerCountsWhereInput: Prisma.EnvelopeWhereInput = { + type: EnvelopeType.DOCUMENT, userId: userIdWhereClause, createdAt, teamId, @@ -223,7 +227,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { let notSignedCountsGroupByArgs = null; let hasSignedCountsGroupByArgs = null; - const visibilityFiltersWhereInput: Prisma.DocumentWhereInput = { + const visibilityFiltersWhereInput: Prisma.EnvelopeWhereInput = { AND: [ { deletedAt: null }, { @@ -267,6 +271,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { if (teamEmail) { ownerCountsWhereInput = { + type: EnvelopeType.DOCUMENT, userId: userIdWhereClause, createdAt, OR: [ @@ -288,6 +293,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { _all: true, }, where: { + type: EnvelopeType.DOCUMENT, userId: userIdWhereClause, createdAt, folderId, @@ -301,7 +307,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { }, deletedAt: null, }, - } satisfies Prisma.DocumentGroupByArgs; + } satisfies Prisma.EnvelopeGroupByArgs; hasSignedCountsGroupByArgs = { by: ['status'], @@ -309,6 +315,7 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { _all: true, }, where: { + type: EnvelopeType.DOCUMENT, userId: userIdWhereClause, createdAt, folderId, @@ -336,18 +343,18 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { }, ], }, - } satisfies Prisma.DocumentGroupByArgs; + } satisfies Prisma.EnvelopeGroupByArgs; } return Promise.all([ - prisma.document.groupBy({ + prisma.envelope.groupBy({ by: ['status'], _count: { _all: true, }, where: ownerCountsWhereInput, }), - notSignedCountsGroupByArgs ? prisma.document.groupBy(notSignedCountsGroupByArgs) : [], - hasSignedCountsGroupByArgs ? prisma.document.groupBy(hasSignedCountsGroupByArgs) : [], + notSignedCountsGroupByArgs ? prisma.envelope.groupBy(notSignedCountsGroupByArgs) : [], + hasSignedCountsGroupByArgs ? prisma.envelope.groupBy(hasSignedCountsGroupByArgs) : [], ]); }; diff --git a/packages/lib/server-only/document/is-recipient-authorized.ts b/packages/lib/server-only/document/is-recipient-authorized.ts index c8d54d2a4..4826ae054 100644 --- a/packages/lib/server-only/document/is-recipient-authorized.ts +++ b/packages/lib/server-only/document/is-recipient-authorized.ts @@ -1,4 +1,4 @@ -import type { Document, Recipient } from '@prisma/client'; +import type { Envelope, Recipient } from '@prisma/client'; import { verifyAuthenticationResponse } from '@simplewebauthn/server'; import { match } from 'ts-pattern'; @@ -17,8 +17,8 @@ import { extractDocumentAuthMethods } from '../../utils/document-auth'; type IsRecipientAuthorizedOptions = { // !: Probably find a better name than 'ACCESS_2FA' if requirements change. type: 'ACCESS' | 'ACCESS_2FA' | 'ACTION'; - documentAuthOptions: Document['authOptions']; - recipient: Pick; + documentAuthOptions: Envelope['authOptions']; + recipient: Pick; /** * The ID of the user who initiated the request. @@ -125,6 +125,7 @@ export const isRecipientAuthorized = async ({ } if (type === 'ACCESS_2FA' && method === 'email') { + // Todo: Envelopes - Need to pass in the secondary ID to parse the document ID for. if (!recipient.documentId) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document ID is required for email 2FA verification', diff --git a/packages/lib/server-only/document/reject-document-with-token.ts b/packages/lib/server-only/document/reject-document-with-token.ts index 2de1d0e81..831feccf2 100644 --- a/packages/lib/server-only/document/reject-document-with-token.ts +++ b/packages/lib/server-only/document/reject-document-with-token.ts @@ -1,4 +1,4 @@ -import { SigningStatus } from '@prisma/client'; +import { EnvelopeType, SigningStatus } from '@prisma/client'; import { jobs } from '@documenso/lib/jobs/client'; import { prisma } from '@documenso/prisma'; @@ -7,17 +7,19 @@ import { AppError, AppErrorCode } from '../../errors/app-error'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { mapSecondaryIdToDocumentId, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; export type RejectDocumentWithTokenOptions = { token: string; - documentId: number; + id: EnvelopeIdOptions; reason: string; requestMetadata?: RequestMetadata; }; export async function rejectDocumentWithToken({ token, - documentId, + id, reason, requestMetadata, }: RejectDocumentWithTokenOptions) { @@ -25,16 +27,16 @@ export async function rejectDocumentWithToken({ const recipient = await prisma.recipient.findFirst({ where: { token, - documentId, + envelope: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), }, include: { - document: true, + envelope: true, }, }); - const document = recipient?.document; + const envelope = recipient?.envelope; - if (!recipient || !document) { + if (!recipient || !envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document or recipient not found', }); @@ -54,7 +56,7 @@ export async function rejectDocumentWithToken({ }), prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ - documentId, + envelopeId: envelope.id, type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED, user: { name: recipient.name, @@ -72,11 +74,13 @@ export async function rejectDocumentWithToken({ }), ]); + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + // Trigger the seal document job to process the document asynchronously await jobs.triggerJob({ name: 'internal.seal-document', payload: { - documentId, + documentId: legacyDocumentId, requestMetadata, }, }); @@ -86,7 +90,7 @@ export async function rejectDocumentWithToken({ name: 'send.signing.rejected.emails', payload: { recipientId: recipient.id, - documentId, + documentId: legacyDocumentId, }, }); @@ -94,7 +98,7 @@ export async function rejectDocumentWithToken({ await jobs.triggerJob({ name: 'send.document.cancelled.emails', payload: { - documentId, + documentId: legacyDocumentId, cancellationReason: reason, requestMetadata, }, diff --git a/packages/lib/server-only/document/resend-document.ts b/packages/lib/server-only/document/resend-document.ts index 66e6fb09f..f1afd9aeb 100644 --- a/packages/lib/server-only/document/resend-document.ts +++ b/packages/lib/server-only/document/resend-document.ts @@ -1,7 +1,13 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import { DocumentStatus, OrganisationType, RecipientRole, SigningStatus } from '@prisma/client'; +import { + DocumentStatus, + EnvelopeType, + OrganisationType, + RecipientRole, + SigningStatus, +} from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document-invite'; @@ -21,7 +27,7 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email' import { isDocumentCompleted } from '../../utils/document'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; -import { getDocumentWhereInput } from './get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type ResendDocumentOptions = { documentId: number; @@ -42,16 +48,25 @@ export const resendDocument = async ({ where: { id: userId, }, + select: { + id: true, + email: true, + name: true, + }, }); - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findUnique({ - where: documentWhereInput, + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, include: { recipients: true, documentMeta: true, @@ -64,31 +79,29 @@ export const resendDocument = async ({ }, }); - const customEmail = document?.documentMeta; - - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - if (document.recipients.length === 0) { + if (envelope.recipients.length === 0) { throw new Error('Document has no recipients'); } - if (document.status === DocumentStatus.DRAFT) { + if (envelope.status === DocumentStatus.DRAFT) { throw new Error('Can not send draft document'); } - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { throw new Error('Can not send completed document'); } - const recipientsToRemind = document.recipients.filter( + const recipientsToRemind = envelope.recipients.filter( (recipient) => recipients.includes(recipient.id) && recipient.signingStatus === SigningStatus.NOT_SIGNED, ); const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientSigningRequest; if (!isRecipientSigningRequestEmailEnabled) { @@ -100,9 +113,9 @@ export const resendDocument = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); await Promise.all( @@ -122,42 +135,42 @@ export const resendDocument = async ({ ._(RECIPIENT_ROLES_DESCRIPTION[recipient.role].actionVerb) .toLowerCase(); - let emailMessage = customEmail?.message || ''; + let emailMessage = envelope.documentMeta.message || ''; let emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} this document`); if (selfSigner) { emailMessage = i18n._( - msg`You have initiated the document ${`"${document.title}"`} that requires you to ${recipientActionVerb} it.`, + msg`You have initiated the document ${`"${envelope.title}"`} that requires you to ${recipientActionVerb} it.`, ); emailSubject = i18n._(msg`Reminder: Please ${recipientActionVerb} your document`); } if (organisationType === OrganisationType.ORGANISATION) { emailSubject = i18n._( - msg`Reminder: ${document.team.name} invited you to ${recipientActionVerb} a document`, + msg`Reminder: ${envelope.team.name} invited you to ${recipientActionVerb} a document`, ); emailMessage = - customEmail?.message || + envelope.documentMeta.message || i18n._( - msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`, + msg`${user.name || user.email} on behalf of "${envelope.team.name}" has invited you to ${recipientActionVerb} the document "${envelope.title}".`, ); } const customEmailTemplate = { 'signer.name': name, 'signer.email': email, - 'document.name': document.title, + 'document.name': envelope.title, }; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const signDocumentLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`; const template = createElement(DocumentInviteEmailTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: user.name || undefined, inviterEmail: organisationType === OrganisationType.ORGANISATION - ? document.team?.teamEmail?.email || user.email + ? envelope.team?.teamEmail?.email || user.email : user.email, assetBaseUrl, signDocumentLink, @@ -165,7 +178,7 @@ export const resendDocument = async ({ role: recipient.role, selfSigner, organisationType, - teamName: document.team?.name, + teamName: envelope.team?.name, }); const [html, text] = await Promise.all([ @@ -189,9 +202,9 @@ export const resendDocument = async ({ }, from: senderEmail, replyTo: replyToEmail, - subject: customEmail?.subject + subject: envelope.documentMeta.subject ? renderCustomEmailTemplate( - i18n._(msg`Reminder: ${customEmail.subject}`), + i18n._(msg`Reminder: ${envelope.documentMeta.subject}`), customEmailTemplate, ) : emailSubject, @@ -202,7 +215,7 @@ export const resendDocument = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, + envelopeId: envelope.id, metadata: requestMetadata, data: { emailType: recipientEmailType, diff --git a/packages/lib/server-only/document/seal-document.ts b/packages/lib/server-only/document/seal-document.ts index 6db44eb73..e153cf992 100644 --- a/packages/lib/server-only/document/seal-document.ts +++ b/packages/lib/server-only/document/seal-document.ts @@ -1,4 +1,10 @@ -import { DocumentStatus, RecipientRole, SigningStatus, WebhookTriggerEvents } from '@prisma/client'; +import { + DocumentStatus, + EnvelopeType, + RecipientRole, + SigningStatus, + WebhookTriggerEvents, +} from '@prisma/client'; import { nanoid } from 'nanoid'; import path from 'node:path'; import { PDFDocument } from 'pdf-lib'; @@ -11,12 +17,17 @@ import { signPdf } from '@documenso/signing'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers'; +import { + type EnvelopeIdOptions, + mapSecondaryIdToDocumentId, + unsafeBuildEnvelopeIdQuery, +} from '../../utils/envelope'; import { getAuditLogsPdf } from '../htmltopdf/get-audit-logs-pdf'; import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf'; import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf'; @@ -30,49 +41,63 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { sendCompletedEmail } from './send-completed-email'; export type SealDocumentOptions = { - documentId: number; + id: EnvelopeIdOptions; sendEmail?: boolean; isResealing?: boolean; requestMetadata?: RequestMetadata; }; export const sealDocument = async ({ - documentId, + id, sendEmail = true, isResealing = false, requestMetadata, }: SealDocumentOptions) => { - const document = await prisma.document.findFirstOrThrow({ - where: { - id: documentId, - }, + const envelope = await prisma.envelope.findFirstOrThrow({ + where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), include: { - documentData: true, + envelopeItems: { + select: { + id: true, + documentData: true, + }, + include: { + field: { + include: { + signature: true, + }, + }, + }, + }, documentMeta: true, - recipients: true, - }, - }); - - const { documentData } = document; - - if (!documentData) { - throw new Error(`Document ${document.id} has no document data`); - } - - const settings = await getTeamSettings({ - userId: document.userId, - teamId: document.teamId, - }); - - const recipients = await prisma.recipient.findMany({ - where: { - documentId: document.id, - role: { - not: RecipientRole.CC, + recipients: { + where: { + role: { + not: RecipientRole.CC, + }, + }, }, }, }); + // Todo: Envelopes + const envelopeItemToSeal = envelope.envelopeItems[0]; + + // Todo: Envelopes + if (envelope.envelopeItems.length !== 1 || !envelopeItemToSeal) { + throw new Error(`Document ${envelope.id} needs exactly 1 envelope item`); + } + + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + const documentData = envelopeItemToSeal.documentData; + const fields = envelopeItemToSeal.field; // Todo: Envelopes - This only takes in the first envelope item fields. + const recipients = envelope.recipients; + + const settings = await getTeamSettings({ + userId: envelope.userId, + teamId: envelope.teamId, + }); + // Determine if the document has been rejected by checking if any recipient has rejected it const rejectedRecipient = recipients.find( (recipient) => recipient.signingStatus === SigningStatus.REJECTED, @@ -88,21 +113,12 @@ export const sealDocument = async ({ !isRejected && recipients.some((recipient) => recipient.signingStatus !== SigningStatus.SIGNED) ) { - throw new Error(`Document ${document.id} has unsigned recipients`); + throw new Error(`Envelope ${envelope.id} has unsigned recipients`); } - const fields = await prisma.field.findMany({ - where: { - documentId: document.id, - }, - include: { - signature: true, - }, - }); - // Skip the field check if the document is rejected if (!isRejected && fieldsContainUnsignedRequiredField(fields)) { - throw new Error(`Document ${document.id} has unsigned required fields`); + throw new Error(`Document ${envelope.id} has unsigned required fields`); } if (isResealing) { @@ -116,8 +132,8 @@ export const sealDocument = async ({ const certificateData = settings.includeSigningCertificate ? await getCertificatePdf({ - documentId, - language: document.documentMeta?.language, + documentId: legacyDocumentId, + language: envelope.documentMeta.language, }).catch((e) => { console.log('Failed to get certificate PDF'); console.error(e); @@ -128,8 +144,8 @@ export const sealDocument = async ({ const auditLogData = settings.includeAuditLog ? await getAuditLogsPdf({ - documentId, - language: document.documentMeta?.language, + documentId: legacyDocumentId, + language: envelope.documentMeta.language, }).catch((e) => { console.log('Failed to get audit logs PDF'); console.error(e); @@ -171,7 +187,7 @@ export const sealDocument = async ({ } for (const field of fields) { - document.useLegacyFieldInsertion + envelope.useLegacyFieldInsertion ? await legacy_insertFieldInPDF(doc, field) : await insertFieldInPDF(doc, field); } @@ -183,7 +199,8 @@ export const sealDocument = async ({ const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) }); - const { name } = path.parse(document.title); + // Todo: Envelopes use EnvelopeItem title instead. + const { name } = path.parse(envelope.title); // Add suffix based on document status const suffix = isRejected ? '_rejected.pdf' : '_signed.pdf'; @@ -201,16 +218,16 @@ export const sealDocument = async ({ distinctId: nanoid(), event: 'App: Document Sealed', properties: { - documentId: document.id, + documentId: envelope.id, isRejected, }, }); } await prisma.$transaction(async (tx) => { - await tx.document.update({ + await tx.envelope.update({ where: { - id: document.id, + id: envelope.id, }, data: { status: isRejected ? DocumentStatus.REJECTED : DocumentStatus.COMPLETED, @@ -230,7 +247,7 @@ export const sealDocument = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_COMPLETED, - documentId: document.id, + envelopeId: envelope.id, requestMetadata, user: null, data: { @@ -242,15 +259,14 @@ export const sealDocument = async ({ }); if (sendEmail && !isResealing) { - await sendCompletedEmail({ documentId, requestMetadata }); + await sendCompletedEmail({ id, requestMetadata }); } - const updatedDocument = await prisma.document.findFirstOrThrow({ + const updatedDocument = await prisma.envelope.findFirstOrThrow({ where: { - id: document.id, + id: envelope.id, }, include: { - documentData: true, documentMeta: true, recipients: true, }, @@ -260,8 +276,8 @@ export const sealDocument = async ({ event: isRejected ? WebhookTriggerEvents.DOCUMENT_REJECTED : WebhookTriggerEvents.DOCUMENT_COMPLETED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)), - userId: document.userId, - teamId: document.teamId ?? undefined, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedDocument)), + userId: envelope.userId, + teamId: envelope.teamId ?? undefined, }); }; diff --git a/packages/lib/server-only/document/search-documents-with-keyword.ts b/packages/lib/server-only/document/search-documents-with-keyword.ts index 600d07df9..d1eabe77e 100644 --- a/packages/lib/server-only/document/search-documents-with-keyword.ts +++ b/packages/lib/server-only/document/search-documents-with-keyword.ts @@ -1,5 +1,6 @@ -import type { Document, Recipient, User } from '@prisma/client'; -import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; +import type { Envelope, Recipient, User } from '@prisma/client'; +import { DocumentVisibility, TeamMemberRole } from '@prisma/client'; import { match } from 'ts-pattern'; import { @@ -9,6 +10,8 @@ import { } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; +import { mapSecondaryIdToDocumentId } from '../../utils/envelope'; + export type SearchDocumentsWithKeywordOptions = { query: string; userId: number; @@ -26,8 +29,9 @@ export const searchDocumentsWithKeyword = async ({ }, }); - const documents = await prisma.document.findMany({ + const envelopes = await prisma.envelope.findMany({ where: { + type: EnvelopeType.DOCUMENT, OR: [ { title: { @@ -128,26 +132,26 @@ export const searchDocumentsWithKeyword = async ({ take: limit, }); - const isOwner = (document: Document, user: User) => document.userId === user.id; + const isOwner = (envelope: Envelope, user: User) => envelope.userId === user.id; const getSigningLink = (recipients: Recipient[], user: User) => `/sign/${recipients.find((r) => r.email === user.email)?.token}`; - const maskedDocuments = documents - .filter((document) => { - if (!document.teamId || isOwner(document, user)) { + const maskedDocuments = envelopes + .filter((envelope) => { + if (!envelope.teamId || isOwner(envelope, user)) { return true; } const teamMemberRole = getHighestTeamRoleInGroup( - document.team.teamGroups.filter((tg) => tg.teamId === document.teamId), + envelope.team.teamGroups.filter((tg) => tg.teamId === envelope.teamId), ); if (!teamMemberRole) { return false; } - const canAccessDocument = match([document.visibility, teamMemberRole]) + const canAccessDocument = match([envelope.visibility, teamMemberRole]) .with([DocumentVisibility.EVERYONE, TeamMemberRole.ADMIN], () => true) .with([DocumentVisibility.EVERYONE, TeamMemberRole.MANAGER], () => true) .with([DocumentVisibility.EVERYONE, TeamMemberRole.MEMBER], () => true) @@ -158,23 +162,29 @@ export const searchDocumentsWithKeyword = async ({ return canAccessDocument; }) - .map((document) => { - const { recipients, ...documentWithoutRecipient } = document; + .map((envelope) => { + const { recipients, ...documentWithoutRecipient } = envelope; let documentPath; - if (isOwner(document, user)) { - documentPath = `${formatDocumentsPath(document.team?.url)}/${document.id}`; - } else if (document.teamId && document.team.teamGroups.length > 0) { - documentPath = `${formatDocumentsPath(document.team.url)}/${document.id}`; + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + + if (isOwner(envelope, user)) { + documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`; + } else if (envelope.teamId && envelope.team.teamGroups.length > 0) { + documentPath = `${formatDocumentsPath(envelope.team.url)}/${legacyDocumentId}`; } else { documentPath = getSigningLink(recipients, user); } return { ...documentWithoutRecipient, + team: { + id: envelope.teamId, + url: envelope.team.url, + }, path: documentPath, - value: [document.id, document.title, ...document.recipients.map((r) => r.email)].join(' '), + value: [envelope.id, envelope.title, ...envelope.recipients.map((r) => r.email)].join(' '), }; }); diff --git a/packages/lib/server-only/document/send-completed-email.ts b/packages/lib/server-only/document/send-completed-email.ts index b5e5ba256..017929a3b 100644 --- a/packages/lib/server-only/document/send-completed-email.ts +++ b/packages/lib/server-only/document/send-completed-email.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import { DocumentSource } from '@prisma/client'; +import { DocumentSource, EnvelopeType } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import { DocumentCompletedEmailTemplate } from '@documenso/email/templates/document-completed'; @@ -14,23 +14,33 @@ import { extractDerivedDocumentEmailSettings } from '../../types/document-email' import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { renderCustomEmailTemplate } from '../../utils/render-custom-email-template'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { formatDocumentsPath } from '../../utils/teams'; import { getEmailContext } from '../email/get-email-context'; export interface SendDocumentOptions { - documentId: number; + id: EnvelopeIdOptions; requestMetadata?: RequestMetadata; } -export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDocumentOptions) => { - const document = await prisma.document.findUnique({ - where: { - id: documentId, - }, +export const sendCompletedEmail = async ({ id, requestMetadata }: SendDocumentOptions) => { + const envelope = await prisma.envelope.findUnique({ + where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), include: { - documentData: true, + envelopeItems: { + include: { + documentData: { + select: { + type: true, + id: true, + data: true, + }, + }, + }, + }, documentMeta: true, recipients: true, user: { @@ -49,13 +59,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo }, }); - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - const isDirectTemplate = document?.source === DocumentSource.TEMPLATE_DIRECT_LINK; + const isDirectTemplate = envelope?.source === DocumentSource.TEMPLATE_DIRECT_LINK; - if (document.recipients.length === 0) { + if (envelope.recipients.length === 0) { throw new Error('Document has no recipients'); } @@ -63,28 +73,37 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); - const { user: owner } = document; + const { user: owner } = envelope; - const completedDocument = await getFileServerSide(document.documentData); + const completedDocumentEmailAttachments = await Promise.all( + envelope.envelopeItems.map(async (document) => { + const file = await getFileServerSide(document.documentData); + + return { + fileName: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf', + content: Buffer.from(file), + }; + }), + ); const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; let documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath( - document.team?.url, - )}/${document.id}`; + envelope.team?.url, + )}/${envelope.id}`; - if (document.team?.url) { - documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${ - document.id + if (envelope.team?.url) { + documentOwnerDownloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/t/${envelope.team.url}/documents/${ + envelope.id }`; } - const emailSettings = extractDerivedDocumentEmailSettings(document.documentMeta); + const emailSettings = extractDerivedDocumentEmailSettings(envelope.documentMeta); const isDocumentCompletedEmailEnabled = emailSettings.documentCompleted; const isOwnerDocumentCompletedEmailEnabled = emailSettings.ownerDocumentCompleted; @@ -95,11 +114,11 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo // - Recipient emails are disabled if ( isOwnerDocumentCompletedEmailEnabled && - (!document.recipients.find((recipient) => recipient.email === owner.email) || + (!envelope.recipients.find((recipient) => recipient.email === owner.email) || !isDocumentCompletedEmailEnabled) ) { const template = createElement(DocumentCompletedEmailTemplate, { - documentName: document.title, + documentName: envelope.title, assetBaseUrl, downloadLink: documentOwnerDownloadLink, }); @@ -127,18 +146,13 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo subject: i18n._(msg`Signing Complete!`), html, text, - attachments: [ - { - filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf', - content: Buffer.from(completedDocument), - }, - ], + attachments: completedDocumentEmailAttachments, }); await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, + envelopeId: envelope.id, user: null, requestMetadata, data: { @@ -158,22 +172,22 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo } await Promise.all( - document.recipients.map(async (recipient) => { + envelope.recipients.map(async (recipient) => { const customEmailTemplate = { 'signer.name': recipient.name, 'signer.email': recipient.email, - 'document.name': document.title, + 'document.name': envelope.title, }; const downloadLink = `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}/complete`; const template = createElement(DocumentCompletedEmailTemplate, { - documentName: document.title, + documentName: envelope.title, assetBaseUrl, downloadLink: recipient.email === owner.email ? documentOwnerDownloadLink : downloadLink, customBody: - isDirectTemplate && document.documentMeta?.message - ? renderCustomEmailTemplate(document.documentMeta.message, customEmailTemplate) + isDirectTemplate && envelope.documentMeta?.message + ? renderCustomEmailTemplate(envelope.documentMeta.message, customEmailTemplate) : undefined, }); @@ -198,23 +212,18 @@ export const sendCompletedEmail = async ({ documentId, requestMetadata }: SendDo from: senderEmail, replyTo: replyToEmail, subject: - isDirectTemplate && document.documentMeta?.subject - ? renderCustomEmailTemplate(document.documentMeta.subject, customEmailTemplate) + isDirectTemplate && envelope.documentMeta?.subject + ? renderCustomEmailTemplate(envelope.documentMeta.subject, customEmailTemplate) : i18n._(msg`Signing Complete!`), html, text, - attachments: [ - { - filename: document.title.endsWith('.pdf') ? document.title : document.title + '.pdf', - content: Buffer.from(completedDocument), - }, - ], + attachments: completedDocumentEmailAttachments, }); await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT, - documentId: document.id, + envelopeId: envelope.id, user: null, requestMetadata, data: { diff --git a/packages/lib/server-only/document/send-delete-email.ts b/packages/lib/server-only/document/send-delete-email.ts index e76ac636b..ff8bfe29c 100644 --- a/packages/lib/server-only/document/send-delete-email.ts +++ b/packages/lib/server-only/document/send-delete-email.ts @@ -14,14 +14,15 @@ import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; export interface SendDeleteEmailOptions { - documentId: number; + envelopeId: string; reason: string; } -export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOptions) => { - const document = await prisma.document.findFirst({ +// Note: Currently only sent by Admin function +export const sendDeleteEmail = async ({ envelopeId, reason }: SendDeleteEmailOptions) => { + const envelope = await prisma.envelope.findFirst({ where: { - id: documentId, + id: envelopeId, }, include: { user: { @@ -35,14 +36,14 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } const isDocumentDeletedEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).documentDeleted; if (!isDocumentDeletedEmailEnabled) { @@ -53,17 +54,17 @@ export const sendDeleteEmail = async ({ documentId, reason }: SendDeleteEmailOpt emailType: 'INTERNAL', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); - const { email, name } = document.user; + const { email, name } = envelope.user; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(DocumentSuperDeleteEmailTemplate, { - documentName: document.title, + documentName: envelope.title, reason, assetBaseUrl, }); diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.ts similarity index 69% rename from packages/lib/server-only/document/send-document.tsx rename to packages/lib/server-only/document/send-document.ts index 22dbcb071..d5e970930 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.ts @@ -1,6 +1,7 @@ import { DocumentSigningOrder, DocumentStatus, + EnvelopeType, RecipientRole, SendStatus, SigningStatus, @@ -16,17 +17,18 @@ import { jobs } from '../../jobs/client'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { isDocumentCompleted } from '../../utils/document'; +import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { insertFormValuesInPdf } from '../pdf/insert-form-values-in-pdf'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; -import { getDocumentWhereInput } from './get-document-by-id'; export type SendDocumentOptions = { - documentId: number; + id: EnvelopeIdOptions; userId: number; teamId: number; sendEmail?: boolean; @@ -34,75 +36,92 @@ export type SendDocumentOptions = { }; export const sendDocument = async ({ - documentId, + id, userId, teamId, sendEmail, requestMetadata, }: SendDocumentOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: { orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }], }, documentMeta: true, - documentData: true, + envelopeItems: { + select: { + id: true, + documentData: { + select: { + type: true, + id: true, + data: true, + }, + }, + }, + }, }, }); - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - if (document.recipients.length === 0) { + if (envelope.recipients.length === 0) { throw new Error('Document has no recipients'); } - if (isDocumentCompleted(document.status)) { + if (isDocumentCompleted(envelope.status)) { throw new Error('Can not send completed document'); } - const signingOrder = document.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL; + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); - let recipientsToNotify = document.recipients; + const signingOrder = envelope.documentMeta?.signingOrder || DocumentSigningOrder.PARALLEL; + + let recipientsToNotify = envelope.recipients; if (signingOrder === DocumentSigningOrder.SEQUENTIAL) { // Get the currently active recipient. - recipientsToNotify = document.recipients + recipientsToNotify = envelope.recipients .filter((r) => r.signingStatus === SigningStatus.NOT_SIGNED && r.role !== RecipientRole.CC) .slice(0, 1); // Secondary filter so we aren't resending if the current active recipient has already - // received the document. + // received the envelope. recipientsToNotify.filter((r) => r.sendStatus !== SendStatus.SENT); } - const { documentData } = document; + const envelopeItem = envelope.envelopeItems[0]; + const documentData = envelopeItem?.documentData; - if (!documentData.data) { - throw new Error('Document data not found'); + // Todo: Envelopes + if (!envelopeItem || !documentData || envelope.envelopeItems.length !== 1) { + throw new Error('Invalid document data'); } - if (document.formValues) { + // Todo: Envelopes need to support multiple envelope items. + if (envelope.formValues) { const file = await getFileServerSide(documentData); const prefilled = await insertFormValuesInPdf({ pdf: Buffer.from(file), // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - formValues: document.formValues as Record, + formValues: envelope.formValues as Record, }); - let fileName = document.title; + let fileName = envelope.title; - if (!document.title.endsWith('.pdf')) { - fileName = `${document.title}.pdf`; + if (!envelope.title.endsWith('.pdf')) { + fileName = `${envelope.title}.pdf`; } const newDocumentData = await putPdfFileServerSide({ @@ -111,9 +130,9 @@ export const sendDocument = async ({ arrayBuffer: async () => Promise.resolve(prefilled), }); - const result = await prisma.document.update({ + const result = await prisma.envelopeItem.update({ where: { - id: document.id, + id: envelopeItem.id, }, data: { documentDataId: newDocumentData.id, @@ -133,7 +152,7 @@ export const sendDocument = async ({ // const fieldsWithSignerEmail = fields.map((field) => ({ // ...field, // signerEmail: - // document.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '', + // envelope.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '', // })); // const everySignerHasSignature = document?.Recipient.every( @@ -148,7 +167,7 @@ export const sendDocument = async ({ // throw new Error('Some signers have not been assigned a signature field.'); // } - const allRecipientsHaveNoActionToTake = document.recipients.every( + const allRecipientsHaveNoActionToTake = envelope.recipients.every( (recipient) => recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED, ); @@ -157,15 +176,15 @@ export const sendDocument = async ({ await jobs.triggerJob({ name: 'internal.seal-document', payload: { - documentId, + documentId: legacyDocumentId, requestMetadata: requestMetadata?.requestMetadata, }, }); // Keep the return type the same for the `sendDocument` method - return await prisma.document.findFirstOrThrow({ + return await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + id: envelope.id, }, include: { documentMeta: true, @@ -174,21 +193,21 @@ export const sendDocument = async ({ }); } - const updatedDocument = await prisma.$transaction(async (tx) => { - if (document.status === DocumentStatus.DRAFT) { + const updatedEnvelope = await prisma.$transaction(async (tx) => { + if (envelope.status === DocumentStatus.DRAFT) { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_SENT, - documentId: document.id, + envelopeId: envelope.id, metadata: requestMetadata, data: {}, }), }); } - return await tx.document.update({ + return await tx.envelope.update({ where: { - id: documentId, + id: envelope.id, }, data: { status: DocumentStatus.PENDING, @@ -201,7 +220,7 @@ export const sendDocument = async ({ }); const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientSigningRequest; // Only send email if one of the following is true: @@ -218,7 +237,7 @@ export const sendDocument = async ({ name: 'send.signing.requested.email', payload: { userId, - documentId, + documentId: legacyDocumentId, recipientId: recipient.id, requestMetadata: requestMetadata?.requestMetadata, }, @@ -229,10 +248,10 @@ export const sendDocument = async ({ await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_SENT, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(updatedDocument)), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(updatedEnvelope)), userId, teamId, }); - return updatedDocument; + return updatedEnvelope; }; diff --git a/packages/lib/server-only/document/send-pending-email.ts b/packages/lib/server-only/document/send-pending-email.ts index 663ba0554..82911a795 100644 --- a/packages/lib/server-only/document/send-pending-email.ts +++ b/packages/lib/server-only/document/send-pending-email.ts @@ -1,6 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; +import { EnvelopeType } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import { DocumentPendingEmailTemplate } from '@documenso/email/templates/document-pending'; @@ -9,18 +10,20 @@ import { prisma } from '@documenso/prisma'; import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { getEmailContext } from '../email/get-email-context'; export interface SendPendingEmailOptions { - documentId: number; + id: EnvelopeIdOptions; recipientId: number; } -export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingEmailOptions) => { - const document = await prisma.document.findFirst({ +export const sendPendingEmail = async ({ id, recipientId }: SendPendingEmailOptions) => { + const envelope = await prisma.envelope.findFirst({ where: { - id: documentId, + ...unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT), recipients: { some: { id: recipientId, @@ -37,11 +40,11 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE }, }); - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - if (document.recipients.length === 0) { + if (envelope.recipients.length === 0) { throw new Error('Document has no recipients'); } @@ -49,27 +52,27 @@ export const sendPendingEmail = async ({ documentId, recipientId }: SendPendingE emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); const isDocumentPendingEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).documentPending; if (!isDocumentPendingEmailEnabled) { return; } - const [recipient] = document.recipients; + const [recipient] = envelope.recipients; const { email, name } = recipient; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(DocumentPendingEmailTemplate, { - documentName: document.title, + documentName: envelope.title, assetBaseUrl, }); diff --git a/packages/lib/server-only/document/update-document.ts b/packages/lib/server-only/document/update-document.ts index 0c1586498..aae861764 100644 --- a/packages/lib/server-only/document/update-document.ts +++ b/packages/lib/server-only/document/update-document.ts @@ -1,7 +1,7 @@ -import { DocumentVisibility } from '@prisma/client'; -import { DocumentStatus, TeamMemberRole } from '@prisma/client'; +import type { DocumentVisibility, Prisma } from '@prisma/client'; +import { EnvelopeType, FolderType } from '@prisma/client'; +import { DocumentStatus } from '@prisma/client'; import { isDeepEqual } from 'remeda'; -import { match } from 'ts-pattern'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -9,10 +9,12 @@ import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/do import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { prisma } from '@documenso/prisma'; +import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth'; -import { getDocumentWhereInput } from './get-document-by-id'; +import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type UpdateDocumentOptions = { userId: number; @@ -25,6 +27,7 @@ export type UpdateDocumentOptions = { globalAccessAuth?: TDocumentAccessAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[]; useLegacyFieldInsertion?: boolean; + folderId?: string | null; }; requestMetadata: ApiRequestMetadata; }; @@ -36,14 +39,18 @@ export const updateDocument = async ({ data, requestMetadata, }: UpdateDocumentOptions) => { - const { documentWhereInput, team } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput, team } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { team: { select: { @@ -57,57 +64,70 @@ export const updateDocument = async ({ }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - const isDocumentOwner = document.userId === userId; - const requestedVisibility = data?.visibility; + const isEnvelopeOwner = envelope.userId === userId; - if (!isDocumentOwner) { - match(team.currentTeamRole) - .with(TeamMemberRole.ADMIN, () => true) - .with(TeamMemberRole.MANAGER, () => { - const allowedVisibilities: DocumentVisibility[] = [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - ]; - - if ( - !allowedVisibilities.includes(document.visibility) || - (requestedVisibility && !allowedVisibilities.includes(requestedVisibility)) - ) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'You do not have permission to update the document visibility', - }); - } - }) - .with(TeamMemberRole.MEMBER, () => { - if ( - document.visibility !== DocumentVisibility.EVERYONE || - (requestedVisibility && requestedVisibility !== DocumentVisibility.EVERYONE) - ) { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'You do not have permission to update the document visibility', - }); - } - }) - .otherwise(() => { - throw new AppError(AppErrorCode.UNAUTHORIZED, { - message: 'You do not have permission to update the document', - }); - }); + // Validate whether the new visibility setting is allowed for the current user. + if ( + !isEnvelopeOwner && + data?.visibility && + !canAccessTeamDocument(team.currentTeamRole, data.visibility) + ) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'You do not have permission to update the document visibility', + }); } // If no data just return the document since this function is normally chained after a meta update. if (!data || Object.values(data).length === 0) { - return document; + return envelope; + } + + let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined; + + // Validate folder ID. + if (data.folderId) { + const folder = await prisma.folder.findFirst({ + where: { + id: data.folderId, + team: buildTeamWhereQuery({ + teamId, + userId, + }), + type: FolderType.DOCUMENT, + visibility: { + in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole], + }, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + folderUpdateQuery = { + connect: { + id: data.folderId, + }, + }; + } + + // Move to root folder if folderId is null. + if (data.folderId === null) { + folderUpdateQuery = { + disconnect: true, + }; } const { documentAuthOption } = extractDocumentAuthMethods({ - documentAuth: document.authOptions, + documentAuth: envelope.authOptions, }); const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null; @@ -120,14 +140,14 @@ export const updateDocument = async ({ data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth; // Check if user has permission to set the global action auth. - if (newGlobalActionAuth.length > 0 && !document.team.organisation.organisationClaim.flags.cfr21) { + if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); } - const isTitleSame = data.title === undefined || data.title === document.title; - const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId; + const isTitleSame = data.title === undefined || data.title === envelope.title; + const isExternalIdSame = data.externalId === undefined || data.externalId === envelope.externalId; const isGlobalAccessSame = documentGlobalAccessAuth === undefined || isDeepEqual(documentGlobalAccessAuth, newGlobalAccessAuth); @@ -135,11 +155,12 @@ export const updateDocument = async ({ documentGlobalActionAuth === undefined || isDeepEqual(documentGlobalActionAuth, newGlobalActionAuth); const isDocumentVisibilitySame = - data.visibility === undefined || data.visibility === document.visibility; + data.visibility === undefined || data.visibility === envelope.visibility; + const isFolderSame = data.folderId === undefined || data.folderId === envelope.folderId; const auditLogs: CreateDocumentAuditLogDataResponse[] = []; - if (!isTitleSame && document.status !== DocumentStatus.DRAFT) { + if (!isTitleSame && envelope.status !== DocumentStatus.DRAFT) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'You cannot update the title if the document has been sent', }); @@ -149,10 +170,10 @@ export const updateDocument = async ({ auditLogs.push( createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_TITLE_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { - from: document.title, + from: envelope.title, to: data.title || '', }, }), @@ -163,10 +184,10 @@ export const updateDocument = async ({ auditLogs.push( createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_EXTERNAL_ID_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { - from: document.externalId, + from: envelope.externalId, to: data.externalId || '', }, }), @@ -177,7 +198,7 @@ export const updateDocument = async ({ auditLogs.push( createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACCESS_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { from: documentGlobalAccessAuth, @@ -191,7 +212,7 @@ export const updateDocument = async ({ auditLogs.push( createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_GLOBAL_AUTH_ACTION_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { from: documentGlobalActionAuth, @@ -205,19 +226,34 @@ export const updateDocument = async ({ auditLogs.push( createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VISIBILITY_UPDATED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { - from: document.visibility, + from: envelope.visibility, to: data.visibility || '', }, }), ); } + // Todo: Decide if we want to log moving the document around. + // if (!isFolderSame) { + // auditLogs.push( + // createDocumentAuditLogData({ + // type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FOLDER_UPDATED, + // envelopeId: envelope.id, + // metadata: requestMetadata, + // data: { + // from: envelope.folderId, + // to: data.folderId || '', + // }, + // }), + // ); + // } + // Early return if nothing is required. - if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined) { - return document; + if (auditLogs.length === 0 && data.useLegacyFieldInsertion === undefined && isFolderSame) { + return envelope; } return await prisma.$transaction(async (tx) => { @@ -226,9 +262,10 @@ export const updateDocument = async ({ globalActionAuth: newGlobalActionAuth, }); - const updatedDocument = await tx.document.update({ + const updatedDocument = await tx.envelope.update({ where: { - id: documentId, + id: envelope.id, + type: EnvelopeType.DOCUMENT, }, data: { title: data.title, @@ -236,6 +273,7 @@ export const updateDocument = async ({ visibility: data.visibility as DocumentVisibility, useLegacyFieldInsertion: data.useLegacyFieldInsertion, authOptions, + folder: folderUpdateQuery, }, }); diff --git a/packages/lib/server-only/document/validate-field-auth.ts b/packages/lib/server-only/document/validate-field-auth.ts index cbb9bf55f..77ac4bd9a 100644 --- a/packages/lib/server-only/document/validate-field-auth.ts +++ b/packages/lib/server-only/document/validate-field-auth.ts @@ -1,4 +1,4 @@ -import type { Document, Field, Recipient } from '@prisma/client'; +import type { Envelope, Field, Recipient } from '@prisma/client'; import { FieldType } from '@prisma/client'; import { AppError, AppErrorCode } from '../../errors/app-error'; @@ -6,8 +6,8 @@ import type { TRecipientActionAuth } from '../../types/document-auth'; import { isRecipientAuthorized } from './is-recipient-authorized'; export type ValidateFieldAuthOptions = { - documentAuthOptions: Document['authOptions']; - recipient: Pick; + documentAuthOptions: Envelope['authOptions']; + recipient: Pick; field: Field; userId?: number; authOptions?: TRecipientActionAuth; diff --git a/packages/lib/server-only/document/viewed-document.ts b/packages/lib/server-only/document/viewed-document.ts index a9faf992a..4a7ee0ef8 100644 --- a/packages/lib/server-only/document/viewed-document.ts +++ b/packages/lib/server-only/document/viewed-document.ts @@ -1,4 +1,4 @@ -import { ReadStatus, SendStatus } from '@prisma/client'; +import { EnvelopeType, ReadStatus, SendStatus } from '@prisma/client'; import { WebhookTriggerEvents } from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -9,7 +9,7 @@ import { prisma } from '@documenso/prisma'; import type { TDocumentAccessAuthTypes } from '../../types/document-auth'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -27,19 +27,30 @@ export const viewedDocument = async ({ const recipient = await prisma.recipient.findFirst({ where: { token, + envelope: { + type: EnvelopeType.DOCUMENT, + }, + }, + include: { + envelope: { + include: { + documentMeta: true, + recipients: true, + }, + }, }, }); - if (!recipient || !recipient.documentId) { + if (!recipient) { return; } - const { documentId } = recipient; + const { envelope } = recipient; await prisma.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED, - documentId, + envelopeId: envelope.id, user: { name: recipient.name, email: recipient.email, @@ -75,7 +86,7 @@ export const viewedDocument = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, - documentId, + envelopeId: envelope.id, user: { name: recipient.name, email: recipient.email, @@ -92,24 +103,10 @@ export const viewedDocument = async ({ }); }); - const document = await prisma.document.findFirst({ - where: { - id: documentId, - }, - include: { - documentMeta: true, - recipients: true, - }, - }); - - if (!document) { - throw new Error('Document not found'); - } - await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_OPENED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(document)), - userId: document.userId, - teamId: document.teamId ?? undefined, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)), + userId: envelope.userId, + teamId: envelope.teamId, }); }; diff --git a/packages/lib/server-only/document/create-document-v2.ts b/packages/lib/server-only/envelope/create-envelope.ts similarity index 53% rename from packages/lib/server-only/document/create-document-v2.ts rename to packages/lib/server-only/envelope/create-envelope.ts index da6655c79..5f45c28fa 100644 --- a/packages/lib/server-only/document/create-document-v2.ts +++ b/packages/lib/server-only/envelope/create-envelope.ts @@ -1,6 +1,7 @@ -import type { DocumentMeta, DocumentVisibility } from '@prisma/client'; +import type { DocumentMeta, DocumentVisibility, TemplateType } from '@prisma/client'; import { DocumentSource, + EnvelopeType, FolderType, RecipientRole, SendStatus, @@ -21,47 +22,68 @@ import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../t import type { TDocumentFormValues } from '../../types/document-form-values'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import { getFileServerSide } from '../../universal/upload/get-file.server'; import { putPdfFileServerSide } from '../../universal/upload/put-file.server'; import { extractDerivedDocumentMeta } from '../../utils/document'; import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth'; -import { determineDocumentVisibility } from '../../utils/document-visibility'; import { buildTeamWhereQuery } from '../../utils/teams'; -import { getMemberRoles } from '../team/get-member-roles'; +import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-id'; import { getTeamSettings } from '../team/get-team-settings'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; -export type CreateDocumentOptions = { +export type CreateEnvelopeOptions = { userId: number; teamId: number; - documentDataId: string; normalizePdf?: boolean; data: { + type: EnvelopeType; title: string; externalId?: string; + envelopeItems: { title?: string; documentDataId: string }[]; + formValues?: TDocumentFormValues; + + timezone?: string; + userTimezone?: string; + + templateType?: TemplateType; + publicTitle?: string; + publicDescription?: string; + visibility?: DocumentVisibility; globalAccessAuth?: TDocumentAccessAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[]; - formValues?: TDocumentFormValues; - recipients: TCreateDocumentTemporaryRequest['recipients']; + recipients?: TCreateDocumentTemporaryRequest['recipients']; folderId?: string; }; - meta?: Partial>; + meta?: Partial>; requestMetadata: ApiRequestMetadata; }; -export const createDocumentV2 = async ({ +export const createEnvelope = async ({ userId, teamId, - documentDataId, normalizePdf, data, meta, requestMetadata, -}: CreateDocumentOptions) => { - const { title, formValues, folderId } = data; +}: CreateEnvelopeOptions) => { + const { + type, + title, + externalId, + formValues, + timezone, + userTimezone, + folderId, + templateType, + globalAccessAuth, + globalActionAuth, + publicTitle, + publicDescription, + visibility: visibilityOverride, + } = data; const team = await prisma.team.findFirst({ where: buildTeamWhereQuery({ teamId, userId }), @@ -80,11 +102,12 @@ export const createDocumentV2 = async ({ }); } + // Verify that the folder exists and is associated with the team. if (folderId) { const folder = await prisma.folder.findUnique({ where: { id: folderId, - type: FolderType.DOCUMENT, + type: data.type === EnvelopeType.TEMPLATE ? FolderType.TEMPLATE : FolderType.DOCUMENT, team: buildTeamWhereQuery({ teamId, userId }), }, }); @@ -101,32 +124,44 @@ export const createDocumentV2 = async ({ teamId, }); + let envelopeItems: { title?: string; documentDataId: string }[] = data.envelopeItems; + if (normalizePdf) { - const documentData = await prisma.documentData.findFirst({ - where: { - id: documentDataId, - }, - }); + envelopeItems = await Promise.all( + data.envelopeItems.map(async (item) => { + const documentData = await prisma.documentData.findFirst({ + where: { + id: item.documentDataId, + }, + }); - if (documentData) { - const buffer = await getFileServerSide(documentData); + if (!documentData) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document data not found', + }); + } - const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer)); + const buffer = await getFileServerSide(documentData); - const newDocumentData = await putPdfFileServerSide({ - name: title.endsWith('.pdf') ? title : `${title}.pdf`, - type: 'application/pdf', - arrayBuffer: async () => Promise.resolve(normalizedPdf), - }); + const normalizedPdf = await makeNormalizedPdf(Buffer.from(buffer)); - // eslint-disable-next-line require-atomic-updates - documentDataId = newDocumentData.id; - } + const newDocumentData = await putPdfFileServerSide({ + name: title.endsWith('.pdf') ? title : `${title}.pdf`, + type: 'application/pdf', + arrayBuffer: async () => Promise.resolve(normalizedPdf), + }); + + return { + title: item.title, + documentDataId: newDocumentData.id, + }; + }), + ); } const authOptions = createDocumentAuthOptions({ - globalAccessAuth: data?.globalAccessAuth || [], - globalActionAuth: data?.globalActionAuth || [], + globalAccessAuth: globalAccessAuth || [], + globalActionAuth: globalActionAuth || [], }); const recipientsHaveActionAuth = data.recipients?.some( @@ -143,15 +178,7 @@ export const createDocumentV2 = async ({ }); } - const { teamRole } = await getMemberRoles({ - teamId, - reference: { - type: 'User', - id: userId, - }, - }); - - const visibility = determineDocumentVisibility(settings.documentVisibility, teamRole); + const visibility = visibilityOverride || settings.documentVisibility; const emailId = meta?.emailId; @@ -171,26 +198,62 @@ export const createDocumentV2 = async ({ } } + // userTimezone is last because it's always passed in regardless of the organisation/team settings + // for uploads from the frontend + const timezoneToUse = timezone || settings.documentTimezone || userTimezone; + + const documentMeta = await prisma.documentMeta.create({ + data: extractDerivedDocumentMeta(settings, { + ...meta, + timezone: timezoneToUse, + }), + }); + + const secondaryId = + type === EnvelopeType.DOCUMENT + ? await incrementDocumentId().then((v) => v.formattedDocumentId) + : await incrementTemplateId().then((v) => v.formattedTemplateId); + return await prisma.$transaction(async (tx) => { - const document = await tx.document.create({ + const envelope = await tx.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId, + type, title, qrToken: prefixedId('qr'), - externalId: data.externalId, - documentDataId, + externalId, + envelopeItems: { + createMany: { + data: envelopeItems.map((item) => ({ + id: prefixedId('envelope_item'), + title: item.title || title, + documentDataId: item.documentDataId, + })), + }, + }, userId, teamId, authOptions, visibility, folderId, formValues, - source: DocumentSource.DOCUMENT, - documentMeta: { - create: extractDerivedDocumentMeta(settings, meta), - }, + source: DocumentSource.DOCUMENT, // Todo: Migration + documentMetaId: documentMeta.id, + + // Template specific fields. + templateType: type === EnvelopeType.TEMPLATE ? templateType : undefined, + publicTitle: type === EnvelopeType.TEMPLATE ? publicTitle : undefined, + publicDescription: type === EnvelopeType.TEMPLATE ? publicDescription : undefined, + }, + include: { + envelopeItems: true, }, }); + // Todo: Envelopes - Support multiple envelope items. + const firstEnvelopeItemId = envelope.envelopeItems[0].id; + await Promise.all( (data.recipients || []).map(async (recipient) => { const recipientAuthOptions = createRecipientAuthOptions({ @@ -200,7 +263,7 @@ export const createDocumentV2 = async ({ await tx.recipient.create({ data: { - documentId: document.id, + envelopeId: envelope.id, name: recipient.name, email: recipient.email, role: recipient.role, @@ -213,7 +276,8 @@ export const createDocumentV2 = async ({ fields: { createMany: { data: (recipient.fields || []).map((field) => ({ - documentId: document.id, + envelopeId: envelope.id, + envelopeItemId: firstEnvelopeItemId, type: field.type, page: field.pageNumber, positionX: field.pageX, @@ -231,48 +295,49 @@ export const createDocumentV2 = async ({ }), ); - // Todo: Is it necessary to create a full audit logs with all fields and recipients audit logs? - - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, - documentId: document.id, - metadata: requestMetadata, - data: { - title, - source: { - type: DocumentSource.DOCUMENT, - }, - }, - }), - }); - - const createdDocument = await tx.document.findFirst({ + const createdEnvelope = await tx.envelope.findFirst({ where: { - id: document.id, + id: envelope.id, }, include: { - documentData: true, documentMeta: true, recipients: true, fields: true, folder: true, + envelopeItems: true, }, }); - if (!createdDocument) { + if (!createdEnvelope) { throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', + message: 'Envelope not found', }); } - await triggerWebhook({ - event: WebhookTriggerEvents.DOCUMENT_CREATED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)), - userId, - teamId, - }); + // Only create audit logs and webhook events for documents. + if (type === EnvelopeType.DOCUMENT) { + await tx.documentAuditLog.create({ + data: createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, + envelopeId: envelope.id, + metadata: requestMetadata, + data: { + title, + source: { + type: DocumentSource.DOCUMENT, + }, + }, + }), + }); - return createdDocument; + await triggerWebhook({ + event: WebhookTriggerEvents.DOCUMENT_CREATED, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), + userId, + teamId, + }); + } + + return createdEnvelope; }); }; diff --git a/packages/lib/server-only/envelope/get-envelope-by-id.ts b/packages/lib/server-only/envelope/get-envelope-by-id.ts new file mode 100644 index 000000000..687a07b5a --- /dev/null +++ b/packages/lib/server-only/envelope/get-envelope-by-id.ts @@ -0,0 +1,168 @@ +import type { Prisma } from '@prisma/client'; +import type { EnvelopeType } from '@prisma/client'; + +import { prisma } from '@documenso/prisma'; + +import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; +import { AppError, AppErrorCode } from '../../errors/app-error'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; +import { getTeamById } from '../team/get-team'; + +export type GetEnvelopeByIdOptions = { + id: EnvelopeIdOptions; + + /** + * The validated team ID. + */ + userId: number; + + /** + * The unvalidated team ID. + */ + teamId: number; + + /** + * The type of envelope to get. + * + * Set to null to bypass check. + */ + type: EnvelopeType | null; +}; + +export const getEnvelopeById = async ({ id, userId, teamId, type }: GetEnvelopeByIdOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + userId, + teamId, + type, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + folder: true, + documentMeta: true, + user: { + select: { + id: true, + name: true, + email: true, + }, + }, + recipients: true, + fields: true, + team: { + select: { + id: true, + url: true, + }, + }, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope could not be found', + }); + } + + return envelope; +}; + +export type GetEnvelopeByIdResponse = Awaited>; + +export type GetEnvelopeWhereInputOptions = { + id: EnvelopeIdOptions; + + /** + * The user ID who has been authenticated. + */ + userId: number; + + /** + * The unknown teamId from the request. + */ + teamId: number; + + /** + * The type of envelope to get. + * + * Set to null to bypass check. + */ + type: EnvelopeType | null; +}; + +/** + * Generate the where input for a given Prisma envelope query. + * + * This will return a query that allows a user to get a document if they have valid access to it. + * + * NOTE: Be extremely careful when modifying this function. Needs at minimum two reviewers to approve any changes. + */ +export const getEnvelopeWhereInput = async ({ + id, + userId, + teamId, + type, +}: GetEnvelopeWhereInputOptions) => { + // Validate that the user belongs to the team provided. + const team = await getTeamById({ teamId, userId }); + + const envelopeOrInput: Prisma.EnvelopeWhereInput[] = [ + // Allow access if they own the document. + { + userId, + }, + // Or, if they belong to the team that the document is associated with. + { + visibility: { + in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole], + }, + teamId: team.id, + }, + ]; + + // Allow access to documents sent from the team email. + if (team.teamEmail) { + envelopeOrInput.push({ + user: { + email: team.teamEmail.email, + }, + }); + } + + // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + // NOTE: DO NOT PUT ANY CODE AFTER THIS POINT. + // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + + const envelopeWhereInput: Prisma.EnvelopeWhereUniqueInput = { + ...unsafeBuildEnvelopeIdQuery(id, type), + OR: envelopeOrInput, + }; + + // Final backup validation incase something goes wrong. + if ( + !envelopeWhereInput.OR || + envelopeWhereInput.OR.length < 2 || + !userId || + !teamId || + !team.id || + teamId !== team.id + ) { + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Query not valid', + }); + } + + // Do not modify this return directly, all adjustments need to be made prior to the above if statement. + return { + envelopeWhereInput, + team, + }; +}; diff --git a/packages/lib/server-only/envelope/increment-id.ts b/packages/lib/server-only/envelope/increment-id.ts new file mode 100644 index 000000000..35d5bce0f --- /dev/null +++ b/packages/lib/server-only/envelope/increment-id.ts @@ -0,0 +1,39 @@ +import { prisma } from '@documenso/prisma'; + +import { mapDocumentIdToSecondaryId, mapTemplateIdToSecondaryId } from '../../utils/envelope'; + +export const incrementDocumentId = async () => { + const documentIdCounter = await prisma.counter.update({ + where: { + id: 'document', + }, + data: { + value: { + increment: 1, + }, + }, + }); + + return { + documentId: documentIdCounter.value, + formattedDocumentId: mapDocumentIdToSecondaryId(documentIdCounter.value), + }; +}; + +export const incrementTemplateId = async () => { + const templateIdCounter = await prisma.counter.update({ + where: { + id: 'template', + }, + data: { + value: { + increment: 1, + }, + }, + }); + + return { + templateId: templateIdCounter.value, + formattedTemplateId: mapTemplateIdToSecondaryId(templateIdCounter.value), + }; +}; diff --git a/packages/lib/server-only/field/create-document-fields.ts b/packages/lib/server-only/field/create-document-fields.ts deleted file mode 100644 index aa315625b..000000000 --- a/packages/lib/server-only/field/create-document-fields.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; -import type { TFieldAndMeta } from '@documenso/lib/types/field-meta'; -import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; -import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; - -export interface CreateDocumentFieldsOptions { - userId: number; - teamId: number; - documentId: number; - fields: (TFieldAndMeta & { - recipientId: number; - pageNumber: number; - pageX: number; - pageY: number; - width: number; - height: number; - })[]; - requestMetadata: ApiRequestMetadata; -} - -export const createDocumentFields = async ({ - userId, - teamId, - documentId, - fields, - requestMetadata, -}: CreateDocumentFieldsOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, - userId, - teamId, - }); - - const document = await prisma.document.findFirst({ - where: documentWhereInput, - include: { - recipients: true, - fields: true, - }, - }); - - if (!document) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', - }); - } - - if (document.completedAt) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: 'Document already complete', - }); - } - - // Field validation. - const validatedFields = fields.map((field) => { - const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId); - - // Each field MUST have a recipient associated with it. - if (!recipient) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: `Recipient ${field.recipientId} not found`, - }); - } - - // Check whether the recipient associated with the field can have new fields created. - if (!canRecipientFieldsBeModified(recipient, document.fields)) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: - 'Recipient type cannot have fields, or they have already interacted with the document.', - }); - } - - return { - ...field, - recipientEmail: recipient.email, - }; - }); - - const createdFields = await prisma.$transaction(async (tx) => { - return await Promise.all( - validatedFields.map(async (field) => { - const createdField = await tx.field.create({ - data: { - type: field.type, - page: field.pageNumber, - positionX: field.pageX, - positionY: field.pageY, - width: field.width, - height: field.height, - customText: '', - inserted: false, - fieldMeta: field.fieldMeta, - documentId, - recipientId: field.recipientId, - }, - }); - - // Handle field created audit log. - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED, - documentId, - metadata: requestMetadata, - data: { - fieldId: createdField.secondaryId, - fieldRecipientEmail: field.recipientEmail, - fieldRecipientId: createdField.recipientId, - fieldType: createdField.type, - }, - }), - }); - - return createdField; - }), - ); - }); - - return { - fields: createdFields, - }; -}; diff --git a/packages/lib/server-only/field/create-envelope-fields.ts b/packages/lib/server-only/field/create-envelope-fields.ts new file mode 100644 index 000000000..24ac0170e --- /dev/null +++ b/packages/lib/server-only/field/create-envelope-fields.ts @@ -0,0 +1,167 @@ +import { EnvelopeType } from '@prisma/client'; + +import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; +import type { TFieldAndMeta } from '@documenso/lib/types/field-meta'; +import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; +import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; +import { prisma } from '@documenso/prisma'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { canRecipientFieldsBeModified } from '../../utils/recipients'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; + +export interface CreateEnvelopeFieldsOptions { + userId: number; + teamId: number; + id: EnvelopeIdOptions; + + fields: (TFieldAndMeta & { + /** + * The ID of the item to insert the fields into. + * + * If blank, the first item will be used. + */ + envelopeItemId?: string; + + recipientId: number; + pageNumber: number; + pageX: number; + pageY: number; + width: number; + height: number; + })[]; + requestMetadata: ApiRequestMetadata; +} + +export const createEnvelopeFields = async ({ + userId, + teamId, + id, + fields, + requestMetadata, +}: CreateEnvelopeFieldsOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: null, // Null to allow any type of envelope. + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + include: { + recipients: true, + fields: true, + envelopeItems: { + select: { + id: true, + }, + }, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope not found', + }); + } + + if (envelope.type === EnvelopeType.DOCUMENT && envelope.completedAt) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Envelope already complete', + }); + } + + const firstEnvelopeItem = envelope.envelopeItems[0]; + + if (!firstEnvelopeItem) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Envelope item not found', + }); + } + + // Field validation. + const validatedFields = fields.map((field) => { + const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId); + + // The item to attach the fields to MUST belong to the document. + if ( + field.envelopeItemId && + !envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === field.envelopeItemId) // Todo: Migration test this + ) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Item to attach fields to must belong to the document', + }); + } + + // Each field MUST have a recipient associated with it. + if (!recipient) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: `Recipient ${field.recipientId} not found`, + }); + } + + // Check whether the recipient associated with the field can have new fields created. + if (!canRecipientFieldsBeModified(recipient, envelope.fields)) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: + 'Recipient type cannot have fields, or they have already interacted with the document.', + }); + } + + return { + ...field, + envelopeItemId: field.envelopeItemId || firstEnvelopeItem.id, // Fallback to first envelope item if no envelope item ID is provided. + recipientEmail: recipient.email, + }; + }); + + const createdFields = await prisma.$transaction(async (tx) => { + const newlyCreatedFields = await tx.field.createManyAndReturn({ + data: validatedFields.map((field) => ({ + type: field.type, + page: field.pageNumber, + positionX: field.pageX, + positionY: field.pageY, + width: field.width, + height: field.height, + customText: '', + inserted: false, + fieldMeta: field.fieldMeta, + envelopeId: envelope.id, + envelopeItemId: field.envelopeItemId, + recipientId: field.recipientId, + })), + }); + + // Handle field created audit log. + if (envelope.type === EnvelopeType.DOCUMENT) { + await tx.documentAuditLog.createMany({ + data: newlyCreatedFields.map((createdField) => { + const recipient = validatedFields.find( + (field) => field.recipientId === createdField.recipientId, + ); + + return createDocumentAuditLogData({ + type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED, + envelopeId: envelope.id, + metadata: requestMetadata, + data: { + fieldId: createdField.secondaryId, + fieldRecipientEmail: recipient?.recipientEmail || '', + fieldRecipientId: createdField.recipientId, + fieldType: createdField.type, + }, + }); + }), + }); + } + + return newlyCreatedFields; + }); + + return { + fields: createdFields, + }; +}; diff --git a/packages/lib/server-only/field/create-field.ts b/packages/lib/server-only/field/create-field.ts deleted file mode 100644 index 7f0342677..000000000 --- a/packages/lib/server-only/field/create-field.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { FieldType } from '@prisma/client'; -import { match } from 'ts-pattern'; - -import { prisma } from '@documenso/prisma'; - -import { - ZCheckboxFieldMeta, - ZDropdownFieldMeta, - ZNumberFieldMeta, - ZRadioFieldMeta, - ZTextFieldMeta, -} from '../../types/field-meta'; -import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta'; -import type { RequestMetadata } from '../../universal/extract-request-metadata'; -import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; - -export type CreateFieldOptions = { - documentId: number; - userId: number; - teamId: number; - recipientId: number; - type: FieldType; - pageNumber: number; - pageX: number; - pageY: number; - pageWidth: number; - pageHeight: number; - fieldMeta?: FieldMeta; - requestMetadata?: RequestMetadata; -}; - -export const createField = async ({ - documentId, - userId, - teamId, - recipientId, - type, - pageNumber, - pageX, - pageY, - pageWidth, - pageHeight, - fieldMeta, - requestMetadata, -}: CreateFieldOptions) => { - const { documentWhereInput, team } = await getDocumentWhereInput({ - documentId, - userId, - teamId, - }); - - const document = await prisma.document.findFirst({ - where: documentWhereInput, - select: { - id: true, - }, - }); - - if (!document) { - throw new Error('Document not found'); - } - - 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: {}, - })) - .with('FREE_SIGNATURE', () => ({ - success: false, - error: 'FREE_SIGNATURE is not supported', - data: {}, - })) - .exhaustive(); - - if (!result.success) { - throw new Error('Field meta parsing failed'); - } - - const field = await prisma.field.create({ - data: { - documentId, - recipientId, - type, - page: pageNumber, - positionX: pageX, - positionY: pageY, - width: pageWidth, - height: pageHeight, - customText: '', - inserted: false, - fieldMeta: result.data, - }, - include: { - recipient: true, - }, - }); - - await prisma.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: 'FIELD_CREATED', - documentId, - user: { - id: team.id, - email: team.name, - name: '', - }, - data: { - fieldId: field.secondaryId, - fieldRecipientEmail: field.recipient?.email ?? '', - fieldRecipientId: recipientId, - fieldType: field.type, - }, - requestMetadata, - }), - }); - - return field; -}; diff --git a/packages/lib/server-only/field/create-template-fields.ts b/packages/lib/server-only/field/create-template-fields.ts deleted file mode 100644 index 69faf7e17..000000000 --- a/packages/lib/server-only/field/create-template-fields.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { FieldType } from '@prisma/client'; - -import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { buildTeamWhereQuery } from '../../utils/teams'; - -export interface CreateTemplateFieldsOptions { - userId: number; - teamId: number; - templateId: number; - fields: { - recipientId: number; - type: FieldType; - pageNumber: number; - pageX: number; - pageY: number; - width: number; - height: number; - fieldMeta?: TFieldMetaSchema; - }[]; -} - -export const createTemplateFields = async ({ - userId, - teamId, - templateId, - fields, -}: CreateTemplateFieldsOptions) => { - const template = await prisma.template.findFirst({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - include: { - recipients: true, - fields: true, - }, - }); - - if (!template) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'template not found', - }); - } - - // Field validation. - const validatedFields = fields.map((field) => { - const recipient = template.recipients.find((recipient) => recipient.id === field.recipientId); - - // Each field MUST have a recipient associated with it. - if (!recipient) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: `Recipient ${field.recipientId} not found`, - }); - } - - // Check whether the recipient associated with the field can have new fields created. - if (!canRecipientFieldsBeModified(recipient, template.fields)) { - throw new AppError(AppErrorCode.INVALID_REQUEST, { - message: - 'Recipient type cannot have fields, or they have already interacted with the template.', - }); - } - - return { - ...field, - recipientEmail: recipient.email, - }; - }); - - const createdFields = await prisma.$transaction(async (tx) => { - return await Promise.all( - validatedFields.map(async (field) => { - const createdField = await tx.field.create({ - data: { - type: field.type, - page: field.pageNumber, - positionX: field.pageX, - positionY: field.pageY, - width: field.width, - height: field.height, - customText: '', - inserted: false, - fieldMeta: field.fieldMeta, - templateId, - recipientId: field.recipientId, - }, - }); - - return createdField; - }), - ); - }); - - return { - fields: createdFields, - }; -}; diff --git a/packages/lib/server-only/field/delete-document-field.ts b/packages/lib/server-only/field/delete-document-field.ts index eea449fa9..ee734f4ba 100644 --- a/packages/lib/server-only/field/delete-document-field.ts +++ b/packages/lib/server-only/field/delete-document-field.ts @@ -1,3 +1,5 @@ +import { EnvelopeType } from '@prisma/client'; + import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; @@ -5,7 +7,7 @@ import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface DeleteDocumentFieldOptions { userId: number; @@ -19,7 +21,8 @@ export const deleteDocumentField = async ({ teamId, fieldId, requestMetadata, -}: DeleteDocumentFieldOptions): Promise => { +}: DeleteDocumentFieldOptions) => { + // Unauthenticated check, we do the real check later. const field = await prisma.field.findFirst({ where: { id: fieldId, @@ -32,22 +35,18 @@ export const deleteDocumentField = async ({ }); } - const documentId = field.documentId; - - if (!documentId) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Field does not belong to a document. Use delete template field instead.', - }); - } - - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: field.envelopeId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, include: { recipients: { where: { @@ -60,19 +59,19 @@ export const deleteDocumentField = async ({ }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); } - const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId); + const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId); if (!recipient) { throw new AppError(AppErrorCode.INVALID_REQUEST, { @@ -87,10 +86,11 @@ export const deleteDocumentField = async ({ }); } - await prisma.$transaction(async (tx) => { + return await prisma.$transaction(async (tx) => { const deletedField = await tx.field.delete({ where: { id: fieldId, + envelopeId: envelope.id, }, }); @@ -98,7 +98,7 @@ export const deleteDocumentField = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED, - documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { fieldId: deletedField.secondaryId, @@ -108,5 +108,7 @@ export const deleteDocumentField = async ({ }, }), }); + + return deletedField; }); }; diff --git a/packages/lib/server-only/field/delete-field.ts b/packages/lib/server-only/field/delete-field.ts deleted file mode 100644 index abcb1c189..000000000 --- a/packages/lib/server-only/field/delete-field.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { Team } from '@prisma/client'; - -import { prisma } from '@documenso/prisma'; - -import type { RequestMetadata } from '../../universal/extract-request-metadata'; -import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; -import { buildTeamWhereQuery } from '../../utils/teams'; - -export type DeleteFieldOptions = { - fieldId: number; - documentId: number; - userId: number; - teamId: number; - requestMetadata?: RequestMetadata; -}; - -export const deleteField = async ({ - fieldId, - userId, - teamId, - documentId, - requestMetadata, -}: DeleteFieldOptions) => { - const field = await prisma.field.delete({ - where: { - id: fieldId, - document: { - id: documentId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - }, - include: { - recipient: true, - }, - }); - - const user = await prisma.user.findFirstOrThrow({ - where: { - id: userId, - }, - select: { - id: true, - name: true, - email: true, - }, - }); - - let team: Team | null = null; - - if (teamId) { - team = await prisma.team.findFirstOrThrow({ - where: { - id: teamId, - }, - }); - } - - await prisma.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: 'FIELD_DELETED', - documentId, - user: { - id: team?.id ?? user.id, - email: team?.name ?? user.email, - name: team ? '' : user.name, - }, - data: { - fieldId: field.secondaryId, - fieldRecipientEmail: field.recipient?.email ?? '', - fieldRecipientId: field.recipientId ?? -1, - fieldType: field.type, - }, - requestMetadata, - }), - }); - - return field; -}; diff --git a/packages/lib/server-only/field/delete-template-field.ts b/packages/lib/server-only/field/delete-template-field.ts index 87a522068..a1a96555d 100644 --- a/packages/lib/server-only/field/delete-template-field.ts +++ b/packages/lib/server-only/field/delete-template-field.ts @@ -1,7 +1,10 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface DeleteTemplateFieldOptions { userId: number; @@ -17,21 +20,34 @@ export const deleteTemplateField = async ({ const field = await prisma.field.findFirst({ where: { id: fieldId, - template: { + envelope: { + type: EnvelopeType.TEMPLATE, team: buildTeamWhereQuery({ teamId, userId }), }, }, }); - if (!field || !field.templateId) { + if (!field) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Field not found', }); } + // Additional validation to check visibility. + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: field.envelopeId, + }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + await prisma.field.delete({ where: { - id: fieldId, + id: field.id, + envelope: envelopeWhereInput, }, }); }; diff --git a/packages/lib/server-only/field/get-completed-fields-for-token.ts b/packages/lib/server-only/field/get-completed-fields-for-token.ts index 658e913e8..fc8f1ec90 100644 --- a/packages/lib/server-only/field/get-completed-fields-for-token.ts +++ b/packages/lib/server-only/field/get-completed-fields-for-token.ts @@ -1,4 +1,4 @@ -import { SigningStatus } from '@prisma/client'; +import { EnvelopeType, SigningStatus } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -6,10 +6,12 @@ export type GetCompletedFieldsForTokenOptions = { token: string; }; +// Todo: Envelopes - This needs to be redone since we need to determine which document to show the fields on. export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => { return await prisma.field.findMany({ where: { - document: { + envelope: { + type: EnvelopeType.DOCUMENT, recipients: { some: { token, diff --git a/packages/lib/server-only/field/get-field-by-id.ts b/packages/lib/server-only/field/get-field-by-id.ts index e91a99461..7432a0723 100644 --- a/packages/lib/server-only/field/get-field-by-id.ts +++ b/packages/lib/server-only/field/get-field-by-id.ts @@ -1,49 +1,56 @@ +import type { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type GetFieldByIdOptions = { userId: number; teamId: number; fieldId: number; + envelopeType: EnvelopeType; }; -export const getFieldById = async ({ userId, teamId, fieldId }: GetFieldByIdOptions) => { +export const getFieldById = async ({ + userId, + teamId, + fieldId, + envelopeType, +}: GetFieldByIdOptions) => { const field = await prisma.field.findFirst({ where: { id: fieldId, - }, - include: { - document: { - select: { - teamId: true, - }, - }, - template: { - select: { - teamId: true, - }, + envelope: { + type: envelopeType, + team: buildTeamWhereQuery({ teamId, userId }), }, }, }); - const foundTeamId = field?.document?.teamId || field?.template?.teamId; - - if (!field || !foundTeamId || foundTeamId !== teamId) { + if (!field) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Field not found', }); } - const team = await prisma.team.findUnique({ - where: buildTeamWhereQuery({ - teamId: foundTeamId, - userId, - }), + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: field.envelopeId, + }, + type: envelopeType, + userId, + teamId, }); - if (!team) { + // Additional validation to check visibility. + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, + }); + + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Field not found', }); diff --git a/packages/lib/server-only/field/get-fields-for-document.ts b/packages/lib/server-only/field/get-fields-for-document.ts deleted file mode 100644 index 007a6c2ba..000000000 --- a/packages/lib/server-only/field/get-fields-for-document.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { prisma } from '@documenso/prisma'; - -import { buildTeamWhereQuery } from '../../utils/teams'; - -export interface GetFieldsForDocumentOptions { - documentId: number; - userId: number; - teamId: number; -} - -export type DocumentField = Awaited>[number]; - -export const getFieldsForDocument = async ({ - documentId, - userId, - teamId, -}: GetFieldsForDocumentOptions) => { - const fields = await prisma.field.findMany({ - where: { - document: { - id: documentId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - }, - include: { - signature: true, - recipient: { - select: { - name: true, - email: true, - signingStatus: true, - }, - }, - }, - orderBy: { - id: 'asc', - }, - }); - - return fields; -}; diff --git a/packages/lib/server-only/field/get-fields-for-token.ts b/packages/lib/server-only/field/get-fields-for-token.ts index 1d27b2a6a..83d136a81 100644 --- a/packages/lib/server-only/field/get-fields-for-token.ts +++ b/packages/lib/server-only/field/get-fields-for-token.ts @@ -1,4 +1,4 @@ -import { FieldType, RecipientRole, SigningStatus } from '@prisma/client'; +import { EnvelopeType, FieldType, RecipientRole, SigningStatus } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -6,6 +6,7 @@ export type GetFieldsForTokenOptions = { token: string; }; +// Todo: Envelopes, this will return all fields, might need to filter based on actual documentId. export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => { if (!token) { throw new Error('Missing token'); @@ -35,7 +36,10 @@ export const getFieldsForToken = async ({ token }: GetFieldsForTokenOptions) => gte: recipient.signingOrder ?? 0, }, }, - documentId: recipient.documentId, + envelope: { + id: recipient.envelopeId, + type: EnvelopeType.DOCUMENT, + }, }, { recipientId: recipient.id, diff --git a/packages/lib/server-only/field/remove-signed-field-with-token.ts b/packages/lib/server-only/field/remove-signed-field-with-token.ts index e95a8048d..70be50629 100644 --- a/packages/lib/server-only/field/remove-signed-field-with-token.ts +++ b/packages/lib/server-only/field/remove-signed-field-with-token.ts @@ -41,19 +41,19 @@ export const removeSignedFieldWithToken = async ({ }, }, include: { - document: true, + envelope: true, recipient: true, }, }); - const { document } = field; + const { envelope } = field; - if (!document) { + if (!envelope) { throw new Error(`Document not found for field ${field.id}`); } - if (document.status !== DocumentStatus.PENDING) { - throw new Error(`Document ${document.id} must be pending`); + if (envelope.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${envelope.id} must be pending`); } if ( @@ -89,7 +89,7 @@ export const removeSignedFieldWithToken = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED, - documentId: document.id, + envelopeId: envelope.id, user: { name: recipient.name, email: recipient.email, diff --git a/packages/lib/server-only/field/set-fields-for-document.ts b/packages/lib/server-only/field/set-fields-for-document.ts index 497f01078..2295cf733 100644 --- a/packages/lib/server-only/field/set-fields-for-document.ts +++ b/packages/lib/server-only/field/set-fields-for-document.ts @@ -1,5 +1,4 @@ -import type { Field } from '@prisma/client'; -import { FieldType } from '@prisma/client'; +import { EnvelopeType, type Field, FieldType } from '@prisma/client'; import { isDeepEqual } from 'remeda'; import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; @@ -26,7 +25,7 @@ import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface SetFieldsForDocumentOptions { userId: number; @@ -43,39 +42,49 @@ export const setFieldsForDocument = async ({ fields, requestMetadata, }: SetFieldsForDocumentOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, + envelopeItems: { + select: { + id: true, + }, + }, + fields: { + include: { + recipient: true, + }, + }, }, }); - if (!document) { + // Todo: Envelopes + const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id; + + if (!envelope || !firstEnvelopeItemId) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); } - const existingFields = await prisma.field.findMany({ - where: { - documentId, - }, - include: { - recipient: true, - }, - }); + const existingFields = envelope.fields; const removedFields = existingFields.filter( (existingField) => !fields.find((field) => field.id === existingField.id), @@ -84,7 +93,7 @@ export const setFieldsForDocument = async ({ const linkedFields = fields.map((field) => { const existing = existingFields.find((existingField) => existingField.id === field.id); - const recipient = document.recipients.find((recipient) => recipient.id === field.recipientId); + const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId); // Each field MUST have a recipient associated with it. if (!recipient) { @@ -197,7 +206,7 @@ export const setFieldsForDocument = async ({ const upsertedField = await tx.field.upsert({ where: { id: field._persisted?.id ?? -1, - documentId, + envelopeId: envelope.id, }, update: { page: field.pageNumber, @@ -208,6 +217,8 @@ export const setFieldsForDocument = async ({ fieldMeta: parsedFieldMeta, }, create: { + envelopeId: envelope.id, + envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes type: field.type, page: field.pageNumber, positionX: field.pageX, @@ -217,17 +228,7 @@ export const setFieldsForDocument = async ({ customText: '', inserted: false, fieldMeta: parsedFieldMeta, - document: { - connect: { - id: documentId, - }, - }, - recipient: { - connect: { - id: field.recipientId, - documentId, - }, - }, + recipientId: field._recipient.id, }, }); @@ -249,7 +250,7 @@ export const setFieldsForDocument = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { changes, @@ -264,7 +265,7 @@ export const setFieldsForDocument = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { ...baseAuditLog, @@ -292,7 +293,7 @@ export const setFieldsForDocument = async ({ data: removedFields.map((field) => createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { fieldId: field.secondaryId, diff --git a/packages/lib/server-only/field/set-fields-for-template.ts b/packages/lib/server-only/field/set-fields-for-template.ts index a116c6275..d32000c8e 100644 --- a/packages/lib/server-only/field/set-fields-for-template.ts +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -1,4 +1,4 @@ -import { FieldType } from '@prisma/client'; +import { EnvelopeType, FieldType } from '@prisma/client'; import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown'; @@ -16,7 +16,7 @@ import { } from '@documenso/lib/types/field-meta'; import { prisma } from '@documenso/prisma'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type SetFieldsForTemplateOptions = { userId: number; @@ -42,25 +42,40 @@ export const setFieldsForTemplate = async ({ templateId, fields, }: SetFieldsForTemplateOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), + }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, + include: { + envelopeItems: { + select: { + id: true, + }, + }, + fields: { + include: { + recipient: true, + }, + }, }, }); - if (!template) { + // Todo: Envelopes + const firstEnvelopeItemId = envelope?.envelopeItems[0]?.id; + + if (!envelope || !firstEnvelopeItemId) { throw new Error('Template not found'); } - const existingFields = await prisma.field.findMany({ - where: { - templateId, - }, - include: { - recipient: true, - }, - }); + const existingFields = envelope.fields; const removedFields = existingFields.filter( (existingField) => !fields.find((field) => field.id === existingField.id), @@ -143,7 +158,8 @@ export const setFieldsForTemplate = async ({ return prisma.field.upsert({ where: { id: field._persisted?.id ?? -1, - templateId, + envelopeId: envelope.id, + envelopeItemId: firstEnvelopeItemId, // Todo: Envelopes }, update: { page: field.pageNumber, @@ -163,15 +179,20 @@ export const setFieldsForTemplate = async ({ customText: '', inserted: false, fieldMeta: parsedFieldMeta, - template: { + envelope: { connect: { - id: templateId, + id: envelope.id, + }, + }, + envelopeItem: { + connect: { + id: firstEnvelopeItemId, // Todo: Envelopes }, }, recipient: { connect: { id: field.recipientId, - templateId, + envelopeId: envelope.id, }, }, }, diff --git a/packages/lib/server-only/field/sign-field-with-token.ts b/packages/lib/server-only/field/sign-field-with-token.ts index c3e18cb98..8c6c81c2b 100644 --- a/packages/lib/server-only/field/sign-field-with-token.ts +++ b/packages/lib/server-only/field/sign-field-with-token.ts @@ -81,7 +81,7 @@ export const signFieldWithToken = async ({ }, }, include: { - document: { + envelope: { include: { recipients: true, }, @@ -90,9 +90,9 @@ export const signFieldWithToken = async ({ }, }); - const { document } = field; + const { envelope } = field; - if (!document) { + if (!envelope) { throw new Error(`Document not found for field ${field.id}`); } @@ -100,12 +100,12 @@ export const signFieldWithToken = async ({ throw new Error(`Recipient not found for field ${field.id}`); } - if (document.deletedAt) { - throw new Error(`Document ${document.id} has been deleted`); + if (envelope.deletedAt) { + throw new Error(`Document ${envelope.id} has been deleted`); } - if (document.status !== DocumentStatus.PENDING) { - throw new Error(`Document ${document.id} must be pending for signing`); + if (envelope.status !== DocumentStatus.PENDING) { + throw new Error(`Document ${envelope.id} must be pending for signing`); } if ( @@ -172,7 +172,7 @@ export const signFieldWithToken = async ({ } const derivedRecipientActionAuth = await validateFieldAuth({ - documentAuthOptions: document.authOptions, + documentAuthOptions: envelope.authOptions, recipient, field, userId, @@ -181,7 +181,9 @@ export const signFieldWithToken = async ({ const documentMeta = await prisma.documentMeta.findFirst({ where: { - documentId: document.id, + envelope: { + id: envelope.id, + }, }, }); @@ -272,7 +274,7 @@ export const signFieldWithToken = async ({ assistant && field.recipientId !== assistant.id ? DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_PREFILLED : DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, - documentId: document.id, + envelopeId: envelope.id, user: { email: assistant?.email ?? recipient.email, name: assistant?.name ?? recipient.name, diff --git a/packages/lib/server-only/field/update-document-fields.ts b/packages/lib/server-only/field/update-document-fields.ts index dc87db70d..1237b0284 100644 --- a/packages/lib/server-only/field/update-document-fields.ts +++ b/packages/lib/server-only/field/update-document-fields.ts @@ -1,4 +1,4 @@ -import type { FieldType } from '@prisma/client'; +import { EnvelopeType, type FieldType } from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; @@ -11,7 +11,7 @@ import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface UpdateDocumentFieldsOptions { userId: number; @@ -37,34 +37,38 @@ export const updateDocumentFields = async ({ fields, requestMetadata, }: UpdateDocumentFieldsOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, fields: true, }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); } const fieldsToUpdate = fields.map((field) => { - const originalField = document.fields.find((existingField) => existingField.id === field.id); + const originalField = envelope.fields.find((existingField) => existingField.id === field.id); if (!originalField) { throw new AppError(AppErrorCode.NOT_FOUND, { @@ -72,7 +76,7 @@ export const updateDocumentFields = async ({ }); } - const recipient = document.recipients.find( + const recipient = envelope.recipients.find( (recipient) => recipient.id === originalField.recipientId, ); @@ -84,7 +88,7 @@ export const updateDocumentFields = async ({ } // Check whether the recipient associated with the field can be modified. - if (!canRecipientFieldsBeModified(recipient, document.fields)) { + if (!canRecipientFieldsBeModified(recipient, envelope.fields)) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Cannot modify a field where the recipient has already interacted with the document', @@ -123,7 +127,7 @@ export const updateDocumentFields = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { fieldId: updatedField.secondaryId, diff --git a/packages/lib/server-only/field/update-field.ts b/packages/lib/server-only/field/update-field.ts deleted file mode 100644 index 727d2cf98..000000000 --- a/packages/lib/server-only/field/update-field.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { FieldType, Team } from '@prisma/client'; - -import { type TFieldMetaSchema as FieldMeta } from '@documenso/lib/types/field-meta'; -import { prisma } from '@documenso/prisma'; - -import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; -import type { RequestMetadata } from '../../universal/extract-request-metadata'; -import { createDocumentAuditLogData, diffFieldChanges } from '../../utils/document-audit-logs'; -import { buildTeamWhereQuery } from '../../utils/teams'; - -export type UpdateFieldOptions = { - fieldId: number; - documentId: number; - userId: number; - teamId: number; - recipientId?: number; - type?: FieldType; - pageNumber?: number; - pageX?: number; - pageY?: number; - pageWidth?: number; - pageHeight?: number; - requestMetadata?: RequestMetadata; - fieldMeta?: FieldMeta; -}; - -export const updateField = async ({ - fieldId, - documentId, - userId, - teamId, - recipientId, - type, - pageNumber, - pageX, - pageY, - pageWidth, - pageHeight, - requestMetadata, - fieldMeta, -}: UpdateFieldOptions) => { - if (type === 'FREE_SIGNATURE') { - throw new Error('Cannot update a FREE_SIGNATURE field'); - } - - const oldField = await prisma.field.findFirstOrThrow({ - where: { - id: fieldId, - document: { - id: documentId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - }, - }); - - const field = prisma.$transaction(async (tx) => { - const updatedField = await tx.field.update({ - where: { - id: fieldId, - }, - data: { - recipientId, - type, - page: pageNumber, - positionX: pageX, - positionY: pageY, - width: pageWidth, - height: pageHeight, - fieldMeta, - }, - include: { - recipient: true, - }, - }); - - const user = await prisma.user.findFirstOrThrow({ - where: { - id: userId, - }, - select: { - id: true, - name: true, - email: true, - }, - }); - - let team: Team | null = null; - - if (teamId) { - team = await prisma.team.findFirst({ - where: buildTeamWhereQuery({ teamId, userId }), - }); - } - - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED, - documentId, - user: { - id: team?.id ?? user.id, - email: team?.name ?? user.email, - name: team ? '' : user.name, - }, - data: { - fieldId: updatedField.secondaryId, - fieldRecipientEmail: updatedField.recipient?.email ?? '', - fieldRecipientId: recipientId ?? -1, - fieldType: updatedField.type, - changes: diffFieldChanges(oldField, updatedField), - }, - requestMetadata, - }), - }); - - return updatedField; - }); - - return field; -}; diff --git a/packages/lib/server-only/field/update-template-fields.ts b/packages/lib/server-only/field/update-template-fields.ts index 4b62937bd..78a8df020 100644 --- a/packages/lib/server-only/field/update-template-fields.ts +++ b/packages/lib/server-only/field/update-template-fields.ts @@ -1,11 +1,11 @@ -import type { FieldType } from '@prisma/client'; +import { EnvelopeType, type FieldType } from '@prisma/client'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { canRecipientFieldsBeModified } from '../../utils/recipients'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface UpdateTemplateFieldsOptions { userId: number; @@ -29,25 +29,32 @@ export const updateTemplateFields = async ({ templateId, fields, }: UpdateTemplateFieldsOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, fields: true, }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } const fieldsToUpdate = fields.map((field) => { - const originalField = template.fields.find((existingField) => existingField.id === field.id); + const originalField = envelope.fields.find((existingField) => existingField.id === field.id); if (!originalField) { throw new AppError(AppErrorCode.NOT_FOUND, { @@ -55,7 +62,7 @@ export const updateTemplateFields = async ({ }); } - const recipient = template.recipients.find( + const recipient = envelope.recipients.find( (recipient) => recipient.id === originalField.recipientId, ); @@ -67,7 +74,7 @@ export const updateTemplateFields = async ({ } // Check whether the recipient associated with the field can be modified. - if (!canRecipientFieldsBeModified(recipient, template.fields)) { + if (!canRecipientFieldsBeModified(recipient, envelope.fields)) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Cannot modify a field where the recipient has already interacted with the document', diff --git a/packages/lib/server-only/folder/create-folder.ts b/packages/lib/server-only/folder/create-folder.ts index d361ae37d..9939f9634 100644 --- a/packages/lib/server-only/folder/create-folder.ts +++ b/packages/lib/server-only/folder/create-folder.ts @@ -2,8 +2,6 @@ import { prisma } from '@documenso/prisma'; import type { TFolderType } from '../../types/folder-type'; import { FolderType } from '../../types/folder-type'; -import { determineDocumentVisibility } from '../../utils/document-visibility'; -import { getTeamById } from '../team/get-team'; import { getTeamSettings } from '../team/get-team-settings'; export interface CreateFolderOptions { @@ -21,8 +19,7 @@ export const createFolder = async ({ parentId, type = FolderType.DOCUMENT, }: CreateFolderOptions) => { - const team = await getTeamById({ userId, teamId }); - + // This indirectly verifies whether the user has access to the team. const settings = await getTeamSettings({ userId, teamId }); return await prisma.folder.create({ @@ -32,7 +29,7 @@ export const createFolder = async ({ teamId, parentId, type, - visibility: determineDocumentVisibility(settings.documentVisibility, team.currentTeamRole), + visibility: settings.documentVisibility, }, }); }; diff --git a/packages/lib/server-only/folder/delete-folder.ts b/packages/lib/server-only/folder/delete-folder.ts index 128bd35ae..c7e9615af 100644 --- a/packages/lib/server-only/folder/delete-folder.ts +++ b/packages/lib/server-only/folder/delete-folder.ts @@ -1,10 +1,7 @@ -import { DocumentVisibility, TeamMemberRole } from '@prisma/client'; -import { match } from 'ts-pattern'; - import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { buildTeamWhereQuery, canAccessTeamDocument } from '../../utils/teams'; import { getTeamById } from '../team/get-team'; export interface DeleteFolderOptions { @@ -24,11 +21,6 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt userId, }), }, - include: { - documents: true, - subfolders: true, - templates: true, - }, }); if (!folder) { @@ -37,11 +29,7 @@ export const deleteFolder = async ({ userId, teamId, folderId }: DeleteFolderOpt }); } - const hasPermission = match(team.currentTeamRole) - .with(TeamMemberRole.ADMIN, () => true) - .with(TeamMemberRole.MANAGER, () => folder.visibility !== DocumentVisibility.ADMIN) - .with(TeamMemberRole.MEMBER, () => folder.visibility === DocumentVisibility.EVERYONE) - .otherwise(() => false); + const hasPermission = canAccessTeamDocument(team.currentTeamRole, folder.visibility); if (!hasPermission) { throw new AppError(AppErrorCode.UNAUTHORIZED, { diff --git a/packages/lib/server-only/folder/find-folders.ts b/packages/lib/server-only/folder/find-folders.ts index fc4760c97..957870ef9 100644 --- a/packages/lib/server-only/folder/find-folders.ts +++ b/packages/lib/server-only/folder/find-folders.ts @@ -1,9 +1,8 @@ -import { TeamMemberRole } from '@prisma/client'; -import { match } from 'ts-pattern'; +import { EnvelopeType } from '@prisma/client'; import { prisma } from '@documenso/prisma'; -import { DocumentVisibility } from '../../types/document-visibility'; +import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; import type { TFolderType } from '../../types/folder-type'; import { getTeamById } from '../team/get-team'; @@ -17,22 +16,11 @@ export interface FindFoldersOptions { export const findFolders = async ({ userId, teamId, parentId, type }: FindFoldersOptions) => { const team = await getTeamById({ userId, teamId }); - const visibilityFilters = match(team.currentTeamRole) - .with(TeamMemberRole.ADMIN, () => ({ - visibility: { - in: [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - DocumentVisibility.ADMIN, - ], - }, - })) - .with(TeamMemberRole.MANAGER, () => ({ - visibility: { - in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], - }, - })) - .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); + const visibilityFilters = { + visibility: { + in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole], + }, + }; const whereClause = { AND: [ @@ -69,13 +57,15 @@ export const findFolders = async ({ userId, teamId, parentId, type }: FindFolder createdAt: 'desc', }, }), - prisma.document.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.DOCUMENT, folderId: folder.id, }, }), - prisma.template.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.TEMPLATE, folderId: folder.id, }, }), diff --git a/packages/lib/server-only/folder/move-document-to-folder.ts b/packages/lib/server-only/folder/move-document-to-folder.ts deleted file mode 100644 index 9cc0e25e7..000000000 --- a/packages/lib/server-only/folder/move-document-to-folder.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { TeamMemberRole } from '@prisma/client'; -import { match } from 'ts-pattern'; - -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; -import { FolderType } from '@documenso/lib/types/folder-type'; -import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; -import { prisma } from '@documenso/prisma'; - -import { getTeamById } from '../team/get-team'; - -export interface MoveDocumentToFolderOptions { - userId: number; - teamId: number; - documentId: number; - folderId?: string | null; - requestMetadata?: ApiRequestMetadata; -} - -export const moveDocumentToFolder = async ({ - userId, - teamId, - documentId, - folderId, -}: MoveDocumentToFolderOptions) => { - const team = await getTeamById({ userId, teamId }); - - const visibilityFilters = match(team.currentTeamRole) - .with(TeamMemberRole.ADMIN, () => ({ - visibility: { - in: [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - DocumentVisibility.ADMIN, - ], - }, - })) - .with(TeamMemberRole.MANAGER, () => ({ - visibility: { - in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], - }, - })) - .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })); - - const documentWhereClause = { - id: documentId, - OR: [ - { teamId, ...visibilityFilters }, - { userId, teamId }, - ], - }; - - const document = await prisma.document.findFirst({ - where: documentWhereClause, - }); - - if (!document) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document not found', - }); - } - - if (folderId) { - const folderWhereClause = { - id: folderId, - type: FolderType.DOCUMENT, - OR: [ - { teamId, ...visibilityFilters }, - { userId, teamId }, - ], - }; - - const folder = await prisma.folder.findFirst({ - where: folderWhereClause, - }); - - if (!folder) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - } - - return await prisma.document.update({ - where: { - id: documentId, - }, - data: { - folderId, - }, - }); -}; diff --git a/packages/lib/server-only/folder/move-template-to-folder.ts b/packages/lib/server-only/folder/move-template-to-folder.ts deleted file mode 100644 index c2e759de6..000000000 --- a/packages/lib/server-only/folder/move-template-to-folder.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { FolderType } from '@documenso/lib/types/folder-type'; -import { prisma } from '@documenso/prisma'; - -import { buildTeamWhereQuery } from '../../utils/teams'; - -export interface MoveTemplateToFolderOptions { - userId: number; - teamId?: number; - templateId: number; - folderId?: string | null; -} - -export const moveTemplateToFolder = async ({ - userId, - teamId, - templateId, - folderId, -}: MoveTemplateToFolderOptions) => { - const template = await prisma.template.findFirst({ - where: { - id: templateId, - team: buildTeamWhereQuery({ - teamId, - userId, - }), - }, - }); - - if (!template) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Template not found', - }); - } - - if (folderId !== null) { - const folder = await prisma.folder.findFirst({ - where: { - id: folderId, - team: buildTeamWhereQuery({ - teamId, - userId, - }), - type: FolderType.TEMPLATE, - }, - }); - - if (!folder) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - } - - return await prisma.template.update({ - where: { - id: templateId, - }, - data: { - folderId, - }, - }); -}; diff --git a/packages/lib/server-only/profile/get-public-profile-by-url.ts b/packages/lib/server-only/profile/get-public-profile-by-url.ts index 1c9a2133e..e92c39f00 100644 --- a/packages/lib/server-only/profile/get-public-profile-by-url.ts +++ b/packages/lib/server-only/profile/get-public-profile-by-url.ts @@ -1,5 +1,5 @@ -import type { Template, TemplateDirectLink } from '@prisma/client'; -import { type TeamProfile, TemplateType } from '@prisma/client'; +import type { Envelope, TemplateDirectLink } from '@prisma/client'; +import { EnvelopeType, type TeamProfile, TemplateType } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -9,11 +9,8 @@ export type GetPublicProfileByUrlOptions = { profileUrl: string; }; -type PublicDirectLinkTemplate = Template & { - type: 'PUBLIC'; - directLink: TemplateDirectLink & { - enabled: true; - }; +type PublicDirectLinkTemplate = Pick & { + directLink: TemplateDirectLink; }; type GetPublicProfileByUrlResponse = { @@ -43,12 +40,13 @@ export const getPublicProfileByUrl = async ({ }, include: { profile: true, - templates: { + envelopes: { where: { + type: EnvelopeType.TEMPLATE, + templateType: TemplateType.PUBLIC, directLink: { enabled: true, }, - type: TemplateType.PUBLIC, }, include: { directLink: true, @@ -68,13 +66,28 @@ export const getPublicProfileByUrl = async ({ type: 'Premium', since: team.createdAt, }, - profile: team.profile, + profile: { + teamId: team.profile.teamId, + id: team.profile.id, + enabled: team.profile.enabled, + bio: team.profile.bio, + }, url: profileUrl, avatarImageId: team.avatarImageId, name: team.name || '', - templates: team.templates.filter( - (template): template is PublicDirectLinkTemplate => - template.directLink?.enabled === true && template.type === TemplateType.PUBLIC, - ), + templates: team.envelopes.map((template) => { + const directLink = template.directLink; + + if (!directLink || !directLink.enabled || template.templateType !== TemplateType.PUBLIC) { + throw new Error('Not possible'); + } + + return { + id: template.id, + publicTitle: template.publicTitle, + publicDescription: template.publicDescription, + directLink, + }; + }), }; }; diff --git a/packages/lib/server-only/recipient/create-document-recipients.ts b/packages/lib/server-only/recipient/create-document-recipients.ts index 4baac9f1c..7505adfd7 100644 --- a/packages/lib/server-only/recipient/create-document-recipients.ts +++ b/packages/lib/server-only/recipient/create-document-recipients.ts @@ -1,4 +1,4 @@ -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { SendStatus, SigningStatus } from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -11,12 +11,13 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface CreateDocumentRecipientsOptions { userId: number; teamId: number; - documentId: number; + id: EnvelopeIdOptions; recipients: { email: string; name: string; @@ -31,18 +32,19 @@ export interface CreateDocumentRecipientsOptions { export const createDocumentRecipients = async ({ userId, teamId, - documentId, + id, recipients: recipientsToCreate, requestMetadata, }: CreateDocumentRecipientsOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, team: { @@ -57,13 +59,13 @@ export const createDocumentRecipients = async ({ }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); @@ -74,7 +76,7 @@ export const createDocumentRecipients = async ({ ); // Check if user has permission to set the global action auth. - if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) { + if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); @@ -95,7 +97,7 @@ export const createDocumentRecipients = async ({ const createdRecipient = await tx.recipient.create({ data: { - documentId, + envelopeId: envelope.id, name: recipient.name, email: recipient.email, role: recipient.role, @@ -112,7 +114,7 @@ export const createDocumentRecipients = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { recipientEmail: createdRecipient.email, diff --git a/packages/lib/server-only/recipient/create-template-recipients.ts b/packages/lib/server-only/recipient/create-template-recipients.ts index 1621e87b7..52a954dec 100644 --- a/packages/lib/server-only/recipient/create-template-recipients.ts +++ b/packages/lib/server-only/recipient/create-template-recipients.ts @@ -1,4 +1,4 @@ -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { SendStatus, SigningStatus } from '@prisma/client'; import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth'; @@ -8,7 +8,7 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface CreateTemplateRecipientsOptions { userId: number; @@ -30,11 +30,18 @@ export const createTemplateRecipients = async ({ templateId, recipients: recipientsToCreate, }: CreateTemplateRecipientsOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const template = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, team: { @@ -81,7 +88,7 @@ export const createTemplateRecipients = async ({ const createdRecipient = await tx.recipient.create({ data: { - templateId, + envelopeId: template.id, name: recipient.name, email: recipient.email, role: recipient.role, diff --git a/packages/lib/server-only/recipient/delete-document-recipient.ts b/packages/lib/server-only/recipient/delete-document-recipient.ts index bff48beb7..dfefd1f23 100644 --- a/packages/lib/server-only/recipient/delete-document-recipient.ts +++ b/packages/lib/server-only/recipient/delete-document-recipient.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; -import { SendStatus } from '@prisma/client'; +import { EnvelopeType, SendStatus } from '@prisma/client'; import { mailer } from '@documenso/email/mailer'; import RecipientRemovedFromDocumentTemplate from '@documenso/email/templates/recipient-removed-from-document'; @@ -30,9 +30,10 @@ export const deleteDocumentRecipient = async ({ teamId, recipientId, requestMetadata, -}: DeleteDocumentRecipientOptions): Promise => { - const document = await prisma.document.findFirst({ +}: DeleteDocumentRecipientOptions) => { + const envelope = await prisma.envelope.findFirst({ where: { + type: EnvelopeType.DOCUMENT, recipients: { some: { id: recipientId, @@ -62,13 +63,13 @@ export const deleteDocumentRecipient = async ({ }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); @@ -80,7 +81,7 @@ export const deleteDocumentRecipient = async ({ }); } - const recipientToDelete = document.recipients[0]; + const recipientToDelete = envelope.recipients[0]; if (!recipientToDelete || recipientToDelete.id !== recipientId) { throw new AppError(AppErrorCode.NOT_FOUND, { @@ -88,17 +89,11 @@ export const deleteDocumentRecipient = async ({ }); } - await prisma.$transaction(async (tx) => { - await tx.recipient.delete({ - where: { - id: recipientId, - }, - }); - + const deletedRecipient = await prisma.$transaction(async (tx) => { await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED, - documentId: document.id, + envelopeId: envelope.id, metadata: requestMetadata, data: { recipientEmail: recipientToDelete.email, @@ -108,10 +103,16 @@ export const deleteDocumentRecipient = async ({ }, }), }); + + return await tx.recipient.delete({ + where: { + id: recipientId, + }, + }); }); const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientRemoved; // Send email to deleted recipient. @@ -119,8 +120,8 @@ export const deleteDocumentRecipient = async ({ const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(RecipientRemovedFromDocumentTemplate, { - documentName: document.title, - inviterName: document.team?.name || user.name || undefined, + documentName: envelope.title, + inviterName: envelope.team?.name || user.name || undefined, assetBaseUrl, }); @@ -128,9 +129,9 @@ export const deleteDocumentRecipient = async ({ emailType: 'RECIPIENT', source: { type: 'team', - teamId: document.teamId, + teamId: envelope.teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); const [html, text] = await Promise.all([ @@ -152,4 +153,6 @@ export const deleteDocumentRecipient = async ({ text, }); } + + return deletedRecipient; }; diff --git a/packages/lib/server-only/recipient/delete-recipient.ts b/packages/lib/server-only/recipient/delete-recipient.ts deleted file mode 100644 index 4596c2427..000000000 --- a/packages/lib/server-only/recipient/delete-recipient.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { SendStatus } from '@prisma/client'; - -import { prisma } from '@documenso/prisma'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import type { RequestMetadata } from '../../universal/extract-request-metadata'; -import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; -import { buildTeamWhereQuery } from '../../utils/teams'; - -export type DeleteRecipientOptions = { - documentId: number; - recipientId: number; - userId: number; - teamId: number; - requestMetadata?: RequestMetadata; -}; - -export const deleteRecipient = async ({ - documentId, - recipientId, - userId, - teamId, - requestMetadata, -}: DeleteRecipientOptions) => { - const recipient = await prisma.recipient.findFirst({ - where: { - id: recipientId, - document: { - id: documentId, - userId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - }, - }); - - 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 user = await prisma.user.findFirstOrThrow({ - where: { - id: userId, - }, - }); - - const team = await prisma.team.findFirst({ - where: buildTeamWhereQuery({ teamId, userId }), - }); - - if (!team) { - throw new AppError(AppErrorCode.NOT_FOUND); - } - - const deletedRecipient = await prisma.$transaction(async (tx) => { - const deleted = await tx.recipient.delete({ - where: { - id: recipient.id, - }, - }); - - await tx.documentAuditLog.create({ - data: createDocumentAuditLogData({ - type: 'RECIPIENT_DELETED', - documentId, - user: { - id: team?.id ?? user.id, - email: team?.name ?? user.email, - name: team ? '' : user.name, - }, - data: { - recipientEmail: recipient.email, - recipientName: recipient.name, - recipientId: recipient.id, - recipientRole: recipient.role, - }, - requestMetadata, - }), - }); - - return deleted; - }); - - return deletedRecipient; -}; diff --git a/packages/lib/server-only/recipient/delete-template-recipient.ts b/packages/lib/server-only/recipient/delete-template-recipient.ts index d042d1d2d..839526f3b 100644 --- a/packages/lib/server-only/recipient/delete-template-recipient.ts +++ b/packages/lib/server-only/recipient/delete-template-recipient.ts @@ -1,7 +1,10 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface DeleteTemplateRecipientOptions { userId: number; @@ -14,31 +17,31 @@ export const deleteTemplateRecipient = async ({ teamId, recipientId, }: DeleteTemplateRecipientOptions): Promise => { - const template = await prisma.template.findFirst({ + const recipientToDelete = await prisma.recipient.findFirst({ where: { - recipients: { - some: { - id: recipientId, - }, - }, - team: buildTeamWhereQuery({ teamId, userId }), - }, - include: { - recipients: { - where: { - id: recipientId, - }, + id: recipientId, + envelope: { + type: EnvelopeType.TEMPLATE, + team: buildTeamWhereQuery({ teamId, userId }), }, }, }); - if (!template) { + if (!recipientToDelete) { throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Template not found', + message: 'Recipient not found', }); } - const recipientToDelete = template.recipients[0]; + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'envelopeId', + id: recipientToDelete.envelopeId, + }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); if (!recipientToDelete || recipientToDelete.id !== recipientId) { throw new AppError(AppErrorCode.NOT_FOUND, { @@ -49,6 +52,7 @@ export const deleteTemplateRecipient = async ({ await prisma.recipient.delete({ where: { id: recipientId, + envelope: envelopeWhereInput, }, }); }; diff --git a/packages/lib/server-only/recipient/get-is-recipient-turn.ts b/packages/lib/server-only/recipient/get-is-recipient-turn.ts index e8ef9f126..44127f8ef 100644 --- a/packages/lib/server-only/recipient/get-is-recipient-turn.ts +++ b/packages/lib/server-only/recipient/get-is-recipient-turn.ts @@ -1,4 +1,4 @@ -import { DocumentSigningOrder, SigningStatus } from '@prisma/client'; +import { DocumentSigningOrder, EnvelopeType, SigningStatus } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -7,8 +7,9 @@ export type GetIsRecipientTurnOptions = { }; export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOptions) { - const document = await prisma.document.findFirstOrThrow({ + const envelope = await prisma.envelope.findFirstOrThrow({ where: { + type: EnvelopeType.DOCUMENT, recipients: { some: { token, @@ -25,11 +26,11 @@ export async function getIsRecipientsTurnToSign({ token }: GetIsRecipientTurnOpt }, }); - if (document.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) { + if (envelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL) { return true; } - const { recipients } = document; + const { recipients } = envelope; const currentRecipientIndex = recipients.findIndex((r) => r.token === token); diff --git a/packages/lib/server-only/recipient/get-next-pending-recipient.ts b/packages/lib/server-only/recipient/get-next-pending-recipient.ts index 5b3811268..14158ab23 100644 --- a/packages/lib/server-only/recipient/get-next-pending-recipient.ts +++ b/packages/lib/server-only/recipient/get-next-pending-recipient.ts @@ -1,5 +1,9 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; +import { mapDocumentIdToSecondaryId } from '../../utils/envelope'; + export const getNextPendingRecipient = async ({ documentId, currentRecipientId, @@ -9,7 +13,10 @@ export const getNextPendingRecipient = async ({ }) => { const recipients = await prisma.recipient.findMany({ where: { - documentId, + envelope: { + type: EnvelopeType.DOCUMENT, + secondaryId: mapDocumentIdToSecondaryId(documentId), + }, }, orderBy: [ { diff --git a/packages/lib/server-only/recipient/get-recipient-by-email.ts b/packages/lib/server-only/recipient/get-recipient-by-email.ts deleted file mode 100644 index 349149105..000000000 --- a/packages/lib/server-only/recipient/get-recipient-by-email.ts +++ /dev/null @@ -1,21 +0,0 @@ -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; -}; diff --git a/packages/lib/server-only/recipient/get-recipient-by-id-v1-api.ts b/packages/lib/server-only/recipient/get-recipient-by-id-v1-api.ts deleted file mode 100644 index 6f5cef204..000000000 --- a/packages/lib/server-only/recipient/get-recipient-by-id-v1-api.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { prisma } from '@documenso/prisma'; - -export type GetRecipientByIdOptions = { - id: number; - documentId: number; -}; - -export const getRecipientByIdV1Api = async ({ documentId, id }: GetRecipientByIdOptions) => { - const recipient = await prisma.recipient.findFirst({ - where: { - documentId, - id, - }, - }); - - if (!recipient) { - throw new Error('Recipient not found'); - } - - return recipient; -}; diff --git a/packages/lib/server-only/recipient/get-recipient-by-id.ts b/packages/lib/server-only/recipient/get-recipient-by-id.ts index a647d021b..c1162cf50 100644 --- a/packages/lib/server-only/recipient/get-recipient-by-id.ts +++ b/packages/lib/server-only/recipient/get-recipient-by-id.ts @@ -1,12 +1,17 @@ +import { EnvelopeType } from '@prisma/client'; +import { match } from 'ts-pattern'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../../utils/envelope'; import { buildTeamWhereQuery } from '../../utils/teams'; export type GetRecipientByIdOptions = { recipientId: number; userId: number; teamId: number; + type: EnvelopeType; }; /** @@ -17,16 +22,23 @@ export const getRecipientById = async ({ recipientId, userId, teamId, + type, }: GetRecipientByIdOptions) => { const recipient = await prisma.recipient.findFirst({ where: { id: recipientId, - document: { + envelope: { + type, team: buildTeamWhereQuery({ teamId, userId }), }, }, include: { fields: true, + envelope: { + select: { + secondaryId: true, + }, + }, }, }); @@ -36,5 +48,24 @@ export const getRecipientById = async ({ }); } - return recipient; + const legacyId = match(type) + .with(EnvelopeType.DOCUMENT, () => ({ + documentId: mapSecondaryIdToDocumentId(recipient.envelope.secondaryId), + })) + .with(EnvelopeType.TEMPLATE, () => ({ + templateId: mapSecondaryIdToTemplateId(recipient.envelope.secondaryId), + })) + .exhaustive(); + + // Backwards compatibility mapping. + return { + ...recipient, + ...legacyId, + + // eslint-disable-next-line unused-imports/no-unused-vars + fields: recipient.fields.map((field) => ({ + ...field, + ...legacyId, + })), + }; }; diff --git a/packages/lib/server-only/recipient/get-recipient-suggestions.ts b/packages/lib/server-only/recipient/get-recipient-suggestions.ts index 9ffd64c29..94342df03 100644 --- a/packages/lib/server-only/recipient/get-recipient-suggestions.ts +++ b/packages/lib/server-only/recipient/get-recipient-suggestions.ts @@ -1,11 +1,11 @@ -import { Prisma } from '@prisma/client'; +import { EnvelopeType, Prisma } from '@prisma/client'; import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; import { prisma } from '@documenso/prisma'; export type GetRecipientSuggestionsOptions = { userId: number; - teamId?: number; + teamId: number; query: string; }; @@ -37,7 +37,8 @@ export const getRecipientSuggestions = async ({ const recipients = await prisma.recipient.findMany({ where: { - document: { + envelope: { + type: EnvelopeType.DOCUMENT, team: buildTeamWhereQuery({ teamId, userId }), }, ...nameEmailFilter, @@ -45,7 +46,7 @@ export const getRecipientSuggestions = async ({ select: { name: true, email: true, - document: { + envelope: { select: { createdAt: true, }, @@ -53,7 +54,7 @@ export const getRecipientSuggestions = async ({ }, distinct: ['email'], orderBy: { - document: { + envelope: { createdAt: 'desc', }, }, diff --git a/packages/lib/server-only/recipient/get-recipients-for-assistant.ts b/packages/lib/server-only/recipient/get-recipients-for-assistant.ts index ba5154fb3..c79a57927 100644 --- a/packages/lib/server-only/recipient/get-recipients-for-assistant.ts +++ b/packages/lib/server-only/recipient/get-recipients-for-assistant.ts @@ -23,7 +23,7 @@ export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssis let recipients = await prisma.recipient.findMany({ where: { - documentId: assistant.documentId, + envelopeId: assistant.envelopeId, signingOrder: { gte: assistant.signingOrder ?? 0, }, @@ -39,7 +39,7 @@ export const getRecipientsForAssistant = async ({ token }: GetRecipientsForAssis type: { not: FieldType.SIGNATURE, }, - documentId: assistant.documentId, + envelopeId: assistant.envelopeId, }, ], }, diff --git a/packages/lib/server-only/recipient/get-recipients-for-document.ts b/packages/lib/server-only/recipient/get-recipients-for-document.ts index 3cfd79e4d..ffa6d2505 100644 --- a/packages/lib/server-only/recipient/get-recipients-for-document.ts +++ b/packages/lib/server-only/recipient/get-recipients-for-document.ts @@ -1,6 +1,8 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface GetRecipientsForDocumentOptions { documentId: number; @@ -13,15 +15,19 @@ export const getRecipientsForDocument = async ({ userId, teamId, }: GetRecipientsForDocumentOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); const recipients = await prisma.recipient.findMany({ where: { - document: documentWhereInput, + envelope: envelopeWhereInput, }, orderBy: { id: 'asc', diff --git a/packages/lib/server-only/recipient/set-document-recipients.ts b/packages/lib/server-only/recipient/set-document-recipients.ts index fd9ba5730..c042f7502 100644 --- a/packages/lib/server-only/recipient/set-document-recipients.ts +++ b/packages/lib/server-only/recipient/set-document-recipients.ts @@ -2,7 +2,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/core/macro'; import type { Recipient } from '@prisma/client'; -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { SendStatus, SigningStatus } from '@prisma/client'; import { isDeepEqual } from 'remeda'; @@ -29,8 +29,8 @@ import { AppError, AppErrorCode } from '../../errors/app-error'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { canRecipientBeModified } from '../../utils/recipients'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; import { getEmailContext } from '../email/get-email-context'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface SetDocumentRecipientsOptions { userId: number; @@ -47,14 +47,18 @@ export const setDocumentRecipients = async ({ recipients, requestMetadata, }: SetDocumentRecipientsOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { fields: true, documentMeta: true, @@ -67,6 +71,7 @@ export const setDocumentRecipients = async ({ }, }, }, + recipients: true, }, }); @@ -81,11 +86,11 @@ export const setDocumentRecipients = async ({ }, }); - if (!document) { + if (!envelope) { throw new Error('Document not found'); } - if (document.completedAt) { + if (envelope.completedAt) { throw new Error('Document already complete'); } @@ -95,7 +100,7 @@ export const setDocumentRecipients = async ({ type: 'team', teamId, }, - meta: document.documentMeta, + meta: envelope.documentMeta, }); const recipientsHaveActionAuth = recipients.some( @@ -103,7 +108,7 @@ export const setDocumentRecipients = async ({ ); // Check if user has permission to set the global action auth. - if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) { + if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); @@ -114,11 +119,7 @@ export const setDocumentRecipients = async ({ email: recipient.email.toLowerCase(), })); - const existingRecipients = await prisma.recipient.findMany({ - where: { - documentId, - }, - }); + const existingRecipients = envelope.recipients; const removedRecipients = existingRecipients.filter( (existingRecipient) => @@ -131,12 +132,12 @@ export const setDocumentRecipients = async ({ ); const canPersistedRecipientBeModified = - existing && canRecipientBeModified(existing, document.fields); + existing && canRecipientBeModified(existing, envelope.fields); if ( existing && hasRecipientBeenChanged(existing, recipient) && - !canRecipientBeModified(existing, document.fields) + !canRecipientBeModified(existing, envelope.fields) ) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Cannot modify a recipient who has already interacted with the document', @@ -172,14 +173,14 @@ export const setDocumentRecipients = async ({ const upsertedRecipient = await tx.recipient.upsert({ where: { id: recipient._persisted?.id ?? -1, - documentId, + envelopeId: envelope.id, }, update: { name: recipient.name, email: recipient.email, role: recipient.role, signingOrder: recipient.signingOrder, - documentId, + envelopeId: envelope.id, sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT, signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED, @@ -191,7 +192,7 @@ export const setDocumentRecipients = async ({ role: recipient.role, signingOrder: recipient.signingOrder, token: nanoid(), - documentId, + envelopeId: envelope.id, sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT, signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED, @@ -230,7 +231,7 @@ export const setDocumentRecipients = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { changes, @@ -245,7 +246,7 @@ export const setDocumentRecipients = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { ...baseAuditLog, @@ -278,7 +279,7 @@ export const setDocumentRecipients = async ({ data: removedRecipients.map((recipient) => createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { recipientEmail: recipient.email, @@ -292,7 +293,7 @@ export const setDocumentRecipients = async ({ }); const isRecipientRemovedEmailEnabled = extractDerivedDocumentEmailSettings( - document.documentMeta, + envelope.documentMeta, ).recipientRemoved; // Send emails to deleted recipients. @@ -305,7 +306,7 @@ export const setDocumentRecipients = async ({ const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const template = createElement(RecipientRemovedFromDocumentTemplate, { - documentName: document.title, + documentName: envelope.title, inviterName: user.name || undefined, assetBaseUrl, }); diff --git a/packages/lib/server-only/recipient/set-template-recipients.ts b/packages/lib/server-only/recipient/set-template-recipients.ts index 955ff94f1..3d0f411b9 100644 --- a/packages/lib/server-only/recipient/set-template-recipients.ts +++ b/packages/lib/server-only/recipient/set-template-recipients.ts @@ -1,5 +1,5 @@ import type { Recipient } from '@prisma/client'; -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL, @@ -14,12 +14,13 @@ import { } from '../../types/document-auth'; import { nanoid } from '../../universal/id'; import { createRecipientAuthOptions } from '../../utils/document-auth'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type SetTemplateRecipientsOptions = { userId: number; teamId: number; - templateId: number; + id: EnvelopeIdOptions; recipients: { id?: number; email: string; @@ -33,14 +34,18 @@ export type SetTemplateRecipientsOptions = { export const setTemplateRecipients = async ({ userId, teamId, - templateId, + id, recipients, }: SetTemplateRecipientsOptions) => { - const template = await prisma.template.findFirst({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { directLink: true, team: { @@ -52,10 +57,11 @@ export const setTemplateRecipients = async ({ }, }, }, + recipients: true, }, }); - if (!template) { + if (!envelope) { throw new Error('Template not found'); } @@ -64,7 +70,7 @@ export const setTemplateRecipients = async ({ ); // Check if user has permission to set the global action auth. - if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) { + if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); @@ -72,7 +78,7 @@ export const setTemplateRecipients = async ({ const normalizedRecipients = recipients.map((recipient) => { // Force replace any changes to the name or email of the direct recipient. - if (template.directLink && recipient.id === template.directLink.directTemplateRecipientId) { + if (envelope.directLink && recipient.id === envelope.directLink.directTemplateRecipientId) { return { ...recipient, email: DIRECT_TEMPLATE_RECIPIENT_EMAIL, @@ -86,24 +92,20 @@ export const setTemplateRecipients = async ({ }; }); - const existingRecipients = await prisma.recipient.findMany({ - where: { - templateId, - }, - }); + const existingRecipients = envelope.recipients; const removedRecipients = existingRecipients.filter( (existingRecipient) => !normalizedRecipients.find((recipient) => recipient.id === existingRecipient.id), ); - if (template.directLink !== null) { + if (envelope.directLink !== null) { const updatedDirectRecipient = recipients.find( - (recipient) => recipient.id === template.directLink?.directTemplateRecipientId, + (recipient) => recipient.id === envelope.directLink?.directTemplateRecipientId, ); const deletedDirectRecipient = removedRecipients.find( - (recipient) => recipient.id === template.directLink?.directTemplateRecipientId, + (recipient) => recipient.id === envelope.directLink?.directTemplateRecipientId, ); if (updatedDirectRecipient?.role === RecipientRole.CC) { @@ -145,14 +147,14 @@ export const setTemplateRecipients = async ({ const upsertedRecipient = await tx.recipient.upsert({ where: { id: recipient._persisted?.id ?? -1, - templateId, + envelopeId: envelope.id, }, update: { name: recipient.name, email: recipient.email, role: recipient.role, signingOrder: recipient.signingOrder, - templateId, + envelopeId: envelope.id, authOptions, }, create: { @@ -161,7 +163,7 @@ export const setTemplateRecipients = async ({ role: recipient.role, signingOrder: recipient.signingOrder, token: nanoid(), - templateId, + envelopeId: envelope.id, authOptions, }, }); diff --git a/packages/lib/server-only/recipient/update-document-recipients.ts b/packages/lib/server-only/recipient/update-document-recipients.ts index 6372f56a2..c451599b9 100644 --- a/packages/lib/server-only/recipient/update-document-recipients.ts +++ b/packages/lib/server-only/recipient/update-document-recipients.ts @@ -1,4 +1,4 @@ -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { SendStatus, SigningStatus } from '@prisma/client'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -16,13 +16,14 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; import { canRecipientBeModified } from '../../utils/recipients'; -import { getDocumentWhereInput } from '../document/get-document-by-id'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface UpdateDocumentRecipientsOptions { userId: number; teamId: number; - documentId: number; + id: EnvelopeIdOptions; recipients: RecipientData[]; requestMetadata: ApiRequestMetadata; } @@ -30,18 +31,19 @@ export interface UpdateDocumentRecipientsOptions { export const updateDocumentRecipients = async ({ userId, teamId, - documentId, + id, recipients, requestMetadata, }: UpdateDocumentRecipientsOptions) => { - const { documentWhereInput } = await getDocumentWhereInput({ - documentId, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.DOCUMENT, userId, teamId, }); - const document = await prisma.document.findFirst({ - where: documentWhereInput, + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { fields: true, recipients: true, @@ -57,13 +59,13 @@ export const updateDocumentRecipients = async ({ }, }); - if (!document) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } - if (document.completedAt) { + if (envelope.completedAt) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document already complete', }); @@ -74,14 +76,14 @@ export const updateDocumentRecipients = async ({ ); // Check if user has permission to set the global action auth. - if (recipientsHaveActionAuth && !document.team.organisation.organisationClaim.flags.cfr21) { + if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); } const recipientsToUpdate = recipients.map((recipient) => { - const originalRecipient = document.recipients.find( + const originalRecipient = envelope.recipients.find( (existingRecipient) => existingRecipient.id === recipient.id, ); @@ -91,7 +93,7 @@ export const updateDocumentRecipients = async ({ }); } - if (!canRecipientBeModified(originalRecipient, document.fields)) { + if (!canRecipientBeModified(originalRecipient, envelope.fields)) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Cannot modify a recipient who has already interacted with the document', }); @@ -123,14 +125,14 @@ export const updateDocumentRecipients = async ({ const updatedRecipient = await tx.recipient.update({ where: { id: originalRecipient.id, - documentId, + envelopeId: envelope.id, }, data: { name: mergedRecipient.name, email: mergedRecipient.email, role: mergedRecipient.role, signingOrder: mergedRecipient.signingOrder, - documentId, + envelopeId: envelope.id, sendStatus: mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT, signingStatus: @@ -164,7 +166,7 @@ export const updateDocumentRecipients = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED, - documentId: documentId, + envelopeId: envelope.id, metadata: requestMetadata, data: { recipientEmail: updatedRecipient.email, diff --git a/packages/lib/server-only/recipient/update-template-recipients.ts b/packages/lib/server-only/recipient/update-template-recipients.ts index aca4520f8..208853e47 100644 --- a/packages/lib/server-only/recipient/update-template-recipients.ts +++ b/packages/lib/server-only/recipient/update-template-recipients.ts @@ -1,4 +1,4 @@ -import { RecipientRole } from '@prisma/client'; +import { EnvelopeType, RecipientRole } from '@prisma/client'; import { SendStatus, SigningStatus } from '@prisma/client'; import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth'; @@ -10,7 +10,7 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export interface UpdateTemplateRecipientsOptions { userId: number; @@ -33,11 +33,18 @@ export const updateTemplateRecipients = async ({ templateId, recipients, }: UpdateTemplateRecipientsOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, team: { @@ -52,7 +59,7 @@ export const updateTemplateRecipients = async ({ }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found', }); @@ -63,14 +70,14 @@ export const updateTemplateRecipients = async ({ ); // Check if user has permission to set the global action auth. - if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) { + if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); } const recipientsToUpdate = recipients.map((recipient) => { - const originalRecipient = template.recipients.find( + const originalRecipient = envelope.recipients.find( (existingRecipient) => existingRecipient.id === recipient.id, ); @@ -109,14 +116,14 @@ export const updateTemplateRecipients = async ({ const updatedRecipient = await tx.recipient.update({ where: { id: originalRecipient.id, - templateId, + envelopeId: envelope.id, }, data: { name: mergedRecipient.name, email: mergedRecipient.email, role: mergedRecipient.role, signingOrder: mergedRecipient.signingOrder, - templateId, + envelopeId: envelope.id, sendStatus: mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT, signingStatus: diff --git a/packages/lib/server-only/share/create-or-get-share-link.ts b/packages/lib/server-only/share/create-or-get-share-link.ts index 1569b98af..63066f218 100644 --- a/packages/lib/server-only/share/create-or-get-share-link.ts +++ b/packages/lib/server-only/share/create-or-get-share-link.ts @@ -1,8 +1,11 @@ +import { EnvelopeType } from '@prisma/client'; import { P, match } from 'ts-pattern'; import { prisma } from '@documenso/prisma'; +import { AppError, AppErrorCode } from '../../errors/app-error'; import { alphaid } from '../../universal/id'; +import { unsafeBuildEnvelopeIdQuery } from '../../utils/envelope'; export type CreateSharingIdOptions = | { @@ -15,12 +18,29 @@ export type CreateSharingIdOptions = }; export const createOrGetShareLink = async ({ documentId, ...options }: CreateSharingIdOptions) => { + const envelope = await prisma.envelope.findUnique({ + where: unsafeBuildEnvelopeIdQuery( + { + type: 'documentId', + id: documentId, + }, + EnvelopeType.DOCUMENT, + ), + select: { + id: true, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND); + } + const email = await match(options) .with({ token: P.string }, async ({ token }) => { return await prisma.recipient .findFirst({ where: { - documentId, + envelopeId: envelope.id, token, }, }) @@ -32,6 +52,9 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha where: { id: userId, }, + select: { + email: true, + }, }) .then((user) => user?.email); }) @@ -43,14 +66,14 @@ export const createOrGetShareLink = async ({ documentId, ...options }: CreateSha return await prisma.documentShareLink.upsert({ where: { - documentId_email: { + envelopeId_email: { + envelopeId: envelope.id, email, - documentId, }, }, create: { email, - documentId, + envelopeId: envelope.id, slug: alphaid(14), }, update: {}, diff --git a/packages/lib/server-only/share/get-share-link-by-slug.ts b/packages/lib/server-only/share/get-share-link-by-slug.ts deleted file mode 100644 index 4486de81a..000000000 --- a/packages/lib/server-only/share/get-share-link-by-slug.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { prisma } from '@documenso/prisma'; - -export type GetShareLinkBySlugOptions = { - slug: string; -}; - -export const getShareLinkBySlug = async ({ slug }: GetShareLinkBySlugOptions) => { - return await prisma.documentShareLink.findFirstOrThrow({ - where: { - slug, - }, - }); -}; diff --git a/packages/lib/server-only/team/get-member-roles.ts b/packages/lib/server-only/team/get-member-roles.ts index 247bf7145..090df533f 100644 --- a/packages/lib/server-only/team/get-member-roles.ts +++ b/packages/lib/server-only/team/get-member-roles.ts @@ -21,7 +21,14 @@ type GetMemberRolesOptions = { * Returns the highest Team role of a given member or user of a team */ export const getMemberRoles = async ({ teamId, reference }: GetMemberRolesOptions) => { - const team = await prisma.team.findFirst({ + // Enforce incase teamId undefined slips through due to invalid types. + if (teamId === undefined) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Team not found', + }); + } + + const team = await prisma.team.findUnique({ where: { id: teamId, }, diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index b223dd49f..196a11a85 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -5,6 +5,7 @@ import type { Field, Signature } from '@prisma/client'; import { DocumentSource, DocumentStatus, + EnvelopeType, FieldType, Prisma, RecipientRole, @@ -31,7 +32,7 @@ import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/doc import { ZFieldMetaSchema } from '../../types/field-meta'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import { isRequiredField } from '../../utils/advanced-fields-helpers'; @@ -43,11 +44,13 @@ import { createRecipientAuthOptions, extractDocumentAuthMethods, } from '../../utils/document-auth'; +import { mapSecondaryIdToTemplateId } from '../../utils/envelope'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { formatDocumentsPath } from '../../utils/teams'; import { sendDocument } from '../document/send-document'; import { validateFieldAuth } from '../document/validate-field-auth'; import { getEmailContext } from '../email/get-email-context'; +import { incrementDocumentId } from '../envelope/increment-id'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; export type CreateDocumentFromDirectTemplateOptions = { @@ -90,7 +93,7 @@ export const createDocumentFromDirectTemplate = async ({ requestMetadata, user, }: CreateDocumentFromDirectTemplateOptions): Promise => { - const template = await prisma.template.findFirst({ + const directTemplateEnvelope = await prisma.envelope.findFirst({ where: { directLink: { token: directTemplateToken, @@ -103,8 +106,12 @@ export const createDocumentFromDirectTemplate = async ({ }, }, directLink: true, - templateDocumentData: true, - templateMeta: true, + envelopeItems: { + select: { + documentData: true, + }, + }, + documentMeta: true, user: { select: { id: true, @@ -115,19 +122,31 @@ export const createDocumentFromDirectTemplate = async ({ }, }); - if (!template?.directLink?.enabled) { + if (!directTemplateEnvelope?.directLink?.enabled) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' }); } + const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId( + directTemplateEnvelope.secondaryId, + ); + const firstEnvelopeItem = directTemplateEnvelope.envelopeItems[0]; + + // Todo: Envelopes + if (directTemplateEnvelope.envelopeItems.length !== 1 || !firstEnvelopeItem) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Invalid number of envelope items', + }); + } + const { branding, settings, senderEmail, emailLanguage } = await getEmailContext({ emailType: 'INTERNAL', source: { type: 'team', - teamId: template.teamId, + teamId: directTemplateEnvelope.teamId, }, }); - const { recipients, directLink, user: templateOwner } = template; + const { recipients, directLink, user: templateOwner } = directTemplateEnvelope; const directTemplateRecipient = recipients.find( (recipient) => recipient.id === directLink.directTemplateRecipientId, @@ -139,7 +158,7 @@ export const createDocumentFromDirectTemplate = async ({ }); } - if (template.updatedAt.getTime() !== templateUpdatedAt.getTime()) { + if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' }); } @@ -151,7 +170,7 @@ export const createDocumentFromDirectTemplate = async ({ const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({ - documentAuth: template.authOptions, + documentAuth: directTemplateEnvelope.authOptions, }); const directRecipientName = user?.name || initialDirectRecipientName; @@ -171,10 +190,13 @@ export const createDocumentFromDirectTemplate = async ({ directTemplateRecipient.authOptions, ); - const nonDirectTemplateRecipients = template.recipients.filter( + const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter( (recipient) => recipient.id !== directTemplateRecipient.id, ); - const derivedDocumentMeta = extractDerivedDocumentMeta(settings, template.templateMeta); + const derivedDocumentMeta = extractDerivedDocumentMeta( + settings, + directTemplateEnvelope.documentMeta, + ); // Associate, validate and map to a query every direct template recipient field with the provided fields. // Only process fields that are either required or have been signed by the user @@ -202,7 +224,7 @@ export const createDocumentFromDirectTemplate = async ({ } const derivedRecipientActionAuth = await validateFieldAuth({ - documentAuthOptions: template.authOptions, + documentAuthOptions: directTemplateEnvelope.authOptions, recipient: { authOptions: directTemplateRecipient.authOptions, email: directRecipientEmail, @@ -267,29 +289,45 @@ export const createDocumentFromDirectTemplate = async ({ const initialRequestTime = new Date(); - const { documentId, recipientId, token } = await prisma.$transaction(async (tx) => { - const documentData = await tx.documentData.create({ - data: { - type: template.templateDocumentData.type, - data: template.templateDocumentData.data, - initialData: template.templateDocumentData.initialData, - }, - }); + // Todo: Envelopes + const documentData = await prisma.documentData.create({ + data: { + type: firstEnvelopeItem.documentData.type, + data: firstEnvelopeItem.documentData.data, + initialData: firstEnvelopeItem.documentData.initialData, + }, + }); - // Create the document and non direct template recipients. - const document = await tx.document.create({ + const documentMeta = await prisma.documentMeta.create({ + data: derivedDocumentMeta, + }); + + const incrementedDocumentId = await incrementDocumentId(); + + const { createdEnvelope, recipientId, token } = await prisma.$transaction(async (tx) => { + // Create the envelope and non direct template recipients. + const createdEnvelope = await tx.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: incrementedDocumentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, qrToken: prefixedId('qr'), source: DocumentSource.TEMPLATE_DIRECT_LINK, - templateId: template.id, - userId: template.userId, - teamId: template.teamId, - title: template.title, + templateId: directTemplateEnvelopeLegacyId, + userId: directTemplateEnvelope.userId, + teamId: directTemplateEnvelope.teamId, + title: directTemplateEnvelope.title, createdAt: initialRequestTime, status: DocumentStatus.PENDING, externalId: directTemplateExternalId, visibility: settings.documentVisibility, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: directTemplateEnvelope.title, // Todo: Envelopes use item title instead + documentDataId: documentData.id, + }, + }, authOptions: createDocumentAuthOptions({ globalAccessAuth: templateAuthOptions.globalAccessAuth, globalActionAuth: templateAuthOptions.globalActionAuth, @@ -319,9 +357,7 @@ export const createDocumentFromDirectTemplate = async ({ }), }, }, - documentMeta: { - create: derivedDocumentMeta, - }, + documentMetaId: documentMeta.id, }, include: { recipients: true, @@ -330,13 +366,20 @@ export const createDocumentFromDirectTemplate = async ({ url: true, }, }, + envelopeItems: { + select: { + id: true, + }, + }, }, }); + const envelopeItemId = createdEnvelope.envelopeItems[0].id; + let nonDirectRecipientFieldsToCreate: Omit[] = []; Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => { - const recipient = document.recipients.find( + const recipient = createdEnvelope.recipients.find( (recipient) => recipient.email === templateRecipient.email, ); @@ -346,7 +389,8 @@ export const createDocumentFromDirectTemplate = async ({ nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat( templateRecipient.fields.map((field) => ({ - documentId: document.id, + envelopeId: createdEnvelope.id, + envelopeItemId: envelopeItemId, // Todo: Envelopes recipientId: recipient.id, type: field.type, page: field.page, @@ -371,7 +415,7 @@ export const createDocumentFromDirectTemplate = async ({ // Create the direct recipient and their non signature fields. const createdDirectRecipient = await tx.recipient.create({ data: { - documentId: document.id, + envelopeId: createdEnvelope.id, email: directRecipientEmail, name: directRecipientName, authOptions: createRecipientAuthOptions({ @@ -387,7 +431,8 @@ export const createDocumentFromDirectTemplate = async ({ fields: { createMany: { data: directTemplateNonSignatureFields.map(({ templateField, customText }) => ({ - documentId: document.id, + envelopeId: createdEnvelope.id, + envelopeItemId: envelopeItemId, // Todo: Envelopes type: templateField.type, page: templateField.page, positionX: templateField.positionX, @@ -417,7 +462,8 @@ export const createDocumentFromDirectTemplate = async ({ const field = await tx.field.create({ data: { - documentId: document.id, + envelopeId: createdEnvelope.id, + envelopeItemId: envelopeItemId, // Todo: Envelopes recipientId: createdDirectRecipient.id, type: templateField.type, page: templateField.page, @@ -466,7 +512,7 @@ export const createDocumentFromDirectTemplate = async ({ const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [ createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, - documentId: document.id, + envelopeId: createdEnvelope.id, user: { id: user?.id, name: user?.name, @@ -474,17 +520,17 @@ export const createDocumentFromDirectTemplate = async ({ }, metadata: requestMetadata, data: { - title: document.title, + title: createdEnvelope.title, source: { type: DocumentSource.TEMPLATE_DIRECT_LINK, - templateId: template.id, + templateId: directTemplateEnvelopeLegacyId, directRecipientEmail, }, }, }), createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED, - documentId: document.id, + envelopeId: createdEnvelope.id, user: { id: user?.id, name: user?.name, @@ -502,7 +548,7 @@ export const createDocumentFromDirectTemplate = async ({ ...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) => createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED, - documentId: document.id, + envelopeId: createdEnvelope.id, user: { id: user?.id, name: user?.name, @@ -547,7 +593,7 @@ export const createDocumentFromDirectTemplate = async ({ ), createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED, - documentId: document.id, + envelopeId: createdEnvelope.id, user: { id: user?.id, name: user?.name, @@ -572,10 +618,10 @@ export const createDocumentFromDirectTemplate = async ({ const emailTemplate = createElement(DocumentCreatedFromDirectTemplateEmailTemplate, { recipientName: directRecipientEmail, recipientRole: directTemplateRecipient.role, - documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(document.team?.url)}/${ - document.id + documentLink: `${NEXT_PUBLIC_WEBAPP_URL()}${formatDocumentsPath(createdEnvelope.team?.url)}/${ + createdEnvelope.id }`, - documentName: document.title, + documentName: createdEnvelope.title, assetBaseUrl: NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000', }); @@ -600,8 +646,8 @@ export const createDocumentFromDirectTemplate = async ({ }); return { + createdEnvelope, token: createdDirectRecipient.token, - documentId: document.id, recipientId: createdDirectRecipient.id, }; }); @@ -609,18 +655,21 @@ export const createDocumentFromDirectTemplate = async ({ try { // This handles sending emails and sealing the document if required. await sendDocument({ - documentId, - userId: template.userId, - teamId: template.teamId, + id: { + type: 'envelopeId', + id: createdEnvelope.id, + }, + userId: createdEnvelope.userId, + teamId: createdEnvelope.teamId, requestMetadata, }); - const createdDocument = await prisma.document.findFirstOrThrow({ + // Refetch envelope so we get the final data. + const refetchedEnvelope = await prisma.envelope.findFirstOrThrow({ where: { - id: documentId, + id: createdEnvelope.id, }, include: { - documentData: true, documentMeta: true, recipients: true, }, @@ -628,9 +677,9 @@ export const createDocumentFromDirectTemplate = async ({ await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_SIGNED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)), - userId: template.userId, - teamId: template.teamId ?? undefined, + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(refetchedEnvelope)), + userId: refetchedEnvelope.userId, + teamId: refetchedEnvelope.teamId ?? undefined, }); } catch (err) { console.error('[CREATE_DOCUMENT_FROM_DIRECT_TEMPLATE]:', err); @@ -641,7 +690,7 @@ export const createDocumentFromDirectTemplate = async ({ return { token, - documentId, + documentId: incrementedDocumentId.documentId, recipientId, }; }; diff --git a/packages/lib/server-only/template/create-document-from-template-legacy.ts b/packages/lib/server-only/template/create-document-from-template-legacy.ts deleted file mode 100644 index 6bc83c667..000000000 --- a/packages/lib/server-only/template/create-document-from-template-legacy.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { DocumentSource, type RecipientRole } from '@prisma/client'; - -import { nanoid, prefixedId } from '@documenso/lib/universal/id'; -import { prisma } from '@documenso/prisma'; - -import { extractDerivedDocumentMeta } from '../../utils/document'; -import { buildTeamWhereQuery } from '../../utils/teams'; -import { getTeamSettings } from '../team/get-team-settings'; - -export type CreateDocumentFromTemplateLegacyOptions = { - templateId: number; - userId: number; - teamId: number; - recipients?: { - name?: string; - email: string; - role?: RecipientRole; - signingOrder?: number | null; - }[]; -}; - -// !TODO: Make this work - -/** - * Legacy server function for /api/v1 - */ -export const createDocumentFromTemplateLegacy = async ({ - templateId, - userId, - teamId, - recipients, -}: CreateDocumentFromTemplateLegacyOptions) => { - const template = await prisma.template.findUnique({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, - include: { - recipients: true, - fields: true, - templateDocumentData: true, - templateMeta: true, - }, - }); - - if (!template) { - throw new Error('Template not found.'); - } - - const settings = await getTeamSettings({ - userId, - teamId, - }); - - const documentData = await prisma.documentData.create({ - data: { - type: template.templateDocumentData.type, - data: template.templateDocumentData.data, - initialData: template.templateDocumentData.initialData, - }, - }); - - const recipientsToCreate = template.recipients.map((recipient) => ({ - id: recipient.id, - email: recipient.email, - name: recipient.name, - role: recipient.role, - signingOrder: recipient.signingOrder, - token: nanoid(), - })); - - const document = await prisma.document.create({ - data: { - qrToken: prefixedId('qr'), - source: DocumentSource.TEMPLATE, - templateId: template.id, - userId, - teamId: template.teamId, - title: template.title, - visibility: settings.documentVisibility, - documentDataId: documentData.id, - useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false, - recipients: { - create: recipientsToCreate.map((recipient) => ({ - email: recipient.email, - name: recipient.name, - role: recipient.role, - signingOrder: recipient.signingOrder, - token: recipient.token, - })), - }, - documentMeta: { - create: extractDerivedDocumentMeta(settings, template.templateMeta), - }, - }, - - include: { - recipients: { - orderBy: { - id: 'asc', - }, - }, - documentData: true, - }, - }); - - await prisma.field.createMany({ - data: template.fields.map((field) => { - const recipient = recipientsToCreate.find((recipient) => recipient.id === field.recipientId); - - const documentRecipient = document.recipients.find( - (documentRecipient) => documentRecipient.token === recipient?.token, - ); - - if (!documentRecipient) { - throw new Error('Recipient not found.'); - } - - return { - type: field.type, - page: field.page, - positionX: field.positionX, - positionY: field.positionY, - width: field.width, - height: field.height, - customText: field.customText, - inserted: field.inserted, - documentId: document.id, - recipientId: documentRecipient.id, - }; - }), - }); - - // Replicate the old logic, get by index and create if we exceed the number of existing recipients. - if (recipients && recipients.length > 0) { - await Promise.all( - recipients.map(async (recipient, index) => { - const existingRecipient = document.recipients.at(index); - - if (existingRecipient) { - return await prisma.recipient.update({ - where: { - id: existingRecipient.id, - documentId: document.id, - }, - data: { - name: recipient.name, - email: recipient.email, - role: recipient.role, - signingOrder: recipient.signingOrder, - }, - }); - } - - return await prisma.recipient.create({ - data: { - documentId: document.id, - name: recipient.name, - email: recipient.email, - role: recipient.role, - signingOrder: recipient.signingOrder, - token: nanoid(), - }, - }); - }), - ); - } - - // Gross but we need to do the additional fetch since we mutate above. - const updatedRecipients = await prisma.recipient.findMany({ - where: { - documentId: document.id, - }, - orderBy: { - id: 'asc', - }, - }); - - return { - ...document, - recipients: updatedRecipients, - }; -}; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts index e7a1011c3..3d70a1c96 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,6 +1,7 @@ import type { DocumentDistributionMethod, DocumentSigningOrder } from '@prisma/client'; import { DocumentSource, + EnvelopeType, type Field, FolderType, type Recipient, @@ -37,7 +38,7 @@ import { } from '../../types/field-meta'; import { ZWebhookDocumentSchema, - mapDocumentToWebhookDocumentPayload, + mapEnvelopeToWebhookDocumentPayload, } from '../../types/webhook-payload'; import type { ApiRequestMetadata } from '../../universal/extract-request-metadata'; import { extractDerivedDocumentMeta } from '../../utils/document'; @@ -47,7 +48,11 @@ import { createRecipientAuthOptions, extractDocumentAuthMethods, } from '../../utils/document-auth'; +import type { EnvelopeIdOptions } from '../../utils/envelope'; +import { mapSecondaryIdToTemplateId } from '../../utils/envelope'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; +import { incrementDocumentId } from '../envelope/increment-id'; import { getTeamSettings } from '../team/get-team-settings'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -60,7 +65,7 @@ type FinalRecipient = Pick< }; export type CreateDocumentFromTemplateOptions = { - templateId: number; + id: EnvelopeIdOptions; externalId?: string | null; userId: number; teamId: number; @@ -268,7 +273,7 @@ const getUpdatedFieldMeta = (field: Field, prefillField?: TFieldMetaPrefillField }; export const createDocumentFromTemplate = async ({ - templateId, + id, externalId, userId, teamId, @@ -279,19 +284,27 @@ export const createDocumentFromTemplate = async ({ folderId, prefillFields, }: CreateDocumentFromTemplateOptions) => { - const template = await prisma.template.findUnique({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const template = await prisma.envelope.findUnique({ + where: envelopeWhereInput, include: { recipients: { include: { fields: true, }, }, - templateDocumentData: true, - templateMeta: true, + envelopeItems: { + include: { + documentData: true, + }, + }, + documentMeta: true, }, }); @@ -317,6 +330,15 @@ export const createDocumentFromTemplate = async ({ } } + const legacyTemplateId = mapSecondaryIdToTemplateId(template.secondaryId); + + // Todo: Envelopes + if (template.envelopeItems.length !== 1) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Template must have exactly 1 envelope item', + }); + } + const settings = await getTeamSettings({ userId, teamId, @@ -354,7 +376,8 @@ export const createDocumentFromTemplate = async ({ }; }); - let parentDocumentData = template.templateDocumentData; + // Todo: Envelopes + let parentDocumentData = template.envelopeItems[0].documentData; if (customDocumentDataId) { const customDocumentData = await prisma.documentData.findFirst({ @@ -380,48 +403,58 @@ export const createDocumentFromTemplate = async ({ }, }); + const incrementedDocumentId = await incrementDocumentId(); + + const documentMeta = await prisma.documentMeta.create({ + data: extractDerivedDocumentMeta(settings, { + subject: override?.subject || template.documentMeta?.subject, + message: override?.message || template.documentMeta?.message, + timezone: override?.timezone || template.documentMeta?.timezone, + dateFormat: override?.dateFormat || template.documentMeta?.dateFormat, + redirectUrl: override?.redirectUrl || template.documentMeta?.redirectUrl, + distributionMethod: override?.distributionMethod || template.documentMeta?.distributionMethod, + emailSettings: override?.emailSettings || template.documentMeta?.emailSettings, + signingOrder: override?.signingOrder || template.documentMeta?.signingOrder, + language: override?.language || template.documentMeta?.language || settings.documentLanguage, + typedSignatureEnabled: + override?.typedSignatureEnabled ?? template.documentMeta?.typedSignatureEnabled, + uploadSignatureEnabled: + override?.uploadSignatureEnabled ?? template.documentMeta?.uploadSignatureEnabled, + drawSignatureEnabled: + override?.drawSignatureEnabled ?? template.documentMeta?.drawSignatureEnabled, + allowDictateNextSigner: + override?.allowDictateNextSigner ?? template.documentMeta?.allowDictateNextSigner, + }), + }); + return await prisma.$transaction(async (tx) => { - const document = await tx.document.create({ + const envelope = await tx.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: incrementedDocumentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, qrToken: prefixedId('qr'), source: DocumentSource.TEMPLATE, externalId: externalId || template.externalId, - templateId: template.id, + templateId: legacyTemplateId, // The template this envelope was created from. userId, folderId, teamId: template.teamId, title: override?.title || template.title, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: override?.title || template.title, + documentDataId: documentData.id, + }, + }, authOptions: createDocumentAuthOptions({ globalAccessAuth: templateAuthOptions.globalAccessAuth, globalActionAuth: templateAuthOptions.globalActionAuth, }), visibility: template.visibility || settings.documentVisibility, useLegacyFieldInsertion: template.useLegacyFieldInsertion ?? false, - documentMeta: { - create: extractDerivedDocumentMeta(settings, { - subject: override?.subject || template.templateMeta?.subject, - message: override?.message || template.templateMeta?.message, - timezone: override?.timezone || template.templateMeta?.timezone, - password: override?.password || template.templateMeta?.password, - dateFormat: override?.dateFormat || template.templateMeta?.dateFormat, - redirectUrl: override?.redirectUrl || template.templateMeta?.redirectUrl, - distributionMethod: - override?.distributionMethod || template.templateMeta?.distributionMethod, - emailSettings: override?.emailSettings || template.templateMeta?.emailSettings, - signingOrder: override?.signingOrder || template.templateMeta?.signingOrder, - language: - override?.language || template.templateMeta?.language || settings.documentLanguage, - typedSignatureEnabled: - override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled, - uploadSignatureEnabled: - override?.uploadSignatureEnabled ?? template.templateMeta?.uploadSignatureEnabled, - drawSignatureEnabled: - override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled, - allowDictateNextSigner: - override?.allowDictateNextSigner ?? template.templateMeta?.allowDictateNextSigner, - }), - }, + documentMetaId: documentMeta.id, recipients: { createMany: { data: finalRecipients.map((recipient) => { @@ -454,11 +487,17 @@ export const createDocumentFromTemplate = async ({ id: 'asc', }, }, - documentData: true, + envelopeItems: { + select: { + id: true, + }, + }, }, }); - let fieldsToCreate: Omit[] = []; + const envelopeItemId = envelope.envelopeItems[0].id; + + let fieldsToCreate: Omit[] = []; // Get all template field IDs first so we can validate later const allTemplateFieldIds = finalRecipients.flatMap((recipient) => @@ -502,7 +541,7 @@ export const createDocumentFromTemplate = async ({ } Object.values(finalRecipients).forEach(({ token, fields }) => { - const recipient = document.recipients.find((recipient) => recipient.token === token); + const recipient = envelope.recipients.find((recipient) => recipient.token === token); if (!recipient) { throw new Error('Recipient not found.'); @@ -513,7 +552,8 @@ export const createDocumentFromTemplate = async ({ const prefillField = prefillFields?.find((value) => value.id === field.id); const payload = { - documentId: document.id, + envelopeItemId, + envelopeId: envelope.id, // Todo: Envelopes recipientId: recipient.id, type: field.type, page: field.page, @@ -544,7 +584,7 @@ export const createDocumentFromTemplate = async ({ } payload.customText = DateTime.fromJSDate(date).toFormat( - template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, + template.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, ); payload.inserted = true; @@ -569,21 +609,21 @@ export const createDocumentFromTemplate = async ({ await tx.documentAuditLog.create({ data: createDocumentAuditLogData({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED, - documentId: document.id, + envelopeId: envelope.id, metadata: requestMetadata, data: { - title: document.title, + title: envelope.title, source: { type: DocumentSource.TEMPLATE, - templateId: template.id, + templateId: legacyTemplateId, }, }, }), }); - const createdDocument = await tx.document.findFirst({ + const createdEnvelope = await tx.envelope.findFirst({ where: { - id: document.id, + id: envelope.id, }, include: { documentMeta: true, @@ -591,17 +631,17 @@ export const createDocumentFromTemplate = async ({ }, }); - if (!createdDocument) { + if (!createdEnvelope) { throw new Error('Document not found'); } await triggerWebhook({ event: WebhookTriggerEvents.DOCUMENT_CREATED, - data: ZWebhookDocumentSchema.parse(mapDocumentToWebhookDocumentPayload(createdDocument)), + data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(createdEnvelope)), userId, teamId, }); - return document; + return envelope; }); }; diff --git a/packages/lib/server-only/template/create-template-direct-link.ts b/packages/lib/server-only/template/create-template-direct-link.ts index db4a2dc46..f11c61083 100644 --- a/packages/lib/server-only/template/create-template-direct-link.ts +++ b/packages/lib/server-only/template/create-template-direct-link.ts @@ -1,4 +1,4 @@ -import type { Recipient } from '@prisma/client'; +import { EnvelopeType, type Recipient } from '@prisma/client'; import { nanoid } from 'nanoid'; import { @@ -8,50 +8,55 @@ import { import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { type EnvelopeIdOptions, mapSecondaryIdToTemplateId } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type CreateTemplateDirectLinkOptions = { - templateId: number; + id: EnvelopeIdOptions; userId: number; teamId: number; directRecipientId?: number; }; export const createTemplateDirectLink = async ({ - templateId, + id, userId, teamId, directRecipientId, }: CreateTemplateDirectLinkOptions) => { - const template = await prisma.template.findFirst({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, directLink: true, }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found' }); } - if (template.directLink) { + if (envelope.directLink) { throw new AppError(AppErrorCode.ALREADY_EXISTS, { message: 'Direct template already exists' }); } if ( directRecipientId && - !template.recipients.find((recipient) => recipient.id === directRecipientId) + !envelope.recipients.find((recipient) => recipient.id === directRecipientId) ) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Recipient not found' }); } if ( !directRecipientId && - template.recipients.find( + envelope.recipients.find( (recipient) => recipient.email.toLowerCase() === DIRECT_TEMPLATE_RECIPIENT_EMAIL, ) ) { @@ -60,13 +65,13 @@ export const createTemplateDirectLink = async ({ }); } - return await prisma.$transaction(async (tx) => { + const createdDirectLink = await prisma.$transaction(async (tx) => { let recipient: Recipient | undefined; if (directRecipientId) { recipient = await tx.recipient.update({ where: { - templateId, + envelopeId: envelope.id, id: directRecipientId, }, data: { @@ -77,7 +82,7 @@ export const createTemplateDirectLink = async ({ } else { recipient = await tx.recipient.create({ data: { - templateId, + envelopeId: envelope.id, name: DIRECT_TEMPLATE_RECIPIENT_NAME, email: DIRECT_TEMPLATE_RECIPIENT_EMAIL, token: nanoid(), @@ -87,11 +92,20 @@ export const createTemplateDirectLink = async ({ return await tx.templateDirectLink.create({ data: { - templateId, + envelopeId: envelope.id, enabled: true, token: nanoid(), directTemplateRecipientId: recipient.id, }, }); }); + + return { + id: createdDirectLink.id, + token: createdDirectLink.token, + createdAt: createdDirectLink.createdAt, + enabled: createdDirectLink.enabled, + directTemplateRecipientId: createdDirectLink.directTemplateRecipientId, + templateId: mapSecondaryIdToTemplateId(envelope.secondaryId), + }; }; diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts deleted file mode 100644 index a733501cd..000000000 --- a/packages/lib/server-only/template/create-template.ts +++ /dev/null @@ -1,112 +0,0 @@ -import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client'; -import type { z } from 'zod'; - -import { prisma } from '@documenso/prisma'; -import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema'; - -import { AppError, AppErrorCode } from '../../errors/app-error'; -import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; -import { extractDerivedDocumentMeta } from '../../utils/document'; -import { createDocumentAuthOptions } from '../../utils/document-auth'; -import { buildTeamWhereQuery } from '../../utils/teams'; -import { getTeamSettings } from '../team/get-team-settings'; - -export type CreateTemplateOptions = { - userId: number; - teamId: number; - templateDocumentDataId: string; - data: { - title: string; - folderId?: string; - externalId?: string | null; - visibility?: DocumentVisibility; - globalAccessAuth?: TDocumentAccessAuthTypes[]; - globalActionAuth?: TDocumentActionAuthTypes[]; - publicTitle?: string; - publicDescription?: string; - type?: Template['type']; - }; - meta?: Partial>; -}; - -export const ZCreateTemplateResponseSchema = TemplateSchema; - -export type TCreateTemplateResponse = z.infer; - -export const createTemplate = async ({ - userId, - teamId, - templateDocumentDataId, - data, - meta = {}, -}: CreateTemplateOptions) => { - const { title, folderId } = data; - - const team = await prisma.team.findFirst({ - where: buildTeamWhereQuery({ teamId, userId }), - }); - - if (!team) { - throw new AppError(AppErrorCode.NOT_FOUND); - } - - if (folderId) { - const folder = await prisma.folder.findFirst({ - where: { - id: folderId, - teamId: team.id, - }, - }); - - if (!folder) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - } - - const settings = await getTeamSettings({ - userId, - teamId, - }); - - const emailId = meta.emailId; - - // Validate that the email ID belongs to the organisation. - if (emailId) { - const email = await prisma.organisationEmail.findFirst({ - where: { - id: emailId, - organisationId: team.organisationId, - }, - }); - - if (!email) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Email not found', - }); - } - } - - return await prisma.template.create({ - data: { - title, - teamId, - userId, - templateDocumentDataId, - folderId, - externalId: data.externalId, - visibility: data.visibility ?? settings.documentVisibility, - authOptions: createDocumentAuthOptions({ - globalAccessAuth: data.globalAccessAuth || [], - globalActionAuth: data.globalActionAuth || [], - }), - publicTitle: data.publicTitle, - publicDescription: data.publicDescription, - type: data.type, - templateMeta: { - create: extractDerivedDocumentMeta(settings, meta), - }, - }, - }); -}; diff --git a/packages/lib/server-only/template/delete-template-direct-link.ts b/packages/lib/server-only/template/delete-template-direct-link.ts index e7971d19e..30f427f75 100644 --- a/packages/lib/server-only/template/delete-template-direct-link.ts +++ b/packages/lib/server-only/template/delete-template-direct-link.ts @@ -1,8 +1,10 @@ +import { EnvelopeType } from '@prisma/client'; + import { generateAvaliableRecipientPlaceholder } from '@documenso/lib/utils/templates'; import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type DeleteTemplateDirectLinkOptions = { templateId: number; @@ -15,24 +17,31 @@ export const deleteTemplateDirectLink = async ({ userId, teamId, }: DeleteTemplateDirectLinkOptions): Promise => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, include: { directLink: true, recipients: true, }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found', }); } - const { directLink } = template; + const { directLink } = envelope; if (!directLink) { return; @@ -41,17 +50,17 @@ export const deleteTemplateDirectLink = async ({ await prisma.$transaction(async (tx) => { await tx.recipient.update({ where: { - templateId: template.id, + envelopeId: envelope.id, id: directLink.directTemplateRecipientId, }, data: { - ...generateAvaliableRecipientPlaceholder(template.recipients), + ...generateAvaliableRecipientPlaceholder(envelope.recipients), }, }); await tx.templateDirectLink.delete({ where: { - templateId, + envelopeId: envelope.id, }, }); }); diff --git a/packages/lib/server-only/template/delete-template.ts b/packages/lib/server-only/template/delete-template.ts index 9d8920031..b6ed690e1 100644 --- a/packages/lib/server-only/template/delete-template.ts +++ b/packages/lib/server-only/template/delete-template.ts @@ -1,6 +1,8 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type DeleteTemplateOptions = { id: number; @@ -9,10 +11,17 @@ export type DeleteTemplateOptions = { }; export const deleteTemplate = async ({ id, userId, teamId }: DeleteTemplateOptions) => { - return await prisma.template.delete({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + return await prisma.envelope.delete({ + where: envelopeWhereInput, }); }; diff --git a/packages/lib/server-only/template/duplicate-template.ts b/packages/lib/server-only/template/duplicate-template.ts index c82a9f70a..71e2996a6 100644 --- a/packages/lib/server-only/template/duplicate-template.ts +++ b/packages/lib/server-only/template/duplicate-template.ts @@ -1,27 +1,30 @@ -import type { Prisma } from '@prisma/client'; +import { DocumentSource, EnvelopeType } from '@prisma/client'; import { omit } from 'remeda'; -import { nanoid } from '@documenso/lib/universal/id'; +import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; -import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { AppError, AppErrorCode } from '../../errors/app-error'; +import { type EnvelopeIdOptions } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; +import { incrementTemplateId } from '../envelope/increment-id'; -export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & { +export type DuplicateTemplateOptions = { userId: number; teamId: number; + id: EnvelopeIdOptions; }; -export const duplicateTemplate = async ({ - templateId, - userId, - teamId, -}: DuplicateTemplateOptions) => { - const template = await prisma.template.findUnique({ - where: { - id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), - }, +export const duplicateTemplate = async ({ id, userId, teamId }: DuplicateTemplateOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findUnique({ + where: envelopeWhereInput, include: { recipients: { select: { @@ -32,79 +35,105 @@ export const duplicateTemplate = async ({ fields: true, }, }, - templateDocumentData: true, - templateMeta: true, - }, - }); - - if (!template) { - throw new Error('Template not found.'); - } - - const documentData = await prisma.documentData.create({ - data: { - type: template.templateDocumentData.type, - data: template.templateDocumentData.data, - initialData: template.templateDocumentData.initialData, - }, - }); - - let templateMeta: Prisma.TemplateCreateArgs['data']['templateMeta'] | undefined = undefined; - - if (template.templateMeta) { - templateMeta = { - create: { - ...omit(template.templateMeta, ['id', 'templateId']), - emailSettings: template.templateMeta.emailSettings || undefined, + envelopeItems: { + include: { + documentData: true, + }, }, - }; + documentMeta: true, + }, + }); + + if (!envelope) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Template not found', + }); } - const duplicatedTemplate = await prisma.template.create({ + const { formattedTemplateId } = await incrementTemplateId(); + + const createdDocumentMeta = await prisma.documentMeta.create({ data: { + ...omit(envelope.documentMeta, ['id']), + emailSettings: envelope.documentMeta.emailSettings || undefined, + }, + }); + + const duplicatedEnvelope = await prisma.envelope.create({ + data: { + id: prefixedId('envelope'), + secondaryId: formattedTemplateId, + type: EnvelopeType.TEMPLATE, userId, teamId, - title: template.title + ' (copy)', - templateDocumentDataId: documentData.id, - authOptions: template.authOptions || undefined, - visibility: template.visibility, - templateMeta, + title: envelope.title + ' (copy)', + documentMetaId: createdDocumentMeta.id, + authOptions: envelope.authOptions || undefined, + visibility: envelope.visibility, + source: DocumentSource.DOCUMENT, // Todo: Migration what to use here. }, include: { recipients: true, }, }); - const recipientsToCreate = template.recipients.map((recipient) => ({ - templateId: duplicatedTemplate.id, - email: recipient.email, - name: recipient.name, - role: recipient.role, - signingOrder: recipient.signingOrder, - token: nanoid(), - fields: { - createMany: { - data: recipient.fields.map((field) => ({ - templateId: duplicatedTemplate.id, - type: field.type, - page: field.page, - positionX: field.positionX, - positionY: field.positionY, - width: field.width, - height: field.height, - customText: '', - inserted: false, - fieldMeta: field.fieldMeta as PrismaJson.FieldMeta, - })), - }, - }, - })); + // Key = original envelope item ID + // Value = duplicated envelope item ID. + const oldEnvelopeItemToNewEnvelopeItemIdMap: Record = {}; - for (const recipientData of recipientsToCreate) { + // Duplicate the envelope items. + await Promise.all( + envelope.envelopeItems.map(async (envelopeItem) => { + const duplicatedDocumentData = await prisma.documentData.create({ + data: { + type: envelopeItem.documentData.type, + data: envelopeItem.documentData.initialData, + initialData: envelopeItem.documentData.initialData, + }, + }); + + const duplicatedEnvelopeItem = await prisma.envelopeItem.create({ + data: { + id: prefixedId('envelope_item'), + title: envelopeItem.title, + envelopeId: duplicatedEnvelope.id, + documentDataId: duplicatedDocumentData.id, + }, + }); + + oldEnvelopeItemToNewEnvelopeItemIdMap[envelopeItem.id] = duplicatedEnvelopeItem.id; + }), + ); + + for (const recipient of envelope.recipients) { await prisma.recipient.create({ - data: recipientData, + data: { + envelopeId: duplicatedEnvelope.id, + email: recipient.email, + name: recipient.name, + role: recipient.role, + signingOrder: recipient.signingOrder, + token: nanoid(), + fields: { + createMany: { + data: recipient.fields.map((field) => ({ + envelopeId: duplicatedEnvelope.id, + envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId], + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: '', + inserted: false, + fieldMeta: field.fieldMeta as PrismaJson.FieldMeta, + })), + }, + }, + }, }); } - return duplicatedTemplate; + return duplicatedEnvelope; }; diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts index a56bc22c4..3a46dd4b4 100644 --- a/packages/lib/server-only/template/find-templates.ts +++ b/packages/lib/server-only/template/find-templates.ts @@ -1,15 +1,16 @@ -import { DocumentVisibility, type Prisma, TeamMemberRole, type Template } from '@prisma/client'; -import { match } from 'ts-pattern'; +import type { TemplateType } from '@prisma/client'; +import { EnvelopeType, type Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; +import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; import { type FindResultResponse } from '../../types/search-params'; import { getMemberRoles } from '../team/get-member-roles'; export type FindTemplatesOptions = { userId: number; teamId: number; - type?: Template['type']; + type?: TemplateType; page?: number; perPage?: number; folderId?: string; @@ -23,46 +24,29 @@ export const findTemplates = async ({ perPage = 10, folderId, }: FindTemplatesOptions) => { - const whereFilter: Prisma.TemplateWhereInput[] = []; + const whereFilter: Prisma.EnvelopeWhereInput[] = []; - if (teamId === undefined) { - whereFilter.push({ userId }); - } + const { teamRole } = await getMemberRoles({ + teamId, + reference: { + type: 'User', + id: userId, + }, + }); - if (teamId !== undefined) { - const { teamRole } = await getMemberRoles({ - teamId, - reference: { - type: 'User', - id: userId, - }, - }); - - whereFilter.push( - { teamId }, - { - OR: [ - match(teamRole) - .with(TeamMemberRole.ADMIN, () => ({ - visibility: { - in: [ - DocumentVisibility.EVERYONE, - DocumentVisibility.MANAGER_AND_ABOVE, - DocumentVisibility.ADMIN, - ], - }, - })) - .with(TeamMemberRole.MANAGER, () => ({ - visibility: { - in: [DocumentVisibility.EVERYONE, DocumentVisibility.MANAGER_AND_ABOVE], - }, - })) - .otherwise(() => ({ visibility: DocumentVisibility.EVERYONE })), - { userId, teamId }, - ], - }, - ); - } + whereFilter.push( + { teamId }, + { + OR: [ + { + visibility: { + in: TEAM_DOCUMENT_VISIBILITY_MAP[teamRole], + }, + }, + { userId, teamId }, + ], + }, + ); if (folderId) { whereFilter.push({ folderId }); @@ -71,9 +55,10 @@ export const findTemplates = async ({ } const [data, count] = await Promise.all([ - prisma.template.findMany({ + prisma.envelope.findMany({ where: { - type, + type: EnvelopeType.TEMPLATE, + templateType: type, AND: whereFilter, }, include: { @@ -85,7 +70,7 @@ export const findTemplates = async ({ }, fields: true, recipients: true, - templateMeta: true, + documentMeta: true, directLink: { select: { token: true, @@ -98,8 +83,10 @@ export const findTemplates = async ({ createdAt: 'desc', }, }), - prisma.template.count({ + prisma.envelope.count({ where: { + type: EnvelopeType.TEMPLATE, + templateType: type, AND: whereFilter, }, }), diff --git a/packages/lib/server-only/template/get-template-by-direct-link-token.ts b/packages/lib/server-only/template/get-template-by-direct-link-token.ts index 04efba70e..f0d62b73f 100644 --- a/packages/lib/server-only/template/get-template-by-direct-link-token.ts +++ b/packages/lib/server-only/template/get-template-by-direct-link-token.ts @@ -1,6 +1,9 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; +import { mapSecondaryIdToTemplateId } from '../../utils/envelope'; export interface GetTemplateByDirectLinkTokenOptions { token: string; @@ -9,8 +12,9 @@ export interface GetTemplateByDirectLinkTokenOptions { export const getTemplateByDirectLinkToken = async ({ token, }: GetTemplateByDirectLinkTokenOptions) => { - const template = await prisma.template.findFirst({ + const envelope = await prisma.envelope.findFirst({ where: { + type: EnvelopeType.TEMPLATE, directLink: { token, enabled: true, @@ -23,21 +27,53 @@ export const getTemplateByDirectLinkToken = async ({ fields: true, }, }, - templateDocumentData: true, - templateMeta: true, + envelopeItems: { + include: { + documentData: true, + }, + }, + documentMeta: true, }, }); - const directLink = template?.directLink; + const directLink = envelope?.directLink; + + // Todo: Envelopes + const firstDocumentData = envelope?.envelopeItems[0]?.documentData; // Doing this to enforce type safety for directLink. - if (!directLink) { + if (!directLink || !firstDocumentData) { throw new AppError(AppErrorCode.NOT_FOUND); } + const recipientsWithMappedFields = envelope.recipients.map((recipient) => ({ + ...recipient, + fields: recipient.fields.map((field) => ({ + ...field, + templateId: mapSecondaryIdToTemplateId(envelope.secondaryId), + documentId: undefined, + })), + })); + + // Backwards compatibility mapping. return { - ...template, + id: mapSecondaryIdToTemplateId(envelope.secondaryId), + type: envelope.templateType, + visibility: envelope.visibility, + externalId: envelope.externalId, + title: envelope.title, + userId: envelope.userId, + teamId: envelope.teamId, + authOptions: envelope.authOptions, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + publicTitle: envelope.publicTitle, + publicDescription: envelope.publicDescription, + folderId: envelope.folderId, + templateDocumentData: firstDocumentData, directLink, - fields: template.recipients.map((recipient) => recipient.fields).flat(), + templateMeta: envelope.documentMeta, + recipients: recipientsWithMappedFields, + fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields), }; }; diff --git a/packages/lib/server-only/template/get-template-by-id.ts b/packages/lib/server-only/template/get-template-by-id.ts index 9a53056c4..e5def0597 100644 --- a/packages/lib/server-only/template/get-template-by-id.ts +++ b/packages/lib/server-only/template/get-template-by-id.ts @@ -1,31 +1,38 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { mapSecondaryIdToTemplateId } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type GetTemplateByIdOptions = { id: number; userId: number; teamId: number; - folderId?: string | null; }; -export const getTemplateById = async ({ - id, - userId, - teamId, - folderId = null, -}: GetTemplateByIdOptions) => { - const template = await prisma.template.findFirst({ - where: { +export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOptions) => { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id, - team: buildTeamWhereQuery({ teamId, userId }), - ...(folderId ? { folderId } : {}), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { directLink: true, - templateDocumentData: true, - templateMeta: true, + documentMeta: true, + envelopeItems: { + select: { + documentData: true, + }, + }, recipients: true, fields: true, user: { @@ -39,11 +46,35 @@ export const getTemplateById = async ({ }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found', }); } - return template; + // Todo: Envelopes + const firstTemplateDocumentData = envelope.envelopeItems[0].documentData; + + if (!firstTemplateDocumentData) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Template document data not found', + }); + } + + // eslint-disable-next-line unused-imports/no-unused-vars + const { envelopeItems, documentMeta, ...rest } = envelope; + + const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId); + + return { + ...rest, + type: envelope.templateType, + templateDocumentData: firstTemplateDocumentData, + templateMeta: envelope.documentMeta, + fields: envelope.fields.map((field) => ({ + ...field, + templateId: legacyTemplateId, + })), + id: mapSecondaryIdToTemplateId(envelope.secondaryId), + }; }; diff --git a/packages/lib/server-only/template/toggle-template-direct-link.ts b/packages/lib/server-only/template/toggle-template-direct-link.ts index 6641f5f5e..c00aca829 100644 --- a/packages/lib/server-only/template/toggle-template-direct-link.ts +++ b/packages/lib/server-only/template/toggle-template-direct-link.ts @@ -1,7 +1,10 @@ +import { EnvelopeType } from '@prisma/client'; + import { prisma } from '@documenso/prisma'; import { AppError, AppErrorCode } from '../../errors/app-error'; -import { buildTeamWhereQuery } from '../../utils/teams'; +import { mapSecondaryIdToTemplateId } from '../../utils/envelope'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type ToggleTemplateDirectLinkOptions = { templateId: number; @@ -16,24 +19,31 @@ export const toggleTemplateDirectLink = async ({ teamId, enabled, }: ToggleTemplateDirectLinkOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput } = await getEnvelopeWhereInput({ + type: EnvelopeType.TEMPLATE, + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { recipients: true, directLink: true, }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found', }); } - const { directLink } = template; + const { directLink } = envelope; if (!directLink) { throw new AppError(AppErrorCode.NOT_FOUND, { @@ -41,13 +51,22 @@ export const toggleTemplateDirectLink = async ({ }); } - return await prisma.templateDirectLink.update({ + const updatedDirectLink = await prisma.templateDirectLink.update({ where: { id: directLink.id, }, data: { - templateId: template.id, + envelopeId: envelope.id, enabled, }, }); + + return { + id: updatedDirectLink.id, + token: updatedDirectLink.token, + createdAt: updatedDirectLink.createdAt, + enabled: updatedDirectLink.enabled, + directTemplateRecipientId: updatedDirectLink.directTemplateRecipientId, + templateId: mapSecondaryIdToTemplateId(envelope.secondaryId), + }; }; diff --git a/packages/lib/server-only/template/update-template.ts b/packages/lib/server-only/template/update-template.ts index 3270c13f9..c70a6fb00 100644 --- a/packages/lib/server-only/template/update-template.ts +++ b/packages/lib/server-only/template/update-template.ts @@ -1,11 +1,19 @@ -import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client'; +import type { Prisma, TemplateType } from '@prisma/client'; +import { + type DocumentMeta, + type DocumentVisibility, + EnvelopeType, + FolderType, +} from '@prisma/client'; import { prisma } from '@documenso/prisma'; +import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; import { createDocumentAuthOptions, extractDocumentAuthMethods } from '../../utils/document-auth'; import { buildTeamWhereQuery } from '../../utils/teams'; +import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; export type UpdateTemplateOptions = { userId: number; @@ -13,13 +21,14 @@ export type UpdateTemplateOptions = { templateId: number; data?: { title?: string; + folderId?: string | null; externalId?: string | null; visibility?: DocumentVisibility; globalAccessAuth?: TDocumentAccessAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[]; publicTitle?: string; publicDescription?: string; - type?: Template['type']; + type?: TemplateType; useLegacyFieldInsertion?: boolean; }; meta?: Partial>; @@ -32,13 +41,20 @@ export const updateTemplate = async ({ meta = {}, data = {}, }: UpdateTemplateOptions) => { - const template = await prisma.template.findFirst({ - where: { + const { envelopeWhereInput, team } = await getEnvelopeWhereInput({ + id: { + type: 'templateId', id: templateId, - team: buildTeamWhereQuery({ teamId, userId }), }, + type: EnvelopeType.TEMPLATE, + userId, + teamId, + }); + + const envelope = await prisma.envelope.findFirst({ + where: envelopeWhereInput, include: { - templateMeta: true, + documentMeta: true, team: { select: { organisationId: true, @@ -52,18 +68,18 @@ export const updateTemplate = async ({ }, }); - if (!template) { + if (!envelope) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found', }); } if (Object.values(data).length === 0 && Object.keys(meta).length === 0) { - return template; + return envelope; } const { documentAuthOption } = extractDocumentAuthMethods({ - documentAuth: template.authOptions, + documentAuth: envelope.authOptions, }); const documentGlobalAccessAuth = documentAuthOption?.globalAccessAuth ?? null; @@ -76,7 +92,7 @@ export const updateTemplate = async ({ data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth; // Check if user has permission to set the global action auth. - if (newGlobalActionAuth.length > 0 && !template.team.organisation.organisationClaim.flags.cfr21) { + if (newGlobalActionAuth.length > 0 && !envelope.team.organisation.organisationClaim.flags.cfr21) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have permission to set the action auth', }); @@ -94,7 +110,7 @@ export const updateTemplate = async ({ const email = await prisma.organisationEmail.findFirst({ where: { id: emailId, - organisationId: template.team.organisationId, + organisationId: envelope.team.organisationId, }, }); @@ -105,32 +121,63 @@ export const updateTemplate = async ({ } } - return await prisma.template.update({ + let folderUpdateQuery: Prisma.FolderUpdateOneWithoutEnvelopesNestedInput | undefined = undefined; + + // Validate folder ID. + if (data.folderId) { + const folder = await prisma.folder.findFirst({ + where: { + id: data.folderId, + team: buildTeamWhereQuery({ + teamId, + userId, + }), + type: FolderType.TEMPLATE, + visibility: { + in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole], + }, + }, + }); + + if (!folder) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Folder not found', + }); + } + + folderUpdateQuery = { + connect: { + id: data.folderId, + }, + }; + } + + // Move template to root folder if folderId is null. + if (data.folderId === null) { + folderUpdateQuery = { + disconnect: true, + }; + } + + return await prisma.envelope.update({ where: { - id: templateId, + id: envelope.id, + type: EnvelopeType.TEMPLATE, }, data: { + templateType: data?.type, title: data?.title, externalId: data?.externalId, - type: data?.type, visibility: data?.visibility, publicDescription: data?.publicDescription, publicTitle: data?.publicTitle, useLegacyFieldInsertion: data?.useLegacyFieldInsertion, + folder: folderUpdateQuery, authOptions, - templateMeta: { - upsert: { - where: { - templateId, - }, - create: { - ...meta, - emailSettings: meta?.emailSettings || undefined, - }, - update: { - ...meta, - emailSettings: meta?.emailSettings || undefined, - }, + documentMeta: { + update: { + ...meta, + emailSettings: meta?.emailSettings || undefined, }, }, }, diff --git a/packages/lib/server-only/user/delete-user.ts b/packages/lib/server-only/user/delete-user.ts index 414e13a46..ccc7f7476 100644 --- a/packages/lib/server-only/user/delete-user.ts +++ b/packages/lib/server-only/user/delete-user.ts @@ -1,4 +1,4 @@ -import { DocumentStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -25,9 +25,10 @@ export const deleteUser = async ({ id }: DeleteUserOptions) => { const serviceAccount = await deletedAccountServiceAccount(); // TODO: Send out cancellations for all pending docs - await prisma.document.updateMany({ + await prisma.envelope.updateMany({ where: { userId: user.id, + type: EnvelopeType.DOCUMENT, status: { in: [DocumentStatus.PENDING, DocumentStatus.REJECTED, DocumentStatus.COMPLETED], }, diff --git a/packages/lib/server-only/user/get-all-users.ts b/packages/lib/server-only/user/get-all-users.ts index 69e07813d..fd3c560db 100644 --- a/packages/lib/server-only/user/get-all-users.ts +++ b/packages/lib/server-only/user/get-all-users.ts @@ -1,4 +1,4 @@ -import { Prisma } from '@prisma/client'; +import { EnvelopeType, Prisma } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -34,12 +34,20 @@ export const findUsers = async ({ const [users, count] = await Promise.all([ prisma.user.findMany({ - include: { - documents: { + select: { + _count: { select: { - id: true, + envelopes: { + where: { + type: EnvelopeType.DOCUMENT, + }, + }, }, }, + id: true, + name: true, + email: true, + roles: true, }, where: whereClause, skip: Math.max(page - 1, 0) * perPage, @@ -51,7 +59,10 @@ export const findUsers = async ({ ]); return { - users, + users: users.map((user) => ({ + ...user, + documentCount: user._count.envelopes, + })), totalPages: Math.ceil(count / perPage), }; }; diff --git a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts index 73916c491..47e3e7c53 100644 --- a/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts +++ b/packages/lib/server-only/webhooks/trigger/generate-sample-data.ts @@ -410,7 +410,6 @@ export const generateSampleWebhookPayload = ( externalId: null, userId: 3, status: DocumentStatus.PENDING, - documentDataId: 'cm6exvn93006hi02ru90a265a', documentMeta: { ...basePayload.documentMeta, id: 'cm6exvn96006ji02rqvzjvwoy', diff --git a/packages/lib/server-only/webhooks/zapier/list-documents.ts b/packages/lib/server-only/webhooks/zapier/list-documents.ts index 73077d40f..9aed6a878 100644 --- a/packages/lib/server-only/webhooks/zapier/list-documents.ts +++ b/packages/lib/server-only/webhooks/zapier/list-documents.ts @@ -1,8 +1,8 @@ -import type { Webhook } from '@prisma/client'; +import { EnvelopeType, type Webhook } from '@prisma/client'; -import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; -import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; +import { prisma } from '@documenso/prisma'; +import { mapSecondaryIdToDocumentId } from '../../../utils/envelope'; import { getWebhooksByTeamId } from '../get-webhooks-by-team-id'; import { validateApiToken } from './validateApiToken'; @@ -14,48 +14,73 @@ export const listDocumentsHandler = async (req: Request) => { return new Response('Unauthorized', { status: 401 }); } - const { user, userId, teamId } = await validateApiToken({ authorization }); + const { user, teamId } = await validateApiToken({ authorization }); - let allWebhooks: Webhook[] = []; + const allWebhooks: Webhook[] = await getWebhooksByTeamId(teamId, user.id); - const documents = await findDocuments({ - userId: userId ?? user.id, - teamId, - perPage: 1, - }); - - const recipients = await getRecipientsForDocument({ - documentId: documents.data[0].id, - userId: userId ?? user.id, - teamId, - }); - - allWebhooks = await getWebhooksByTeamId(teamId, user.id); - - if (documents && documents.data.length > 0 && allWebhooks.length > 0 && recipients.length > 0) { - const testWebhook = { - event: allWebhooks[0].eventTriggers.toString(), - createdAt: allWebhooks[0].createdAt, - webhookEndpoint: allWebhooks[0].webhookUrl, - payload: { - id: documents.data[0].id, - userId: documents.data[0].userId, - title: documents.data[0].title, - status: documents.data[0].status, - documentDataId: documents.data[0].documentDataId, - createdAt: documents.data[0].createdAt, - updatedAt: documents.data[0].updatedAt, - completedAt: documents.data[0].completedAt, - deletedAt: documents.data[0].deletedAt, - teamId: documents.data[0].teamId, - Recipient: recipients, + const document = await prisma.envelope.findFirst({ + where: { + userId: user.id, + teamId, + type: EnvelopeType.DOCUMENT, + }, + include: { + envelopeItems: { + include: { + documentData: true, + }, }, - }; + recipients: true, + }, + }); - return Response.json([testWebhook]); + if ( + !document || + document.envelopeItems.length === 0 || + document.recipients.length === 0 || + allWebhooks.length === 0 + ) { + return Response.json([]); } - return Response.json([]); + const legacyDocumentId = mapSecondaryIdToDocumentId(document.secondaryId); + + const testWebhook = { + event: allWebhooks[0].eventTriggers.toString(), + createdAt: allWebhooks[0].createdAt, + webhookEndpoint: allWebhooks[0].webhookUrl, + payload: { + id: legacyDocumentId, + userId: document.userId, + title: document.title, + status: document.status, + createdAt: document.createdAt, + updatedAt: document.updatedAt, + completedAt: document.completedAt, + deletedAt: document.deletedAt, + teamId: document.teamId, + Recipient: document.recipients.map((recipient) => ({ + id: recipient.id, + documentId: legacyDocumentId, + templateId: null, + email: recipient.email, + name: recipient.name, + token: recipient.token, + documentDeletedAt: recipient.documentDeletedAt, + expired: recipient.expired, + signedAt: recipient.signedAt, + authOptions: recipient.authOptions, + signingOrder: recipient.signingOrder, + rejectionReason: recipient.rejectionReason, + role: recipient.role, + readStatus: recipient.readStatus, + signingStatus: recipient.signingStatus, + sendStatus: recipient.sendStatus, + })), + }, + }; + + return Response.json([testWebhook]); } catch (err) { console.error(err); diff --git a/packages/lib/types/document-audit-logs.ts b/packages/lib/types/document-audit-logs.ts index c604686de..9c6529c6e 100644 --- a/packages/lib/types/document-audit-logs.ts +++ b/packages/lib/types/document-audit-logs.ts @@ -642,7 +642,7 @@ export const ZDocumentAuditLogEventDocumentMovedToTeamSchema = z.object({ export const ZDocumentAuditLogBaseSchema = z.object({ id: z.string(), createdAt: z.date(), - documentId: z.number(), + envelopeId: z.string(), name: z.string().optional().nullable(), email: z.string().optional().nullable(), userId: z.number().optional().nullable(), diff --git a/packages/lib/types/document.ts b/packages/lib/types/document.ts index b42801b72..1476faf8e 100644 --- a/packages/lib/types/document.ts +++ b/packages/lib/types/document.ts @@ -1,11 +1,11 @@ -import type { z } from 'zod'; +import { z } from 'zod'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema'; -import { DocumentSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; +import { LegacyDocumentSchema } from '@documenso/prisma/types/document-legacy-schema'; import { ZFieldSchema } from './field'; import { ZRecipientLiteSchema } from './recipient'; @@ -15,7 +15,7 @@ import { ZRecipientLiteSchema } from './recipient'; * * Mainly used for returning a single document from the API. */ -export const ZDocumentSchema = DocumentSchema.pick({ +export const ZDocumentSchema = LegacyDocumentSchema.pick({ visibility: true, status: true, source: true, @@ -25,15 +25,17 @@ export const ZDocumentSchema = DocumentSchema.pick({ authOptions: true, formValues: true, title: true, - documentDataId: true, createdAt: true, updatedAt: true, completedAt: true, deletedAt: true, teamId: true, - templateId: true, folderId: true, }).extend({ + // Which "Template" the document was created from. Legacy field for backwards compatibility. + // The actual field is now called `createdFromDocumentId`. + templateId: z.number().nullish(), + // Todo: Maybe we want to alter this a bit since this returns a lot of data. documentData: DocumentDataSchema.pick({ type: true, @@ -48,9 +50,7 @@ export const ZDocumentSchema = DocumentSchema.pick({ subject: true, message: true, timezone: true, - password: true, dateFormat: true, - documentId: true, redirectUrl: true, typedSignatureEnabled: true, uploadSignatureEnabled: true, @@ -82,7 +82,7 @@ export type TDocument = z.infer; /** * A lite version of the document response schema without relations. */ -export const ZDocumentLiteSchema = DocumentSchema.pick({ +export const ZDocumentLiteSchema = LegacyDocumentSchema.pick({ visibility: true, status: true, source: true, @@ -92,15 +92,17 @@ export const ZDocumentLiteSchema = DocumentSchema.pick({ authOptions: true, formValues: true, title: true, - documentDataId: true, createdAt: true, updatedAt: true, completedAt: true, deletedAt: true, teamId: true, - templateId: true, folderId: true, useLegacyFieldInsertion: true, +}).extend({ + // Which "Template" the document was created from. Legacy field for backwards compatibility. + // The actual field is now called `createdFromDocumentId`. + templateId: z.number().nullish(), }); export type TDocumentLite = z.infer; @@ -108,7 +110,7 @@ export type TDocumentLite = z.infer; /** * A version of the document response schema when returning multiple documents at once from a single API endpoint. */ -export const ZDocumentManySchema = DocumentSchema.pick({ +export const ZDocumentManySchema = LegacyDocumentSchema.pick({ visibility: true, status: true, source: true, @@ -118,16 +120,18 @@ export const ZDocumentManySchema = DocumentSchema.pick({ authOptions: true, formValues: true, title: true, - documentDataId: true, createdAt: true, updatedAt: true, completedAt: true, deletedAt: true, teamId: true, - templateId: true, folderId: true, useLegacyFieldInsertion: true, }).extend({ + // Which "Template" the document was created from. Legacy field for backwards compatibility. + // The actual field is now called `createdFromDocumentId`. + templateId: z.number().nullish(), + user: UserSchema.pick({ id: true, name: true, diff --git a/packages/lib/types/field.ts b/packages/lib/types/field.ts index 5b3839f0c..98b1a9847 100644 --- a/packages/lib/types/field.ts +++ b/packages/lib/types/field.ts @@ -15,11 +15,11 @@ import { FieldSchema } from '@documenso/prisma/generated/zod/modelSchema/FieldSc * - ./templates.ts */ export const ZFieldSchema = FieldSchema.pick({ + envelopeId: true, + envelopeItemId: true, type: true, id: true, secondaryId: true, - documentId: true, - templateId: true, recipientId: true, page: true, positionX: true, @@ -29,6 +29,10 @@ export const ZFieldSchema = FieldSchema.pick({ customText: true, inserted: true, fieldMeta: true, +}).extend({ + // Todo: Migration - Backwards compatibility. + documentId: z.number().nullish(), + templateId: z.number().nullish(), }); export const ZFieldPageNumberSchema = z diff --git a/packages/lib/types/recipient.ts b/packages/lib/types/recipient.ts index e46681f44..ecce9d649 100644 --- a/packages/lib/types/recipient.ts +++ b/packages/lib/types/recipient.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { RecipientSchema } from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema'; import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; @@ -10,13 +12,12 @@ import { ZFieldSchema } from './field'; * Mainly used for returning a single recipient from the API. */ export const ZRecipientSchema = RecipientSchema.pick({ + envelopeId: true, role: true, readStatus: true, signingStatus: true, sendStatus: true, id: true, - documentId: true, - templateId: true, email: true, name: true, token: true, @@ -28,19 +29,22 @@ export const ZRecipientSchema = RecipientSchema.pick({ rejectionReason: true, }).extend({ fields: ZFieldSchema.array(), + + // Todo: Migration - Backwards compatibility. + documentId: z.number().nullish(), + templateId: z.number().nullish(), }); /** * A lite version of the recipient response schema without relations. */ export const ZRecipientLiteSchema = RecipientSchema.pick({ + envelopeId: true, role: true, readStatus: true, signingStatus: true, sendStatus: true, id: true, - documentId: true, - templateId: true, email: true, name: true, token: true, @@ -50,19 +54,22 @@ export const ZRecipientLiteSchema = RecipientSchema.pick({ authOptions: true, signingOrder: true, rejectionReason: true, +}).extend({ + // Todo: Migration - Backwards compatibility. + documentId: z.number().nullish(), + templateId: z.number().nullish(), }); /** * A version of the recipient response schema when returning multiple recipients at once from a single API endpoint. */ export const ZRecipientManySchema = RecipientSchema.pick({ + envelopeId: true, role: true, readStatus: true, signingStatus: true, sendStatus: true, id: true, - documentId: true, - templateId: true, email: true, name: true, token: true, @@ -83,4 +90,8 @@ export const ZRecipientManySchema = RecipientSchema.pick({ id: true, url: true, }).nullable(), + + // Todo: Migration - Backwards compatibility. + documentId: z.number().nullish(), + templateId: z.number().nullish(), }); diff --git a/packages/lib/types/template.ts b/packages/lib/types/template.ts index de6708c7c..00cc83370 100644 --- a/packages/lib/types/template.ts +++ b/packages/lib/types/template.ts @@ -5,8 +5,8 @@ import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/ import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; -import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; +import { TemplateSchema } from '@documenso/prisma/types/template-legacy-schema'; import { ZFieldSchema } from './field'; import { ZRecipientLiteSchema } from './recipient'; @@ -25,7 +25,6 @@ export const ZTemplateSchema = TemplateSchema.pick({ userId: true, teamId: true, authOptions: true, - templateDocumentDataId: true, createdAt: true, updatedAt: true, publicTitle: true, @@ -51,13 +50,12 @@ export const ZTemplateSchema = TemplateSchema.pick({ drawSignatureEnabled: true, allowDictateNextSigner: true, distributionMethod: true, - templateId: true, redirectUrl: true, language: true, emailSettings: true, emailId: true, emailReplyTo: true, - }).nullable(), + }), directLink: TemplateDirectLinkSchema.nullable(), user: UserSchema.pick({ id: true, @@ -94,7 +92,6 @@ export const ZTemplateLiteSchema = TemplateSchema.pick({ userId: true, teamId: true, authOptions: true, - templateDocumentDataId: true, createdAt: true, updatedAt: true, publicTitle: true, @@ -103,6 +100,8 @@ export const ZTemplateLiteSchema = TemplateSchema.pick({ useLegacyFieldInsertion: true, }); +export type TTemplateLite = z.infer; + /** * A version of the template response schema when returning multiple template at once from a single API endpoint. */ @@ -115,7 +114,6 @@ export const ZTemplateManySchema = TemplateSchema.pick({ userId: true, teamId: true, authOptions: true, - templateDocumentDataId: true, createdAt: true, updatedAt: true, publicTitle: true, @@ -138,3 +136,5 @@ export const ZTemplateManySchema = TemplateSchema.pick({ enabled: true, }).nullable(), }); + +export type TTemplateMany = z.infer; diff --git a/packages/lib/types/webhook-payload.ts b/packages/lib/types/webhook-payload.ts index ffd6c9961..31343656e 100644 --- a/packages/lib/types/webhook-payload.ts +++ b/packages/lib/types/webhook-payload.ts @@ -1,10 +1,11 @@ -import type { Document, DocumentMeta, Recipient, WebhookTriggerEvents } from '@prisma/client'; +import type { DocumentMeta, Envelope, Recipient, WebhookTriggerEvents } from '@prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder, DocumentSource, DocumentStatus, DocumentVisibility, + EnvelopeType, ReadStatus, RecipientRole, SendStatus, @@ -12,6 +13,8 @@ import { } from '@prisma/client'; import { z } from 'zod'; +import { mapSecondaryIdToDocumentId, mapSecondaryIdToTemplateId } from '../utils/envelope'; + /** * Schema for recipient data in webhook payloads. */ @@ -42,7 +45,6 @@ export const ZWebhookDocumentMetaSchema = z.object({ subject: z.string().nullable(), message: z.string().nullable(), timezone: z.string(), - password: z.string().nullable(), dateFormat: z.string(), redirectUrl: z.string().nullable(), signingOrder: z.nativeEnum(DocumentSigningOrder), @@ -67,7 +69,6 @@ export const ZWebhookDocumentSchema = z.object({ visibility: z.nativeEnum(DocumentVisibility), title: z.string(), status: z.nativeEnum(DocumentStatus), - documentDataId: z.string(), createdAt: z.date(), updatedAt: z.date(), completedAt: z.date().nullable(), @@ -94,16 +95,54 @@ export type WebhookPayload = { webhookEndpoint: string; }; -export const mapDocumentToWebhookDocumentPayload = ( - document: Document & { +export const mapEnvelopeToWebhookDocumentPayload = ( + envelope: Envelope & { recipients: Recipient[]; documentMeta: DocumentMeta | null; }, ): TWebhookDocument => { - const { recipients, documentMeta, ...trimmedDocument } = document; + const { recipients: rawRecipients, documentMeta } = envelope; + + const legacyId = + envelope.type === EnvelopeType.DOCUMENT + ? mapSecondaryIdToDocumentId(envelope.secondaryId) + : mapSecondaryIdToTemplateId(envelope.secondaryId); + + const mappedRecipients = rawRecipients.map((recipient) => ({ + id: recipient.id, + documentId: envelope.type === EnvelopeType.DOCUMENT ? legacyId : null, + templateId: envelope.type === EnvelopeType.TEMPLATE ? legacyId : null, + email: recipient.email, + name: recipient.name, + token: recipient.token, + documentDeletedAt: recipient.documentDeletedAt, + expired: recipient.expired, + signedAt: recipient.signedAt, + authOptions: recipient.authOptions, + signingOrder: recipient.signingOrder, + rejectionReason: recipient.rejectionReason, + role: recipient.role, + readStatus: recipient.readStatus, + signingStatus: recipient.signingStatus, + sendStatus: recipient.sendStatus, + })); return { - ...trimmedDocument, + id: legacyId, + externalId: envelope.externalId, + userId: envelope.userId, + authOptions: envelope.authOptions, + formValues: envelope.formValues, + visibility: envelope.visibility, + title: envelope.title, + status: envelope.status, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + completedAt: envelope.completedAt, + deletedAt: envelope.deletedAt, + teamId: envelope.teamId, + templateId: envelope.templateId, + source: envelope.source, documentMeta: documentMeta ? { ...documentMeta, @@ -112,7 +151,7 @@ export const mapDocumentToWebhookDocumentPayload = ( dateFormat: 'yyyy-MM-dd hh:mm a', } : null, - Recipient: recipients, - recipients, + Recipient: mappedRecipients, + recipients: mappedRecipients, }; }; diff --git a/packages/lib/universal/id.ts b/packages/lib/universal/id.ts index d56d923fd..1c34200b3 100644 --- a/packages/lib/universal/id.ts +++ b/packages/lib/universal/id.ts @@ -11,6 +11,10 @@ export const prefixedId = (prefix: string, length = 16) => { }; type DatabaseIdPrefix = + | 'document' + | 'template' + | 'envelope' + | 'envelope_item' | 'email_domain' | 'org' | 'org_email' diff --git a/packages/lib/utils/document-audit-logs.ts b/packages/lib/utils/document-audit-logs.ts index f5a14478e..dc7788e83 100644 --- a/packages/lib/utils/document-audit-logs.ts +++ b/packages/lib/utils/document-audit-logs.ts @@ -22,7 +22,7 @@ import { ZRecipientAuthOptionsSchema } from '../types/document-auth'; import type { ApiRequestMetadata, RequestMetadata } from '../universal/extract-request-metadata'; type CreateDocumentAuditLogDataOptions = { - documentId: number; + envelopeId: string; type: T; data: Extract['data']; user?: { email?: string | null; id?: number | null; name?: string | null } | null; @@ -32,13 +32,13 @@ type CreateDocumentAuditLogDataOptions = { export type CreateDocumentAuditLogDataResponse = Pick< DocumentAuditLog, - 'type' | 'ipAddress' | 'userAgent' | 'email' | 'userId' | 'name' | 'documentId' + 'type' | 'ipAddress' | 'userAgent' | 'email' | 'userId' | 'name' | 'envelopeId' > & { data: TDocumentAuditLog['data']; }; export const createDocumentAuditLogData = ({ - documentId, + envelopeId, type, data, user, @@ -62,7 +62,7 @@ export const createDocumentAuditLogData = ( return { type, data, - documentId, + envelopeId, userId, email, name, @@ -203,7 +203,6 @@ export const diffDocumentMetaChanges = ( const oldMessage = oldData?.message ?? ''; const oldSubject = oldData?.subject ?? ''; const oldTimezone = oldData?.timezone ?? ''; - const oldPassword = oldData?.password ?? null; const oldRedirectUrl = oldData?.redirectUrl ?? ''; const oldEmailId = oldData?.emailId || null; const oldEmailReplyTo = oldData?.emailReplyTo || null; @@ -258,12 +257,6 @@ export const diffDocumentMetaChanges = ( }); } - if (oldPassword !== newData.password) { - diffs.push({ - type: DOCUMENT_META_DIFF_TYPE.PASSWORD, - }); - } - if (oldEmailId !== newEmailId) { diffs.push({ type: DOCUMENT_META_DIFF_TYPE.EMAIL_ID, diff --git a/packages/lib/utils/document-auth.ts b/packages/lib/utils/document-auth.ts index 716e3fa11..c426eb54b 100644 --- a/packages/lib/utils/document-auth.ts +++ b/packages/lib/utils/document-auth.ts @@ -1,4 +1,4 @@ -import type { Document, Recipient } from '@prisma/client'; +import type { Envelope, Recipient } from '@prisma/client'; import type { TDocumentAuthOptions, @@ -10,7 +10,7 @@ import { DocumentAuth } from '../types/document-auth'; import { ZDocumentAuthOptionsSchema, ZRecipientAuthOptionsSchema } from '../types/document-auth'; type ExtractDocumentAuthMethodsOptions = { - documentAuth: Document['authOptions']; + documentAuth: Envelope['authOptions']; recipientAuth?: Recipient['authOptions']; }; diff --git a/packages/lib/utils/document-visibility.ts b/packages/lib/utils/document-visibility.ts deleted file mode 100644 index 270b57870..000000000 --- a/packages/lib/utils/document-visibility.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DocumentVisibility, TeamMemberRole } from '@prisma/client'; - -export const determineDocumentVisibility = ( - globalVisibility: DocumentVisibility | null | undefined, - userRole: TeamMemberRole, -): DocumentVisibility => { - if (globalVisibility) { - return globalVisibility; - } - - if (userRole === TeamMemberRole.ADMIN) { - return DocumentVisibility.ADMIN; - } - - if (userRole === TeamMemberRole.MANAGER) { - return DocumentVisibility.MANAGER_AND_ABOVE; - } - - return DocumentVisibility.EVERYONE; -}; diff --git a/packages/lib/utils/document.ts b/packages/lib/utils/document.ts index db2310a8f..d6f0ebf92 100644 --- a/packages/lib/utils/document.ts +++ b/packages/lib/utils/document.ts @@ -1,10 +1,19 @@ -import type { Document, DocumentMeta, OrganisationGlobalSettings } from '@prisma/client'; +import type { + DocumentMeta, + Envelope, + OrganisationGlobalSettings, + Recipient, + Team, + User, +} from '@prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones'; +import type { TDocumentLite, TDocumentMany } from '../types/document'; import { DEFAULT_DOCUMENT_EMAIL_SETTINGS } from '../types/document-email'; +import { mapSecondaryIdToDocumentId } from './envelope'; -export const isDocumentCompleted = (document: Pick | DocumentStatus) => { +export const isDocumentCompleted = (document: Pick | DocumentStatus) => { const status = typeof document === 'string' ? document : document.status; return status === DocumentStatus.COMPLETED || status === DocumentStatus.REJECTED; @@ -36,7 +45,6 @@ export const extractDerivedDocumentMeta = ( dateFormat: meta.dateFormat || settings.documentDateFormat, message: meta.message || null, subject: meta.subject || null, - password: meta.password || null, redirectUrl: meta.redirectUrl || null, signingOrder: meta.signingOrder || DocumentSigningOrder.PARALLEL, @@ -53,5 +61,81 @@ export const extractDerivedDocumentMeta = ( emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo, emailSettings: meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS, - } satisfies Omit; + } satisfies Omit; +}; + +/** + * Map an envelope to a legacy document lite response entity. + * + * Do not use spread operator here to avoid unexpected behavior. + */ +export const mapEnvelopeToDocumentLite = (envelope: Envelope): TDocumentLite => { + const documentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + + return { + id: documentId, // Use legacy ID. + visibility: envelope.visibility, + status: envelope.status, + source: envelope.source, + externalId: envelope.externalId, + userId: envelope.userId, + authOptions: envelope.authOptions, + formValues: envelope.formValues, + title: envelope.title, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + completedAt: envelope.completedAt, + deletedAt: envelope.deletedAt, + teamId: envelope.teamId, + folderId: envelope.folderId, + useLegacyFieldInsertion: envelope.useLegacyFieldInsertion, + templateId: envelope.templateId, + }; +}; + +type MapEnvelopeToDocumentManyOptions = Envelope & { + user: Pick; + team: Pick; + recipients: Recipient[]; +}; + +/** + * Map an envelope to a legacy document many response entity. + * + * Do not use spread operator here to avoid unexpected behavior. + */ +export const mapEnvelopesToDocumentMany = ( + envelope: MapEnvelopeToDocumentManyOptions, +): TDocumentMany => { + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + + return { + id: legacyDocumentId, // Use legacy ID. + visibility: envelope.visibility, + status: envelope.status, + source: envelope.source, + externalId: envelope.externalId, + userId: envelope.userId, + authOptions: envelope.authOptions, + formValues: envelope.formValues, + title: envelope.title, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + completedAt: envelope.completedAt, + deletedAt: envelope.deletedAt, + teamId: envelope.teamId, + folderId: envelope.folderId, + useLegacyFieldInsertion: envelope.useLegacyFieldInsertion, + templateId: envelope.templateId, + user: { + id: envelope.userId, + name: envelope.user.name, + email: envelope.user.email, + }, + team: { + id: envelope.teamId, + url: envelope.team.url, + }, + recipients: envelope.recipients, + }; }; diff --git a/packages/lib/utils/envelope.ts b/packages/lib/utils/envelope.ts new file mode 100644 index 000000000..e007d7703 --- /dev/null +++ b/packages/lib/utils/envelope.ts @@ -0,0 +1,141 @@ +import { EnvelopeType } from '@prisma/client'; +import { match } from 'ts-pattern'; +import { z } from 'zod'; + +import { AppError, AppErrorCode } from '../errors/app-error'; + +const envelopeDocumentPrefixId = 'document'; +const envelopeTemplatePrefixId = 'template'; + +const ZDocumentIdSchema = z.string().regex(/^document_\d+$/); +const ZTemplateIdSchema = z.string().regex(/^template_\d+$/); +const ZEnvelopeIdSchema = z.string().regex(/^envelope_.{2,}$/); + +export type EnvelopeIdOptions = + | { + type: 'envelopeId'; + id: string; + } + | { + type: 'documentId'; + id: number; + } + | { + type: 'templateId'; + id: number; + }; + +/** + * Parses an unknown document or template ID. + * + * This is UNSAFE because does not validate access, it just builds the query for ID and TYPE. + */ +export const unsafeBuildEnvelopeIdQuery = ( + options: EnvelopeIdOptions, + expectedEnvelopeType: EnvelopeType | null, +) => { + return match(options) + .with({ type: 'envelopeId' }, (value) => { + const parsed = ZEnvelopeIdSchema.safeParse(value.id); + + if (!parsed.success) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Invalid envelope ID', + }); + } + + if (expectedEnvelopeType) { + return { + id: value.id, + type: expectedEnvelopeType, + }; + } + + return { + id: value.id, + }; + }) + .with({ type: 'documentId' }, (value) => { + if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.DOCUMENT) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Invalid document ID', + }); + } + + return { + type: EnvelopeType.DOCUMENT, + secondaryId: mapDocumentIdToSecondaryId(value.id), + }; + }) + .with({ type: 'templateId' }, (value) => { + if (expectedEnvelopeType && expectedEnvelopeType !== EnvelopeType.TEMPLATE) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Invalid template ID', + }); + } + + return { + type: EnvelopeType.TEMPLATE, + secondaryId: mapTemplateIdToSecondaryId(value.id), + }; + }) + .exhaustive(); +}; + +/** + * Maps a legacy document ID number to an envelope secondary ID. + * + * @returns The formatted envelope secondary ID (document_123) + */ +export const mapDocumentIdToSecondaryId = (documentId: number) => { + return `${envelopeDocumentPrefixId}_${documentId}`; +}; + +/** + * Maps a legacy template ID number to an envelope secondary ID. + * + * @returns The formatted envelope secondary ID (template_123) + */ +export const mapTemplateIdToSecondaryId = (templateId: number) => { + return `${envelopeTemplatePrefixId}_${templateId}`; +}; + +/** + * Maps an envelope secondary ID to a legacy document ID number. + * + * Throws an error if the secondary ID is not a document ID. + * + * @param secondaryId The envelope secondary ID (document_123) + * @returns The legacy document ID number (123) + */ +export const mapSecondaryIdToDocumentId = (secondaryId: string) => { + const parsed = ZDocumentIdSchema.safeParse(secondaryId); + + if (!parsed.success) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Invalid document ID', + }); + } + + return parseInt(parsed.data.split('_')[1]); +}; + +/** + * Maps an envelope secondary ID to a legacy template ID number. + * + * Throws an error if the secondary ID is not a template ID. + * + * @param secondaryId The envelope secondary ID (template_123) + * @returns The legacy template ID number (123) + */ +export const mapSecondaryIdToTemplateId = (secondaryId: string) => { + const parsed = ZTemplateIdSchema.safeParse(secondaryId); + + if (!parsed.success) { + throw new AppError(AppErrorCode.INVALID_BODY, { + message: 'Invalid template ID', + }); + } + + return parseInt(parsed.data.split('_')[1]); +}; diff --git a/packages/lib/utils/mask-recipient-tokens-for-document.ts b/packages/lib/utils/mask-recipient-tokens-for-document.ts index ddadb4f3f..4a344ca3e 100644 --- a/packages/lib/utils/mask-recipient-tokens-for-document.ts +++ b/packages/lib/utils/mask-recipient-tokens-for-document.ts @@ -1,14 +1,14 @@ import type { User } from '@prisma/client'; -import type { DocumentWithRecipients } from '@documenso/prisma/types/document-with-recipient'; +import type { EnvelopeWithRecipients } from '@documenso/prisma/types/document-with-recipient'; -export type MaskRecipientTokensForDocumentOptions = { +export type MaskRecipientTokensForDocumentOptions = { document: T; - user?: Pick; + user?: Pick; token?: string; }; -export const maskRecipientTokensForDocument = ({ +export const maskRecipientTokensForDocument = ({ document, user, token, diff --git a/packages/lib/utils/teams.ts b/packages/lib/utils/teams.ts index 3665baf33..867f8ef93 100644 --- a/packages/lib/utils/teams.ts +++ b/packages/lib/utils/teams.ts @@ -1,4 +1,9 @@ -import type { OrganisationGlobalSettings, Prisma, TeamGlobalSettings } from '@prisma/client'; +import type { + DocumentVisibility, + OrganisationGlobalSettings, + Prisma, + TeamGlobalSettings, +} from '@prisma/client'; import type { TeamGroup } from '@documenso/prisma/generated/types'; import type { TeamMemberRole } from '@documenso/prisma/generated/types'; @@ -6,6 +11,7 @@ import type { TeamMemberRole } from '@documenso/prisma/generated/types'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; import { LOWEST_TEAM_ROLE, + TEAM_DOCUMENT_VISIBILITY_MAP, TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP, } from '../constants/teams'; @@ -48,6 +54,17 @@ export const canExecuteTeamAction = ( return TEAM_MEMBER_ROLE_PERMISSIONS_MAP[action].some((i) => i === role); }; +/** + * Determines whether a team role can access the visibility of a document. + * + * @param action The action the user is trying to execute. + * @param role The current role of the user. + * @returns Whether the user can execute the action. + */ +export const canAccessTeamDocument = (role: TeamMemberRole, visibility: DocumentVisibility) => { + return TEAM_DOCUMENT_VISIBILITY_MAP[role].some((i) => i === visibility); +}; + /** * Compares the provided `currentUserRole` with the provided `roleToCheck` to determine * whether the `currentUserRole` has permission to modify the `roleToCheck`. @@ -106,7 +123,7 @@ export const extractTeamSignatureSettings = ( return signatureTypes; }; -type BuildTeamWhereQueryOptions = { +export type BuildTeamWhereQueryOptions = { teamId: number | undefined; userId: number; roles?: TeamMemberRole[]; diff --git a/packages/lib/utils/templates.ts b/packages/lib/utils/templates.ts index edac17011..d6ba04642 100644 --- a/packages/lib/utils/templates.ts +++ b/packages/lib/utils/templates.ts @@ -1,6 +1,9 @@ -import type { Recipient } from '@prisma/client'; +import type { Envelope } from '@prisma/client'; +import { type Recipient } from '@prisma/client'; import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app'; +import type { TTemplateLite } from '../types/template'; +import { mapSecondaryIdToTemplateId } from './envelope'; export const formatDirectTemplatePath = (token: string) => { return `${NEXT_PUBLIC_WEBAPP_URL()}/d/${token}`; @@ -42,3 +45,24 @@ export const generateAvaliableRecipientPlaceholder = (currentRecipients: Recipie return recipientPlaceholder; }; + +export const mapEnvelopeToTemplateLite = (envelope: Envelope): TTemplateLite => { + const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId); + + return { + id: legacyTemplateId, + type: envelope.templateType, + visibility: envelope.visibility, + externalId: envelope.externalId, + title: envelope.title, + userId: envelope.userId, + teamId: envelope.teamId, + authOptions: envelope.authOptions, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + publicTitle: envelope.publicTitle, + publicDescription: envelope.publicDescription, + folderId: envelope.folderId, + useLegacyFieldInsertion: envelope.useLegacyFieldInsertion, + }; +}; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 6e6453089..9f7cb1d0d 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -64,8 +64,7 @@ model User { twoFactorBackupCodes String? folders Folder[] - documents Document[] - templates Template[] + envelopes Envelope[] verificationTokens VerificationToken[] apiTokens ApiToken[] @@ -354,8 +353,7 @@ model Folder { pinned Boolean @default(false) parentId String? parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade) - documents Document[] - templates Template[] + envelopes Envelope[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt subfolders Folder[] @relation("FolderToFolder") @@ -368,53 +366,82 @@ model Folder { @@index([type]) } -/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"]) -model Document { - id Int @id @default(autoincrement()) - qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.") - externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.") +enum EnvelopeType { + DOCUMENT + TEMPLATE +} +model Envelope { + id String @id // document_asdfasdfsadf template_123asdf + secondaryId String @unique // document_456456456 template_45465465 + externalId String? /// @zod.string.describe("A custom external ID you can use to identify the document.") + + type EnvelopeType + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + completedAt DateTime? + deletedAt DateTime? + + title String + status DocumentStatus @default(DRAFT) + source DocumentSource + qrToken String? /// @zod.string.describe("The token for viewing the document using the QR code on the certificate.") + + useLegacyFieldInsertion Boolean @default(false) + + envelopeItems EnvelopeItem[] + recipients Recipient[] + fields Field[] + shareLinks DocumentShareLink[] + auditLogs DocumentAuditLog[] + + // Envelope settings + authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema) + formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema) + visibility DocumentVisibility @default(EVERYONE) + + // Template specific fields. + templateType TemplateType @default(PRIVATE) + publicTitle String @default("") + publicDescription String @default("") + directLink TemplateDirectLink? + templateId Int? // Todo: Migrate from templateId -> This @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + + // Relations userId Int /// @zod.number.describe("The ID of the user that created this document.") user User @relation(fields: [userId], references: [id], onDelete: Cascade) teamId Int team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema) - formValues Json? /// [DocumentFormValues] @zod.custom.use(ZDocumentFormValuesSchema) - visibility DocumentVisibility @default(EVERYONE) - title String - status DocumentStatus @default(DRAFT) - recipients Recipient[] - fields Field[] - shareLinks DocumentShareLink[] + folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + folderId String? + + documentMetaId String @unique + documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id]) +} + +/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"]) +model EnvelopeItem { + id String @id + + title String + documentDataId String - documentMeta DocumentMeta? - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - completedAt DateTime? - deletedAt DateTime? - templateId Int? - source DocumentSource + documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) - useLegacyFieldInsertion Boolean @default(false) + envelopeId String + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) - documentData DocumentData @relation(fields: [documentDataId], references: [id], onDelete: Cascade) - template Template? @relation(fields: [templateId], references: [id], onDelete: SetNull) - - auditLogs DocumentAuditLog[] - folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) - folderId String? + field Field[] @@unique([documentDataId]) - @@index([userId]) - @@index([status]) - @@index([folderId]) } model DocumentAuditLog { id String @id @default(cuid()) - documentId Int + envelopeId String createdAt DateTime @default(now()) type String data Json @@ -426,7 +453,7 @@ model DocumentAuditLog { userAgent String? ipAddress String? - Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) } enum DocumentDataType { @@ -441,12 +468,11 @@ enum DocumentSigningOrder { } model DocumentData { - id String @id @default(cuid()) - type DocumentDataType - data String - initialData String - document Document? - template Template? + id String @id @default(cuid()) + type DocumentDataType + data String + initialData String + envelopeItem EnvelopeItem? } enum DocumentDistributionMethod { @@ -460,7 +486,6 @@ model DocumentMeta { subject String? message String? timezone String? @default("Etc/UTC") @db.Text - password String? dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text redirectUrl String? signingOrder DocumentSigningOrder @default(PARALLEL) @@ -477,11 +502,7 @@ model DocumentMeta { emailReplyTo String? emailId String? - documentId Int? @unique - document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) - - templateId Int? @unique - template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) + envelope Envelope? } enum ReadStatus { @@ -511,8 +532,7 @@ enum RecipientRole { /// @zod.import(["import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';"]) model Recipient { id Int @id @default(autoincrement()) - documentId Int? - templateId Int? + envelopeId String email String @db.VarChar(255) name String @default("") @db.VarChar(255) token String @@ -526,13 +546,11 @@ model Recipient { readStatus ReadStatus @default(NOT_OPENED) signingStatus SigningStatus @default(NOT_SIGNED) sendStatus SendStatus @default(NOT_SENT) - document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) - template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) fields Field[] signatures Signature[] - @@index([documentId]) - @@index([templateId]) + @@index([envelopeId]) @@index([token]) } @@ -552,27 +570,26 @@ enum FieldType { /// @zod.import(["import { ZFieldMetaNotOptionalSchema } from '@documenso/lib/types/field-meta';"]) model Field { - id Int @id @default(autoincrement()) - secondaryId String @unique @default(cuid()) - documentId Int? - templateId Int? - recipientId Int - type FieldType - page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.") - positionX Decimal @default(0) - positionY Decimal @default(0) - width Decimal @default(-1) - height Decimal @default(-1) - customText String - inserted Boolean - document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) - template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) - recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) - signature Signature? - fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema) + id Int @id @default(autoincrement()) + secondaryId String @unique @default(cuid()) + envelopeId String + envelopeItemId String + recipientId Int + type FieldType + page Int /// @zod.number.describe("The page number of the field on the document. Starts from 1.") + positionX Decimal @default(0) + positionY Decimal @default(0) + width Decimal @default(-1) + height Decimal @default(-1) + customText String + inserted Boolean + envelopeItem EnvelopeItem @relation(fields: [envelopeItemId], references: [id], onDelete: Cascade) + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) + recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade) + signature Signature? + fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema) - @@index([documentId]) - @@index([templateId]) + @@index([envelopeId]) @@index([recipientId]) } @@ -594,13 +611,13 @@ model DocumentShareLink { id Int @id @default(autoincrement()) email String slug String @unique - documentId Int + envelopeId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) - @@unique([documentId, email]) + @@unique([envelopeId, email]) } enum OrganisationType { @@ -814,8 +831,7 @@ model Team { profile TeamProfile? - documents Document[] - templates Template[] + envelopes Envelope[] folders Folder[] apiTokens ApiToken[] webhooks Webhook[] @@ -853,53 +869,16 @@ enum TemplateType { PRIVATE } -/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';"]) -model Template { - id Int @id @default(autoincrement()) - externalId String? - type TemplateType @default(PRIVATE) - title String - visibility DocumentVisibility @default(EVERYONE) - authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema) - templateMeta DocumentMeta? - templateDocumentDataId String - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - publicTitle String @default("") - publicDescription String @default("") - - useLegacyFieldInsertion Boolean @default(false) - - userId Int - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - teamId Int - team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) - - templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) - - recipients Recipient[] - fields Field[] - directLink TemplateDirectLink? - documents Document[] - - folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) - folderId String? - - @@unique([templateDocumentDataId]) - @@index([userId]) -} - model TemplateDirectLink { id String @id @unique @default(cuid()) - templateId Int @unique + envelopeId String @unique token String @unique createdAt DateTime @default(now()) enabled Boolean directTemplateRecipientId Int - template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) + envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade) } model SiteSettings { @@ -1026,3 +1005,8 @@ model OrganisationAuthenticationPortal { autoProvisionUsers Boolean @default(true) allowedDomains String[] @default([]) } + +model Counter { + id String @id + value Int +} diff --git a/packages/prisma/seed/documents.ts b/packages/prisma/seed/documents.ts index 8184b2718..81a95374f 100644 --- a/packages/prisma/seed/documents.ts +++ b/packages/prisma/seed/documents.ts @@ -1,17 +1,19 @@ -import type { Document, Team, User } from '@prisma/client'; +import type { Team, User } from '@prisma/client'; import { nanoid } from 'nanoid'; import fs from 'node:fs'; import path from 'node:path'; import { match } from 'ts-pattern'; -import { createDocument } from '@documenso/lib/server-only/document/create-document'; -import { createTemplate } from '@documenso/lib/server-only/template/create-template'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { incrementDocumentId } from '@documenso/lib/server-only/envelope/increment-id'; +import { prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '..'; import { DocumentDataType, DocumentSource, DocumentStatus, + EnvelopeType, FieldType, Prisma, ReadStatus, @@ -30,7 +32,7 @@ type DocumentToSeed = { teamId: number; recipients: (User | string)[]; type: DocumentStatus; - documentOptions?: Partial; + documentOptions?: Partial; }; export const seedDocuments = async (documents: DocumentToSeed[]) => { @@ -75,27 +77,35 @@ export const seedBlankDocument = async ( }, }); - return await prisma.document.create({ + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + const documentId = await incrementDocumentId(); + + return await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, + documentMetaId: documentMeta.id, source: DocumentSource.DOCUMENT, teamId, title: `[TEST] Document ${key} - Draft`, status: DocumentStatus.DRAFT, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `[TEST] Document ${key} - Draft`, + documentDataId: documentData.id, + }, + }, userId: owner.id, ...createDocumentOptions, }, }); }; -export const unseedDocument = async (documentId: number) => { - await prisma.document.delete({ - where: { - id: documentId, - }, - }); -}; - export const seedTeamDocumentWithMeta = async (team: Team) => { const documentData = await prisma.documentData.create({ data: { @@ -120,11 +130,19 @@ export const seedTeamDocumentWithMeta = async (team: Team) => { const ownerUser = organisation.owner; - const document = await createDocument({ + const document = await createEnvelope({ userId: ownerUser.id, teamId: team.id, - title: `[TEST] Document ${nanoid(8)} - Draft`, - documentDataId: documentData.id, + data: { + type: EnvelopeType.DOCUMENT, + title: `[TEST] Document ${nanoid(8)} - Draft`, + envelopeItems: [ + { + title: `[TEST] Document ${nanoid(8)} - Draft`, + documentDataId: documentData.id, + }, + ], + }, normalizePdf: true, requestMetadata: { auth: null, @@ -133,7 +151,7 @@ export const seedTeamDocumentWithMeta = async (team: Team) => { }, }); - await prisma.document.update({ + await prisma.envelope.update({ where: { id: document.id, }, @@ -151,11 +169,7 @@ export const seedTeamDocumentWithMeta = async (team: Team) => { sendStatus: SendStatus.SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, fields: { create: { page: 1, @@ -166,13 +180,14 @@ export const seedTeamDocumentWithMeta = async (team: Team) => { positionY: new Prisma.Decimal(1), width: new Prisma.Decimal(5), height: new Prisma.Decimal(5), - documentId: document.id, + envelopeId: document.id, + envelopeItemId: document.envelopeItems[0].id, }, }, }, }); - return await prisma.document.findFirstOrThrow({ + return await prisma.envelope.findFirstOrThrow({ where: { id: document.id, }, @@ -206,13 +221,23 @@ export const seedTeamTemplateWithMeta = async (team: Team) => { const ownerUser = organisation.owner; - const template = await createTemplate({ + const template = await createEnvelope({ data: { + type: EnvelopeType.TEMPLATE, title: `[TEST] Template ${nanoid(8)} - Draft`, + envelopeItems: [ + { + documentDataId: documentData.id, + }, + ], }, userId: ownerUser.id, teamId: team.id, - templateDocumentDataId: documentData.id, + requestMetadata: { + auth: null, + requestMetadata: {}, + source: 'app', + }, }); await prisma.recipient.create({ @@ -224,11 +249,7 @@ export const seedTeamTemplateWithMeta = async (team: Team) => { sendStatus: SendStatus.SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - template: { - connect: { - id: template.id, - }, - }, + envelopeId: template.id, fields: { create: { page: 1, @@ -239,13 +260,14 @@ export const seedTeamTemplateWithMeta = async (team: Team) => { positionY: new Prisma.Decimal(1), width: new Prisma.Decimal(5), height: new Prisma.Decimal(5), - templateId: template.id, + envelopeId: template.id, + envelopeItemId: template.envelopeItems[0].id, }, }, }, }); - return await prisma.document.findFirstOrThrow({ + return await prisma.envelope.findFirstOrThrow({ where: { id: template.id, }, @@ -271,16 +293,39 @@ export const seedDraftDocument = async ( }, }); - const document = await prisma.document.create({ + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + const documentId = await incrementDocumentId(); + + const document = await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, + documentMetaId: documentMeta.id, source: DocumentSource.DOCUMENT, teamId, title: `[TEST] Document ${key} - Draft`, status: DocumentStatus.DRAFT, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `[TEST] Document ${key} - Draft`, + documentDataId: documentData.id, + }, + }, userId: sender.id, ...createDocumentOptions, }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + }, }); for (const recipient of recipients) { @@ -296,22 +341,19 @@ export const seedDraftDocument = async ( sendStatus: SendStatus.NOT_SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, fields: { create: { page: 1, type: FieldType.NAME, - inserted: true, + inserted: false, customText: name, positionX: new Prisma.Decimal(1), positionY: new Prisma.Decimal(1), width: new Prisma.Decimal(1), height: new Prisma.Decimal(1), - documentId: document.id, + envelopeId: document.id, + envelopeItemId: document.envelopeItems[0].id, }, }, }, @@ -323,7 +365,7 @@ export const seedDraftDocument = async ( type CreateDocumentOptions = { key?: string | number; - createDocumentOptions?: Partial; + createDocumentOptions?: Partial; }; export const seedPendingDocument = async ( @@ -342,16 +384,35 @@ export const seedPendingDocument = async ( }, }); - const document = await prisma.document.create({ + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + const documentId = await incrementDocumentId(); + + const document = await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, + documentMetaId: documentMeta.id, source: DocumentSource.DOCUMENT, teamId, title: `[TEST] Document ${key} - Pending`, status: DocumentStatus.PENDING, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `[TEST] Document ${key} - Pending`, + documentDataId: documentData.id, + }, + }, userId: sender.id, ...createDocumentOptions, }, + include: { + envelopeItems: true, + }, }); for (const recipient of recipients) { @@ -367,11 +428,7 @@ export const seedPendingDocument = async ( sendStatus: SendStatus.SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, fields: { create: { page: 1, @@ -382,19 +439,25 @@ export const seedPendingDocument = async ( positionY: new Prisma.Decimal(1), width: new Prisma.Decimal(1), height: new Prisma.Decimal(1), - documentId: document.id, + envelopeId: document.id, + envelopeItemId: document.envelopeItems[0].id, }, }, }, }); } - return prisma.document.findFirstOrThrow({ + return prisma.envelope.findFirstOrThrow({ where: { id: document.id, }, include: { recipients: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); }; @@ -408,9 +471,9 @@ export const seedPendingDocumentNoFields = async ({ owner: User; recipients: (User | string)[]; teamId: number; - updateDocumentOptions?: Partial; + updateDocumentOptions?: Partial; }) => { - const document: Document = await seedBlankDocument(owner, teamId); + const document = await seedBlankDocument(owner, teamId); for (const recipient of recipients) { const email = typeof recipient === 'string' ? recipient : recipient.email; @@ -425,18 +488,14 @@ export const seedPendingDocumentNoFields = async ({ sendStatus: SendStatus.SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, }, }); } const createdRecipients = await prisma.recipient.findMany({ where: { - documentId: document.id, + envelopeId: document.id, }, include: { fields: true, @@ -444,7 +503,7 @@ export const seedPendingDocumentNoFields = async ({ }); const latestDocument = updateDocumentOptions - ? await prisma.document.update({ + ? await prisma.envelope.update({ where: { id: document.id, }, @@ -468,12 +527,18 @@ export const seedPendingDocumentWithFullFields = async ({ }: { owner: User; recipients: (User | string)[]; - recipientsCreateOptions?: Partial[]; - updateDocumentOptions?: Partial; + recipientsCreateOptions?: Partial[]; + updateDocumentOptions?: Partial; fields?: FieldType[]; teamId: number; }) => { - const document: Document = await seedBlankDocument(owner, teamId); + const document = await seedBlankDocument(owner, teamId); + + const firstItem = await prisma.envelopeItem.findFirstOrThrow({ + where: { + envelopeId: document.id, + }, + }); for (const [recipientIndex, recipient] of recipients.entries()) { const email = typeof recipient === 'string' ? recipient : recipient.email; @@ -488,11 +553,7 @@ export const seedPendingDocumentWithFullFields = async ({ sendStatus: SendStatus.SENT, signingStatus: SigningStatus.NOT_SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, fields: { createMany: { data: fields.map((fieldType, fieldIndex) => ({ @@ -504,7 +565,8 @@ export const seedPendingDocumentWithFullFields = async ({ positionY: new Prisma.Decimal((fieldIndex + 1) * 5), width: new Prisma.Decimal(5), height: new Prisma.Decimal(5), - documentId: document.id, + envelopeId: document.id, + envelopeItemId: firstItem.id, })), }, }, @@ -515,14 +577,14 @@ export const seedPendingDocumentWithFullFields = async ({ const createdRecipients = await prisma.recipient.findMany({ where: { - documentId: document.id, + envelopeId: document.id, }, include: { fields: true, }, }); - const latestDocument = await prisma.document.update({ + const latestDocument = await prisma.envelope.update({ where: { id: document.id, }, @@ -557,16 +619,35 @@ export const seedCompletedDocument = async ( }, }); - const document = await prisma.document.create({ + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + const documentId = await incrementDocumentId(); + + const document = await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, + documentMetaId: documentMeta.id, source: DocumentSource.DOCUMENT, teamId, title: `[TEST] Document ${key} - Completed`, status: DocumentStatus.COMPLETED, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `[TEST] Document ${key} - Completed`, + documentDataId: documentData.id, + }, + }, userId: sender.id, ...createDocumentOptions, }, + include: { + envelopeItems: true, + }, }); for (const recipient of recipients) { @@ -582,11 +663,7 @@ export const seedCompletedDocument = async ( sendStatus: SendStatus.SENT, signingStatus: SigningStatus.SIGNED, signedAt: new Date(), - document: { - connect: { - id: document.id, - }, - }, + envelopeId: document.id, fields: { create: { page: 1, @@ -597,7 +674,8 @@ export const seedCompletedDocument = async ( positionY: new Prisma.Decimal(1), width: new Prisma.Decimal(1), height: new Prisma.Decimal(1), - documentId: document.id, + envelopeId: document.id, + envelopeItemId: document.envelopeItems[0].id, }, }, }, diff --git a/packages/prisma/seed/initial-seed.ts b/packages/prisma/seed/initial-seed.ts index cb99ef416..5176068da 100644 --- a/packages/prisma/seed/initial-seed.ts +++ b/packages/prisma/seed/initial-seed.ts @@ -1,8 +1,11 @@ import fs from 'node:fs'; import path from 'node:path'; +import { incrementDocumentId } from '@documenso/lib/server-only/envelope/increment-id'; +import { prefixedId } from '@documenso/lib/universal/id'; + import { prisma } from '..'; -import { DocumentDataType, DocumentSource } from '../client'; +import { DocumentDataType, DocumentSource, EnvelopeType } from '../client'; import { seedPendingDocument } from './documents'; import { seedDirectTemplate, seedTemplate } from './templates'; import { seedUser } from './users'; @@ -52,11 +55,27 @@ export const seedDatabase = async () => { for (let i = 1; i <= 4; i++) { const documentData = await createDocumentData({ documentData: examplePdf }); - await prisma.document.create({ + const documentId = await incrementDocumentId(); + + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, + documentMetaId: documentMeta.id, source: DocumentSource.DOCUMENT, title: `Example Document ${i}`, - documentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `Example Document ${i}`, + documentDataId: documentData.id, + }, + }, userId: exampleUser.user.id, teamId: exampleUser.team.id, recipients: { @@ -73,11 +92,27 @@ export const seedDatabase = async () => { for (let i = 1; i <= 4; i++) { const documentData = await createDocumentData({ documentData: examplePdf }); - await prisma.document.create({ + const documentId = await incrementDocumentId(); + + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: documentId.formattedDocumentId, + type: EnvelopeType.DOCUMENT, source: DocumentSource.DOCUMENT, title: `Document ${i}`, - documentDataId: documentData.id, + documentMetaId: documentMeta.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `Document ${i}`, + documentDataId: documentData.id, + }, + }, userId: adminUser.user.id, teamId: adminUser.team.id, recipients: { diff --git a/packages/prisma/seed/templates.ts b/packages/prisma/seed/templates.ts index ebd17826b..511ee552b 100644 --- a/packages/prisma/seed/templates.ts +++ b/packages/prisma/seed/templates.ts @@ -5,10 +5,20 @@ import { DIRECT_TEMPLATE_RECIPIENT_EMAIL, DIRECT_TEMPLATE_RECIPIENT_NAME, } from '@documenso/lib/constants/direct-templates'; +import { incrementTemplateId } from '@documenso/lib/server-only/envelope/increment-id'; +import { prefixedId } from '@documenso/lib/universal/id'; import { prisma } from '..'; import type { Prisma, User } from '../client'; -import { DocumentDataType, ReadStatus, RecipientRole, SendStatus, SigningStatus } from '../client'; +import { + DocumentDataType, + DocumentSource, + EnvelopeType, + ReadStatus, + RecipientRole, + SendStatus, + SigningStatus, +} from '../client'; const examplePdf = fs .readFileSync(path.join(__dirname, '../../../assets/example.pdf')) @@ -18,12 +28,12 @@ type SeedTemplateOptions = { title?: string; userId: number; teamId: number; - createTemplateOptions?: Partial; + createTemplateOptions?: Partial; }; type CreateTemplateOptions = { key?: string | number; - createTemplateOptions?: Partial; + createTemplateOptions?: Partial; }; export const seedBlankTemplate = async ( @@ -41,14 +51,38 @@ export const seedBlankTemplate = async ( }, }); - return await prisma.template.create({ + const templateId = await incrementTemplateId(); + + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + return await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: templateId.formattedTemplateId, + type: EnvelopeType.TEMPLATE, title: `[TEST] Template ${key}`, teamId, - templateDocumentDataId: documentData.id, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title: `[TEST] Template ${key}`, + documentDataId: documentData.id, + }, + }, userId: owner.id, + source: DocumentSource.TEMPLATE, + documentMetaId: documentMeta.id, ...createTemplateOptions, }, + include: { + envelopeItems: { + include: { + documentData: true, + }, + }, + }, }); }; @@ -63,19 +97,29 @@ export const seedTemplate = async (options: SeedTemplateOptions) => { }, }); - return await prisma.template.create({ + const templateId = await incrementTemplateId(); + + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + return await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: templateId.formattedTemplateId, + type: EnvelopeType.TEMPLATE, title, - templateDocumentData: { - connect: { - id: documentData.id, - }, - }, - user: { - connect: { - id: userId, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title, + documentDataId: documentData.id, }, }, + source: DocumentSource.TEMPLATE, + documentMetaId: documentMeta.id, + userId, + teamId, recipients: { create: { email: 'recipient.1@documenso.com', @@ -87,9 +131,11 @@ export const seedTemplate = async (options: SeedTemplateOptions) => { role: RecipientRole.SIGNER, }, }, - team: { - connect: { - id: teamId, + }, + include: { + envelopeItems: { + include: { + documentData: true, }, }, }, @@ -107,19 +153,29 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => { }, }); - const template = await prisma.template.create({ + const templateId = await incrementTemplateId(); + + const documentMeta = await prisma.documentMeta.create({ + data: {}, + }); + + const template = await prisma.envelope.create({ data: { + id: prefixedId('envelope'), + secondaryId: templateId.formattedTemplateId, + type: EnvelopeType.TEMPLATE, title, - templateDocumentData: { - connect: { - id: documentData.id, - }, - }, - user: { - connect: { - id: userId, + envelopeItems: { + create: { + id: prefixedId('envelope_item'), + title, + documentDataId: documentData.id, }, }, + source: DocumentSource.TEMPLATE, + documentMetaId: documentMeta.id, + userId, + teamId, recipients: { create: { email: DIRECT_TEMPLATE_RECIPIENT_EMAIL, @@ -127,11 +183,6 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => { token: Math.random().toString().slice(2, 7), }, }, - team: { - connect: { - id: teamId, - }, - }, ...options.createTemplateOptions, }, include: { @@ -150,14 +201,14 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => { await prisma.templateDirectLink.create({ data: { - templateId: template.id, + envelopeId: template.id, enabled: true, token: Math.random().toString(), directTemplateRecipientId: directTemplateRecpient.id, }, }); - return await prisma.template.findFirstOrThrow({ + return await prisma.envelope.findFirstOrThrow({ where: { id: template.id, }, @@ -166,6 +217,11 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => { fields: true, recipients: true, team: true, + envelopeItems: { + select: { + documentData: true, + }, + }, }, }); }; diff --git a/packages/prisma/types/document-legacy-schema.ts b/packages/prisma/types/document-legacy-schema.ts new file mode 100644 index 000000000..ee9e15c12 --- /dev/null +++ b/packages/prisma/types/document-legacy-schema.ts @@ -0,0 +1,53 @@ +/** + * Legacy Document schema to confirm backwards API compatibility since + * we migrated Documents to Envelopes. + */ +import { z } from 'zod'; + +import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth'; +import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values'; + +import DocumentStatusSchema from '../generated/zod/inputTypeSchemas/DocumentStatusSchema'; +import DocumentVisibilitySchema from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema'; + +const DocumentSourceSchema = z.enum(['DOCUMENT', 'TEMPLATE', 'TEMPLATE_DIRECT_LINK']); + +///////////////////////////////////////// +// DOCUMENT SCHEMA +///////////////////////////////////////// + +export const LegacyDocumentSchema = z.object({ + visibility: DocumentVisibilitySchema, + status: DocumentStatusSchema, + source: DocumentSourceSchema, + id: z.number(), + qrToken: z + .string() + .describe('The token for viewing the document using the QR code on the certificate.') + .nullable(), + externalId: z + .string() + .describe('A custom external ID you can use to identify the document.') + .nullable(), + userId: z.number().describe('The ID of the user that created this document.'), + teamId: z.number(), + /** + * [DocumentAuthOptions] + */ + authOptions: ZDocumentAuthOptionsSchema.nullable(), + /** + * [DocumentFormValues] + */ + formValues: ZDocumentFormValuesSchema.nullable(), + title: z.string(), + // documentDataId: z.string(), // Todo: Migration + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + completedAt: z.coerce.date().nullable(), + deletedAt: z.coerce.date().nullable(), + templateId: z.number().nullable(), + useLegacyFieldInsertion: z.boolean(), + folderId: z.string().nullable(), +}); + +export type Document = z.infer; diff --git a/packages/prisma/types/document-with-recipient.ts b/packages/prisma/types/document-with-recipient.ts index 01b0130dc..c5015a3e4 100644 --- a/packages/prisma/types/document-with-recipient.ts +++ b/packages/prisma/types/document-with-recipient.ts @@ -1,10 +1,10 @@ -import type { Document, DocumentData, Recipient } from '@prisma/client'; +import type { DocumentData, Envelope, Recipient } from '@prisma/client'; -export type DocumentWithRecipients = Document & { +export type EnvelopeWithRecipients = Envelope & { recipients: Recipient[]; }; -export type DocumentWithRecipient = Document & { +export type EnvelopeWithRecipient = Envelope & { recipients: Recipient[]; documentData: DocumentData; }; diff --git a/packages/prisma/types/template-legacy-schema.ts b/packages/prisma/types/template-legacy-schema.ts new file mode 100644 index 000000000..fabb52316 --- /dev/null +++ b/packages/prisma/types/template-legacy-schema.ts @@ -0,0 +1,35 @@ +/** + * Legacy Template schema to confirm backwards API compatibility since + * we removed the "Template" prisma schema model. + */ +import { TemplateType } from '@prisma/client'; +import { z } from 'zod'; + +import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth'; + +import { DocumentVisibilitySchema } from '../generated/zod/inputTypeSchemas/DocumentVisibilitySchema'; + +export const TemplateTypeSchema = z.nativeEnum(TemplateType); + +export const TemplateSchema = z.object({ + type: TemplateTypeSchema, + visibility: DocumentVisibilitySchema, + id: z.number(), + externalId: z.string().nullable(), + title: z.string(), + /** + * [DocumentAuthOptions] + */ + authOptions: ZDocumentAuthOptionsSchema.nullable(), + // templateDocumentDataId: z.string(), // Todo: Migration - Removal. + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + publicTitle: z.string(), + publicDescription: z.string(), + useLegacyFieldInsertion: z.boolean(), + userId: z.number(), + teamId: z.number(), + folderId: z.string().nullable(), +}); + +export type Template = z.infer; diff --git a/packages/trpc/server/admin-router/delete-document.ts b/packages/trpc/server/admin-router/delete-document.ts index 70fc96591..83632b4e8 100644 --- a/packages/trpc/server/admin-router/delete-document.ts +++ b/packages/trpc/server/admin-router/delete-document.ts @@ -1,5 +1,5 @@ +import { adminSuperDeleteDocument } from '@documenso/lib/server-only/admin/admin-super-delete-document'; import { sendDeleteEmail } from '@documenso/lib/server-only/document/send-delete-email'; -import { superDeleteDocument } from '@documenso/lib/server-only/document/super-delete-document'; import { adminProcedure } from '../trpc'; import { @@ -19,10 +19,10 @@ export const deleteDocumentRoute = adminProcedure }, }); - await sendDeleteEmail({ documentId: id, reason }); + await sendDeleteEmail({ envelopeId: id, reason }); - await superDeleteDocument({ - id, + await adminSuperDeleteDocument({ + envelopeId: id, requestMetadata: ctx.metadata.requestMetadata, }); }); diff --git a/packages/trpc/server/admin-router/delete-document.types.ts b/packages/trpc/server/admin-router/delete-document.types.ts index 58ff6ff35..cf7bf6426 100644 --- a/packages/trpc/server/admin-router/delete-document.types.ts +++ b/packages/trpc/server/admin-router/delete-document.types.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const ZDeleteDocumentRequestSchema = z.object({ - id: z.number().min(1), + id: z.string(), reason: z.string(), }); diff --git a/packages/trpc/server/admin-router/find-documents.ts b/packages/trpc/server/admin-router/find-documents.ts index 7ce96c1f5..7e4610559 100644 --- a/packages/trpc/server/admin-router/find-documents.ts +++ b/packages/trpc/server/admin-router/find-documents.ts @@ -1,4 +1,5 @@ -import { findDocuments } from '@documenso/lib/server-only/admin/get-all-documents'; +import { adminFindDocuments } from '@documenso/lib/server-only/admin/admin-find-documents'; +import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document'; import { adminProcedure } from '../trpc'; import { ZFindDocumentsRequestSchema, ZFindDocumentsResponseSchema } from './find-documents.types'; @@ -9,5 +10,10 @@ export const findDocumentsRoute = adminProcedure .query(async ({ input }) => { const { query, page, perPage } = input; - return await findDocuments({ query, page, perPage }); + const result = await adminFindDocuments({ query, page, perPage }); + + return { + ...result, + data: result.data.map(mapEnvelopesToDocumentMany), + }; }); diff --git a/packages/trpc/server/admin-router/reseal-document.ts b/packages/trpc/server/admin-router/reseal-document.ts index 7436d29c7..0a43189c8 100644 --- a/packages/trpc/server/admin-router/reseal-document.ts +++ b/packages/trpc/server/admin-router/reseal-document.ts @@ -1,4 +1,6 @@ -import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document'; +import { EnvelopeType } from '@prisma/client'; + +import { unsafeGetEntireEnvelope } from '@documenso/lib/server-only/admin/get-entire-document'; import { sealDocument } from '@documenso/lib/server-only/document/seal-document'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; @@ -20,9 +22,21 @@ export const resealDocumentRoute = adminProcedure }, }); - const document = await getEntireDocument({ id }); + const envelope = await unsafeGetEntireEnvelope({ + id: { + type: 'envelopeId', + id, + }, + type: EnvelopeType.DOCUMENT, + }); - const isResealing = isDocumentCompleted(document.status); + const isResealing = isDocumentCompleted(envelope.status); - await sealDocument({ documentId: id, isResealing }); + await sealDocument({ + id: { + type: 'envelopeId', + id, + }, + isResealing, + }); }); diff --git a/packages/trpc/server/admin-router/reseal-document.types.ts b/packages/trpc/server/admin-router/reseal-document.types.ts index e33c2dc5c..b673789af 100644 --- a/packages/trpc/server/admin-router/reseal-document.types.ts +++ b/packages/trpc/server/admin-router/reseal-document.types.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; export const ZResealDocumentRequestSchema = z.object({ - id: z.number().min(1), + id: z.string(), }); export const ZResealDocumentResponseSchema = z.void(); diff --git a/packages/trpc/server/document-router/create-document-temporary.ts b/packages/trpc/server/document-router/create-document-temporary.ts index fdc64f1d6..5700d1d81 100644 --- a/packages/trpc/server/document-router/create-document-temporary.ts +++ b/packages/trpc/server/document-router/create-document-temporary.ts @@ -1,10 +1,12 @@ -import { DocumentDataType } from '@prisma/client'; +import { DocumentDataType, EnvelopeType } from '@prisma/client'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; -import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; +import { prisma } from '@documenso/prisma'; import { authenticatedProcedure } from '../trpc'; import { @@ -55,12 +57,12 @@ export const createDocumentTemporaryRoute = authenticatedProcedure type: DocumentDataType.S3_PATH, }); - const createdDocument = await createDocumentV2({ + const createdEnvelope = await createEnvelope({ userId: ctx.user.id, teamId, - documentDataId: documentData.id, normalizePdf: false, // Not normalizing because of presigned URL. data: { + type: EnvelopeType.DOCUMENT, title, externalId, visibility, @@ -68,14 +70,44 @@ export const createDocumentTemporaryRoute = authenticatedProcedure globalActionAuth, recipients, folderId, + envelopeItems: [ + { + documentDataId: documentData.id, + }, + ], }, meta, requestMetadata: ctx.metadata, }); + const envelopeItems = await prisma.envelopeItem.findMany({ + where: { + envelopeId: createdEnvelope.id, + }, + include: { + documentData: true, + }, + }); + + const legacyDocumentId = mapSecondaryIdToDocumentId(createdEnvelope.secondaryId); + + const firstDocumentData = envelopeItems[0].documentData; + + if (!firstDocumentData) { + throw new Error('Document data not found'); + } + return { - document: createdDocument, - folder: createdDocument.folder, // Todo: Remove this prior to api-v2 release. + document: { + ...createdEnvelope, + documentData: firstDocumentData, + id: legacyDocumentId, + fields: createdEnvelope.fields.map((field) => ({ + ...field, + documentId: legacyDocumentId, + })), + }, + folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release. uploadUrl: url, }; }); diff --git a/packages/trpc/server/document-router/create-document.ts b/packages/trpc/server/document-router/create-document.ts index bf66f93d3..8704ed13e 100644 --- a/packages/trpc/server/document-router/create-document.ts +++ b/packages/trpc/server/document-router/create-document.ts @@ -1,6 +1,9 @@ +import { EnvelopeType } from '@prisma/client'; + import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { createDocument } from '@documenso/lib/server-only/document/create-document'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { authenticatedProcedure } from '../trpc'; import { @@ -9,7 +12,7 @@ import { } from './create-document.types'; export const createDocumentRoute = authenticatedProcedure - .input(ZCreateDocumentRequestSchema) + .input(ZCreateDocumentRequestSchema) // Note: Before releasing this to public, update the response schema to be correct. .output(ZCreateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { user, teamId } = ctx; @@ -30,18 +33,25 @@ export const createDocumentRoute = authenticatedProcedure }); } - const document = await createDocument({ + const document = await createEnvelope({ userId: user.id, teamId, - title, - documentDataId, + data: { + type: EnvelopeType.DOCUMENT, + title, + userTimezone: timezone, + folderId, + envelopeItems: [ + { + documentDataId, + }, + ], + }, normalizePdf: true, - userTimezone: timezone, requestMetadata: ctx.metadata, - folderId, }); return { - id: document.id, + legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId), }; }); diff --git a/packages/trpc/server/document-router/create-document.types.ts b/packages/trpc/server/document-router/create-document.types.ts index 4dfc89dce..53dcd4d85 100644 --- a/packages/trpc/server/document-router/create-document.types.ts +++ b/packages/trpc/server/document-router/create-document.types.ts @@ -20,7 +20,7 @@ export const ZCreateDocumentRequestSchema = z.object({ }); export const ZCreateDocumentResponseSchema = z.object({ - id: z.number(), + legacyDocumentId: z.number(), }); export type TCreateDocumentRequest = z.infer; diff --git a/packages/trpc/server/document-router/distribute-document.ts b/packages/trpc/server/document-router/distribute-document.ts index 00fe8ef92..79bce76e2 100644 --- a/packages/trpc/server/document-router/distribute-document.ts +++ b/packages/trpc/server/document-router/distribute-document.ts @@ -1,5 +1,6 @@ -import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; +import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; +import { mapEnvelopeToDocumentLite } from '@documenso/lib/utils/document'; import { authenticatedProcedure } from '../trpc'; import { @@ -23,10 +24,13 @@ export const distributeDocumentRoute = authenticatedProcedure }); if (Object.values(meta).length > 0) { - await upsertDocumentMeta({ + await updateDocumentMeta({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, subject: meta.subject, message: meta.message, dateFormat: meta.dateFormat, @@ -41,10 +45,15 @@ export const distributeDocumentRoute = authenticatedProcedure }); } - return await sendDocument({ + const envelope = await sendDocument({ userId: ctx.user.id, - documentId, + id: { + type: 'documentId', + id: documentId, + }, teamId, requestMetadata: ctx.metadata, }); + + return mapEnvelopeToDocumentLite(envelope); }); diff --git a/packages/trpc/server/document-router/download-document-audit-logs.ts b/packages/trpc/server/document-router/download-document-audit-logs.ts index af84c43e0..7ce76049e 100644 --- a/packages/trpc/server/document-router/download-document-audit-logs.ts +++ b/packages/trpc/server/document-router/download-document-audit-logs.ts @@ -1,9 +1,11 @@ +import { EnvelopeType } from '@prisma/client'; import { DateTime } from 'luxon'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt'; -import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { authenticatedProcedure } from '../trpc'; import { @@ -24,20 +26,24 @@ export const downloadDocumentAuditLogsRoute = authenticatedProcedure }, }); - const document = await getDocumentById({ - documentId, + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId: ctx.user.id, teamId, }).catch(() => null); - if (!document || (teamId && document.teamId !== teamId)) { + if (!envelope) { throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You do not have access to this document.', }); } const encrypted = encryptSecondaryData({ - data: document.id.toString(), + data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(), expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(), }); diff --git a/packages/trpc/server/document-router/download-document-certificate.ts b/packages/trpc/server/document-router/download-document-certificate.ts index b59eafbf0..e81ec5082 100644 --- a/packages/trpc/server/document-router/download-document-certificate.ts +++ b/packages/trpc/server/document-router/download-document-certificate.ts @@ -1,10 +1,12 @@ +import { EnvelopeType } from '@prisma/client'; import { DateTime } from 'luxon'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { AppError } from '@documenso/lib/errors/app-error'; import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt'; -import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { authenticatedProcedure } from '../trpc'; import { @@ -25,18 +27,22 @@ export const downloadDocumentCertificateRoute = authenticatedProcedure }, }); - const document = await getDocumentById({ - documentId, + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, userId: ctx.user.id, teamId, }); - if (!isDocumentCompleted(document.status)) { + if (!isDocumentCompleted(envelope.status)) { throw new AppError('DOCUMENT_NOT_COMPLETE'); } const encrypted = encryptSecondaryData({ - data: document.id.toString(), + data: mapSecondaryIdToDocumentId(envelope.secondaryId).toString(), expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(), }); diff --git a/packages/trpc/server/document-router/download-document.ts b/packages/trpc/server/document-router/download-document.ts index a0cbbf104..c7e0f02c7 100644 --- a/packages/trpc/server/document-router/download-document.ts +++ b/packages/trpc/server/document-router/download-document.ts @@ -1,7 +1,8 @@ -import { DocumentDataType } from '@prisma/client'; +import type { DocumentData } from '@prisma/client'; +import { DocumentDataType, EnvelopeType } from '@prisma/client'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id'; +import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getPresignGetUrl } from '@documenso/lib/universal/upload/server-actions'; import { isDocumentCompleted } from '@documenso/lib/utils/document'; @@ -27,45 +28,51 @@ export const downloadDocumentRoute = authenticatedProcedure }, }); + const envelope = await getEnvelopeById({ + id: { + type: 'documentId', + id: documentId, + }, + type: EnvelopeType.DOCUMENT, + userId: user.id, + teamId, + }); + + // This error is done AFTER the get envelope so we can test access controls without S3. if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document downloads are only available when S3 storage is configured.', }); } - const document = await getDocumentById({ - documentId, - userId: user.id, - teamId, - }); + const documentData: DocumentData | undefined = envelope.envelopeItems[0]?.documentData; - if (!document.documentData) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Document data not found', + if (envelope.envelopeItems.length !== 1 || !documentData) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: + 'This endpoint only supports documents with a single item. Use envelopes API instead.', }); } - if (document.documentData.type !== DocumentDataType.S3_PATH) { + if (documentData.type !== DocumentDataType.S3_PATH) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document is not stored in S3 and cannot be downloaded via URL.', }); } - if (version === 'signed' && !isDocumentCompleted(document.status)) { + if (version === 'signed' && !isDocumentCompleted(envelope.status)) { throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Document is not completed yet.', }); } try { - const documentData = - version === 'original' - ? document.documentData.initialData || document.documentData.data - : document.documentData.data; + const data = + version === 'original' ? documentData.initialData || documentData.data : documentData.data; - const { url } = await getPresignGetUrl(documentData); + const { url } = await getPresignGetUrl(data); - const baseTitle = document.title.replace(/\.pdf$/, ''); + const baseTitle = envelope.title.replace(/\.pdf$/, ''); const suffix = version === 'signed' ? '_signed.pdf' : '.pdf'; const filename = `${baseTitle}${suffix}`; diff --git a/packages/trpc/server/document-router/duplicate-document.ts b/packages/trpc/server/document-router/duplicate-document.ts index be8f29d03..dc60906df 100644 --- a/packages/trpc/server/document-router/duplicate-document.ts +++ b/packages/trpc/server/document-router/duplicate-document.ts @@ -22,8 +22,11 @@ export const duplicateDocumentRoute = authenticatedProcedure }); return await duplicateDocument({ + id: { + type: 'documentId', + id: documentId, + }, userId: user.id, teamId, - documentId, }); }); diff --git a/packages/trpc/server/document-router/find-documents-internal.ts b/packages/trpc/server/document-router/find-documents-internal.ts index 47748771b..2dfb38254 100644 --- a/packages/trpc/server/document-router/find-documents-internal.ts +++ b/packages/trpc/server/document-router/find-documents-internal.ts @@ -2,6 +2,7 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-document import type { GetStatsInput } from '@documenso/lib/server-only/document/get-stats'; import { getStats } from '@documenso/lib/server-only/document/get-stats'; import { getTeamById } from '@documenso/lib/server-only/team/get-team'; +import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document'; import { authenticatedProcedure } from '../trpc'; import { @@ -69,6 +70,7 @@ export const findDocumentsInternalRoute = authenticatedProcedure return { ...documents, + data: documents.data.map((envelope) => mapEnvelopesToDocumentMany(envelope)), stats, }; }); diff --git a/packages/trpc/server/document-router/find-documents.ts b/packages/trpc/server/document-router/find-documents.ts index 71684b326..d7b0be598 100644 --- a/packages/trpc/server/document-router/find-documents.ts +++ b/packages/trpc/server/document-router/find-documents.ts @@ -1,4 +1,5 @@ import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; +import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document'; import { authenticatedProcedure } from '../trpc'; import { @@ -39,5 +40,8 @@ export const findDocumentsRoute = authenticatedProcedure orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, }); - return documents; + return { + ...documents, + data: documents.data.map((envelope) => mapEnvelopesToDocumentMany(envelope)), + }; }); diff --git a/packages/trpc/server/document-router/find-inbox.ts b/packages/trpc/server/document-router/find-inbox.ts index a13ef2ad2..fd1177f07 100644 --- a/packages/trpc/server/document-router/find-inbox.ts +++ b/packages/trpc/server/document-router/find-inbox.ts @@ -1,7 +1,8 @@ -import type { Document, Prisma } from '@prisma/client'; -import { DocumentStatus, RecipientRole } from '@prisma/client'; +import type { Envelope, Prisma } from '@prisma/client'; +import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client'; import type { FindResultResponse } from '@documenso/lib/types/search-params'; +import { mapEnvelopesToDocumentMany } from '@documenso/lib/utils/document'; import { maskRecipientTokensForDocument } from '@documenso/lib/utils/mask-recipient-tokens-for-document'; import { prisma } from '@documenso/prisma'; @@ -16,11 +17,16 @@ export const findInboxRoute = authenticatedProcedure const userId = ctx.user.id; - return await findInbox({ + const envelopes = await findInbox({ userId, page, perPage, }); + + return { + ...envelopes, + data: envelopes.data.map(mapEnvelopesToDocumentMany), + }; }); export type FindInboxOptions = { @@ -28,7 +34,7 @@ export type FindInboxOptions = { page?: number; perPage?: number; orderBy?: { - column: keyof Omit; + column: keyof Omit; direction: 'asc' | 'desc'; }; }; @@ -38,12 +44,17 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin where: { id: userId, }, + select: { + id: true, + email: true, + }, }); const orderByColumn = orderBy?.column ?? 'createdAt'; const orderByDirection = orderBy?.direction ?? 'desc'; - const whereClause: Prisma.DocumentWhereInput = { + const whereClause: Prisma.EnvelopeWhereInput = { + type: EnvelopeType.DOCUMENT, status: { not: DocumentStatus.DRAFT, }, @@ -59,7 +70,7 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin }; const [data, count] = await Promise.all([ - prisma.document.findMany({ + prisma.envelope.findMany({ where: whereClause, skip: Math.max(page - 1, 0) * perPage, take: perPage, @@ -83,7 +94,7 @@ export const findInbox = async ({ userId, page = 1, perPage = 10, orderBy }: Fin }, }, }), - prisma.document.count({ + prisma.envelope.count({ where: whereClause, }), ]); diff --git a/packages/trpc/server/document-router/get-document-by-token.ts b/packages/trpc/server/document-router/get-document-by-token.ts index b640f6946..6b4d6007b 100644 --- a/packages/trpc/server/document-router/get-document-by-token.ts +++ b/packages/trpc/server/document-router/get-document-by-token.ts @@ -1,3 +1,5 @@ +import { EnvelopeType } from '@prisma/client'; + import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { prisma } from '@documenso/prisma'; @@ -13,8 +15,9 @@ export const getDocumentByTokenRoute = authenticatedProcedure .query(async ({ input, ctx }) => { const { token } = input; - const document = await prisma.document.findFirst({ + const envelope = await prisma.envelope.findFirst({ where: { + type: EnvelopeType.DOCUMENT, recipients: { some: { token, @@ -23,21 +26,34 @@ export const getDocumentByTokenRoute = authenticatedProcedure }, }, include: { - documentData: true, + envelopeItems: { + include: { + documentData: true, + }, + }, }, }); - if (!document) { + // Todo: Envelopes + const firstDocumentData = envelope?.envelopeItems[0].documentData; + + if (!envelope || !firstDocumentData) { throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Document not found', }); } + if (envelope.envelopeItems.length !== 1) { + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'This endpoint does not support multiple items', + }); + } + ctx.logger.info({ - documentId: document.id, + documentId: envelope.id, }); return { - documentData: document.documentData, + documentData: firstDocumentData, }; }); diff --git a/packages/trpc/server/document-router/get-document.ts b/packages/trpc/server/document-router/get-document.ts index 4dad5291f..38a7ad851 100644 --- a/packages/trpc/server/document-router/get-document.ts +++ b/packages/trpc/server/document-router/get-document.ts @@ -24,6 +24,9 @@ export const getDocumentRoute = authenticatedProcedure return await getDocumentWithDetailsById({ userId: user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, }); }); diff --git a/packages/trpc/server/document-router/get-inbox-count.ts b/packages/trpc/server/document-router/get-inbox-count.ts index f6b7ef0e2..027ac0410 100644 --- a/packages/trpc/server/document-router/get-inbox-count.ts +++ b/packages/trpc/server/document-router/get-inbox-count.ts @@ -1,4 +1,4 @@ -import { DocumentStatus, RecipientRole } from '@prisma/client'; +import { DocumentStatus, EnvelopeType, RecipientRole } from '@prisma/client'; import { prisma } from '@documenso/prisma'; @@ -20,7 +20,8 @@ export const getInboxCountRoute = authenticatedProcedure role: { not: RecipientRole.CC, }, - document: { + envelope: { + type: EnvelopeType.DOCUMENT, status: { not: DocumentStatus.DRAFT, }, diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 8e2e453bd..d49037f8c 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -17,6 +17,7 @@ import { getDocumentByTokenRoute } from './get-document-by-token'; import { getInboxCountRoute } from './get-inbox-count'; import { redistributeDocumentRoute } from './redistribute-document'; import { searchDocumentRoute } from './search-document'; +import { shareDocumentRoute } from './share-document'; import { updateDocumentRoute } from './update-document'; export const documentRouter = router({ @@ -30,6 +31,7 @@ export const documentRouter = router({ distribute: distributeDocumentRoute, redistribute: redistributeDocumentRoute, search: searchDocumentRoute, + share: shareDocumentRoute, // Temporary v2 beta routes to be removed once V2 is fully released. download: downloadDocumentRoute, diff --git a/packages/trpc/server/document-router/share-document.ts b/packages/trpc/server/document-router/share-document.ts new file mode 100644 index 000000000..edaee8172 --- /dev/null +++ b/packages/trpc/server/document-router/share-document.ts @@ -0,0 +1,28 @@ +import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link'; + +import { procedure } from '../trpc'; +import { ZShareDocumentRequestSchema, ZShareDocumentResponseSchema } from './share-document.types'; + +// Note: This is an unauthenticated route. +export const shareDocumentRoute = procedure + .input(ZShareDocumentRequestSchema) + .output(ZShareDocumentResponseSchema) + .mutation(async ({ input, ctx }) => { + const { documentId, token } = input; + + ctx.logger.info({ + input: { + documentId, + }, + }); + + if (token) { + return await createOrGetShareLink({ documentId, token }); + } + + if (!ctx.user?.id) { + throw new Error('You must either provide a token or be logged in to create a sharing link.'); + } + + return await createOrGetShareLink({ documentId, userId: ctx.user.id }); + }); diff --git a/packages/trpc/server/document-router/share-document.types.ts b/packages/trpc/server/document-router/share-document.types.ts new file mode 100644 index 000000000..df96200a3 --- /dev/null +++ b/packages/trpc/server/document-router/share-document.types.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +import DocumentShareLinkSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentShareLinkSchema'; + +export const ZShareDocumentRequestSchema = z.object({ + documentId: z.number(), + token: z.string().optional(), +}); + +export const ZShareDocumentResponseSchema = DocumentShareLinkSchema.pick({ + slug: true, + email: true, +}); + +export type TShareDocumentRequest = z.infer; +export type TShareDocumentResponse = z.infer; diff --git a/packages/trpc/server/document-router/update-document.ts b/packages/trpc/server/document-router/update-document.ts index 44a7fb990..d786c94f3 100644 --- a/packages/trpc/server/document-router/update-document.ts +++ b/packages/trpc/server/document-router/update-document.ts @@ -1,18 +1,19 @@ -import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; +import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { updateDocument } from '@documenso/lib/server-only/document/update-document'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { authenticatedProcedure } from '../trpc'; import { ZUpdateDocumentRequestSchema, ZUpdateDocumentResponseSchema, } from './update-document.types'; -import { updateDocumentMeta } from './update-document.types'; +import { updateDocumentMeta as updateDocumentTrpcMeta } from './update-document.types'; /** * Public route. */ export const updateDocumentRoute = authenticatedProcedure - .meta(updateDocumentMeta) + .meta(updateDocumentTrpcMeta) .input(ZUpdateDocumentRequestSchema) .output(ZUpdateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { @@ -28,10 +29,13 @@ export const updateDocumentRoute = authenticatedProcedure const userId = ctx.user.id; if (Object.values(meta).length > 0) { - await upsertDocumentMeta({ + await updateDocumentMeta({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, subject: meta.subject, message: meta.message, timezone: meta.timezone, @@ -51,11 +55,18 @@ export const updateDocumentRoute = authenticatedProcedure }); } - return await updateDocument({ + const envelope = await updateDocument({ userId, teamId, documentId, data, requestMetadata: ctx.metadata, }); + + const mappedDocument = { + ...envelope, + id: mapSecondaryIdToDocumentId(envelope.secondaryId), + }; + + return mappedDocument; }); diff --git a/packages/trpc/server/document-router/update-document.types.ts b/packages/trpc/server/document-router/update-document.types.ts index 03e5159e8..871550ea3 100644 --- a/packages/trpc/server/document-router/update-document.types.ts +++ b/packages/trpc/server/document-router/update-document.types.ts @@ -45,6 +45,7 @@ export const ZUpdateDocumentRequestSchema = z.object({ globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(), globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(), useLegacyFieldInsertion: z.boolean().optional(), + folderId: z.string().nullish(), }) .optional(), meta: z diff --git a/packages/trpc/server/embedding-router/apply-multi-sign-signature.ts b/packages/trpc/server/embedding-router/apply-multi-sign-signature.ts index 34d4b082d..700690c4e 100644 --- a/packages/trpc/server/embedding-router/apply-multi-sign-signature.ts +++ b/packages/trpc/server/embedding-router/apply-multi-sign-signature.ts @@ -68,7 +68,7 @@ export const applyMultiSignSignatureRoute = procedure const signatureFields = await prisma.field.findMany({ where: { - documentId: envelope.document.id, + envelopeId: envelope.document.id, recipientId: envelope.recipient.id, type: FieldType.SIGNATURE, inserted: false, diff --git a/packages/trpc/server/embedding-router/create-embedding-document.ts b/packages/trpc/server/embedding-router/create-embedding-document.ts index e3271c1b4..447a57ae7 100644 --- a/packages/trpc/server/embedding-router/create-embedding-document.ts +++ b/packages/trpc/server/embedding-router/create-embedding-document.ts @@ -1,6 +1,9 @@ +import { EnvelopeType } from '@prisma/client'; + import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { createDocumentV2 } from '@documenso/lib/server-only/document/create-document-v2'; import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { procedure } from '../trpc'; import { @@ -29,27 +32,34 @@ export const createEmbeddingDocumentRoute = procedure const { title, documentDataId, externalId, recipients, meta } = input; - const document = await createDocumentV2({ + const envelope = await createEnvelope({ data: { + type: EnvelopeType.DOCUMENT, title, externalId, recipients, + envelopeItems: [ + { + documentDataId, + }, + ], }, meta, - documentDataId, userId: apiToken.userId, teamId: apiToken.teamId ?? undefined, requestMetadata: metadata, }); - if (!document.id) { + if (!envelope.id) { throw new AppError(AppErrorCode.UNKNOWN_ERROR, { message: 'Failed to create document: missing document ID', }); } + const legacyDocumentId = mapSecondaryIdToDocumentId(envelope.secondaryId); + return { - documentId: document.id, + documentId: legacyDocumentId, }; } catch (error) { if (error instanceof AppError) { diff --git a/packages/trpc/server/embedding-router/create-embedding-template.ts b/packages/trpc/server/embedding-router/create-embedding-template.ts index d7c1aeb21..81412e73d 100644 --- a/packages/trpc/server/embedding-router/create-embedding-template.ts +++ b/packages/trpc/server/embedding-router/create-embedding-template.ts @@ -1,6 +1,9 @@ +import { EnvelopeType } from '@prisma/client'; + import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; -import { createTemplate } from '@documenso/lib/server-only/template/create-template'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; import { prisma } from '@documenso/prisma'; import { procedure } from '../trpc'; @@ -12,7 +15,7 @@ import { export const createEmbeddingTemplateRoute = procedure .input(ZCreateEmbeddingTemplateRequestSchema) .output(ZCreateEmbeddingTemplateResponseSchema) - .mutation(async ({ input, ctx: { req } }) => { + .mutation(async ({ input, ctx: { req, metadata } }) => { try { const authorizationHeader = req.headers.get('authorization'); @@ -31,20 +34,30 @@ export const createEmbeddingTemplateRoute = procedure const { title, documentDataId, recipients, meta } = input; // First create the template - const template = await createTemplate({ + const template = await createEnvelope({ userId: apiToken.userId, - data: { - title, - }, - templateDocumentDataId: documentDataId, teamId: apiToken.teamId ?? undefined, + data: { + type: EnvelopeType.TEMPLATE, + title, + envelopeItems: [ + { + documentDataId, + }, + ], + }, + meta, // Todo: Migration - Test this. + requestMetadata: metadata, }); + // Todo: Envelopes - Support multiple items. + const firstEnvelopeItem = template.envelopeItems[0]; + await Promise.all( recipients.map(async (recipient) => { const createdRecipient = await prisma.recipient.create({ data: { - templateId: template.id, + envelopeId: template.id, email: recipient.email, name: recipient.name || '', role: recipient.role || 'SIGNER', @@ -57,6 +70,8 @@ export const createEmbeddingTemplateRoute = procedure const createdFields = await prisma.field.createMany({ data: fields.map((field) => ({ + envelopeId: template.id, + envelopeItemId: firstEnvelopeItem.id, recipientId: createdRecipient.id, type: field.type, page: field.pageNumber, @@ -66,7 +81,6 @@ export const createEmbeddingTemplateRoute = procedure height: field.height, customText: '', inserted: false, - templateId: template.id, })), }); @@ -77,37 +91,6 @@ export const createEmbeddingTemplateRoute = procedure }), ); - // Update the template meta if needed - if (meta) { - const upsertMetaData = { - subject: meta.subject, - message: meta.message, - timezone: meta.timezone, - dateFormat: meta.dateFormat, - distributionMethod: meta.distributionMethod, - signingOrder: meta.signingOrder, - redirectUrl: meta.redirectUrl, - language: meta.language, - typedSignatureEnabled: meta.typedSignatureEnabled, - drawSignatureEnabled: meta.drawSignatureEnabled, - uploadSignatureEnabled: meta.uploadSignatureEnabled, - emailSettings: meta.emailSettings, - }; - - await prisma.documentMeta.upsert({ - where: { - templateId: template.id, - }, - create: { - templateId: template.id, - ...upsertMetaData, - }, - update: { - ...upsertMetaData, - }, - }); - } - if (!template.id) { throw new AppError(AppErrorCode.UNKNOWN_ERROR, { message: 'Failed to create template: missing template ID', @@ -115,7 +98,7 @@ export const createEmbeddingTemplateRoute = procedure } return { - templateId: template.id, + templateId: mapSecondaryIdToTemplateId(template.secondaryId), }; } catch (error) { if (error instanceof AppError) { diff --git a/packages/trpc/server/embedding-router/get-multi-sign-document.types.ts b/packages/trpc/server/embedding-router/get-multi-sign-document.types.ts index 2ab73db72..d4f7f5cc2 100644 --- a/packages/trpc/server/embedding-router/get-multi-sign-document.types.ts +++ b/packages/trpc/server/embedding-router/get-multi-sign-document.types.ts @@ -25,9 +25,7 @@ export const ZGetMultiSignDocumentResponseSchema = ZDocumentLiteSchema.extend({ subject: true, message: true, timezone: true, - password: true, dateFormat: true, - documentId: true, redirectUrl: true, typedSignatureEnabled: true, uploadSignatureEnabled: true, diff --git a/packages/trpc/server/embedding-router/update-embedding-document.ts b/packages/trpc/server/embedding-router/update-embedding-document.ts index cd8dc305c..1f1d15665 100644 --- a/packages/trpc/server/embedding-router/update-embedding-document.ts +++ b/packages/trpc/server/embedding-router/update-embedding-document.ts @@ -1,5 +1,5 @@ import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; +import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; import { updateDocument } from '@documenso/lib/server-only/document/update-document'; import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embedding-presign/verify-embedding-presign-token'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; @@ -40,8 +40,11 @@ export const updateEmbeddingDocumentRoute = procedure const { documentId, title, externalId, recipients, meta } = input; if (meta && Object.values(meta).length > 0) { - await upsertDocumentMeta({ - documentId: documentId, + await updateDocumentMeta({ + id: { + type: 'documentId', + id: documentId, + }, userId: apiToken.userId, teamId: apiToken.teamId ?? undefined, ...meta, diff --git a/packages/trpc/server/embedding-router/update-embedding-template.ts b/packages/trpc/server/embedding-router/update-embedding-template.ts index 38bf2a27a..2802da8f5 100644 --- a/packages/trpc/server/embedding-router/update-embedding-template.ts +++ b/packages/trpc/server/embedding-router/update-embedding-template.ts @@ -57,7 +57,10 @@ export const updateEmbeddingTemplateRoute = procedure const { recipients: updatedRecipients } = await setTemplateRecipients({ userId: apiToken.userId, teamId: apiToken.teamId ?? undefined, - templateId, + id: { + type: 'templateId', + id: templateId, + }, recipients: recipientsWithClientId.map((recipient) => ({ id: recipient.id, email: recipient.email, diff --git a/packages/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index 462635544..fffa5a652 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -1,5 +1,6 @@ -import { createDocumentFields } from '@documenso/lib/server-only/field/create-document-fields'; -import { createTemplateFields } from '@documenso/lib/server-only/field/create-template-fields'; +import { EnvelopeType } from '@prisma/client'; + +import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields'; import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field'; import { deleteTemplateField } from '@documenso/lib/server-only/field/delete-template-field'; import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; @@ -72,6 +73,7 @@ export const fieldRouter = router({ userId: ctx.user.id, teamId, fieldId, + envelopeType: EnvelopeType.DOCUMENT, }); }), @@ -100,10 +102,13 @@ export const fieldRouter = router({ }, }); - const createdFields = await createDocumentFields({ + const createdFields = await createEnvelopeFields({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, fields: [field], requestMetadata: ctx.metadata, }); @@ -136,10 +141,13 @@ export const fieldRouter = router({ }, }); - return await createDocumentFields({ + return await createEnvelopeFields({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, fields, requestMetadata: ctx.metadata, }); @@ -312,11 +320,15 @@ export const fieldRouter = router({ }, }); - const createdFields = await createTemplateFields({ + const createdFields = await createEnvelopeFields({ userId: ctx.user.id, teamId, - templateId, + id: { + type: 'templateId', + id: templateId, + }, fields: [field], + requestMetadata: ctx.metadata, }); return createdFields.fields[0]; @@ -352,6 +364,7 @@ export const fieldRouter = router({ userId: ctx.user.id, teamId, fieldId, + envelopeType: EnvelopeType.TEMPLATE, }); }), @@ -380,11 +393,15 @@ export const fieldRouter = router({ }, }); - return await createTemplateFields({ + return await createEnvelopeFields({ userId: ctx.user.id, teamId, - templateId, + id: { + type: 'templateId', + id: templateId, + }, fields, + requestMetadata: ctx.metadata, }); }), diff --git a/packages/trpc/server/folder-router/router.ts b/packages/trpc/server/folder-router/router.ts index 742567b30..53148c43b 100644 --- a/packages/trpc/server/folder-router/router.ts +++ b/packages/trpc/server/folder-router/router.ts @@ -4,13 +4,10 @@ import { deleteFolder } from '@documenso/lib/server-only/folder/delete-folder'; import { findFolders } from '@documenso/lib/server-only/folder/find-folders'; import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-folder-breadcrumbs'; import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id'; -import { moveDocumentToFolder } from '@documenso/lib/server-only/folder/move-document-to-folder'; import { moveFolder } from '@documenso/lib/server-only/folder/move-folder'; -import { moveTemplateToFolder } from '@documenso/lib/server-only/folder/move-template-to-folder'; import { pinFolder } from '@documenso/lib/server-only/folder/pin-folder'; import { unpinFolder } from '@documenso/lib/server-only/folder/unpin-folder'; import { updateFolder } from '@documenso/lib/server-only/folder/update-folder'; -import { FolderType } from '@documenso/lib/types/folder-type'; import { authenticatedProcedure, router } from '../trpc'; import { @@ -21,9 +18,7 @@ import { ZGenericSuccessResponse, ZGetFoldersResponseSchema, ZGetFoldersSchema, - ZMoveDocumentToFolderSchema, ZMoveFolderSchema, - ZMoveTemplateToFolderSchema, ZPinFolderSchema, ZSuccessResponseSchema, ZUnpinFolderSchema, @@ -266,95 +261,6 @@ export const folderRouter = router({ }; }), - /** - * @private - */ - moveDocumentToFolder: authenticatedProcedure - .input(ZMoveDocumentToFolderSchema) - .mutation(async ({ input, ctx }) => { - const { teamId, user } = ctx; - const { documentId, folderId } = input; - - ctx.logger.info({ - input: { - documentId, - folderId, - }, - }); - - if (folderId !== null) { - try { - await getFolderById({ - userId: user.id, - teamId, - folderId, - type: FolderType.DOCUMENT, - }); - } catch (error) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - } - - const result = await moveDocumentToFolder({ - userId: user.id, - teamId, - documentId, - folderId, - requestMetadata: ctx.metadata, - }); - - return { - ...result, - type: FolderType.DOCUMENT, - }; - }), - - /** - * @private - */ - moveTemplateToFolder: authenticatedProcedure - .input(ZMoveTemplateToFolderSchema) - .mutation(async ({ input, ctx }) => { - const { teamId, user } = ctx; - const { templateId, folderId } = input; - - ctx.logger.info({ - input: { - templateId, - folderId, - }, - }); - - if (folderId !== null) { - try { - await getFolderById({ - userId: user.id, - teamId, - folderId, - type: FolderType.TEMPLATE, - }); - } catch (error) { - throw new AppError(AppErrorCode.NOT_FOUND, { - message: 'Folder not found', - }); - } - } - - const result = await moveTemplateToFolder({ - userId: user.id, - teamId, - templateId, - folderId, - }); - - return { - ...result, - type: FolderType.TEMPLATE, - }; - }), - /** * @private */ diff --git a/packages/trpc/server/folder-router/schema.ts b/packages/trpc/server/folder-router/schema.ts index 6595cea4d..53e512dc9 100644 --- a/packages/trpc/server/folder-router/schema.ts +++ b/packages/trpc/server/folder-router/schema.ts @@ -77,18 +77,6 @@ export const ZMoveFolderSchema = z.object({ type: ZFolderTypeSchema.optional(), }); -export const ZMoveDocumentToFolderSchema = z.object({ - documentId: z.number(), - folderId: z.string().nullable().optional(), - type: z.enum(['DOCUMENT']).optional(), -}); - -export const ZMoveTemplateToFolderSchema = z.object({ - templateId: z.number(), - folderId: z.string().nullable().optional(), - type: z.enum(['TEMPLATE']).optional(), -}); - export const ZPinFolderSchema = z.object({ folderId: z.string(), type: ZFolderTypeSchema.optional(), diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index 401251b27..39406b0c4 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -1,3 +1,5 @@ +import { EnvelopeType } from '@prisma/client'; + import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { rejectDocumentWithToken } from '@documenso/lib/server-only/document/reject-document-with-token'; import { createDocumentRecipients } from '@documenso/lib/server-only/recipient/create-document-recipients'; @@ -77,6 +79,7 @@ export const recipientRouter = router({ userId: ctx.user.id, teamId, recipientId, + type: EnvelopeType.DOCUMENT, }); }), @@ -108,7 +111,10 @@ export const recipientRouter = router({ const createdRecipients = await createDocumentRecipients({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, recipients: [recipient], requestMetadata: ctx.metadata, }); @@ -144,7 +150,10 @@ export const recipientRouter = router({ return await createDocumentRecipients({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, recipients, requestMetadata: ctx.metadata, }); @@ -178,7 +187,10 @@ export const recipientRouter = router({ const updatedRecipients = await updateDocumentRecipients({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, recipients: [recipient], requestMetadata: ctx.metadata, }); @@ -214,7 +226,10 @@ export const recipientRouter = router({ return await updateDocumentRecipients({ userId: ctx.user.id, teamId, - documentId, + id: { + type: 'documentId', + id: documentId, + }, recipients, requestMetadata: ctx.metadata, }); @@ -316,6 +331,7 @@ export const recipientRouter = router({ userId: ctx.user.id, teamId, recipientId, + type: EnvelopeType.TEMPLATE, }); }), @@ -507,7 +523,10 @@ export const recipientRouter = router({ return await setTemplateRecipients({ userId: ctx.user.id, teamId, - templateId, + id: { + type: 'templateId', + id: templateId, + }, recipients: recipients.map((recipient) => ({ id: recipient.nativeId, email: recipient.email, @@ -533,9 +552,12 @@ export const recipientRouter = router({ }, }); - return await completeDocumentWithToken({ + await completeDocumentWithToken({ token, - documentId, + id: { + type: 'documentId', + id: documentId, + }, authOptions, accessAuthOptions, nextSigner, @@ -560,7 +582,10 @@ export const recipientRouter = router({ return await rejectDocumentWithToken({ token, - documentId, + id: { + type: 'documentId', + id: documentId, + }, reason, requestMetadata: ctx.metadata.requestMetadata, }); diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index fbe35147c..871f60d62 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -9,7 +9,6 @@ import { folderRouter } from './folder-router/router'; import { organisationRouter } from './organisation-router/router'; import { profileRouter } from './profile-router/router'; import { recipientRouter } from './recipient-router/router'; -import { shareLinkRouter } from './share-link-router/router'; import { teamRouter } from './team-router/router'; import { templateRouter } from './template-router/router'; import { router } from './trpc'; @@ -25,7 +24,6 @@ export const appRouter = router({ recipient: recipientRouter, admin: adminRouter, organisation: organisationRouter, - shareLink: shareLinkRouter, apiToken: apiTokenRouter, team: teamRouter, template: templateRouter, diff --git a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts deleted file mode 100644 index 20764d89d..000000000 --- a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; -import { buildTeamWhereQuery } from '@documenso/lib/utils/teams'; -import { prisma } from '@documenso/prisma'; - -import { procedure } from '../trpc'; -import { - ZGetDocumentInternalUrlForQRCodeInput, - ZGetDocumentInternalUrlForQRCodeOutput, -} from './get-document-internal-url-for-qr-code.types'; - -export const getDocumentInternalUrlForQRCodeRoute = procedure - .input(ZGetDocumentInternalUrlForQRCodeInput) - .output(ZGetDocumentInternalUrlForQRCodeOutput) - .query(async ({ input, ctx }) => { - const { documentId } = input; - - ctx.logger.info({ - input: { - documentId, - }, - }); - - if (!ctx.user) { - return null; - } - - const document = await prisma.document.findFirst({ - where: { - OR: [ - { - id: documentId, - userId: ctx.user.id, - }, - { - id: documentId, - team: buildTeamWhereQuery({ teamId: undefined, userId: ctx.user.id }), - }, - ], - }, - include: { - team: true, - }, - }); - - if (!document) { - return null; - } - - return `${NEXT_PUBLIC_WEBAPP_URL()}/t/${document.team.url}/documents/${document.id}`; - }); diff --git a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts b/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts deleted file mode 100644 index cc7a56c1d..000000000 --- a/packages/trpc/server/share-link-router/get-document-internal-url-for-qr-code.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; - -export const ZGetDocumentInternalUrlForQRCodeInput = z.object({ - documentId: z.number(), -}); - -export type TGetDocumentInternalUrlForQRCodeInput = z.infer< - typeof ZGetDocumentInternalUrlForQRCodeInput ->; - -export const ZGetDocumentInternalUrlForQRCodeOutput = z.string().nullable(); - -export type TGetDocumentInternalUrlForQRCodeOutput = z.infer< - typeof ZGetDocumentInternalUrlForQRCodeOutput ->; diff --git a/packages/trpc/server/share-link-router/router.ts b/packages/trpc/server/share-link-router/router.ts deleted file mode 100644 index 3f8ac695f..000000000 --- a/packages/trpc/server/share-link-router/router.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createOrGetShareLink } from '@documenso/lib/server-only/share/create-or-get-share-link'; - -import { procedure, router } from '../trpc'; -import { getDocumentInternalUrlForQRCodeRoute } from './get-document-internal-url-for-qr-code'; -import { ZCreateOrGetShareLinkMutationSchema } from './schema'; - -export const shareLinkRouter = router({ - createOrGetShareLink: procedure - .input(ZCreateOrGetShareLinkMutationSchema) - .mutation(async ({ ctx, input }) => { - const { documentId, token } = input; - - ctx.logger.info({ - input: { - documentId, - }, - }); - - if (token) { - return await createOrGetShareLink({ documentId, token }); - } - - if (!ctx.user?.id) { - throw new Error( - 'You must either provide a token or be logged in to create a sharing link.', - ); - } - - return await createOrGetShareLink({ documentId, userId: ctx.user.id }); - }), - - getDocumentInternalUrlForQRCode: getDocumentInternalUrlForQRCodeRoute, -}); diff --git a/packages/trpc/server/share-link-router/schema.ts b/packages/trpc/server/share-link-router/schema.ts deleted file mode 100644 index 9ea599042..000000000 --- a/packages/trpc/server/share-link-router/schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod'; - -export const ZCreateOrGetShareLinkMutationSchema = z.object({ - documentId: z.number(), - token: z.string().optional(), -}); - -export type TCreateOrGetShareLinkMutationSchema = z.infer< - typeof ZCreateOrGetShareLinkMutationSchema ->; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index a9041ad3e..58990cb42 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -1,5 +1,5 @@ -import type { Document } from '@prisma/client'; -import { DocumentDataType } from '@prisma/client'; +import type { Envelope } from '@prisma/client'; +import { DocumentDataType, EnvelopeType } from '@prisma/client'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; @@ -7,15 +7,12 @@ import { jobs } from '@documenso/lib/jobs/client'; import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; +import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { ZCreateDocumentFromDirectTemplateResponseSchema, createDocumentFromDirectTemplate, } from '@documenso/lib/server-only/template/create-document-from-direct-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; -import { - ZCreateTemplateResponseSchema, - createTemplate, -} from '@documenso/lib/server-only/template/create-template'; import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/delete-template-direct-link'; @@ -25,6 +22,8 @@ import { getTemplateById } from '@documenso/lib/server-only/template/get-templat import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link'; import { updateTemplate } from '@documenso/lib/server-only/template/update-template'; import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions'; +import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope'; +import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates'; import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema'; import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc'; @@ -36,6 +35,7 @@ import { ZCreateTemplateDirectLinkRequestSchema, ZCreateTemplateDirectLinkResponseSchema, ZCreateTemplateMutationSchema, + ZCreateTemplateResponseSchema, ZCreateTemplateV2RequestSchema, ZCreateTemplateV2ResponseSchema, ZDeleteTemplateDirectLinkRequestSchema, @@ -77,11 +77,41 @@ export const templateRouter = router({ }, }); - return await findTemplates({ + const result = await findTemplates({ userId: ctx.user.id, teamId, ...input, }); + + // Remapping for backwards compatibility. + return { + ...result, + data: result.data.map((envelope) => { + const legacyTemplateId = mapSecondaryIdToTemplateId(envelope.secondaryId); + + return { + id: legacyTemplateId, + type: envelope.templateType, + visibility: envelope.visibility, + externalId: envelope.externalId, + title: envelope.title, + userId: envelope.userId, + teamId: envelope.teamId, + authOptions: envelope.authOptions, + createdAt: envelope.createdAt, + updatedAt: envelope.updatedAt, + publicTitle: envelope.publicTitle, + publicDescription: envelope.publicDescription, + folderId: envelope.folderId, + useLegacyFieldInsertion: envelope.useLegacyFieldInsertion, + team: envelope.team, + fields: envelope.fields, + recipients: envelope.recipients, + templateMeta: envelope.documentMeta, + directLink: envelope.directLink, + }; + }), + }; }), /** @@ -121,7 +151,7 @@ export const templateRouter = router({ * @private */ createTemplate: authenticatedProcedure - // .meta({ + // .meta({ // Note before releasing this to public, update the response schema to be correct. // openapi: { // method: 'POST', // path: '/template/create', @@ -142,15 +172,25 @@ export const templateRouter = router({ }, }); - return await createTemplate({ + const envelope = await createEnvelope({ userId: ctx.user.id, teamId, - templateDocumentDataId, data: { + type: EnvelopeType.TEMPLATE, title, folderId, + envelopeItems: [ + { + documentDataId: templateDocumentDataId, + }, + ], }, + requestMetadata: ctx.metadata, }); + + return { + legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId), + }; }), /** @@ -197,26 +237,34 @@ export const templateRouter = router({ type: DocumentDataType.S3_PATH, }); - const createdTemplate = await createTemplate({ + const createdTemplate = await createEnvelope({ userId: user.id, teamId, - templateDocumentDataId: templateDocumentData.id, data: { + type: EnvelopeType.TEMPLATE, title, + envelopeItems: [ + { + documentDataId: templateDocumentData.id, + }, + ], folderId, - externalId, + externalId: externalId ?? undefined, visibility, globalAccessAuth, globalActionAuth, + templateType: type, publicTitle, publicDescription, - type, }, meta, + requestMetadata: ctx.metadata, }); + const legacyTemplateId = mapSecondaryIdToTemplateId(createdTemplate.secondaryId); + const fullTemplate = await getTemplateById({ - id: createdTemplate.id, + id: legacyTemplateId, userId: user.id, teamId, }); @@ -252,13 +300,15 @@ export const templateRouter = router({ }, }); - return await updateTemplate({ + const envelope = await updateTemplate({ userId, teamId, templateId, data, meta, }); + + return mapEnvelopeToTemplateLite(envelope); }), /** @@ -285,11 +335,16 @@ export const templateRouter = router({ }, }); - return await duplicateTemplate({ + const envelope = await duplicateTemplate({ userId: ctx.user.id, teamId, - templateId, + id: { + type: 'templateId', + id: templateId, + }, }); + + return mapEnvelopeToTemplateLite(envelope); }), /** @@ -360,8 +415,11 @@ export const templateRouter = router({ throw new Error('You have reached your document limit.'); } - const document: Document = await createDocumentFromTemplate({ - templateId, + const envelope: Envelope = await createDocumentFromTemplate({ + id: { + type: 'templateId', + id: templateId, + }, teamId, userId: ctx.user.id, recipients, @@ -373,7 +431,10 @@ export const templateRouter = router({ if (distributeDocument) { await sendDocument({ - documentId: document.id, + id: { + type: 'envelopeId', + id: envelope.id, + }, userId: ctx.user.id, teamId, requestMetadata: ctx.metadata, @@ -385,7 +446,10 @@ export const templateRouter = router({ } return getDocumentWithDetailsById({ - documentId: document.id, + id: { + type: 'envelopeId', + id: envelope.id, + }, userId: ctx.user.id, teamId, }); @@ -480,7 +544,15 @@ export const templateRouter = router({ }); } - return await createTemplateDirectLink({ userId, teamId, templateId, directRecipientId }); + return await createTemplateDirectLink({ + userId, + teamId, + id: { + type: 'templateId', + id: templateId, + }, + directRecipientId, + }); }), /** diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts index a00717f33..62daa4b10 100644 --- a/packages/trpc/server/template-router/schema.ts +++ b/packages/trpc/server/template-router/schema.ts @@ -146,11 +146,13 @@ export const ZCreateTemplateDirectLinkRequestSchema = z.object({ const GenericDirectLinkResponseSchema = TemplateDirectLinkSchema.pick({ id: true, - templateId: true, token: true, createdAt: true, enabled: true, directTemplateRecipientId: true, + // envelopeId: true, // Todo: Envelopes +}).extend({ + templateId: z.number(), }); export const ZCreateTemplateDirectLinkResponseSchema = GenericDirectLinkResponseSchema; @@ -194,6 +196,10 @@ export const ZCreateTemplateV2ResponseSchema = z.object({ uploadUrl: z.string().min(1), }); +export const ZCreateTemplateResponseSchema = z.object({ + legacyTemplateId: z.number(), +}); + export const ZUpdateTemplateRequestSchema = z.object({ templateId: z.number(), data: z @@ -207,6 +213,7 @@ export const ZUpdateTemplateRequestSchema = z.object({ publicDescription: ZTemplatePublicDescriptionSchema.optional(), type: z.nativeEnum(TemplateType).optional(), useLegacyFieldInsertion: z.boolean().optional(), + folderId: z.string().nullish(), }) .optional(), meta: ZTemplateMetaUpsertSchema.optional(), diff --git a/packages/ui/components/document/document-read-only-fields.tsx b/packages/ui/components/document/document-read-only-fields.tsx index affa6e4e9..5d3cb2539 100644 --- a/packages/ui/components/document/document-read-only-fields.tsx +++ b/packages/ui/components/document/document-read-only-fields.tsx @@ -8,7 +8,6 @@ import { Clock, EyeOffIcon } from 'lucide-react'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template'; -import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document'; import { parseMessageDescriptor } from '@documenso/lib/utils/i18n'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { FieldRootContainer } from '@documenso/ui/components/field/field'; @@ -34,6 +33,10 @@ const getRecipientDisplayText = (recipient: { name: string; email: string }) => return recipient.email; }; +export type DocumentField = Field & { + recipient: Pick; +}; + export type DocumentReadOnlyFieldsProps = { fields: DocumentField[]; documentMeta?: Pick; diff --git a/packages/ui/components/document/document-share-button.tsx b/packages/ui/components/document/document-share-button.tsx index 23e47794b..f546abaeb 100644 --- a/packages/ui/components/document/document-share-button.tsx +++ b/packages/ui/components/document/document-share-button.tsx @@ -60,7 +60,7 @@ export const DocumentShareButton = ({ mutateAsync: createOrGetShareLink, data: shareLink, isPending: isCreatingOrGettingShareLink, - } = trpc.shareLink.createOrGetShareLink.useMutation(); + } = trpc.document.share.useMutation(); const isLoading = isCreatingOrGettingShareLink || isCopyingShareLink; diff --git a/packages/ui/components/field/field-tooltip.tsx b/packages/ui/components/field/field-tooltip.tsx index f658875e8..308da7cbd 100644 --- a/packages/ui/components/field/field-tooltip.tsx +++ b/packages/ui/components/field/field-tooltip.tsx @@ -29,7 +29,10 @@ const tooltipVariants = cva('font-semibold', { interface FieldToolTipProps extends VariantProps { children: React.ReactNode; className?: string; - field: Field; + field: Pick< + Field, + 'id' | 'inserted' | 'fieldMeta' | 'positionX' | 'positionY' | 'width' | 'height' | 'page' + >; } /** diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index eef021fe0..26e134e45 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -120,7 +120,7 @@ export const AddFieldsFormPartial = ({ defaultValues: { fields: fields.map((field) => ({ nativeId: field.id, - formId: `${field.id}-${field.documentId}`, + formId: `${field.id}-${field.envelopeItemId}`, pageNumber: field.page, type: field.type, pageX: Number(field.positionX), diff --git a/packages/ui/primitives/template-flow/add-template-fields.tsx b/packages/ui/primitives/template-flow/add-template-fields.tsx index 7db417036..321251f1f 100644 --- a/packages/ui/primitives/template-flow/add-template-fields.tsx +++ b/packages/ui/primitives/template-flow/add-template-fields.tsx @@ -109,7 +109,7 @@ export const AddTemplateFieldsFormPartial = ({ defaultValues: { fields: fields.map((field) => ({ nativeId: field.id, - formId: `${field.id}-${field.templateId}`, + formId: `${field.id}-${field.envelopeItemId}`, pageNumber: field.page, type: field.type, pageX: Number(field.positionX),