Compare commits

..
5 changed files with 41 additions and 54 deletions
@@ -8,6 +8,7 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { EnvelopeType } from '@prisma/client';
import type { Logger } from 'pino';
import { match, P } from 'ts-pattern';
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
import { authenticatedProcedure } from '../trpc';
@@ -148,19 +149,11 @@ export const createEnvelopeRouteCaller = async ({
accessAuth: recipient.accessAuth,
actionAuth: recipient.actionAuth,
fields: recipient.fields?.map((field) => {
let documentDataId: string | 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.at(0)?.documentDataId;
}
const documentDataId = match(field.identifier)
.with(P.string, (title) => envelopeItems.find((item) => item.title === title)?.documentDataId)
.with(P.number, (index) => envelopeItems.at(index)?.documentDataId)
.with(undefined, () => envelopeItems.at(0)?.documentDataId)
.exhaustive();
if (!documentDataId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
@@ -39,8 +39,8 @@ export const signEnvelopeFieldRoute = procedure
const field = await prisma.field.findFirst({
where: {
id: fieldId,
recipient: {
...(recipient.role === RecipientRole.ASSISTANT
recipient:
recipient.role === RecipientRole.ASSISTANT
? {
signingStatus: {
not: SigningStatus.SIGNED,
@@ -52,8 +52,7 @@ export const signEnvelopeFieldRoute = procedure
}
: {
id: recipient.id,
}),
},
},
},
include: {
envelope: {
@@ -6,6 +6,7 @@ import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { EnvelopeType } from '@prisma/client';
import { match, P } from 'ts-pattern';
import { authenticatedProcedure } from '../trpc';
import { useEnvelopeMeta, ZUseEnvelopeRequestSchema, ZUseEnvelopeResponseSchema } from './use-envelope.types';
@@ -87,20 +88,11 @@ export const useEnvelopeRoute = authenticatedProcedure
// Map custom document data using identifiers
const customDocumentDataMapped = customDocumentData?.map((mapping) => {
let documentDataId: string | undefined;
// Find the uploaded file by identifier
if (typeof mapping.identifier === 'string') {
documentDataId = uploadedFiles.find((file) => file.name === mapping.identifier)?.documentDataId;
}
if (typeof mapping.identifier === 'number') {
documentDataId = uploadedFiles.at(mapping.identifier)?.documentDataId;
}
if (mapping.identifier === undefined) {
documentDataId = uploadedFiles.at(0)?.documentDataId;
}
const documentDataId = match(mapping.identifier)
.with(P.string, (name) => uploadedFiles.find((file) => file.name === name)?.documentDataId)
.with(P.number, (index) => uploadedFiles.at(index)?.documentDataId)
.exhaustive();
if (!documentDataId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
+24 -22
View File
@@ -10,7 +10,7 @@ const CONTENT_TYPE_MULTIPART = 'multipart/form-data';
const getUrlEncodedBody = async (req: Request) => {
const params = new URLSearchParams(await req.text());
const data: Record<string, string[]> = {};
const data: Record<string, unknown> = {};
for (const key of params.keys()) {
data[key] = params.getAll(key);
@@ -22,20 +22,18 @@ const getUrlEncodedBody = async (req: Request) => {
const getMultipartBody = async (req: Request) => {
const formData = await req.formData();
const data: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
const data: Record<string, unknown> = {};
for (const [key, value] of formData.entries()) {
// !: Handles cases where our generated SDKs send key[] syntax for arrays.
const normalizedKey = key.endsWith('[]') ? key.slice(0, -2) : key;
const existing = data[normalizedKey];
if (existing === undefined) {
if (data[normalizedKey] === undefined) {
data[normalizedKey] = value;
} else if (Array.isArray(existing)) {
existing.push(value);
} else if (Array.isArray(data[normalizedKey])) {
data[normalizedKey].push(value);
} else {
data[normalizedKey] = [existing, value];
data[normalizedKey] = [data[normalizedKey], value];
}
}
@@ -140,10 +138,8 @@ const createRequestProxy = async (req: Request, url?: string) => {
}
default:
// SAFETY: Every property this trap does not special-case is forwarded from the
// original Request, so `prop` can only be a key the caller reads off a Request.
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return target[prop as keyof Request];
// 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];
}
},
});
@@ -164,25 +160,23 @@ export const createOpenApiFetchHandler = async <TRouter extends OpenApiRouter>(
const url = new URL(opts.req.url.replace(opts.endpoint, ''));
const req: Request = await createRequestProxy(opts.req, url.toString());
// The handler is typed against Node HTTP req/res, but only reads properties our request
// proxy and mock response provide, so we declare it against the fetch-based types we pass.
// @ts-expect-error Inherited from original fetch handler in `trpc-to-openapi`
const openApiHttpHandler: (req: Request, res: ServerResponse) => void = createOpenApiNodeHttpHandler(opts);
const openApiHttpHandler = createOpenApiNodeHttpHandler(opts);
return new Promise<Response>((resolve) => {
let statusCode: number;
// SAFETY: The Node HTTP handler only calls setHeader/statusCode/end on the response,
// which this mock implements to bridge Node HTTP APIs with a Fetch API Response.
// 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 | string[]) => {
if (Array.isArray(value)) {
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);
}
} else {
resHeaders.set(key, value);
}
},
get statusCode() {
@@ -201,6 +195,14 @@ export const createOpenApiFetchHandler = async <TRouter extends OpenApiRouter>(
},
} as ServerResponse;
void openApiHttpHandler(req, res);
// 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);
});
};
+3 -2
View File
@@ -5,6 +5,7 @@ import {
BRANDING_LOGO_MAX_SIZE_MB,
} from '@documenso/lib/constants/branding';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import type { ZodRawShape } from 'zod';
import z from 'zod';
import { zfd } from 'zod-form-data';
@@ -48,10 +49,10 @@ export const zfdBrandingImageFile = () => {
* an error. This provides the same functionality as `zfd.formData()` but
* can be considered somewhat safer.
*/
export const zodFormData = <T extends Parameters<typeof z.object>[0]>(schema: T) => {
export const zodFormData = <T extends ZodRawShape>(schema: T) => {
return z.preprocess((data) => {
if (data instanceof FormData) {
const formData: Record<string, FormDataEntryValue | FormDataEntryValue[]> = {};
const formData: Record<string, unknown> = {};
for (const key of data.keys()) {
const values = data.getAll(key);