mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
2 Commits
36b9a14563
...
feat/allow
| Author | SHA1 | Date | |
|---|---|---|---|
| 87aa628dc8 | |||
| c85c0cf610 |
@ -7,9 +7,9 @@ import { FilePlus, Loader } from 'lucide-react';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@ -54,13 +54,17 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
|
|||||||
setIsUploadingFile(true);
|
setIsUploadingFile(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await putPdfFile(file);
|
const payload = {
|
||||||
|
|
||||||
const { legacyTemplateId: id } = await createTemplate({
|
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId: response.id,
|
|
||||||
folderId: folderId,
|
folderId: folderId,
|
||||||
});
|
} satisfies TCreateTemplatePayloadSchema;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const { envelopeId: id } = await createTemplate(formData);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template document uploaded`),
|
title: _(msg`Template document uploaded`),
|
||||||
|
|||||||
@ -16,9 +16,9 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l
|
|||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -62,14 +62,18 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const response = await putPdfFile(file);
|
const payload = {
|
||||||
|
|
||||||
const { legacyDocumentId: id } = await createDocument({
|
|
||||||
title: file.name,
|
title: file.name,
|
||||||
documentDataId: response.id,
|
timezone: userTimezone,
|
||||||
timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field.
|
|
||||||
folderId: folderId ?? undefined,
|
folderId: folderId ?? undefined,
|
||||||
});
|
} satisfies TCreateDocumentPayloadSchema;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const { envelopeId: id } = await createDocument(formData);
|
||||||
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
|
||||||
import {
|
import {
|
||||||
@ -73,14 +73,18 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) =
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const response = await putPdfFile(file);
|
const payload = {
|
||||||
|
|
||||||
const { legacyDocumentId: id } = await createDocument({
|
|
||||||
title: file.name,
|
title: file.name,
|
||||||
documentDataId: response.id,
|
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
folderId: folderId ?? undefined,
|
folderId: folderId ?? undefined,
|
||||||
});
|
} satisfies TCreateDocumentPayloadSchema;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const { envelopeId: id } = await createDocument(formData);
|
||||||
|
|
||||||
void refreshLimits();
|
void refreshLimits();
|
||||||
|
|
||||||
|
|||||||
@ -14,9 +14,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
|
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
|
||||||
import {
|
import {
|
||||||
@ -78,35 +78,24 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const result = await Promise.all(
|
const payload = {
|
||||||
files.map(async (file) => {
|
|
||||||
try {
|
|
||||||
const response = await putPdfFile(file);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: file.name,
|
|
||||||
documentDataId: response.id,
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
throw new Error('Failed to upload document');
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const envelopeItemsToCreate = result.filter(
|
|
||||||
(item): item is { title: string; documentDataId: string } => item !== undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { id } = await createEnvelope({
|
|
||||||
folderId,
|
folderId,
|
||||||
type,
|
type,
|
||||||
title: files[0].name,
|
title: files[0].name,
|
||||||
items: envelopeItemsToCreate,
|
|
||||||
meta: {
|
meta: {
|
||||||
timezone: userTimezone,
|
timezone: userTimezone,
|
||||||
},
|
},
|
||||||
}).catch((error) => {
|
} satisfies TCreateEnvelopePayload;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await createEnvelope(formData).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -10,9 +10,9 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
import { formatTemplatesPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
@ -40,13 +40,17 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
const documentData = await putPdfFile(file);
|
const payload = {
|
||||||
|
|
||||||
const { legacyTemplateId: id } = await createTemplate({
|
|
||||||
title: file.name,
|
title: file.name,
|
||||||
templateDocumentDataId: documentData.id,
|
|
||||||
folderId: folderId ?? undefined,
|
folderId: folderId ?? undefined,
|
||||||
});
|
} satisfies TCreateTemplatePayloadSchema;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const { envelopeId: id } = await createTemplate(formData);
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Template uploaded`),
|
title: _(msg`Template uploaded`),
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import type { Context } from 'hono';
|
import type { Context } from 'hono';
|
||||||
import { createOpenApiFetchHandler } from 'trpc-to-openapi';
|
|
||||||
|
|
||||||
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
|
import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
|
||||||
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
||||||
import { createTrpcContext } from '@documenso/trpc/server/context';
|
import { createTrpcContext } from '@documenso/trpc/server/context';
|
||||||
import { appRouter } from '@documenso/trpc/server/router';
|
import { appRouter } from '@documenso/trpc/server/router';
|
||||||
|
import { createOpenApiFetchHandler } from '@documenso/trpc/utils/openapi-fetch-handler';
|
||||||
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
|
||||||
|
|
||||||
export const openApiTrpcServerHandler = async (c: Context) => {
|
export const openApiTrpcServerHandler = async (c: Context) => {
|
||||||
|
|||||||
820
package-lock.json
generated
820
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -44,7 +44,7 @@
|
|||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"@lingui/cli": "^5.2.0",
|
"@lingui/cli": "^5.2.0",
|
||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.18.0",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"dotenv-cli": "^8.0.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
@ -54,11 +54,21 @@
|
|||||||
"nodemailer": "^6.10.1",
|
"nodemailer": "^6.10.1",
|
||||||
"playwright": "1.52.0",
|
"playwright": "1.52.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.18.0",
|
||||||
"prisma-extension-kysely": "^3.0.0",
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"turbo": "^1.9.3",
|
"turbo": "^1.9.3",
|
||||||
|
"@trpc/client": "11.7.0",
|
||||||
|
"@trpc/react-query": "11.7.0",
|
||||||
|
"@trpc/server": "11.7.0",
|
||||||
|
"superjson": "^2.2.5",
|
||||||
|
"trpc-to-openapi": "2.4.0",
|
||||||
|
"zod-openapi": "^4.2.4",
|
||||||
|
"@ts-rest/core": "^3.52.1",
|
||||||
|
"@ts-rest/open-api": "^3.52.1",
|
||||||
|
"@ts-rest/serverless": "^3.52.1",
|
||||||
|
"zod-prisma-types": "3.3.5",
|
||||||
"vite": "^6.3.5"
|
"vite": "^6.3.5"
|
||||||
},
|
},
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
@ -76,12 +86,12 @@
|
|||||||
"mupdf": "^1.0.0",
|
"mupdf": "^1.0.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,14 +17,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@ts-rest/core": "^3.30.5",
|
"@ts-rest/core": "^3.52.0",
|
||||||
"@ts-rest/open-api": "^3.33.0",
|
"@ts-rest/open-api": "^3.52.0",
|
||||||
"@ts-rest/serverless": "^3.30.5",
|
"@ts-rest/serverless": "^3.52.0",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^2.2.5",
|
||||||
"swagger-ui-react": "^5.21.0",
|
"swagger-ui-react": "^5.21.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -20,6 +20,6 @@
|
|||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,6 +19,6 @@
|
|||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,11 +55,11 @@
|
|||||||
"skia-canvas": "^3.0.8",
|
"skia-canvas": "^3.0.8",
|
||||||
"stripe": "^12.7.0",
|
"stripe": "^12.7.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/browser-chromium": "1.52.0",
|
"@playwright/browser-chromium": "1.52.0",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/pg": "^8.11.4"
|
"@types/pg": "^8.11.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -16,11 +16,16 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
|||||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
|
||||||
|
|
||||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
import type {
|
||||||
|
TDocumentAccessAuthTypes,
|
||||||
|
TDocumentActionAuthTypes,
|
||||||
|
TRecipientAccessAuthTypes,
|
||||||
|
TRecipientActionAuthTypes,
|
||||||
|
} from '../../types/document-auth';
|
||||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||||
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
|
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
|
||||||
|
import type { TFieldAndMeta } from '../../types/field-meta';
|
||||||
import {
|
import {
|
||||||
ZWebhookDocumentSchema,
|
ZWebhookDocumentSchema,
|
||||||
mapEnvelopeToWebhookDocumentPayload,
|
mapEnvelopeToWebhookDocumentPayload,
|
||||||
@ -34,6 +39,25 @@ import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-
|
|||||||
import { getTeamSettings } from '../team/get-team-settings';
|
import { getTeamSettings } from '../team/get-team-settings';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
|
|
||||||
|
type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & {
|
||||||
|
documentDataId: string;
|
||||||
|
page: number;
|
||||||
|
positionX: number;
|
||||||
|
positionY: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateEnvelopeRecipientOptions = {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: RecipientRole;
|
||||||
|
signingOrder?: number;
|
||||||
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
|
fields?: CreateEnvelopeRecipientFieldOptions[];
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateEnvelopeOptions = {
|
export type CreateEnvelopeOptions = {
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId: number;
|
teamId: number;
|
||||||
@ -56,7 +80,7 @@ export type CreateEnvelopeOptions = {
|
|||||||
visibility?: DocumentVisibility;
|
visibility?: DocumentVisibility;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
recipients?: TCreateEnvelopeRequest['recipients'];
|
recipients?: CreateEnvelopeRecipientOptions[];
|
||||||
folderId?: string;
|
folderId?: string;
|
||||||
};
|
};
|
||||||
attachments?: Array<{
|
attachments?: Array<{
|
||||||
|
|||||||
@ -1,13 +1,22 @@
|
|||||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||||
|
|
||||||
|
import { AppError } from '../../errors/app-error';
|
||||||
import { flattenAnnotations } from './flatten-annotations';
|
import { flattenAnnotations } from './flatten-annotations';
|
||||||
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
||||||
|
|
||||||
export const normalizePdf = async (pdf: Buffer) => {
|
export const normalizePdf = async (pdf: Buffer) => {
|
||||||
const pdfDoc = await PDFDocument.load(pdf).catch(() => null);
|
const pdfDoc = await PDFDocument.load(pdf).catch((e) => {
|
||||||
|
console.error(`PDF normalization error: ${e.message}`);
|
||||||
|
|
||||||
if (!pdfDoc) {
|
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||||
return pdf;
|
message: 'The document is not a valid PDF',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pdfDoc.isEncrypted) {
|
||||||
|
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||||
|
message: 'The document is encrypted',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeOptionalContentGroups(pdfDoc);
|
removeOptionalContentGroups(pdfDoc);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { env } from '@documenso/lib/utils/env';
|
|||||||
|
|
||||||
import { AppError } from '../../errors/app-error';
|
import { AppError } from '../../errors/app-error';
|
||||||
import { createDocumentData } from '../../server-only/document-data/create-document-data';
|
import { createDocumentData } from '../../server-only/document-data/create-document-data';
|
||||||
|
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
|
||||||
import { uploadS3File } from './server-actions';
|
import { uploadS3File } from './server-actions';
|
||||||
|
|
||||||
type File = {
|
type File = {
|
||||||
@ -43,6 +44,28 @@ export const putPdfFileServerSide = async (file: File) => {
|
|||||||
return await createDocumentData({ type, data });
|
return await createDocumentData({ type, data });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a pdf file and normalizes it.
|
||||||
|
*/
|
||||||
|
export const putNormalizedPdfFileServerSide = async (file: File) => {
|
||||||
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
|
|
||||||
|
const normalized = await normalizePdf(buffer);
|
||||||
|
|
||||||
|
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
|
||||||
|
|
||||||
|
const documentData = await putFileServerSide({
|
||||||
|
name: fileName,
|
||||||
|
type: 'application/pdf',
|
||||||
|
arrayBuffer: async () => Promise.resolve(normalized),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await createDocumentData({
|
||||||
|
type: documentData.type,
|
||||||
|
data: documentData.data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a file to the appropriate storage location.
|
* Uploads a file to the appropriate storage location.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -21,14 +21,14 @@
|
|||||||
"seed": "tsx ./seed-database.ts"
|
"seed": "tsx ./seed-database.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.18.0",
|
||||||
"kysely": "0.26.3",
|
"kysely": "0.26.3",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.18.0",
|
||||||
"prisma-extension-kysely": "^3.0.0",
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"prisma-json-types-generator": "^3.2.2",
|
"prisma-json-types-generator": "^3.6.2",
|
||||||
"ts-pattern": "^5.0.6",
|
"ts-pattern": "^5.0.6",
|
||||||
"zod-prisma-types": "3.2.4"
|
"zod-prisma-types": "3.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
|||||||
@ -134,8 +134,8 @@ model Passkey {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now())
|
updatedAt DateTime @default(now())
|
||||||
lastUsedAt DateTime?
|
lastUsedAt DateTime?
|
||||||
credentialId Bytes
|
credentialId Bytes /// @zod.custom.use(z.instanceof(Uint8Array))
|
||||||
credentialPublicKey Bytes
|
credentialPublicKey Bytes /// @zod.custom.use(z.instanceof(Uint8Array))
|
||||||
counter BigInt
|
counter BigInt
|
||||||
credentialDeviceType String
|
credentialDeviceType String
|
||||||
credentialBackedUp Boolean
|
credentialBackedUp Boolean
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import { createTRPCClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
|
import {
|
||||||
import SuperJSON from 'superjson';
|
createTRPCClient,
|
||||||
|
httpBatchLink,
|
||||||
|
httpLink,
|
||||||
|
isNonJsonSerializable,
|
||||||
|
splitLink,
|
||||||
|
} from '@trpc/client';
|
||||||
|
|
||||||
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
import type { AppRouter } from '../server/router';
|
import type { AppRouter } from '../server/router';
|
||||||
|
import { dataTransformer } from '../utils/data-transformer';
|
||||||
|
|
||||||
export const trpc = createTRPCClient<AppRouter>({
|
export const trpc = createTRPCClient<AppRouter>({
|
||||||
links: [
|
links: [
|
||||||
splitLink({
|
splitLink({
|
||||||
condition: (op) => op.context.skipBatch === true,
|
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input),
|
||||||
true: httpLink({
|
true: httpLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
transformer: SuperJSON,
|
transformer: dataTransformer,
|
||||||
headers: (opts) => {
|
headers: (opts) => {
|
||||||
if (typeof opts.op.context.teamId === 'string') {
|
if (typeof opts.op.context.teamId === 'string') {
|
||||||
return {
|
return {
|
||||||
@ -24,7 +30,7 @@ export const trpc = createTRPCClient<AppRouter>({
|
|||||||
}),
|
}),
|
||||||
false: httpBatchLink({
|
false: httpBatchLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
transformer: SuperJSON,
|
transformer: dataTransformer,
|
||||||
headers: (opts) => {
|
headers: (opts) => {
|
||||||
const operationWithTeamId = opts.opList.find(
|
const operationWithTeamId = opts.opList.find(
|
||||||
(op) => op.context.teamId && typeof op.context.teamId === 'string',
|
(op) => op.context.teamId && typeof op.context.teamId === 'string',
|
||||||
|
|||||||
@ -12,15 +12,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"@tanstack/react-query": "5.59.15",
|
"@tanstack/react-query": "5.90.5",
|
||||||
"@trpc/client": "11.0.0-rc.648",
|
"@trpc/client": "11.7.0",
|
||||||
"@trpc/react-query": "11.0.0-rc.648",
|
"@trpc/react-query": "11.7.0",
|
||||||
"@trpc/server": "11.0.0-rc.648",
|
"@trpc/server": "11.7.0",
|
||||||
"@ts-rest/core": "^3.30.5",
|
"@ts-rest/core": "^3.52.0",
|
||||||
|
"formidable": "^3.5.4",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^2.2.5",
|
||||||
"trpc-to-openapi": "2.0.4",
|
"trpc-to-openapi": "2.4.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76",
|
||||||
|
"zod-form-data": "^2.0.8",
|
||||||
|
"zod-openapi": "^4.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/formidable": "^3.4.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
|
import { httpBatchLink, httpLink, isNonJsonSerializable, splitLink } from '@trpc/client';
|
||||||
import { createTRPCReact } from '@trpc/react-query';
|
import { createTRPCReact } from '@trpc/react-query';
|
||||||
import SuperJSON from 'superjson';
|
|
||||||
|
|
||||||
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
import type { AppRouter } from '../server/router';
|
import type { AppRouter } from '../server/router';
|
||||||
|
import { dataTransformer } from '../utils/data-transformer';
|
||||||
|
|
||||||
export { getQueryKey } from '@trpc/react-query';
|
export { getQueryKey } from '@trpc/react-query';
|
||||||
|
|
||||||
@ -44,16 +44,16 @@ export function TrpcProvider({ children, headers }: TrpcProviderProps) {
|
|||||||
trpc.createClient({
|
trpc.createClient({
|
||||||
links: [
|
links: [
|
||||||
splitLink({
|
splitLink({
|
||||||
condition: (op) => op.context.skipBatch === true,
|
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input),
|
||||||
true: httpLink({
|
true: httpLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
headers,
|
headers,
|
||||||
transformer: SuperJSON,
|
transformer: dataTransformer,
|
||||||
}),
|
}),
|
||||||
false: httpBatchLink({
|
false: httpBatchLink({
|
||||||
url: `${getBaseUrl()}/api/trpc`,
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
headers,
|
headers,
|
||||||
transformer: SuperJSON,
|
transformer: dataTransformer,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
136
packages/trpc/server/document-router/create-document-formdata.ts
Normal file
136
packages/trpc/server/document-router/create-document-formdata.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
|
||||||
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||||
|
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { authenticatedProcedure } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZCreateDocumentFormDataRequestSchema,
|
||||||
|
ZCreateDocumentFormDataResponseSchema,
|
||||||
|
createDocumentFormDataMeta,
|
||||||
|
} from './create-document-formdata.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary endpoint for V2 Beta until we allow passthrough documents on create.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const createDocumentFormDataRoute = authenticatedProcedure
|
||||||
|
.meta(createDocumentFormDataMeta)
|
||||||
|
.input(ZCreateDocumentFormDataRequestSchema)
|
||||||
|
.output(ZCreateDocumentFormDataResponseSchema)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const { teamId, user } = ctx;
|
||||||
|
|
||||||
|
const { payload, file } = input;
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
externalId,
|
||||||
|
visibility,
|
||||||
|
globalAccessAuth,
|
||||||
|
globalActionAuth,
|
||||||
|
recipients,
|
||||||
|
meta,
|
||||||
|
folderId,
|
||||||
|
attachments,
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const { remaining } = await getServerLimits({ userId: user.id, teamId });
|
||||||
|
|
||||||
|
if (remaining.documents <= 0) {
|
||||||
|
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
|
||||||
|
message: 'You have reached your document limit for this month. Please upgrade your plan.',
|
||||||
|
statusCode: 400,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentData = await putPdfFileServerSide(file);
|
||||||
|
|
||||||
|
const createdEnvelope = await createEnvelope({
|
||||||
|
userId: ctx.user.id,
|
||||||
|
teamId,
|
||||||
|
normalizePdf: false, // Not normalizing because of presigned URL.
|
||||||
|
internalVersion: 1,
|
||||||
|
data: {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title,
|
||||||
|
externalId,
|
||||||
|
visibility,
|
||||||
|
globalAccessAuth,
|
||||||
|
globalActionAuth,
|
||||||
|
recipients: (recipients || []).map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
fields: (recipient.fields || []).map((field) => ({
|
||||||
|
...field,
|
||||||
|
page: field.pageNumber,
|
||||||
|
positionX: field.pageX,
|
||||||
|
positionY: field.pageY,
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
folderId,
|
||||||
|
envelopeItems: [
|
||||||
|
{
|
||||||
|
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
|
||||||
|
documentDataId: documentData.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
attachments,
|
||||||
|
meta: {
|
||||||
|
...meta,
|
||||||
|
emailSettings: meta?.emailSettings ?? undefined,
|
||||||
|
},
|
||||||
|
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: {
|
||||||
|
...createdEnvelope,
|
||||||
|
envelopeId: createdEnvelope.id,
|
||||||
|
documentDataId: firstDocumentData.id,
|
||||||
|
documentData: {
|
||||||
|
...firstDocumentData,
|
||||||
|
envelopeItemId: envelopeItems[0].id,
|
||||||
|
},
|
||||||
|
documentMeta: {
|
||||||
|
...createdEnvelope.documentMeta,
|
||||||
|
documentId: legacyDocumentId,
|
||||||
|
},
|
||||||
|
id: legacyDocumentId,
|
||||||
|
fields: createdEnvelope.fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
documentId: legacyDocumentId,
|
||||||
|
templateId: null,
|
||||||
|
})),
|
||||||
|
recipients: createdEnvelope.recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
documentId: legacyDocumentId,
|
||||||
|
templateId: null,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release.
|
||||||
|
};
|
||||||
|
});
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { zfd } from 'zod-form-data';
|
||||||
|
|
||||||
|
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
||||||
|
import {
|
||||||
|
ZDocumentAccessAuthTypesSchema,
|
||||||
|
ZDocumentActionAuthTypesSchema,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||||
|
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
|
||||||
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
|
import {
|
||||||
|
ZFieldHeightSchema,
|
||||||
|
ZFieldPageNumberSchema,
|
||||||
|
ZFieldPageXSchema,
|
||||||
|
ZFieldPageYSchema,
|
||||||
|
ZFieldWidthSchema,
|
||||||
|
} from '@documenso/lib/types/field';
|
||||||
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
|
||||||
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
|
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||||
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
|
import {
|
||||||
|
ZDocumentExternalIdSchema,
|
||||||
|
ZDocumentTitleSchema,
|
||||||
|
ZDocumentVisibilitySchema,
|
||||||
|
} from './schema';
|
||||||
|
|
||||||
|
export const createDocumentFormDataMeta: TrpcRouteMeta = {
|
||||||
|
openapi: {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/document/create/formdata',
|
||||||
|
contentTypes: ['multipart/form-data'],
|
||||||
|
summary: 'Create document',
|
||||||
|
description: 'Create a document using form data.',
|
||||||
|
tags: ['Document'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZCreateDocumentFormDataPayloadRequestSchema = z.object({
|
||||||
|
title: ZDocumentTitleSchema,
|
||||||
|
externalId: ZDocumentExternalIdSchema.optional(),
|
||||||
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
|
formValues: ZDocumentFormValuesSchema.optional(),
|
||||||
|
folderId: z
|
||||||
|
.string()
|
||||||
|
.describe(
|
||||||
|
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
recipients: z
|
||||||
|
.array(
|
||||||
|
ZCreateRecipientSchema.extend({
|
||||||
|
fields: ZFieldAndMetaSchema.and(
|
||||||
|
z.object({
|
||||||
|
pageNumber: ZFieldPageNumberSchema,
|
||||||
|
pageX: ZFieldPageXSchema,
|
||||||
|
pageY: ZFieldPageYSchema,
|
||||||
|
width: ZFieldWidthSchema,
|
||||||
|
height: ZFieldHeightSchema,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
.optional(),
|
||||||
|
attachments: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string().min(1, 'Label is required'),
|
||||||
|
data: z.string().url('Must be a valid URL'),
|
||||||
|
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
meta: ZDocumentMetaCreateSchema.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// !: Can't use zfd.formData() here because it receives `undefined`
|
||||||
|
// !: somewhere in the pipeline of our openapi schema generation and throws
|
||||||
|
// !: an error.
|
||||||
|
export const ZCreateDocumentFormDataRequestSchema = zodFormData({
|
||||||
|
payload: zfd.json(ZCreateDocumentFormDataPayloadRequestSchema),
|
||||||
|
file: zfd.file(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ZCreateDocumentFormDataResponseSchema = z.object({
|
||||||
|
document: ZDocumentSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TCreateDocumentFormDataRequest = z.infer<typeof ZCreateDocumentFormDataRequestSchema>;
|
||||||
|
export type TCreateDocumentFormDataResponse = z.infer<typeof ZCreateDocumentFormDataResponseSchema>;
|
||||||
@ -3,6 +3,7 @@ import { EnvelopeType } from '@prisma/client';
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||||
|
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
import { authenticatedProcedure } from '../trpc';
|
import { authenticatedProcedure } from '../trpc';
|
||||||
@ -16,7 +17,12 @@ export const createDocumentRoute = authenticatedProcedure
|
|||||||
.output(ZCreateDocumentResponseSchema)
|
.output(ZCreateDocumentResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { user, teamId } = ctx;
|
const { user, teamId } = ctx;
|
||||||
const { title, documentDataId, timezone, folderId, attachments } = input;
|
|
||||||
|
const { payload, file } = input;
|
||||||
|
|
||||||
|
const { title, timezone, folderId, attachments } = payload;
|
||||||
|
|
||||||
|
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
@ -55,6 +61,7 @@ export const createDocumentRoute = authenticatedProcedure
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
envelopeId: document.id,
|
||||||
legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId),
|
legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,23 +1,27 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { zfd } from 'zod-form-data';
|
||||||
|
|
||||||
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
|
||||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||||
|
|
||||||
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
import { ZDocumentTitleSchema } from './schema';
|
import { ZDocumentTitleSchema } from './schema';
|
||||||
|
|
||||||
// Currently not in use until we allow passthrough documents on create.
|
// Currently not in use until we allow passthrough documents on create.
|
||||||
// export const createDocumentMeta: TrpcRouteMeta = {
|
export const createDocumentMeta: TrpcRouteMeta = {
|
||||||
// openapi: {
|
openapi: {
|
||||||
// method: 'POST',
|
method: 'POST',
|
||||||
// path: '/document/create',
|
path: '/document/create',
|
||||||
// summary: 'Create document',
|
contentTypes: ['multipart/form-data'],
|
||||||
// tags: ['Document'],
|
summary: 'Create document',
|
||||||
// },
|
description: 'Create a document using form data.',
|
||||||
// };
|
tags: ['Document'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentRequestSchema = z.object({
|
export const ZCreateDocumentPayloadSchema = z.object({
|
||||||
title: ZDocumentTitleSchema,
|
title: ZDocumentTitleSchema,
|
||||||
documentDataId: z.string().min(1),
|
|
||||||
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
timezone: ZDocumentMetaTimezoneSchema.optional(),
|
||||||
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
|
folderId: z.string().describe('The ID of the folder to create the document in').optional(),
|
||||||
attachments: z
|
attachments: z
|
||||||
@ -31,9 +35,16 @@ export const ZCreateDocumentRequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZCreateDocumentRequestSchema = zodFormData({
|
||||||
|
payload: zfd.json(ZCreateDocumentPayloadSchema),
|
||||||
|
file: zfd.file(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentResponseSchema = z.object({
|
export const ZCreateDocumentResponseSchema = z.object({
|
||||||
|
envelopeId: z.string(),
|
||||||
legacyDocumentId: z.number(),
|
legacyDocumentId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TCreateDocumentPayloadSchema = z.infer<typeof ZCreateDocumentPayloadSchema>;
|
||||||
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
|
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
|
||||||
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;
|
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { deleteAttachmentRoute } from './attachment/delete-attachment';
|
|||||||
import { findAttachmentsRoute } from './attachment/find-attachments';
|
import { findAttachmentsRoute } from './attachment/find-attachments';
|
||||||
import { updateAttachmentRoute } from './attachment/update-attachment';
|
import { updateAttachmentRoute } from './attachment/update-attachment';
|
||||||
import { createDocumentRoute } from './create-document';
|
import { createDocumentRoute } from './create-document';
|
||||||
|
import { createDocumentFormDataRoute } from './create-document-formdata';
|
||||||
import { createDocumentTemporaryRoute } from './create-document-temporary';
|
import { createDocumentTemporaryRoute } from './create-document-temporary';
|
||||||
import { deleteDocumentRoute } from './delete-document';
|
import { deleteDocumentRoute } from './delete-document';
|
||||||
import { distributeDocumentRoute } from './distribute-document';
|
import { distributeDocumentRoute } from './distribute-document';
|
||||||
@ -40,6 +41,7 @@ export const documentRouter = router({
|
|||||||
// Temporary v2 beta routes to be removed once V2 is fully released.
|
// Temporary v2 beta routes to be removed once V2 is fully released.
|
||||||
download: downloadDocumentRoute,
|
download: downloadDocumentRoute,
|
||||||
createDocumentTemporary: createDocumentTemporaryRoute,
|
createDocumentTemporary: createDocumentTemporaryRoute,
|
||||||
|
createDocumentFormData: createDocumentFormDataRoute,
|
||||||
|
|
||||||
// Internal document routes for custom frontend requests.
|
// Internal document routes for custom frontend requests.
|
||||||
getDocumentByToken: getDocumentByTokenRoute,
|
getDocumentByToken: getDocumentByTokenRoute,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
|
||||||
|
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
|
|
||||||
import { authenticatedProcedure } from '../trpc';
|
import { authenticatedProcedure } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -13,6 +14,9 @@ export const createEnvelopeRoute = authenticatedProcedure
|
|||||||
.output(ZCreateEnvelopeResponseSchema)
|
.output(ZCreateEnvelopeResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { user, teamId } = ctx;
|
const { user, teamId } = ctx;
|
||||||
|
|
||||||
|
const { payload, files } = input;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
type,
|
type,
|
||||||
@ -22,10 +26,9 @@ export const createEnvelopeRoute = authenticatedProcedure
|
|||||||
globalActionAuth,
|
globalActionAuth,
|
||||||
recipients,
|
recipients,
|
||||||
folderId,
|
folderId,
|
||||||
items,
|
|
||||||
meta,
|
meta,
|
||||||
attachments,
|
attachments,
|
||||||
} = input;
|
} = payload;
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
@ -45,13 +48,62 @@ export const createEnvelopeRoute = authenticatedProcedure
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.length > maximumEnvelopeItemCount) {
|
if (files.length > maximumEnvelopeItemCount) {
|
||||||
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
|
||||||
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For each file, stream to s3 and create the document data.
|
||||||
|
const envelopeItems = await Promise.all(
|
||||||
|
files.map(async (file) => {
|
||||||
|
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: file.name,
|
||||||
|
documentDataId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipientsToCreate = recipients?.map((recipient) => ({
|
||||||
|
email: recipient.email,
|
||||||
|
name: recipient.name,
|
||||||
|
role: recipient.role,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
accessAuth: recipient.accessAuth,
|
||||||
|
actionAuth: recipient.actionAuth,
|
||||||
|
fields: recipient.fields?.map((field) => {
|
||||||
|
let documentDataId: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (typeof field.identifier === 'string') {
|
||||||
|
documentDataId = envelopeItems.find(
|
||||||
|
(item) => item.title === field.identifier,
|
||||||
|
)?.documentDataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof field.identifier === 'number') {
|
||||||
|
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.identifier === undefined) {
|
||||||
|
documentDataId = envelopeItems[0]?.documentDataId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!documentDataId) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Document data not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
documentDataId,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const envelope = await createEnvelope({
|
const envelope = await createEnvelope({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
teamId,
|
teamId,
|
||||||
@ -63,9 +115,9 @@ export const createEnvelopeRoute = authenticatedProcedure
|
|||||||
visibility,
|
visibility,
|
||||||
globalAccessAuth,
|
globalAccessAuth,
|
||||||
globalActionAuth,
|
globalActionAuth,
|
||||||
recipients,
|
recipients: recipientsToCreate,
|
||||||
folderId,
|
folderId,
|
||||||
envelopeItems: items,
|
envelopeItems,
|
||||||
},
|
},
|
||||||
attachments,
|
attachments,
|
||||||
meta,
|
meta,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { EnvelopeType } from '@prisma/client';
|
import { EnvelopeType } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { zfd } from 'zod-form-data';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ZDocumentAccessAuthTypesSchema,
|
ZDocumentAccessAuthTypesSchema,
|
||||||
@ -17,24 +18,28 @@ import {
|
|||||||
} from '@documenso/lib/types/field';
|
} from '@documenso/lib/types/field';
|
||||||
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
|
||||||
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
import {
|
import {
|
||||||
ZDocumentExternalIdSchema,
|
ZDocumentExternalIdSchema,
|
||||||
ZDocumentTitleSchema,
|
ZDocumentTitleSchema,
|
||||||
ZDocumentVisibilitySchema,
|
ZDocumentVisibilitySchema,
|
||||||
} from '../document-router/schema';
|
} from '../document-router/schema';
|
||||||
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
import { ZCreateRecipientSchema } from '../recipient-router/schema';
|
||||||
|
import type { TrpcRouteMeta } from '../trpc';
|
||||||
|
|
||||||
// Currently not in use until we allow passthrough documents on create.
|
// Currently not in use until we allow passthrough documents on create.
|
||||||
// export const createEnvelopeMeta: TrpcRouteMeta = {
|
export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||||
// openapi: {
|
openapi: {
|
||||||
// method: 'POST',
|
method: 'POST',
|
||||||
// path: '/envelope/create',
|
path: '/envelope/create',
|
||||||
// summary: 'Create envelope',
|
contentTypes: ['multipart/form-data'],
|
||||||
// tags: ['Envelope'],
|
summary: 'Create envelope',
|
||||||
// },
|
description: 'Create a envelope using form data.',
|
||||||
// };
|
tags: ['Envelope'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const ZCreateEnvelopeRequestSchema = z.object({
|
export const ZCreateEnvelopePayloadSchema = z.object({
|
||||||
title: ZDocumentTitleSchema,
|
title: ZDocumentTitleSchema,
|
||||||
type: z.nativeEnum(EnvelopeType),
|
type: z.nativeEnum(EnvelopeType),
|
||||||
externalId: ZDocumentExternalIdSchema.optional(),
|
externalId: ZDocumentExternalIdSchema.optional(),
|
||||||
@ -42,12 +47,6 @@ export const ZCreateEnvelopeRequestSchema = z.object({
|
|||||||
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
formValues: ZDocumentFormValuesSchema.optional(),
|
formValues: ZDocumentFormValuesSchema.optional(),
|
||||||
items: z
|
|
||||||
.object({
|
|
||||||
title: ZDocumentTitleSchema.optional(),
|
|
||||||
documentDataId: z.string(),
|
|
||||||
})
|
|
||||||
.array(),
|
|
||||||
folderId: z
|
folderId: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@ -59,11 +58,12 @@ export const ZCreateEnvelopeRequestSchema = z.object({
|
|||||||
ZCreateRecipientSchema.extend({
|
ZCreateRecipientSchema.extend({
|
||||||
fields: ZFieldAndMetaSchema.and(
|
fields: ZFieldAndMetaSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
documentDataId: z
|
identifier: z
|
||||||
.string()
|
.union([z.string(), z.number()])
|
||||||
.describe(
|
.describe(
|
||||||
'The ID of the document data to create the field on. If empty, the first document data will be used.',
|
'Either the filename or the index of the file that was uploaded to attach the field to.',
|
||||||
),
|
)
|
||||||
|
.optional(),
|
||||||
page: ZFieldPageNumberSchema,
|
page: ZFieldPageNumberSchema,
|
||||||
positionX: ZFieldPageXSchema,
|
positionX: ZFieldPageXSchema,
|
||||||
positionY: ZFieldPageYSchema,
|
positionY: ZFieldPageYSchema,
|
||||||
@ -88,9 +88,15 @@ export const ZCreateEnvelopeRequestSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZCreateEnvelopeRequestSchema = zodFormData({
|
||||||
|
payload: zfd.json(ZCreateEnvelopePayloadSchema),
|
||||||
|
files: zfd.repeatableOfType(zfd.file()),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZCreateEnvelopeResponseSchema = z.object({
|
export const ZCreateEnvelopeResponseSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TCreateEnvelopePayload = z.infer<typeof ZCreateEnvelopePayloadSchema>;
|
||||||
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
|
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
|
||||||
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;
|
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { deleteTemplateDirectLink } from '@documenso/lib/server-only/template/de
|
|||||||
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
|
||||||
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
|
||||||
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link';
|
||||||
|
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
|
||||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||||
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
|
import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
|
||||||
@ -159,20 +160,27 @@ export const templateRouter = router({
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
createTemplate: authenticatedProcedure
|
createTemplate: authenticatedProcedure
|
||||||
// .meta({ // Note before releasing this to public, update the response schema to be correct.
|
.meta({
|
||||||
// openapi: {
|
// Note before releasing this to public, update the response schema to be correct.
|
||||||
// method: 'POST',
|
openapi: {
|
||||||
// path: '/template/create',
|
method: 'POST',
|
||||||
// summary: 'Create template',
|
path: '/template/create',
|
||||||
// description: 'Create a new template',
|
contentTypes: ['multipart/form-data'],
|
||||||
// tags: ['Template'],
|
summary: 'Create template',
|
||||||
// },
|
description: 'Create a new template',
|
||||||
// })
|
tags: ['Template'],
|
||||||
|
},
|
||||||
|
})
|
||||||
.input(ZCreateTemplateMutationSchema)
|
.input(ZCreateTemplateMutationSchema)
|
||||||
.output(ZCreateTemplateResponseSchema)
|
.output(ZCreateTemplateResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const { teamId } = ctx;
|
const { teamId } = ctx;
|
||||||
const { title, templateDocumentDataId, folderId } = input;
|
|
||||||
|
const { payload, file } = input;
|
||||||
|
|
||||||
|
const { title, folderId } = payload;
|
||||||
|
|
||||||
|
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file);
|
||||||
|
|
||||||
ctx.logger.info({
|
ctx.logger.info({
|
||||||
input: {
|
input: {
|
||||||
@ -198,6 +206,7 @@ export const templateRouter = router({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
envelopeId: envelope.id,
|
||||||
legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
legacyTemplateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@prisma/client';
|
import { DocumentSigningOrder, DocumentVisibility, TemplateType } from '@prisma/client';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { zfd } from 'zod-form-data';
|
||||||
|
|
||||||
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
import { ZDocumentSchema } from '@documenso/lib/types/document';
|
||||||
import {
|
import {
|
||||||
@ -29,6 +30,7 @@ import {
|
|||||||
} from '@documenso/lib/types/template';
|
} from '@documenso/lib/types/template';
|
||||||
import { LegacyTemplateDirectLinkSchema } from '@documenso/prisma/types/template-legacy-schema';
|
import { LegacyTemplateDirectLinkSchema } from '@documenso/prisma/types/template-legacy-schema';
|
||||||
|
|
||||||
|
import { zodFormData } from '../../utils/zod-form-data';
|
||||||
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
|
||||||
|
|
||||||
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
|
export const MAX_TEMPLATE_PUBLIC_TITLE_LENGTH = 50;
|
||||||
@ -77,12 +79,16 @@ export const ZTemplateMetaUpsertSchema = z.object({
|
|||||||
allowDictateNextSigner: z.boolean().optional(),
|
allowDictateNextSigner: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateTemplateMutationSchema = z.object({
|
export const ZCreateTemplatePayloadSchema = z.object({
|
||||||
title: z.string().min(1).trim(),
|
title: z.string().min(1).trim(),
|
||||||
templateDocumentDataId: z.string().min(1),
|
|
||||||
folderId: z.string().optional(),
|
folderId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ZCreateTemplateMutationSchema = zodFormData({
|
||||||
|
payload: zfd.json(ZCreateTemplatePayloadSchema),
|
||||||
|
file: zfd.file(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
|
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
|
||||||
directRecipientName: z.string().max(255).optional(),
|
directRecipientName: z.string().max(255).optional(),
|
||||||
directRecipientEmail: z.string().email().max(254),
|
directRecipientEmail: z.string().email().max(254),
|
||||||
@ -218,6 +224,7 @@ export const ZCreateTemplateV2ResponseSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const ZCreateTemplateResponseSchema = z.object({
|
export const ZCreateTemplateResponseSchema = z.object({
|
||||||
|
envelopeId: z.string(),
|
||||||
legacyTemplateId: z.number(),
|
legacyTemplateId: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -267,6 +274,7 @@ export const ZBulkSendTemplateMutationSchema = z.object({
|
|||||||
sendImmediately: z.boolean(),
|
sendImmediately: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type TCreateTemplatePayloadSchema = z.infer<typeof ZCreateTemplatePayloadSchema>;
|
||||||
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
|
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
|
||||||
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
|
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
|
||||||
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;
|
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { TRPCError, initTRPC } from '@trpc/server';
|
import { TRPCError, initTRPC } from '@trpc/server';
|
||||||
import SuperJSON from 'superjson';
|
|
||||||
import type { AnyZodObject } from 'zod';
|
import type { AnyZodObject } from 'zod';
|
||||||
|
|
||||||
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
|
||||||
@ -9,6 +8,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
|||||||
import { alphaid } from '@documenso/lib/universal/id';
|
import { alphaid } from '@documenso/lib/universal/id';
|
||||||
import { isAdmin } from '@documenso/lib/utils/is-admin';
|
import { isAdmin } from '@documenso/lib/utils/is-admin';
|
||||||
|
|
||||||
|
import { dataTransformer } from '../utils/data-transformer';
|
||||||
import type { TrpcContext } from './context';
|
import type { TrpcContext } from './context';
|
||||||
|
|
||||||
// Can't import type from trpc-to-openapi because it breaks build, not sure why.
|
// Can't import type from trpc-to-openapi because it breaks build, not sure why.
|
||||||
@ -35,7 +35,7 @@ const t = initTRPC
|
|||||||
.meta<TrpcRouteMeta>()
|
.meta<TrpcRouteMeta>()
|
||||||
.context<TrpcContext>()
|
.context<TrpcContext>()
|
||||||
.create({
|
.create({
|
||||||
transformer: SuperJSON,
|
transformer: dataTransformer,
|
||||||
errorFormatter(opts) {
|
errorFormatter(opts) {
|
||||||
const { shape, error } = opts;
|
const { shape, error } = opts;
|
||||||
|
|
||||||
|
|||||||
17
packages/trpc/utils/data-transformer.ts
Normal file
17
packages/trpc/utils/data-transformer.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { DataTransformer } from '@trpc/server';
|
||||||
|
import SuperJSON from 'superjson';
|
||||||
|
|
||||||
|
export const dataTransformer: DataTransformer = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
serialize: (data: any) => {
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SuperJSON.serialize(data);
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
deserialize: (data: any) => {
|
||||||
|
return SuperJSON.deserialize(data);
|
||||||
|
},
|
||||||
|
};
|
||||||
202
packages/trpc/utils/openapi-fetch-handler.ts
Normal file
202
packages/trpc/utils/openapi-fetch-handler.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { TRPCError } from '@trpc/server';
|
||||||
|
import type { FetchHandlerOptions } from '@trpc/server/adapters/fetch';
|
||||||
|
import type { ServerResponse } from 'node:http';
|
||||||
|
import { type OpenApiRouter, createOpenApiNodeHttpHandler } from 'trpc-to-openapi';
|
||||||
|
|
||||||
|
const CONTENT_TYPE_JSON = 'application/json';
|
||||||
|
const CONTENT_TYPE_URLENCODED = 'application/x-www-form-urlencoded';
|
||||||
|
const CONTENT_TYPE_MULTIPART = 'multipart/form-data';
|
||||||
|
|
||||||
|
const getUrlEncodedBody = async (req: Request) => {
|
||||||
|
const params = new URLSearchParams(await req.text());
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of params.keys()) {
|
||||||
|
data[key] = params.getAll(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMultipartBody = async (req: Request) => {
|
||||||
|
const formData = await req.formData();
|
||||||
|
|
||||||
|
const data: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of formData.keys()) {
|
||||||
|
const values = formData.getAll(key);
|
||||||
|
|
||||||
|
// Return array for multiple values, single value otherwise (matches URL-encoded behavior)
|
||||||
|
data[key] = values.length > 1 ? values : values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Parses the request body based on its content type.
|
||||||
|
*
|
||||||
|
* Handles JSON, URL-encoded, and multipart/form-data requests.
|
||||||
|
* For multipart requests, converts FormData to a plain object (similar to URL-encoded)
|
||||||
|
* so it can be validated by tRPC schemas. The content-type header is rewritten
|
||||||
|
* later to prevent downstream parsing issues.
|
||||||
|
*/
|
||||||
|
const getRequestBody = async (req: Request) => {
|
||||||
|
try {
|
||||||
|
const contentType = req.headers.get('content-type') || '';
|
||||||
|
|
||||||
|
if (contentType.includes(CONTENT_TYPE_JSON)) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
// Use JSON.parse instead of req.json() because req.json() does not throw on invalid JSON
|
||||||
|
data: JSON.parse(await req.text()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType.includes(CONTENT_TYPE_URLENCODED)) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
data: await getUrlEncodedBody(req),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle multipart/form-data by parsing as FormData and converting to a plain object.
|
||||||
|
// This mirrors how URL-encoded data is structured, allowing tRPC to validate it normally.
|
||||||
|
// The content-type header is rewritten to application/json later via the request proxy
|
||||||
|
// because createOpenApiNodeHttpHandler aborts on any bodied request that isn't application/json.
|
||||||
|
if (contentType.includes(CONTENT_TYPE_MULTIPART)) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
data: await getMultipartBody(req),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
data: req.body,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
cause: err,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a proxy around the original Request that intercepts property access
|
||||||
|
* to transform the request for compatibility with the Node HTTP handler.
|
||||||
|
*
|
||||||
|
* Key transformations:
|
||||||
|
* - Parses and provides the body as a plain object (handles multipart/form-data conversion)
|
||||||
|
* - Rewrites content-type header for multipart requests to application/json
|
||||||
|
* (required because createOpenApiNodeHttpHandler aborts on non-JSON bodied requests)
|
||||||
|
*/
|
||||||
|
const createRequestProxy = async (req: Request, url?: string) => {
|
||||||
|
const body = await getRequestBody(req);
|
||||||
|
|
||||||
|
const originalContentType = req.headers.get('content-type') || '';
|
||||||
|
|
||||||
|
const isMultipart = originalContentType.includes(CONTENT_TYPE_MULTIPART);
|
||||||
|
|
||||||
|
return new Proxy(req, {
|
||||||
|
get: (target, prop) => {
|
||||||
|
switch (prop) {
|
||||||
|
case 'url':
|
||||||
|
return url ?? target.url;
|
||||||
|
|
||||||
|
case 'body': {
|
||||||
|
if (!body.isValid) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'PARSE_ERROR',
|
||||||
|
message: 'Failed to parse request body',
|
||||||
|
cause: body.cause,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return body.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'headers': {
|
||||||
|
const headers = new Headers(target.headers);
|
||||||
|
|
||||||
|
// Rewrite content-type header for multipart requests to application/json.
|
||||||
|
// This is necessary because `createOpenApiNodeHttpHandler` aborts on any bodied
|
||||||
|
// request that isn't application/json. Since we've already parsed the multipart
|
||||||
|
// data into a plain object above, this is safe to do.
|
||||||
|
if (isMultipart) {
|
||||||
|
headers.set('content-type', CONTENT_TYPE_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/no-explicit-any
|
||||||
|
return (target as unknown as Record<string | number | symbol, unknown>)[prop];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateOpenApiFetchHandlerOptions<TRouter extends OpenApiRouter> = Omit<
|
||||||
|
FetchHandlerOptions<TRouter>,
|
||||||
|
'batching'
|
||||||
|
> & {
|
||||||
|
req: Request;
|
||||||
|
endpoint: `/${string}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createOpenApiFetchHandler = async <TRouter extends OpenApiRouter>(
|
||||||
|
opts: CreateOpenApiFetchHandlerOptions<TRouter>,
|
||||||
|
): Promise<Response> => {
|
||||||
|
const resHeaders = new Headers();
|
||||||
|
const url = new URL(opts.req.url.replace(opts.endpoint, ''));
|
||||||
|
const req: Request = await createRequestProxy(opts.req, url.toString());
|
||||||
|
|
||||||
|
// @ts-expect-error Inherited from original fetch handler in `trpc-to-openapi`
|
||||||
|
const openApiHttpHandler = createOpenApiNodeHttpHandler(opts);
|
||||||
|
|
||||||
|
return new Promise<Response>((resolve) => {
|
||||||
|
let statusCode: number;
|
||||||
|
|
||||||
|
// Create a mock ServerResponse object that bridges Node HTTP APIs with Fetch API Response.
|
||||||
|
// This allows the Node HTTP handler to work with Fetch API Request objects.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const res = {
|
||||||
|
setHeader: (key: string, value: string | readonly string[]) => {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
resHeaders.set(key, value);
|
||||||
|
} else {
|
||||||
|
for (const v of value) {
|
||||||
|
resHeaders.append(key, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
get statusCode() {
|
||||||
|
return statusCode;
|
||||||
|
},
|
||||||
|
set statusCode(code: number) {
|
||||||
|
statusCode = code;
|
||||||
|
},
|
||||||
|
end: (body: string) => {
|
||||||
|
resolve(
|
||||||
|
new Response(body, {
|
||||||
|
headers: resHeaders,
|
||||||
|
status: statusCode,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
} as ServerResponse;
|
||||||
|
|
||||||
|
// Type assertions are necessary here for interop between Fetch API Request/Response
|
||||||
|
// and Node HTTP IncomingMessage/ServerResponse types.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const nodeReq = req as unknown as Parameters<typeof openApiHttpHandler>[0];
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
const nodeRes = res as unknown as Parameters<typeof openApiHttpHandler>[1];
|
||||||
|
|
||||||
|
void openApiHttpHandler(nodeReq, nodeRes);
|
||||||
|
});
|
||||||
|
};
|
||||||
32
packages/trpc/utils/zod-form-data.ts
Normal file
32
packages/trpc/utils/zod-form-data.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import type { ZodRawShape } from 'zod';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper takes the place of the `z.object` at the root of your schema.
|
||||||
|
* It wraps your schema in a `z.preprocess` that extracts all the data out of a `FormData`
|
||||||
|
* and transforms it into a regular object.
|
||||||
|
* If the `FormData` contains multiple entries with the same field name,
|
||||||
|
* it will automatically turn that field into an array.
|
||||||
|
*
|
||||||
|
* This is used instead of `zfd.formData()` because it receives `undefined`
|
||||||
|
* somewhere in the pipeline of our openapi schema generation and throws
|
||||||
|
* an error. This provides the same functionality as `zfd.formData()` but
|
||||||
|
* can be considered somewhat safer.
|
||||||
|
*/
|
||||||
|
export const zodFormData = <T extends ZodRawShape>(schema: T) => {
|
||||||
|
return z.preprocess((data) => {
|
||||||
|
if (data instanceof FormData) {
|
||||||
|
const formData: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
for (const key of data.keys()) {
|
||||||
|
const values = data.getAll(key);
|
||||||
|
|
||||||
|
formData[key] = values.length > 1 ? values : values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, z.object(schema));
|
||||||
|
};
|
||||||
@ -78,6 +78,6 @@
|
|||||||
"tailwind-merge": "^1.12.0",
|
"tailwind-merge": "^1.12.0",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user