From b4a7f1887dc32f9906277fe60c9e7dd0ae7f2e2f Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Sat, 14 Dec 2024 01:23:35 +0900 Subject: [PATCH] feat: add trpc openapi (#1535) --- apps/web/package.json | 1 + .../document-page-view-recent-activity.tsx | 6 +- .../documents/[id]/edit-document.tsx | 8 +- .../app/(dashboard)/documents/data-table.tsx | 12 +- .../template-page-view-recent-activity.tsx | 6 +- .../templates/move-template-dialog.tsx | 16 +- apps/web/src/pages/api/beta/[...trpc].ts | 47 +++ apps/web/src/pages/api/beta/open-api.json.ts | 9 + apps/web/src/pages/api/trpc/[trpc].ts | 1 + package-lock.json | 314 +++++++++++++++++- packages/api/v1/implementation.ts | 9 +- .../server-only/document/create-document.ts | 9 +- .../document/duplicate-document-by-id.ts | 18 +- .../server-only/document/find-documents.ts | 28 +- .../get-document-with-details-by-id.ts | 36 +- .../document/move-document-to-team.ts | 8 +- .../server-only/document/resend-document.tsx | 2 +- .../server-only/document/send-document.tsx | 17 +- .../document/update-document-settings.ts | 8 +- .../lib/server-only/field/get-field-by-id.ts | 17 +- .../field/set-fields-for-document.ts | 22 +- .../field/set-fields-for-template.ts | 15 +- .../recipient/set-recipients-for-document.ts | 16 +- .../recipient/set-recipients-for-template.ts | 17 +- .../create-document-from-direct-template.ts | 13 +- .../template/create-document-from-template.ts | 22 +- .../template/create-template-direct-link.ts | 12 +- .../server-only/template/create-template.ts | 7 + .../template/duplicate-template.ts | 8 +- .../server-only/template/find-templates.ts | 39 ++- .../template/move-template-to-team.ts | 19 +- .../template/toggle-template-direct-link.ts | 11 +- .../template/update-template-settings.ts | 9 +- packages/lib/utils/logger.ts | 4 +- .../trpc/server/document-router/router.ts | 167 +++++++--- .../trpc/server/document-router/schema.ts | 23 +- packages/trpc/server/field-router/router.ts | 40 ++- packages/trpc/server/open-api.ts | 12 + .../trpc/server/recipient-router/router.ts | 30 +- packages/trpc/server/team-router/router.ts | 273 ++++++++------- .../trpc/server/template-router/router.ts | 131 ++++++-- packages/trpc/server/trpc.ts | 77 +++-- 42 files changed, 1198 insertions(+), 341 deletions(-) create mode 100644 apps/web/src/pages/api/beta/[...trpc].ts create mode 100644 apps/web/src/pages/api/beta/open-api.json.ts create mode 100644 packages/trpc/server/open-api.ts diff --git a/apps/web/package.json b/apps/web/package.json index 6a82aca2a..3979eaf50 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -56,6 +56,7 @@ "recharts": "^2.7.2", "remeda": "^2.17.3", "sharp": "0.32.6", + "trpc-openapi": "^1.2.0", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", diff --git a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx index f35a4255b..c6e0787bb 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/document-page-view-recent-activity.tsx @@ -37,10 +37,8 @@ export const DocumentPageViewRecentActivity = ({ { documentId, filterForRecentActivity: true, - orderBy: { - column: 'createdAt', - direction: 'asc', - }, + orderByColumn: 'createdAt', + orderByDirection: 'asc', perPage: 10, }, { diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index 679d4933d..01a483d86 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -12,8 +12,8 @@ import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION, SKIP_QUERY_BATCH_META, } from '@documenso/lib/constants/trpc'; +import type { TGetDocumentWithDetailsByIdResponse } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; import { DocumentDistributionMethod, DocumentStatus } from '@documenso/prisma/client'; -import type { DocumentWithDetails } from '@documenso/prisma/types/document'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; @@ -35,7 +35,7 @@ import { useOptionalCurrentTeam } from '~/providers/team'; export type EditDocumentFormProps = { className?: string; - initialDocument: DocumentWithDetails; + initialDocument: TGetDocumentWithDetailsByIdResponse; documentRootPath: string; isDocumentEnterprise: boolean; }; @@ -103,7 +103,7 @@ export const EditDocumentForm = ({ const { mutateAsync: addFields } = trpc.field.addFields.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, - onSuccess: (newFields) => { + onSuccess: ({ fields: newFields }) => { utils.document.getDocumentWithDetailsById.setData( { documentId: initialDocument.id, @@ -134,7 +134,7 @@ export const EditDocumentForm = ({ const { mutateAsync: addSigners } = trpc.recipient.addSigners.useMutation({ ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, - onSuccess: (newRecipients) => { + onSuccess: ({ recipients: newRecipients }) => { utils.document.getDocumentWithDetailsById.setData( { documentId: initialDocument.id, diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 83df7d50d..895a4158b 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -9,8 +9,8 @@ import { DateTime } from 'luxon'; import { useSession } from 'next-auth/react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import type { FindResultResponse } from '@documenso/lib/types/search-params'; -import type { Document, Recipient, Team, User } from '@documenso/prisma/client'; +import type { TFindDocumentsResponse } from '@documenso/lib/server-only/document/find-documents'; +import type { Team } from '@documenso/prisma/client'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table'; import { DataTable } from '@documenso/ui/primitives/data-table'; @@ -24,13 +24,7 @@ import { DataTableActionDropdown } from './data-table-action-dropdown'; import { DataTableTitle } from './data-table-title'; export type DocumentsDataTableProps = { - results: FindResultResponse< - Document & { - Recipient: Recipient[]; - User: Pick; - team: Pick | null; - } - >; + results: TFindDocumentsResponse; showSenderColumn?: boolean; team?: Pick & { teamEmail?: string }; }; diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx index 2e8887f39..53cb7a148 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/template-page-view-recent-activity.tsx @@ -26,10 +26,8 @@ export const TemplatePageViewRecentActivity = ({ const { data, isLoading, isLoadingError, refetch } = trpc.document.findDocuments.useQuery({ templateId, teamId, - orderBy: { - column: 'createdAt', - direction: 'asc', - }, + orderByColumn: 'createdAt', + orderByDirection: 'asc', perPage: 5, }); diff --git a/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx index 5420af2a1..205efaf37 100644 --- a/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/move-template-dialog.tsx @@ -4,8 +4,10 @@ import { useRouter } from 'next/navigation'; import { Trans, msg } from '@lingui/macro'; import { useLingui } from '@lingui/react'; +import { match } from 'ts-pattern'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { Avatar, AvatarFallback, AvatarImage } from '@documenso/ui/primitives/avatar'; import { Button } from '@documenso/ui/primitives/button'; @@ -51,10 +53,20 @@ export const MoveTemplateDialog = ({ templateId, open, onOpenChange }: MoveTempl }); onOpenChange(false); }, - onError: (error) => { + onError: (err) => { + const error = AppError.parseError(err); + + const errorMessage = match(error.code) + .with( + AppErrorCode.NOT_FOUND, + () => msg`Template not found or already associated with a team.`, + ) + .with(AppErrorCode.UNAUTHORIZED, () => msg`You are not a member of this team.`) + .otherwise(() => msg`An error occurred while moving the template.`); + toast({ title: _(msg`Error`), - description: error.message || _(msg`An error occurred while moving the template.`), + description: _(errorMessage), variant: 'destructive', duration: 7500, }); diff --git a/apps/web/src/pages/api/beta/[...trpc].ts b/apps/web/src/pages/api/beta/[...trpc].ts new file mode 100644 index 000000000..edba9ae92 --- /dev/null +++ b/apps/web/src/pages/api/beta/[...trpc].ts @@ -0,0 +1,47 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { createOpenApiNextHandler } from 'trpc-openapi'; + +import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { buildLogger } from '@documenso/lib/utils/logger'; +import type { TRPCError } from '@documenso/trpc/server'; +import { createTrpcContext } from '@documenso/trpc/server/context'; +import { appRouter } from '@documenso/trpc/server/router'; + +const logger = buildLogger(); + +export default createOpenApiNextHandler({ + router: appRouter, + createContext: async ({ req, res }: { req: NextApiRequest; res: NextApiResponse }) => + createTrpcContext({ req, res }), + onError: ({ error, path }: { error: TRPCError; path?: string }) => { + // Always log the error for now. + console.error(error.message); + + const appError = AppError.parseError(error.cause || error); + + const isAppError = error.cause instanceof AppError; + + // Only log AppErrors that are explicitly set to 500 or the error code + // is in the errorCodesToAlertOn list. + const isLoggableAppError = + isAppError && (appError.statusCode === 500 || errorCodesToAlertOn.includes(appError.code)); + + // Only log TRPC errors that are in the `errorCodesToAlertOn` list and is + // not an AppError. + const isLoggableTrpcError = !isAppError && errorCodesToAlertOn.includes(error.code); + + if (isLoggableAppError || isLoggableTrpcError) { + logger.error(error, { + method: path, + context: { + source: '/v2/api', + appError: AppError.toJSON(appError), + }, + }); + } + }, + responseMeta: () => {}, +}); + +const errorCodesToAlertOn = [AppErrorCode.UNKNOWN_ERROR, 'INTERNAL_SERVER_ERROR']; diff --git a/apps/web/src/pages/api/beta/open-api.json.ts b/apps/web/src/pages/api/beta/open-api.json.ts new file mode 100644 index 000000000..2e62b3f46 --- /dev/null +++ b/apps/web/src/pages/api/beta/open-api.json.ts @@ -0,0 +1,9 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +import { openApiDocument } from '@documenso/trpc/server/open-api'; + +const handler = (_req: NextApiRequest, res: NextApiResponse) => { + res.status(200).send(openApiDocument); +}; + +export default handler; diff --git a/apps/web/src/pages/api/trpc/[trpc].ts b/apps/web/src/pages/api/trpc/[trpc].ts index 3857e75c0..0e572bffc 100644 --- a/apps/web/src/pages/api/trpc/[trpc].ts +++ b/apps/web/src/pages/api/trpc/[trpc].ts @@ -45,6 +45,7 @@ export default trpcNext.createNextApiHandler({ logger.error(error, { method: path, context: { + source: 'trpc', appError: AppError.toJSON(appError), }, }); diff --git a/package-lock.json b/package-lock.json index 6e9bdee83..75d5a1913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -536,6 +536,7 @@ "recharts": "^2.7.2", "remeda": "^2.17.3", "sharp": "0.32.6", + "trpc-openapi": "^1.2.0", "ts-pattern": "^5.0.5", "ua-parser-js": "^1.0.37", "uqr": "^0.1.2", @@ -3494,6 +3495,11 @@ "node": ">=6" } }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==" + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -12007,6 +12013,18 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -14311,6 +14329,21 @@ } } }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/code-block-writer": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", @@ -14612,6 +14645,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -14627,6 +14668,17 @@ "simple-wcswidth": "^1.0.1" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -14712,9 +14764,9 @@ } }, "node_modules/cookie-es": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.0.0.tgz", - "integrity": "sha512-mWYvfOLrfEc996hlKcdABeIiPHUPC6DM2QYZdGGOvhOTbA3tjm2eBwqlJpoFdjC89NI4Qt6h0Pu06Mp+1Pj5OQ==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==" }, "node_modules/copy-anything": { "version": "3.0.5", @@ -14880,6 +14932,14 @@ "node": ">= 8" } }, + "node_modules/crossws": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.1.tgz", + "integrity": "sha512-HsZgeVYaG+b5zA+9PbIPGq4+J/CJynJuearykPsXx4V/eMhyQ5EDVg3Ak2FBZtVXCiOLu/U7IiwDHTr9MA+IKw==", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -15587,6 +15647,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" + }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -15693,6 +15758,11 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==" + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -18123,6 +18193,14 @@ } } }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -18793,6 +18871,23 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/h3": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.13.0.tgz", + "integrity": "sha512-vFEAu/yf8UMUcB4s43OaDaigcqpQd14yanmOsn+NcRX3/guSKncyE2rOYhq8RIchgJrPSs/QiIddnTTR1ddiAg==", + "dependencies": { + "cookie-es": "^1.2.2", + "crossws": ">=0.2.0 <0.4.0", + "defu": "^6.1.4", + "destr": "^2.0.3", + "iron-webcrypto": "^1.2.1", + "ohash": "^1.1.4", + "radix3": "^1.1.2", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unenv": "^1.10.0" + } + }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -19844,6 +19939,14 @@ "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", "dev": true }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/inflection": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/inflection/-/inflection-2.0.1.tgz", @@ -20219,6 +20322,14 @@ "node": ">= 10" } }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -22035,8 +22146,7 @@ "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -22863,6 +22973,14 @@ "esbuild": "0.*" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -22917,6 +23035,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-refs": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.2.2.tgz", @@ -22990,6 +23116,14 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micro": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/micro/-/micro-10.0.1.tgz", @@ -23733,6 +23867,17 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -24196,7 +24341,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -24665,6 +24809,11 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==" + }, "node_modules/node-gyp": { "version": "9.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.1.tgz", @@ -24993,6 +25142,38 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/node-mocks-http": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.16.2.tgz", + "integrity": "sha512-2Sh6YItRp1oqewZNlck3LaFp5vbyW2u51HX2p1VLxQ9U/bG90XV8JY9O7Nk+HDd6OOn/oV3nA5Tx5k4Rki0qlg==", + "dependencies": { + "accepts": "^1.3.7", + "content-disposition": "^0.5.3", + "depd": "^1.1.0", + "fresh": "^0.5.2", + "merge-descriptors": "^1.0.1", + "methods": "^1.1.2", + "mime": "^1.3.4", + "parseurl": "^1.3.3", + "range-parser": "^1.2.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@types/express": "^4.17.21 || ^5.0.0", + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -25580,6 +25761,11 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", @@ -25681,6 +25867,11 @@ } } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, "node_modules/openapi3-ts": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", @@ -26440,6 +26631,14 @@ "url": "https://ko-fi.com/killymxi" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/partysocket": { "version": "0.0.17", "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-0.0.17.tgz", @@ -26595,8 +26794,7 @@ "node_modules/pathe": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, "node_modules/pathval": { "version": "1.1.1", @@ -27908,6 +28106,11 @@ "node": ">=8" } }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==" + }, "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", @@ -27946,6 +28149,14 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.1.tgz", @@ -32782,6 +32993,32 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/trpc-openapi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/trpc-openapi/-/trpc-openapi-1.2.0.tgz", + "integrity": "sha512-pfYoCd/3KYXWXvUPZBKJw455OOwngKN/6SIcj7Yit19OMLJ+8yVZkEvGEeg5wUSwfsiTdRsKuvqkRPXVSwV7ew==", + "workspaces": [ + ".", + "examples/with-nextjs", + "examples/with-express", + "examples/with-interop", + "examples/with-serverless", + "examples/with-fastify", + "examples/with-nuxtjs" + ], + "dependencies": { + "co-body": "^6.1.0", + "h3": "^1.6.4", + "lodash.clonedeep": "^4.5.0", + "node-mocks-http": "^1.12.2", + "openapi-types": "^12.1.1", + "zod-to-json-schema": "^3.21.1" + }, + "peerDependencies": { + "@trpc/server": "^10.0.0", + "zod": "^3.14.4" + } + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -33547,6 +33784,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", @@ -33665,10 +33914,9 @@ } }, "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==" }, "node_modules/ulid": { "version": "2.3.0", @@ -33703,6 +33951,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==" + }, "node_modules/undici": { "version": "5.28.2", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", @@ -33720,6 +33973,29 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unenv": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", + "dependencies": { + "consola": "^3.2.3", + "defu": "^6.1.4", + "mime": "^3.0.0", + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" + } + }, + "node_modules/unenv/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/unified": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", @@ -35409,9 +35685,9 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -35454,6 +35730,14 @@ "@prisma/debug": "5.22.0" } }, + "node_modules/zod-to-json-schema": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.1.tgz", + "integrity": "sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 4b7d866be..95fc86880 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -29,7 +29,7 @@ import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/s import { updateRecipient } from '@documenso/lib/server-only/recipient/update-recipient'; import { createTeamMemberInvites } from '@documenso/lib/server-only/team/create-team-member-invites'; import { deleteTeamMembers } from '@documenso/lib/server-only/team/delete-team-members'; -import type { CreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template'; +import type { TCreateDocumentFromTemplateResponse } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/template/create-document-from-template-legacy'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; @@ -345,7 +345,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { }); } - const recipients = await setRecipientsForDocument({ + const { recipients } = await setRecipientsForDocument({ userId: user.id, teamId: team?.id, documentId: document.id, @@ -560,7 +560,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { const templateId = Number(params.templateId); - let document: CreateDocumentFromTemplateResponse | null = null; + let document: TCreateDocumentFromTemplateResponse | null = null; try { document = await createDocumentFromTemplate({ @@ -630,7 +630,6 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { token: recipient.token, role: recipient.role, signingOrder: recipient.signingOrder, - signingUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/sign/${recipient.token}`, })), }, @@ -786,7 +785,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { } try { - const newRecipients = await setRecipientsForDocument({ + const { recipients: newRecipients } = await setRecipientsForDocument({ documentId: Number(documentId), userId: user.id, teamId: team?.id, diff --git a/packages/lib/server-only/document/create-document.ts b/packages/lib/server-only/document/create-document.ts index ddca04bac..51df41166 100644 --- a/packages/lib/server-only/document/create-document.ts +++ b/packages/lib/server-only/document/create-document.ts @@ -1,5 +1,7 @@ 'use server'; +import type { z } from 'zod'; + import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -8,6 +10,7 @@ import { prisma } from '@documenso/prisma'; import { DocumentSource, DocumentVisibility, WebhookTriggerEvents } from '@documenso/prisma/client'; import type { Team, TeamGlobalSettings } from '@documenso/prisma/client'; import { TeamMemberRole } from '@documenso/prisma/client'; +import { DocumentSchema } from '@documenso/prisma/generated/zod'; import { ZWebhookDocumentSchema } from '../../types/webhook-payload'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; @@ -22,6 +25,10 @@ export type CreateDocumentOptions = { requestMetadata?: RequestMetadata; }; +export const ZCreateDocumentResponseSchema = DocumentSchema; + +export type TCreateDocumentResponse = z.infer; + export const createDocument = async ({ userId, title, @@ -30,7 +37,7 @@ export const createDocument = async ({ teamId, formValues, requestMetadata, -}: CreateDocumentOptions) => { +}: CreateDocumentOptions): Promise => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, 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 87d0252c7..1c7d0a38d 100644 --- a/packages/lib/server-only/document/duplicate-document-by-id.ts +++ b/packages/lib/server-only/document/duplicate-document-by-id.ts @@ -1,19 +1,27 @@ +import { z } from 'zod'; + import { prisma } from '@documenso/prisma'; import { DocumentSource, type Prisma } from '@documenso/prisma/client'; import { getDocumentWhereInput } from './get-document-by-id'; -export interface DuplicateDocumentByIdOptions { +export interface DuplicateDocumentOptions { documentId: number; userId: number; teamId?: number; } -export const duplicateDocumentById = async ({ +export const ZDuplicateDocumentResponseSchema = z.object({ + documentId: z.number(), +}); + +export type TDuplicateDocumentResponse = z.infer; + +export const duplicateDocument = async ({ documentId, userId, teamId, -}: DuplicateDocumentByIdOptions) => { +}: DuplicateDocumentOptions): Promise => { const documentWhereInput = await getDocumentWhereInput({ documentId, userId, @@ -78,5 +86,7 @@ export const duplicateDocumentById = async ({ const createdDocument = await prisma.document.create(createDocumentArguments); - return createdDocument.id; + return { + documentId: createdDocument.id, + }; }; diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index 7b47cac33..ad53af829 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; import { match } from 'ts-pattern'; +import type { z } from 'zod'; import { prisma } from '@documenso/prisma'; import type { @@ -11,10 +12,16 @@ import type { User, } from '@documenso/prisma/client'; import { RecipientRole, SigningStatus, TeamMemberRole } from '@documenso/prisma/client'; +import { + DocumentSchema, + RecipientSchema, + TeamSchema, + UserSchema, +} from '@documenso/prisma/generated/zod'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { DocumentVisibility } from '../../types/document-visibility'; -import type { FindResultResponse } from '../../types/search-params'; +import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params'; import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document'; export type PeriodSelectorValue = '' | '7d' | '14d' | '30d'; @@ -36,6 +43,23 @@ export type FindDocumentsOptions = { query?: string; }; +export const ZFindDocumentsResponseSchema = ZFindResultResponse.extend({ + data: DocumentSchema.extend({ + User: UserSchema.pick({ + id: true, + name: true, + email: true, + }), + Recipient: RecipientSchema.array(), + team: TeamSchema.pick({ + id: true, + url: true, + }).nullable(), + }).array(), // Todo: openapi remap. +}); + +export type TFindDocumentsResponse = z.infer; + export const findDocuments = async ({ userId, teamId, @@ -48,7 +72,7 @@ export const findDocuments = async ({ period, senderIds, query, -}: FindDocumentsOptions) => { +}: FindDocumentsOptions): Promise => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, 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 20d0e069e..bde03c337 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,6 +1,15 @@ -import { prisma } from '@documenso/prisma'; -import type { DocumentWithDetails } from '@documenso/prisma/types/document'; +import type { z } from 'zod'; +import { prisma } from '@documenso/prisma'; +import { + DocumentDataSchema, + DocumentMetaSchema, + DocumentSchema, + FieldSchema, + RecipientSchema, +} from '@documenso/prisma/generated/zod'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; import { getDocumentWhereInput } from './get-document-by-id'; export type GetDocumentWithDetailsByIdOptions = { @@ -9,18 +18,29 @@ export type GetDocumentWithDetailsByIdOptions = { teamId?: number; }; +export const ZGetDocumentWithDetailsByIdResponseSchema = DocumentSchema.extend({ + documentData: DocumentDataSchema, + documentMeta: DocumentMetaSchema.nullable(), + Recipient: RecipientSchema.array(), + Field: FieldSchema.array(), +}); + +export type TGetDocumentWithDetailsByIdResponse = z.infer< + typeof ZGetDocumentWithDetailsByIdResponseSchema +>; + export const getDocumentWithDetailsById = async ({ documentId, userId, teamId, -}: GetDocumentWithDetailsByIdOptions): Promise => { +}: GetDocumentWithDetailsByIdOptions): Promise => { const documentWhereInput = await getDocumentWhereInput({ documentId, userId, teamId, }); - return await prisma.document.findFirstOrThrow({ + const document = await prisma.document.findFirst({ where: documentWhereInput, include: { documentData: true, @@ -29,4 +49,12 @@ export const getDocumentWithDetailsById = async ({ Field: true, }, }); + + if (!document) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document not found', + }); + } + + return document; }; diff --git a/packages/lib/server-only/document/move-document-to-team.ts b/packages/lib/server-only/document/move-document-to-team.ts index d916ee6cc..8aad63429 100644 --- a/packages/lib/server-only/document/move-document-to-team.ts +++ b/packages/lib/server-only/document/move-document-to-team.ts @@ -1,7 +1,9 @@ import { TRPCError } from '@trpc/server'; +import type { z } from 'zod'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { prisma } from '@documenso/prisma'; +import { DocumentSchema } from '@documenso/prisma/generated/zod'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; @@ -13,12 +15,16 @@ export type MoveDocumentToTeamOptions = { requestMetadata?: RequestMetadata; }; +export const ZMoveDocumentToTeamResponseSchema = DocumentSchema; + +export type TMoveDocumentToTeamResponse = z.infer; + export const moveDocumentToTeam = async ({ documentId, teamId, userId, requestMetadata, -}: MoveDocumentToTeamOptions) => { +}: MoveDocumentToTeamOptions): Promise => { return await prisma.$transaction(async (tx) => { const user = await tx.user.findUniqueOrThrow({ where: { id: userId }, diff --git a/packages/lib/server-only/document/resend-document.tsx b/packages/lib/server-only/document/resend-document.tsx index fa7952213..1ed899fe1 100644 --- a/packages/lib/server-only/document/resend-document.tsx +++ b/packages/lib/server-only/document/resend-document.tsx @@ -38,7 +38,7 @@ export const resendDocument = async ({ recipients, teamId, requestMetadata, -}: ResendDocumentOptions) => { +}: ResendDocumentOptions): Promise => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, diff --git a/packages/lib/server-only/document/send-document.tsx b/packages/lib/server-only/document/send-document.tsx index cdea1fcd0..8fc2674fc 100644 --- a/packages/lib/server-only/document/send-document.tsx +++ b/packages/lib/server-only/document/send-document.tsx @@ -1,3 +1,5 @@ +import type { z } from 'zod'; + import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -11,6 +13,11 @@ import { SigningStatus, WebhookTriggerEvents, } from '@documenso/prisma/client'; +import { + DocumentMetaSchema, + DocumentSchema, + RecipientSchema, +} from '@documenso/prisma/generated/zod'; import { jobs } from '../../jobs/client'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; @@ -27,13 +34,20 @@ export type SendDocumentOptions = { requestMetadata?: RequestMetadata; }; +export const ZSendDocumentResponseSchema = DocumentSchema.extend({ + documentMeta: DocumentMetaSchema.nullable(), + Recipient: RecipientSchema.array(), +}); + +export type TSendDocumentResponse = z.infer; + export const sendDocument = async ({ documentId, userId, teamId, sendEmail, requestMetadata, -}: SendDocumentOptions) => { +}: SendDocumentOptions): Promise => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, @@ -211,6 +225,7 @@ export const sendDocument = async ({ id: documentId, }, include: { + documentMeta: true, Recipient: true, }, }); diff --git a/packages/lib/server-only/document/update-document-settings.ts b/packages/lib/server-only/document/update-document-settings.ts index f9ee5c89e..ae9b0d2db 100644 --- a/packages/lib/server-only/document/update-document-settings.ts +++ b/packages/lib/server-only/document/update-document-settings.ts @@ -1,6 +1,7 @@ 'use server'; import { match } from 'ts-pattern'; +import type { z } from 'zod'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs'; @@ -10,6 +11,7 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit- import { prisma } from '@documenso/prisma'; import { DocumentVisibility } from '@documenso/prisma/client'; import { DocumentStatus, TeamMemberRole } from '@documenso/prisma/client'; +import { DocumentSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; @@ -29,13 +31,17 @@ export type UpdateDocumentSettingsOptions = { requestMetadata?: RequestMetadata; }; +export const ZUpdateDocumentSettingsResponseSchema = DocumentSchema; + +export type TUpdateDocumentSettingsResponse = z.infer; + export const updateDocumentSettings = async ({ userId, teamId, documentId, data, requestMetadata, -}: UpdateDocumentSettingsOptions) => { +}: UpdateDocumentSettingsOptions): Promise => { if (!data.title && !data.globalAccessAuth && !data.globalActionAuth) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Missing data to update', 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 71cebc95b..13aa75d54 100644 --- a/packages/lib/server-only/field/get-field-by-id.ts +++ b/packages/lib/server-only/field/get-field-by-id.ts @@ -1,4 +1,9 @@ +import type { z } from 'zod'; + import { prisma } from '@documenso/prisma'; +import { FieldSchema } from '@documenso/prisma/generated/zod'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; export type GetFieldByIdOptions = { userId: number; @@ -8,13 +13,17 @@ export type GetFieldByIdOptions = { templateId?: number; }; +export const ZGetFieldByIdResponseSchema = FieldSchema; + +export type TGetFieldByIdResponse = z.infer; + export const getFieldById = async ({ userId, teamId, fieldId, documentId, templateId, -}: GetFieldByIdOptions) => { +}: GetFieldByIdOptions): Promise => { const field = await prisma.field.findFirst({ where: { id: fieldId, @@ -45,5 +54,11 @@ export const getFieldById = async ({ }, }); + if (!field) { + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Field not found', + }); + } + return field; }; 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 62c9d42ca..6b3a79bf2 100644 --- a/packages/lib/server-only/field/set-fields-for-document.ts +++ b/packages/lib/server-only/field/set-fields-for-document.ts @@ -1,4 +1,5 @@ import { isDeepEqual } from 'remeda'; +import { z } from 'zod'; import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown'; @@ -23,6 +24,7 @@ import { import { prisma } from '@documenso/prisma'; import type { Field } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client'; +import { FieldSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { canRecipientFieldsBeModified } from '../../utils/recipients'; @@ -34,12 +36,18 @@ export interface SetFieldsForDocumentOptions { requestMetadata?: RequestMetadata; } +export const ZSetFieldsForDocumentResponseSchema = z.object({ + fields: z.array(FieldSchema), +}); + +export type TSetFieldsForDocumentResponse = z.infer; + export const setFieldsForDocument = async ({ userId, documentId, fields, requestMetadata, -}: SetFieldsForDocumentOptions): Promise => { +}: SetFieldsForDocumentOptions): Promise => { const document = await prisma.document.findFirst({ where: { id: documentId, @@ -75,11 +83,15 @@ export const setFieldsForDocument = async ({ }); if (!document) { - throw new Error('Document not found'); + throw new AppError(AppErrorCode.NOT_FOUND, { + message: 'Document not found', + }); } if (document.completedAt) { - throw new Error('Document already complete'); + throw new AppError(AppErrorCode.INVALID_REQUEST, { + message: 'Document already complete', + }); } const existingFields = await prisma.field.findMany({ @@ -335,7 +347,9 @@ export const setFieldsForDocument = async ({ return !isRemoved && !isUpdated; }); - return [...filteredFields, ...persistedFields]; + return { + fields: [...filteredFields, ...persistedFields], + }; }; /** 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 88002479a..e89fa6250 100644 --- a/packages/lib/server-only/field/set-fields-for-template.ts +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox'; import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown'; import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number'; @@ -14,6 +16,7 @@ import { } from '@documenso/lib/types/field-meta'; import { prisma } from '@documenso/prisma'; import { FieldType } from '@documenso/prisma/client'; +import { FieldSchema } from '@documenso/prisma/generated/zod'; export type SetFieldsForTemplateOptions = { userId: number; @@ -31,11 +34,17 @@ export type SetFieldsForTemplateOptions = { }[]; }; +export const ZSetFieldsForTemplateResponseSchema = z.object({ + fields: z.array(FieldSchema), +}); + +export type TSetFieldsForTemplateResponse = z.infer; + export const setFieldsForTemplate = async ({ userId, templateId, fields, -}: SetFieldsForTemplateOptions) => { +}: SetFieldsForTemplateOptions): Promise => { const template = await prisma.template.findFirst({ where: { id: templateId, @@ -206,5 +215,7 @@ export const setFieldsForTemplate = async ({ return !isRemoved && !isUpdated; }); - return [...filteredFields, ...persistedFields]; + return { + fields: [...filteredFields, ...persistedFields], + }; }; diff --git a/packages/lib/server-only/recipient/set-recipients-for-document.ts b/packages/lib/server-only/recipient/set-recipients-for-document.ts index 56fe720d2..fb1a50997 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-document.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-document.ts @@ -1,6 +1,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/macro'; +import { z } from 'zod'; import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { mailer } from '@documenso/email/mailer'; @@ -21,6 +22,7 @@ import { prisma } from '@documenso/prisma'; import type { Recipient } from '@documenso/prisma/client'; import { RecipientRole } from '@documenso/prisma/client'; import { SendStatus, SigningStatus } from '@documenso/prisma/client'; +import { RecipientSchema } from '@documenso/prisma/generated/zod'; import { getI18nInstance } from '../../client-only/providers/i18n.server'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; @@ -39,13 +41,21 @@ export interface SetRecipientsForDocumentOptions { requestMetadata?: RequestMetadata; } +export const ZSetRecipientsForDocumentResponseSchema = z.object({ + recipients: RecipientSchema.array(), +}); + +export type TSetRecipientsForDocumentResponse = z.infer< + typeof ZSetRecipientsForDocumentResponseSchema +>; + export const setRecipientsForDocument = async ({ userId, teamId, documentId, recipients, requestMetadata, -}: SetRecipientsForDocumentOptions): Promise => { +}: SetRecipientsForDocumentOptions): Promise => { const document = await prisma.document.findFirst({ where: { id: documentId, @@ -344,7 +354,9 @@ export const setRecipientsForDocument = async ({ return !isRemoved && !isUpdated; }); - return [...filteredRecipients, ...persistedRecipients]; + return { + recipients: [...filteredRecipients, ...persistedRecipients], + }; }; /** diff --git a/packages/lib/server-only/recipient/set-recipients-for-template.ts b/packages/lib/server-only/recipient/set-recipients-for-template.ts index ff0f2900e..82859ca73 100644 --- a/packages/lib/server-only/recipient/set-recipients-for-template.ts +++ b/packages/lib/server-only/recipient/set-recipients-for-template.ts @@ -1,3 +1,5 @@ +import { z } from 'zod'; + import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL, @@ -6,6 +8,7 @@ import { import { prisma } from '@documenso/prisma'; import type { Recipient } from '@documenso/prisma/client'; import { RecipientRole } from '@documenso/prisma/client'; +import { RecipientSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; import { @@ -29,12 +32,20 @@ export type SetRecipientsForTemplateOptions = { }[]; }; +export const ZSetRecipientsForTemplateResponseSchema = z.object({ + recipients: RecipientSchema.array(), +}); + +export type TSetRecipientsForTemplateResponse = z.infer< + typeof ZSetRecipientsForTemplateResponseSchema +>; + export const setRecipientsForTemplate = async ({ userId, teamId, templateId, recipients, -}: SetRecipientsForTemplateOptions) => { +}: SetRecipientsForTemplateOptions): Promise => { const template = await prisma.template.findFirst({ where: { id: templateId, @@ -220,5 +231,7 @@ export const setRecipientsForTemplate = async ({ return !isRemoved && !isUpdated; }); - return [...filteredRecipients, ...persistedRecipients]; + return { + recipients: [...filteredRecipients, ...persistedRecipients], + }; }; 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 b7246e5fa..68b0d8060 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 @@ -3,6 +3,7 @@ import { createElement } from 'react'; import { msg } from '@lingui/macro'; import { DateTime } from 'luxon'; import { match } from 'ts-pattern'; +import { z } from 'zod'; import { mailer } from '@documenso/email/mailer'; import { DocumentCreatedFromDirectTemplateEmailTemplate } from '@documenso/email/templates/document-created-from-direct-template'; @@ -67,6 +68,16 @@ type CreatedDirectRecipientField = { derivedRecipientActionAuth: TRecipientActionAuthTypes | null; }; +export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({ + token: z.string(), + documentId: z.number(), + recipientId: z.number(), +}); + +export type TCreateDocumentFromDirectTemplateResponse = z.infer< + typeof ZCreateDocumentFromDirectTemplateResponseSchema +>; + export const createDocumentFromDirectTemplate = async ({ directRecipientName: initialDirectRecipientName, directRecipientEmail, @@ -76,7 +87,7 @@ export const createDocumentFromDirectTemplate = async ({ templateUpdatedAt, requestMetadata, user, -}: CreateDocumentFromDirectTemplateOptions) => { +}: CreateDocumentFromDirectTemplateOptions): Promise => { const template = await prisma.template.findFirst({ where: { directLink: { 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 cc38b68ca..949de80e5 100644 --- a/packages/lib/server-only/template/create-document-from-template.ts +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -1,3 +1,5 @@ +import type { z } from 'zod'; + import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import type { DocumentDistributionMethod } from '@documenso/prisma/client'; @@ -11,6 +13,11 @@ import { SigningStatus, WebhookTriggerEvents, } from '@documenso/prisma/client'; +import { + DocumentDataSchema, + DocumentSchema, + RecipientSchema, +} from '@documenso/prisma/generated/zod'; import type { SupportedLanguageCodes } from '../../constants/i18n'; import { AppError, AppErrorCode } from '../../errors/app-error'; @@ -36,10 +43,6 @@ type FinalRecipient = Pick< fields: Field[]; }; -export type CreateDocumentFromTemplateResponse = Awaited< - ReturnType ->; - export type CreateDocumentFromTemplateOptions = { templateId: number; externalId?: string | null; @@ -72,6 +75,15 @@ export type CreateDocumentFromTemplateOptions = { requestMetadata?: RequestMetadata; }; +export const ZCreateDocumentFromTemplateResponseSchema = DocumentSchema.extend({ + documentData: DocumentDataSchema, + Recipient: RecipientSchema.array(), +}); + +export type TCreateDocumentFromTemplateResponse = z.infer< + typeof ZCreateDocumentFromTemplateResponseSchema +>; + export const createDocumentFromTemplate = async ({ templateId, externalId, @@ -80,7 +92,7 @@ export const createDocumentFromTemplate = async ({ recipients, override, requestMetadata, -}: CreateDocumentFromTemplateOptions) => { +}: CreateDocumentFromTemplateOptions): Promise => { const user = await prisma.user.findFirstOrThrow({ where: { id: userId, 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 47cd674c8..388739498 100644 --- a/packages/lib/server-only/template/create-template-direct-link.ts +++ b/packages/lib/server-only/template/create-template-direct-link.ts @@ -1,13 +1,15 @@ 'use server'; import { nanoid } from 'nanoid'; +import type { z } from 'zod'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL, DIRECT_TEMPLATE_RECIPIENT_NAME, } from '@documenso/lib/constants/direct-templates'; import { prisma } from '@documenso/prisma'; -import type { Recipient, TemplateDirectLink } from '@documenso/prisma/client'; +import type { Recipient } from '@documenso/prisma/client'; +import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; @@ -17,11 +19,17 @@ export type CreateTemplateDirectLinkOptions = { directRecipientId?: number; }; +export const ZCreateTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema; + +export type TCreateTemplateDirectLinkResponse = z.infer< + typeof ZCreateTemplateDirectLinkResponseSchema +>; + export const createTemplateDirectLink = async ({ templateId, userId, directRecipientId, -}: CreateTemplateDirectLinkOptions): Promise => { +}: CreateTemplateDirectLinkOptions): Promise => { const template = await prisma.template.findFirst({ where: { id: templateId, diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts index e51d69485..e5dd10ecc 100644 --- a/packages/lib/server-only/template/create-template.ts +++ b/packages/lib/server-only/template/create-template.ts @@ -1,4 +1,7 @@ +import type { z } from 'zod'; + import { prisma } from '@documenso/prisma'; +import { TemplateSchema } from '@documenso/prisma/generated/zod'; import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; export type CreateTemplateOptions = TCreateTemplateMutationSchema & { @@ -6,6 +9,10 @@ export type CreateTemplateOptions = TCreateTemplateMutationSchema & { teamId?: number; }; +export const ZCreateTemplateResponseSchema = TemplateSchema; + +export type TCreateTemplateResponse = z.infer; + export const createTemplate = async ({ title, userId, diff --git a/packages/lib/server-only/template/duplicate-template.ts b/packages/lib/server-only/template/duplicate-template.ts index f4348f019..8d5722a82 100644 --- a/packages/lib/server-only/template/duplicate-template.ts +++ b/packages/lib/server-only/template/duplicate-template.ts @@ -1,19 +1,25 @@ import { omit } from 'remeda'; +import type { z } from 'zod'; import { nanoid } from '@documenso/lib/universal/id'; import { prisma } from '@documenso/prisma'; import type { Prisma } from '@documenso/prisma/client'; +import { TemplateSchema } from '@documenso/prisma/generated/zod'; import type { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & { userId: number; }; +export const ZDuplicateTemplateResponseSchema = TemplateSchema; + +export type TDuplicateTemplateResponse = z.infer; + export const duplicateTemplate = async ({ templateId, userId, teamId, -}: DuplicateTemplateOptions) => { +}: DuplicateTemplateOptions): Promise => { let templateWhereFilter: Prisma.TemplateWhereUniqueInput = { id: templateId, userId, diff --git a/packages/lib/server-only/template/find-templates.ts b/packages/lib/server-only/template/find-templates.ts index f33b0031b..a9b7d9075 100644 --- a/packages/lib/server-only/template/find-templates.ts +++ b/packages/lib/server-only/template/find-templates.ts @@ -1,7 +1,18 @@ +import type { z } from 'zod'; + import { prisma } from '@documenso/prisma'; import type { Prisma, Template } from '@documenso/prisma/client'; +import { + DocumentDataSchema, + FieldSchema, + RecipientSchema, + TeamSchema, + TemplateDirectLinkSchema, + TemplateMetaSchema, + TemplateSchema, +} from '@documenso/prisma/generated/zod'; -import type { FindResultResponse } from '../../types/search-params'; +import { type FindResultResponse, ZFindResultResponse } from '../../types/search-params'; export type FindTemplatesOptions = { userId: number; @@ -11,8 +22,28 @@ export type FindTemplatesOptions = { perPage?: number; }; -export type FindTemplatesResponse = Awaited>; -export type FindTemplateRow = FindTemplatesResponse['data'][number]; +export const ZFindTemplatesResponseSchema = ZFindResultResponse.extend({ + data: TemplateSchema.extend({ + templateDocumentData: DocumentDataSchema, + team: TeamSchema.pick({ + id: true, + url: true, + }).nullable(), + Field: FieldSchema.array(), + Recipient: RecipientSchema.array(), + templateMeta: TemplateMetaSchema.pick({ + signingOrder: true, + distributionMethod: true, + }).nullable(), + directLink: TemplateDirectLinkSchema.pick({ + token: true, + enabled: true, + }).nullable(), + }).array(), // Todo: openapi. +}); + +export type TFindTemplatesResponse = z.infer; +export type FindTemplateRow = TFindTemplatesResponse['data'][number]; export const findTemplates = async ({ userId, @@ -20,7 +51,7 @@ export const findTemplates = async ({ type, page = 1, perPage = 10, -}: FindTemplatesOptions) => { +}: FindTemplatesOptions): Promise => { let whereFilter: Prisma.TemplateWhereInput = { userId, teamId: null, diff --git a/packages/lib/server-only/template/move-template-to-team.ts b/packages/lib/server-only/template/move-template-to-team.ts index 522aa5e9b..9dae002a1 100644 --- a/packages/lib/server-only/template/move-template-to-team.ts +++ b/packages/lib/server-only/template/move-template-to-team.ts @@ -1,6 +1,9 @@ -import { TRPCError } from '@trpc/server'; +import type { z } from 'zod'; import { prisma } from '@documenso/prisma'; +import { TemplateSchema } from '@documenso/prisma/generated/zod'; + +import { AppError, AppErrorCode } from '../../errors/app-error'; export type MoveTemplateToTeamOptions = { templateId: number; @@ -8,11 +11,15 @@ export type MoveTemplateToTeamOptions = { userId: number; }; +export const ZMoveTemplateToTeamResponseSchema = TemplateSchema; + +export type TMoveTemplateToTeamResponse = z.infer; + export const moveTemplateToTeam = async ({ templateId, teamId, userId, -}: MoveTemplateToTeamOptions) => { +}: MoveTemplateToTeamOptions): Promise => { return await prisma.$transaction(async (tx) => { const template = await tx.template.findFirst({ where: { @@ -23,8 +30,7 @@ export const moveTemplateToTeam = async ({ }); if (!template) { - throw new TRPCError({ - code: 'NOT_FOUND', + throw new AppError(AppErrorCode.NOT_FOUND, { message: 'Template not found or already associated with a team.', }); } @@ -41,9 +47,8 @@ export const moveTemplateToTeam = async ({ }); if (!team) { - throw new TRPCError({ - code: 'FORBIDDEN', - message: 'You are not a member of this team.', + throw new AppError(AppErrorCode.UNAUTHORIZED, { + message: 'Team does not exist or you are not a member of this team.', }); } 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 5b8b7bf38..7c1573ef9 100644 --- a/packages/lib/server-only/template/toggle-template-direct-link.ts +++ b/packages/lib/server-only/template/toggle-template-direct-link.ts @@ -1,6 +1,9 @@ 'use server'; +import type { z } from 'zod'; + import { prisma } from '@documenso/prisma'; +import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; @@ -10,11 +13,17 @@ export type ToggleTemplateDirectLinkOptions = { enabled: boolean; }; +export const ZToggleTemplateDirectLinkResponseSchema = TemplateDirectLinkSchema; + +export type TToggleTemplateDirectLinkResponse = z.infer< + typeof ZToggleTemplateDirectLinkResponseSchema +>; + export const toggleTemplateDirectLink = async ({ templateId, userId, enabled, -}: ToggleTemplateDirectLinkOptions) => { +}: ToggleTemplateDirectLinkOptions): Promise => { const template = await prisma.template.findFirst({ where: { id: templateId, diff --git a/packages/lib/server-only/template/update-template-settings.ts b/packages/lib/server-only/template/update-template-settings.ts index c12b5c815..97d3bdbbe 100644 --- a/packages/lib/server-only/template/update-template-settings.ts +++ b/packages/lib/server-only/template/update-template-settings.ts @@ -1,9 +1,12 @@ 'use server'; +import type { z } from 'zod'; + import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise'; import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { prisma } from '@documenso/prisma'; import type { Template, TemplateMeta } from '@documenso/prisma/client'; +import { TemplateSchema } from '@documenso/prisma/generated/zod'; import { AppError, AppErrorCode } from '../../errors/app-error'; import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth'; @@ -26,13 +29,17 @@ export type UpdateTemplateSettingsOptions = { requestMetadata?: RequestMetadata; }; +export const ZUpdateTemplateSettingsResponseSchema = TemplateSchema; + +export type TUpdateTemplateSettingsResponse = z.infer; + export const updateTemplateSettings = async ({ userId, teamId, templateId, meta, data, -}: UpdateTemplateSettingsOptions) => { +}: UpdateTemplateSettingsOptions): Promise => { if (Object.values(data).length === 0 && Object.keys(meta ?? {}).length === 0) { throw new AppError(AppErrorCode.INVALID_BODY, { message: 'Missing data to update', diff --git a/packages/lib/utils/logger.ts b/packages/lib/utils/logger.ts index ed44cd571..c38cee434 100644 --- a/packages/lib/utils/logger.ts +++ b/packages/lib/utils/logger.ts @@ -78,7 +78,9 @@ class HoneybadgerLogger implements Logger { error(error: Error, options?: LoggerDescriptionOptions): void { const { context = {}, level = 'error', method, path } = options || {}; - const tags = [`level:${level}`]; + // const tags = [`level:${level}`]; + const tags = []; + let errorMessage = error.message; if (method) { diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 8ba577362..c2e8af48a 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -8,19 +8,40 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto'; import { AppError } from '@documenso/lib/errors/app-error'; import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt'; import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta'; -import { createDocument } from '@documenso/lib/server-only/document/create-document'; +import { + ZCreateDocumentResponseSchema, + createDocument, +} from '@documenso/lib/server-only/document/create-document'; import { deleteDocument } from '@documenso/lib/server-only/document/delete-document'; -import { duplicateDocumentById } from '@documenso/lib/server-only/document/duplicate-document-by-id'; +import { + ZDuplicateDocumentResponseSchema, + duplicateDocument, +} from '@documenso/lib/server-only/document/duplicate-document-by-id'; import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs'; -import { findDocuments } from '@documenso/lib/server-only/document/find-documents'; +import { + ZFindDocumentsResponseSchema, + findDocuments, +} from '@documenso/lib/server-only/document/find-documents'; 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 { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id'; -import { moveDocumentToTeam } from '@documenso/lib/server-only/document/move-document-to-team'; +import { + ZGetDocumentWithDetailsByIdResponseSchema, + getDocumentWithDetailsById, +} from '@documenso/lib/server-only/document/get-document-with-details-by-id'; +import { + ZMoveDocumentToTeamResponseSchema, + moveDocumentToTeam, +} from '@documenso/lib/server-only/document/move-document-to-team'; import { resendDocument } from '@documenso/lib/server-only/document/resend-document'; import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword'; -import { sendDocument } from '@documenso/lib/server-only/document/send-document'; -import { updateDocumentSettings } from '@documenso/lib/server-only/document/update-document-settings'; +import { + ZSendDocumentResponseSchema, + sendDocument, +} from '@documenso/lib/server-only/document/send-document'; +import { + ZUpdateDocumentSettingsResponseSchema, + updateDocumentSettings, +} from '@documenso/lib/server-only/document/update-document-settings'; import { updateTitle } from '@documenso/lib/server-only/document/update-title'; import { symmetricEncrypt } from '@documenso/lib/universal/crypto'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -32,12 +53,13 @@ import { ZDeleteDocumentMutationSchema, ZDownloadAuditLogsMutationSchema, ZDownloadCertificateMutationSchema, + ZDuplicateDocumentMutationSchema, ZFindDocumentAuditLogsQuerySchema, ZFindDocumentsQuerySchema, ZGetDocumentByIdQuerySchema, ZGetDocumentByTokenQuerySchema, ZGetDocumentWithDetailsByIdQuerySchema, - ZMoveDocumentsToTeamSchema, + ZMoveDocumentToTeamSchema, ZResendDocumentMutationSchema, ZSearchDocumentsMutationSchema, ZSendDocumentMutationSchema, @@ -49,7 +71,9 @@ import { } from './schema'; export const documentRouter = router({ - // Internal endpoint for now. + /** + * @private + */ getDocumentById: authenticatedProcedure .input(ZGetDocumentByIdQuerySchema) .query(async ({ input, ctx }) => { @@ -59,7 +83,9 @@ export const documentRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ getDocumentByToken: procedure .input(ZGetDocumentByTokenQuerySchema) .query(async ({ input, ctx }) => { @@ -71,6 +97,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ findDocuments: authenticatedProcedure .meta({ openapi: { @@ -82,11 +111,21 @@ export const documentRouter = router({ }, }) .input(ZFindDocumentsQuerySchema) - .output(z.unknown()) + .output(ZFindDocumentsResponseSchema) .query(async ({ input, ctx }) => { const { user } = ctx; - const { query, teamId, templateId, page, perPage, orderBy, source, status } = input; + const { + query, + teamId, + templateId, + page, + perPage, + orderByDirection, + orderByColumn, + source, + status, + } = input; const documents = await findDocuments({ userId: user.id, @@ -97,12 +136,17 @@ export const documentRouter = router({ status, page, perPage, - orderBy, + orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, }); return documents; }), + /** + * @public + * + * Todo: Refactor to getDocumentById. + */ getDocumentWithDetailsById: authenticatedProcedure .meta({ openapi: { @@ -114,7 +158,7 @@ export const documentRouter = router({ }, }) .input(ZGetDocumentWithDetailsByIdQuerySchema) - .output(z.unknown()) + .output(ZGetDocumentWithDetailsByIdResponseSchema) .query(async ({ input, ctx }) => { return await getDocumentWithDetailsById({ ...input, @@ -122,6 +166,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ createDocument: authenticatedProcedure .meta({ openapi: { @@ -132,7 +179,7 @@ export const documentRouter = router({ }, }) .input(ZCreateDocumentMutationSchema) - .output(z.unknown()) + .output(ZCreateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { title, documentDataId, teamId } = input; @@ -154,7 +201,11 @@ export const documentRouter = router({ }); }), - // Todo: Refactor to updateDocument. + /** + * @public + * + * Todo: Refactor to updateDocument. + */ setSettingsForDocument: authenticatedProcedure .meta({ openapi: { @@ -165,7 +216,7 @@ export const documentRouter = router({ }, }) .input(ZSetSettingsForDocumentMutationSchema) - .output(z.unknown()) + .output(ZUpdateDocumentSettingsResponseSchema) .mutation(async ({ input, ctx }) => { const { documentId, teamId, data, meta } = input; @@ -194,6 +245,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ deleteDocument: authenticatedProcedure .meta({ openapi: { @@ -204,13 +258,13 @@ export const documentRouter = router({ }, }) .input(ZDeleteDocumentMutationSchema) - .output(z.unknown()) + .output(z.void()) .mutation(async ({ input, ctx }) => { const { documentId, teamId } = input; const userId = ctx.user.id; - return await deleteDocument({ + await deleteDocument({ id: documentId, userId, teamId, @@ -218,6 +272,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ moveDocumentToTeam: authenticatedProcedure .meta({ openapi: { @@ -228,8 +285,8 @@ export const documentRouter = router({ tags: ['Documents'], }, }) - .input(ZMoveDocumentsToTeamSchema) - .output(z.unknown()) + .input(ZMoveDocumentToTeamSchema) + .output(ZMoveDocumentToTeamResponseSchema) .mutation(async ({ input, ctx }) => { const { documentId, teamId } = input; const userId = ctx.user.id; @@ -242,7 +299,9 @@ export const documentRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ // Should probably use `updateDocument` setTitleForDocument: authenticatedProcedure .input(ZSetTitleForDocumentMutationSchema) @@ -260,7 +319,9 @@ export const documentRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ setPasswordForDocument: authenticatedProcedure .input(ZSetPasswordForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { @@ -285,7 +346,9 @@ export const documentRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ setSigningOrderForDocument: authenticatedProcedure .input(ZSetSigningOrderForDocumentMutationSchema) .mutation(async ({ input, ctx }) => { @@ -299,7 +362,9 @@ export const documentRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ updateTypedSignatureSettings: authenticatedProcedure .input(ZUpdateTypedSignatureSettingsMutationSchema) .mutation(async ({ input, ctx }) => { @@ -326,8 +391,12 @@ export const documentRouter = router({ }); }), - // Todo: Refactor to distributeDocument. - // Todo: Rework before releasing API. + /** + * @public + * + * Todo: Refactor to distributeDocument. + * Todo: Rework before releasing API. + */ sendDocument: authenticatedProcedure .meta({ openapi: { @@ -339,7 +408,7 @@ export const documentRouter = router({ }, }) .input(ZSendDocumentMutationSchema) - .output(z.unknown()) + .output(ZSendDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { documentId, teamId, meta } = input; @@ -374,6 +443,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ resendDocument: authenticatedProcedure .meta({ openapi: { @@ -386,7 +458,7 @@ export const documentRouter = router({ }, }) .input(ZResendDocumentMutationSchema) - .output(z.unknown()) + .output(z.void()) .mutation(async ({ input, ctx }) => { return await resendDocument({ userId: ctx.user.id, @@ -395,6 +467,9 @@ export const documentRouter = router({ }); }), + /** + * @public + */ duplicateDocument: authenticatedProcedure .meta({ openapi: { @@ -404,16 +479,18 @@ export const documentRouter = router({ tags: ['Documents'], }, }) - .input(ZGetDocumentByIdQuerySchema) - .output(z.unknown()) + .input(ZDuplicateDocumentMutationSchema) + .output(ZDuplicateDocumentResponseSchema) .mutation(async ({ input, ctx }) => { - return await duplicateDocumentById({ + return await duplicateDocument({ userId: ctx.user.id, ...input, }); }), - // Internal endpoint for now. + /** + * @private + */ searchDocuments: authenticatedProcedure .input(ZSearchDocumentsMutationSchema) .query(async ({ input, ctx }) => { @@ -427,11 +504,21 @@ export const documentRouter = router({ return documents; }), - // Internal endpoint for now. + /** + * @private + */ findDocumentAuditLogs: authenticatedProcedure .input(ZFindDocumentAuditLogsQuerySchema) .query(async ({ input, ctx }) => { - const { page, perPage, documentId, cursor, filterForRecentActivity, orderBy } = input; + const { + page, + perPage, + documentId, + cursor, + filterForRecentActivity, + orderByColumn, + orderByDirection, + } = input; return await findDocumentAuditLogs({ page, @@ -439,12 +526,14 @@ export const documentRouter = router({ documentId, cursor, filterForRecentActivity, - orderBy, + orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined, userId: ctx.user.id, }); }), - // Internal endpoint for now. + /** + * @private + */ downloadAuditLogs: authenticatedProcedure .input(ZDownloadAuditLogsMutationSchema) .mutation(async ({ input, ctx }) => { @@ -473,7 +562,9 @@ export const documentRouter = router({ }; }), - // Internal endpoint for now. + /** + * @private + */ downloadCertificate: authenticatedProcedure .input(ZDownloadCertificateMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index 8c96b3e22..af7845986 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -23,24 +23,16 @@ export const ZFindDocumentsQuerySchema = ZFindSearchParamsSchema.extend({ templateId: z.number().min(1).optional(), source: z.nativeEnum(DocumentSource).optional(), status: z.nativeEnum(DocumentStatus).optional(), - orderBy: z - .object({ - column: z.enum(['createdAt']), - direction: z.enum(['asc', 'desc']), - }) - .optional(), + orderByColumn: z.enum(['createdAt']).optional(), + orderByDirection: z.enum(['asc', 'desc']).default('desc'), }); export const ZFindDocumentAuditLogsQuerySchema = ZFindSearchParamsSchema.extend({ documentId: z.number().min(1), cursor: z.string().optional(), filterForRecentActivity: z.boolean().optional(), - orderBy: z - .object({ - column: z.enum(['createdAt', 'type']), - direction: z.enum(['asc', 'desc']), - }) - .optional(), + orderByColumn: z.enum(['createdAt', 'type']).optional(), + orderByDirection: z.enum(['asc', 'desc']).default('desc'), }); export const ZGetDocumentByIdQuerySchema = z.object({ @@ -48,6 +40,11 @@ export const ZGetDocumentByIdQuerySchema = z.object({ teamId: z.number().min(1).optional(), }); +export const ZDuplicateDocumentMutationSchema = z.object({ + documentId: z.number().min(1), + teamId: z.number().min(1).optional(), +}); + export type TGetDocumentByIdQuerySchema = z.infer; export const ZGetDocumentByTokenQuerySchema = z.object({ @@ -223,7 +220,7 @@ export const ZDownloadCertificateMutationSchema = z.object({ teamId: z.number().optional(), }); -export const ZMoveDocumentsToTeamSchema = z.object({ +export const ZMoveDocumentToTeamSchema = z.object({ documentId: z.number(), teamId: z.number(), }); diff --git a/packages/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index eb11875f4..d1b31e9a8 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -1,9 +1,16 @@ -import { z } from 'zod'; - -import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id'; +import { + ZGetFieldByIdResponseSchema, + getFieldById, +} from '@documenso/lib/server-only/field/get-field-by-id'; import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token'; -import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; -import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; +import { + ZSetFieldsForDocumentResponseSchema, + setFieldsForDocument, +} from '@documenso/lib/server-only/field/set-fields-for-document'; +import { + ZSetFieldsForTemplateResponseSchema, + setFieldsForTemplate, +} from '@documenso/lib/server-only/field/set-fields-for-template'; import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; @@ -17,6 +24,9 @@ import { } from './schema'; export const fieldRouter = router({ + /** + * @public + */ getField: authenticatedProcedure .meta({ openapi: { @@ -28,7 +38,7 @@ export const fieldRouter = router({ }, }) .input(ZGetFieldQuerySchema) - .output(z.unknown()) + .output(ZGetFieldByIdResponseSchema) .query(async ({ input, ctx }) => { const { fieldId, teamId } = input; @@ -39,6 +49,9 @@ export const fieldRouter = router({ }); }), + /** + * @public + */ addFields: authenticatedProcedure .meta({ openapi: { @@ -49,7 +62,7 @@ export const fieldRouter = router({ }, }) .input(ZAddFieldsMutationSchema) - .output(z.unknown()) + .output(ZSetFieldsForDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { documentId, fields } = input; @@ -71,6 +84,9 @@ export const fieldRouter = router({ }); }), + /** + * @public + */ addTemplateFields: authenticatedProcedure .meta({ openapi: { @@ -81,7 +97,7 @@ export const fieldRouter = router({ }, }) .input(ZAddTemplateFieldsMutationSchema) - .output(z.unknown()) + .output(ZSetFieldsForTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, fields } = input; @@ -102,7 +118,9 @@ export const fieldRouter = router({ }); }), - // Internal endpoint for now. + /** + * @internal + */ signFieldWithToken: procedure .input(ZSignFieldWithTokenMutationSchema) .mutation(async ({ input, ctx }) => { @@ -119,7 +137,9 @@ export const fieldRouter = router({ }); }), - // Internal endpoint for now. + /** + * @internal + */ removeSignedFieldWithToken: procedure .input(ZRemovedSignedFieldWithTokenMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/open-api.ts b/packages/trpc/server/open-api.ts new file mode 100644 index 000000000..05414cf7b --- /dev/null +++ b/packages/trpc/server/open-api.ts @@ -0,0 +1,12 @@ +import { generateOpenApiDocument } from 'trpc-openapi'; + +import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; + +import { appRouter } from './router'; + +export const openApiDocument = generateOpenApiDocument(appRouter, { + title: 'Do not use.', + version: '0.0.0', + baseUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/api/beta`, + // docsUrl: '', // Todo +}); diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index 6cdc5788a..f440f54ea 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -1,9 +1,13 @@ -import { z } from 'zod'; - 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 { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; -import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; +import { + ZSetRecipientsForDocumentResponseSchema, + setRecipientsForDocument, +} from '@documenso/lib/server-only/recipient/set-recipients-for-document'; +import { + ZSetRecipientsForTemplateResponseSchema, + setRecipientsForTemplate, +} from '@documenso/lib/server-only/recipient/set-recipients-for-template'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { authenticatedProcedure, procedure, router } from '../trpc'; @@ -15,6 +19,9 @@ import { } from './schema'; export const recipientRouter = router({ + /** + * @internal + */ addSigners: authenticatedProcedure .meta({ openapi: { @@ -25,7 +32,7 @@ export const recipientRouter = router({ }, }) .input(ZAddSignersMutationSchema) - .output(z.unknown()) + .output(ZSetRecipientsForDocumentResponseSchema) .mutation(async ({ input, ctx }) => { const { documentId, teamId, signers } = input; @@ -45,6 +52,9 @@ export const recipientRouter = router({ }); }), + /** + * @internal + */ addTemplateSigners: authenticatedProcedure .meta({ openapi: { @@ -55,7 +65,7 @@ export const recipientRouter = router({ }, }) .input(ZAddTemplateSignersMutationSchema) - .output(z.unknown()) + .output(ZSetRecipientsForTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, signers, teamId } = input; @@ -74,7 +84,9 @@ export const recipientRouter = router({ }); }), - // Internal endpoint for now. + /** + * @internal + */ completeDocumentWithToken: procedure .input(ZCompleteDocumentWithTokenMutationSchema) .mutation(async ({ input, ctx }) => { @@ -89,7 +101,9 @@ export const recipientRouter = router({ }); }), - // Internal endpoint for now. + /** + * @internal + */ rejectDocumentWithToken: procedure .input(ZRejectDocumentWithTokenMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/team-router/router.ts b/packages/trpc/server/team-router/router.ts index 109cd168a..67eda92de 100644 --- a/packages/trpc/server/team-router/router.ts +++ b/packages/trpc/server/team-router/router.ts @@ -79,16 +79,17 @@ export const teamRouter = router({ return await getTeams({ userId: ctx.user.id }); }), + // Todo: Public endpoint. findTeams: authenticatedProcedure - .meta({ - openapi: { - method: 'GET', - path: '/team', - summary: 'Find teams', - description: 'Find your teams based on a search criteria', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'GET', + // path: '/team', + // summary: 'Find teams', + // description: 'Find your teams based on a search criteria', + // tags: ['Teams'], + // }, + // }) .input(ZFindTeamsQuerySchema) .output(z.unknown()) .query(async ({ input, ctx }) => { @@ -98,30 +99,32 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. getTeam: authenticatedProcedure - .meta({ - openapi: { - method: 'GET', - path: '/team/{teamId}', - summary: 'Get team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'GET', + // path: '/team/{teamId}', + // summary: 'Get team', + // tags: ['Teams'], + // }, + // }) .input(ZGetTeamQuerySchema) .output(z.unknown()) .query(async ({ input, ctx }) => { return await getTeamById({ teamId: input.teamId, userId: ctx.user.id }); }), + // Todo: Public endpoint. createTeam: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/create', - summary: 'Create team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/create', + // summary: 'Create team', + // tags: ['Teams'], + // }, + // }) .input(ZCreateTeamMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -131,15 +134,16 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. updateTeam: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}', - summary: 'Update team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}', + // summary: 'Update team', + // tags: ['Teams'], + // }, + // }) .input(ZUpdateTeamMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -149,15 +153,16 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. deleteTeam: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/delete', - summary: 'Delete team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/delete', + // summary: 'Delete team', + // tags: ['Teams'], + // }, + // }) .input(ZDeleteTeamMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -167,16 +172,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. leaveTeam: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/leave', - summary: 'Leave a team', - description: '', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/leave', + // summary: 'Leave a team', + // description: '', + // tags: ['Teams'], + // }, + // }) .input(ZLeaveTeamMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -186,16 +192,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. findTeamMemberInvites: authenticatedProcedure - .meta({ - openapi: { - method: 'GET', - path: '/team/{teamId}/member/invite', - summary: 'Find member invites', - description: 'Returns pending team member invites', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'GET', + // path: '/team/{teamId}/member/invite', + // summary: 'Find member invites', + // description: 'Returns pending team member invites', + // tags: ['Teams'], + // }, + // }) .input(ZFindTeamMemberInvitesQuerySchema) .output(z.unknown()) .query(async ({ input, ctx }) => { @@ -205,16 +212,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. createTeamMemberInvites: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/member/invite', - summary: 'Invite members', - description: 'Send email invitations to users to join the team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/member/invite', + // summary: 'Invite members', + // description: 'Send email invitations to users to join the team', + // tags: ['Teams'], + // }, + // }) .input(ZCreateTeamMemberInvitesMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -225,16 +233,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. resendTeamMemberInvitation: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/member/invite/{invitationId}/resend', - summary: 'Resend member invite', - description: 'Resend an email invitation to a user to join the team', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/member/invite/{invitationId}/resend', + // summary: 'Resend member invite', + // description: 'Resend an email invitation to a user to join the team', + // tags: ['Teams'], + // }, + // }) .input(ZResendTeamMemberInvitationMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -245,16 +254,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. deleteTeamMemberInvitations: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/member/invite/delete', - summary: 'Delete member invite', - description: 'Delete a pending team member invite', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/member/invite/delete', + // summary: 'Delete member invite', + // description: 'Delete a pending team member invite', + // tags: ['Teams'], + // }, + // }) .input(ZDeleteTeamMemberInvitationsMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -264,31 +274,33 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. getTeamMembers: authenticatedProcedure - .meta({ - openapi: { - method: 'GET', - path: '/team/{teamId}/member', - summary: 'Get members', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'GET', + // path: '/team/{teamId}/member', + // summary: 'Get members', + // tags: ['Teams'], + // }, + // }) .input(ZGetTeamMembersQuerySchema) .output(z.unknown()) .query(async ({ input, ctx }) => { return await getTeamMembers({ teamId: input.teamId, userId: ctx.user.id }); }), + // Todo: Public endpoint. findTeamMembers: authenticatedProcedure - .meta({ - openapi: { - method: 'GET', - path: '/team/{teamId}/member/find', - summary: 'Find members', - description: 'Find team members based on a search criteria', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'GET', + // path: '/team/{teamId}/member/find', + // summary: 'Find members', + // description: 'Find team members based on a search criteria', + // tags: ['Teams'], + // }, + // }) .input(ZFindTeamMembersQuerySchema) .output(z.unknown()) .query(async ({ input, ctx }) => { @@ -298,15 +310,16 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. updateTeamMember: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/member/{teamMemberId}', - summary: 'Update member', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/member/{teamMemberId}', + // summary: 'Update member', + // tags: ['Teams'], + // }, + // }) .input(ZUpdateTeamMemberMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -316,16 +329,17 @@ export const teamRouter = router({ }); }), + // Todo: Public endpoint. deleteTeamMembers: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/member/delete', - summary: 'Delete members', - description: '', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/member/delete', + // summary: 'Delete members', + // description: '', + // tags: ['Teams'], + // }, + // }) .input(ZDeleteTeamMembersMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { @@ -374,16 +388,17 @@ export const teamRouter = router({ return await getTeamInvitations({ email: ctx.user.email }); }), + // Todo: Public endpoint. updateTeamPublicProfile: authenticatedProcedure - .meta({ - openapi: { - method: 'POST', - path: '/team/{teamId}/profile', - summary: 'Update a team public profile', - description: '', - tags: ['Teams'], - }, - }) + // .meta({ + // openapi: { + // method: 'POST', + // path: '/team/{teamId}/profile', + // summary: 'Update a team public profile', + // description: '', + // tags: ['Teams'], + // }, + // }) .input(ZUpdateTeamPublicProfileMutationSchema) .output(z.unknown()) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts index eb092557e..df5c2ab93 100644 --- a/packages/trpc/server/template-router/router.ts +++ b/packages/trpc/server/template-router/router.ts @@ -4,19 +4,50 @@ import { z } from 'zod'; import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { isValidLanguageCode } from '@documenso/lib/constants/i18n'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; +import { + ZGetDocumentWithDetailsByIdResponseSchema, + getDocumentWithDetailsById, +} from '@documenso/lib/server-only/document/get-document-with-details-by-id'; import { sendDocument } from '@documenso/lib/server-only/document/send-document'; -import { createDocumentFromDirectTemplate } from '@documenso/lib/server-only/template/create-document-from-direct-template'; +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 { createTemplate } from '@documenso/lib/server-only/template/create-template'; -import { createTemplateDirectLink } from '@documenso/lib/server-only/template/create-template-direct-link'; +import { + ZCreateTemplateResponseSchema, + createTemplate, +} from '@documenso/lib/server-only/template/create-template'; +import { + ZCreateTemplateDirectLinkResponseSchema, + 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'; -import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template'; -import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; -import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; -import { moveTemplateToTeam } from '@documenso/lib/server-only/template/move-template-to-team'; -import { toggleTemplateDirectLink } from '@documenso/lib/server-only/template/toggle-template-direct-link'; -import { updateTemplateSettings } from '@documenso/lib/server-only/template/update-template-settings'; +import { + ZDuplicateTemplateResponseSchema, + duplicateTemplate, +} from '@documenso/lib/server-only/template/duplicate-template'; +import { + ZFindTemplatesResponseSchema, + findTemplates, +} from '@documenso/lib/server-only/template/find-templates'; +import { + ZGetTemplateByIdResponseSchema, + getTemplateById, +} from '@documenso/lib/server-only/template/get-template-by-id'; +import { + ZMoveTemplateToTeamResponseSchema, + moveTemplateToTeam, +} from '@documenso/lib/server-only/template/move-template-to-team'; +import { + ZToggleTemplateDirectLinkResponseSchema, + toggleTemplateDirectLink, +} from '@documenso/lib/server-only/template/toggle-template-direct-link'; +import { + ZUpdateTemplateSettingsResponseSchema, + updateTemplateSettings, +} from '@documenso/lib/server-only/template/update-template-settings'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import type { Document } from '@documenso/prisma/client'; @@ -39,6 +70,9 @@ import { } from './schema'; export const templateRouter = router({ + /** + * @public + */ findTemplates: authenticatedProcedure .meta({ openapi: { @@ -50,7 +84,7 @@ export const templateRouter = router({ }, }) .input(ZFindTemplatesQuerySchema) - .output(z.unknown()) + .output(ZFindTemplatesResponseSchema) .query(async ({ input, ctx }) => { return await findTemplates({ userId: ctx.user.id, @@ -58,6 +92,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ getTemplateById: authenticatedProcedure .meta({ openapi: { @@ -68,7 +105,7 @@ export const templateRouter = router({ }, }) .input(ZGetTemplateByIdQuerySchema) - .output(z.unknown()) + .output(ZGetTemplateByIdResponseSchema) .query(async ({ input, ctx }) => { const { templateId, teamId } = input; @@ -79,6 +116,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ createTemplate: authenticatedProcedure .meta({ openapi: { @@ -90,7 +130,7 @@ export const templateRouter = router({ }, }) .input(ZCreateTemplateMutationSchema) - .output(z.unknown()) + .output(ZCreateTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId, title, templateDocumentDataId } = input; @@ -102,6 +142,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ updateTemplate: authenticatedProcedure .meta({ openapi: { @@ -112,7 +155,7 @@ export const templateRouter = router({ }, }) .input(ZUpdateTemplateSettingsMutationSchema) - .output(z.unknown()) + .output(ZUpdateTemplateSettingsResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, teamId, data, meta } = input; @@ -133,6 +176,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ duplicateTemplate: authenticatedProcedure .meta({ openapi: { @@ -143,7 +189,7 @@ export const templateRouter = router({ }, }) .input(ZDuplicateTemplateMutationSchema) - .output(z.unknown()) + .output(ZDuplicateTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { teamId, templateId } = input; @@ -154,6 +200,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ deleteTemplate: authenticatedProcedure .meta({ openapi: { @@ -164,7 +213,7 @@ export const templateRouter = router({ }, }) .input(ZDeleteTemplateMutationSchema) - .output(z.unknown()) + .output(z.void()) .mutation(async ({ input, ctx }) => { const { templateId, teamId } = input; @@ -173,6 +222,9 @@ export const templateRouter = router({ await deleteTemplate({ userId, id: templateId, teamId }); }), + /** + * @public + */ createDocumentFromTemplate: authenticatedProcedure .meta({ openapi: { @@ -184,9 +236,9 @@ export const templateRouter = router({ }, }) .input(ZCreateDocumentFromTemplateMutationSchema) - .output(z.unknown()) + .output(ZGetDocumentWithDetailsByIdResponseSchema) .mutation(async ({ input, ctx }) => { - const { templateId, teamId, recipients } = input; + const { templateId, teamId, recipients, distributeDocument } = input; const limits = await getServerLimits({ email: ctx.user.email, teamId }); @@ -196,7 +248,7 @@ export const templateRouter = router({ const requestMetadata = extractNextApiRequestMetadata(ctx.req); - let document: Document = await createDocumentFromTemplate({ + const document: Document = await createDocumentFromTemplate({ templateId, teamId, userId: ctx.user.id, @@ -204,8 +256,8 @@ export const templateRouter = router({ requestMetadata, }); - if (input.distributeDocument) { - document = await sendDocument({ + if (distributeDocument) { + await sendDocument({ documentId: document.id, userId: ctx.user.id, teamId, @@ -217,9 +269,16 @@ export const templateRouter = router({ }); } - return document; + return getDocumentWithDetailsById({ + documentId: document.id, + userId: ctx.user.id, + teamId, + }); }), + /** + * @public + */ createDocumentFromDirectTemplate: maybeAuthenticatedProcedure .meta({ openapi: { @@ -231,7 +290,7 @@ export const templateRouter = router({ }, }) .input(ZCreateDocumentFromDirectTemplateMutationSchema) - .output(z.unknown()) + .output(ZCreateDocumentFromDirectTemplateResponseSchema) .mutation(async ({ input, ctx }) => { const { directRecipientName, @@ -262,7 +321,9 @@ export const templateRouter = router({ }); }), - // Internal endpoint for now. + /** + * @private + */ setSigningOrderForTemplate: authenticatedProcedure .input(ZSetSigningOrderForTemplateMutationSchema) .mutation(async ({ input, ctx }) => { @@ -278,6 +339,9 @@ export const templateRouter = router({ }); }), + /** + * @public + */ createTemplateDirectLink: authenticatedProcedure .meta({ openapi: { @@ -289,7 +353,7 @@ export const templateRouter = router({ }, }) .input(ZCreateTemplateDirectLinkMutationSchema) - .output(z.unknown()) + .output(ZCreateTemplateDirectLinkResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, teamId, directRecipientId } = input; @@ -308,6 +372,9 @@ export const templateRouter = router({ return await createTemplateDirectLink({ userId, templateId, directRecipientId }); }), + /** + * @public + */ deleteTemplateDirectLink: authenticatedProcedure .meta({ openapi: { @@ -319,7 +386,7 @@ export const templateRouter = router({ }, }) .input(ZDeleteTemplateDirectLinkMutationSchema) - .output(z.unknown()) + .output(z.void()) .mutation(async ({ input, ctx }) => { const { templateId } = input; @@ -328,6 +395,9 @@ export const templateRouter = router({ await deleteTemplateDirectLink({ userId, templateId }); }), + /** + * @public + */ toggleTemplateDirectLink: authenticatedProcedure .meta({ openapi: { @@ -339,7 +409,7 @@ export const templateRouter = router({ }, }) .input(ZToggleTemplateDirectLinkMutationSchema) - .output(z.unknown()) + .output(ZToggleTemplateDirectLinkResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, enabled } = input; @@ -348,6 +418,9 @@ export const templateRouter = router({ return await toggleTemplateDirectLink({ userId, templateId, enabled }); }), + /** + * @public + */ moveTemplateToTeam: authenticatedProcedure .meta({ openapi: { @@ -359,7 +432,7 @@ export const templateRouter = router({ }, }) .input(ZMoveTemplatesToTeamSchema) - .output(z.unknown()) + .output(ZMoveTemplateToTeamResponseSchema) .mutation(async ({ input, ctx }) => { const { templateId, teamId } = input; const userId = ctx.user.id; @@ -371,7 +444,9 @@ export const templateRouter = router({ }); }), - // Internal endpoint for now. + /** + * @internal + */ updateTemplateTypedSignatureSettings: authenticatedProcedure .input(ZUpdateTemplateTypedSignatureSettingsMutationSchema) .mutation(async ({ input, ctx }) => { diff --git a/packages/trpc/server/trpc.ts b/packages/trpc/server/trpc.ts index b008287b3..bbf08e854 100644 --- a/packages/trpc/server/trpc.ts +++ b/packages/trpc/server/trpc.ts @@ -1,43 +1,73 @@ import { TRPCError, initTRPC } from '@trpc/server'; import SuperJSON from 'superjson'; +import type { OpenApiMeta } from 'trpc-openapi'; import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error'; import { isAdmin } from '@documenso/lib/next-auth/guards/is-admin'; +import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token'; import type { TrpcContext } from './context'; -const t = initTRPC.context().create({ - transformer: SuperJSON, - errorFormatter(opts) { - const { shape, error } = opts; +const t = initTRPC + .meta() + .context() + .create({ + transformer: SuperJSON, + errorFormatter(opts) { + const { shape, error } = opts; - const originalError = error.cause; + const originalError = error.cause; - let data: Record = shape.data; + let data: Record = shape.data; - if (originalError instanceof AppError) { - data = { - ...data, - appError: AppError.toJSON(originalError), - code: originalError.code, - httpStatus: - originalError.statusCode ?? - genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ?? - 500, + // Default unknown errors to 400, since if you're throwing an AppError it is expected + // that you already know what you're doing. + if (originalError instanceof AppError) { + data = { + ...data, + appError: AppError.toJSON(originalError), + code: originalError.code, + httpStatus: + originalError.statusCode ?? + genericErrorCodeToTrpcErrorCodeMap[originalError.code]?.status ?? + 400, + }; + } + + return { + ...shape, + data, }; - } - - return { - ...shape, - data, - }; - }, -}); + }, + }); /** * Middlewares */ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => { + const authorizationHeader = ctx.req.headers.authorization; + + // Taken from `authenticatedMiddleware` in `@documenso/api/v1/middleware/authenticated.ts`. + if (authorizationHeader) { + // Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx" + const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0); + + if (!token) { + throw new Error('Token was not provided for authenticated middleware'); + } + + const apiToken = await getApiTokenByToken({ token }); + + return await next({ + ctx: { + ...ctx, + user: apiToken.user, + session: null, + source: 'api', + }, + }); + } + if (!ctx.session) { throw new TRPCError({ code: 'UNAUTHORIZED', @@ -50,6 +80,7 @@ export const authenticatedMiddleware = t.middleware(async ({ ctx, next }) => { ...ctx, user: ctx.user, session: ctx.session, + source: 'app', }, }); });