From 082931121451af02b692d37d474e99902dde74b1 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:37:56 +0300 Subject: [PATCH] feat: prefill fields via api (#1261) ## Description Configure the advanced field via API. ## Checklist - [x] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [x] I have updated the documentation to reflect these changes, if applicable. - [x] I have followed the project's coding style guidelines. - [ ] I have addressed the code review feedback from the previous submission, if applicable. ## Summary by CodeRabbit - **New Features** - Enhanced API functionality to support field metadata during field creation. - Introduced validation checks for field metadata to ensure necessary information is provided for advanced field types. - **Bug Fixes** - Improved error handling during field creation to return properly formatted error responses. - **Documentation** - Updated API schemas to include field metadata, enhancing data validation and response structures. --- .gitignore | 2 + packages/api/v1/implementation.ts | 85 +++++++++++-------- packages/api/v1/schema.ts | 3 + .../lib/server-only/field/create-field.ts | 49 +++++++++++ 4 files changed, 105 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 3b0569b15f..b95fcc7d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +packages/prisma/generated/types.ts + # dependencies node_modules .pnp diff --git a/packages/api/v1/implementation.ts b/packages/api/v1/implementation.ts index 74afa80c07..ad9aaaac45 100644 --- a/packages/api/v1/implementation.ts +++ b/packages/api/v1/implementation.ts @@ -31,6 +31,7 @@ import { createDocumentFromTemplateLegacy } from '@documenso/lib/server-only/tem import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { putPdfFile } from '@documenso/lib/universal/upload/put-file'; @@ -869,7 +870,17 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { createField: authenticatedMiddleware(async (args, user, team) => { const { id: documentId } = args.params; - const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY } = args.body; + const { recipientId, type, pageNumber, pageWidth, pageHeight, pageX, pageY, fieldMeta } = + args.body; + + if (pageNumber <= 0) { + return { + status: 400, + body: { + message: 'Invalid page number', + }, + }; + } const document = await getDocumentById({ id: Number(documentId), @@ -918,41 +929,47 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, { }; } - const field = await createField({ - documentId: Number(documentId), - recipientId: Number(recipientId), - userId: user.id, - teamId: team?.id, - type, - pageNumber, - pageX, - pageY, - pageWidth, - pageHeight, - requestMetadata: extractNextApiRequestMetadata(args.req), - }); + try { + const field = await createField({ + documentId: Number(documentId), + recipientId: Number(recipientId), + userId: user.id, + teamId: team?.id, + type, + pageNumber, + pageX, + pageY, + pageWidth, + pageHeight, + fieldMeta, + requestMetadata: extractNextApiRequestMetadata(args.req), + }); - const remappedField = { - id: field.id, - documentId: field.documentId, - recipientId: field.recipientId ?? -1, - type: field.type, - pageNumber: field.page, - pageX: Number(field.positionX), - pageY: Number(field.positionY), - pageWidth: Number(field.width), - pageHeight: Number(field.height), - customText: field.customText, - inserted: field.inserted, - }; + const remappedField = { + id: field.id, + documentId: field.documentId, + recipientId: field.recipientId ?? -1, + type: field.type, + pageNumber: field.page, + pageX: Number(field.positionX), + pageY: Number(field.positionY), + pageWidth: Number(field.width), + pageHeight: Number(field.height), + customText: field.customText, + fieldMeta: ZFieldMetaSchema.parse(field.fieldMeta), + inserted: field.inserted, + }; - return { - status: 200, - body: { - ...remappedField, - documentId: Number(documentId), - }, - }; + return { + status: 200, + body: { + ...remappedField, + documentId: Number(documentId), + }, + }; + } catch (err) { + return AppError.toRestAPIError(err); + } }), updateField: authenticatedMiddleware(async (args, user, team) => { diff --git a/packages/api/v1/schema.ts b/packages/api/v1/schema.ts index 90d3f65bfc..42205d5403 100644 --- a/packages/api/v1/schema.ts +++ b/packages/api/v1/schema.ts @@ -5,6 +5,7 @@ import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/const import '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { ZUrlSchema } from '@documenso/lib/schemas/common'; +import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { DocumentDataType, FieldType, @@ -300,6 +301,7 @@ export const ZCreateFieldMutationSchema = z.object({ pageY: z.number(), pageWidth: z.number(), pageHeight: z.number(), + fieldMeta: ZFieldMetaSchema, }); export type TCreateFieldMutationSchema = z.infer; @@ -323,6 +325,7 @@ export const ZSuccessfulFieldResponseSchema = z.object({ pageWidth: z.number(), pageHeight: z.number(), customText: z.string(), + fieldMeta: ZFieldMetaSchema, inserted: z.boolean(), }); diff --git a/packages/lib/server-only/field/create-field.ts b/packages/lib/server-only/field/create-field.ts index 7a3aa3959f..9aafc7ab85 100644 --- a/packages/lib/server-only/field/create-field.ts +++ b/packages/lib/server-only/field/create-field.ts @@ -1,6 +1,16 @@ +import { match } from 'ts-pattern'; + import { prisma } from '@documenso/prisma'; import type { FieldType, Team } from '@documenso/prisma/client'; +import { + ZCheckboxFieldMeta, + ZDropdownFieldMeta, + ZNumberFieldMeta, + ZRadioFieldMeta, + ZTextFieldMeta, +} from '../../types/field-meta'; +import type { TFieldMetaSchema as FieldMeta } from '../../types/field-meta'; import type { RequestMetadata } from '../../universal/extract-request-metadata'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; @@ -15,6 +25,7 @@ export type CreateFieldOptions = { pageY: number; pageWidth: number; pageHeight: number; + fieldMeta?: FieldMeta; requestMetadata?: RequestMetadata; }; @@ -29,6 +40,7 @@ export const createField = async ({ pageY, pageWidth, pageHeight, + fieldMeta, requestMetadata, }: CreateFieldOptions) => { const document = await prisma.document.findFirst({ @@ -85,6 +97,42 @@ export const createField = async ({ }); } + const advancedField = ['NUMBER', 'RADIO', 'CHECKBOX', 'DROPDOWN', 'TEXT'].includes(type); + + if (advancedField && !fieldMeta) { + throw new Error( + 'Field meta is required for this type of field. Please provide the appropriate field meta object.', + ); + } + + if (fieldMeta && fieldMeta.type.toLowerCase() !== String(type).toLowerCase()) { + throw new Error('Field meta type does not match the field type'); + } + + const result = match(type) + .with('RADIO', () => { + return ZRadioFieldMeta.safeParse(fieldMeta); + }) + .with('CHECKBOX', () => { + return ZCheckboxFieldMeta.safeParse(fieldMeta); + }) + .with('DROPDOWN', () => { + return ZDropdownFieldMeta.safeParse(fieldMeta); + }) + .with('NUMBER', () => { + return ZNumberFieldMeta.safeParse(fieldMeta); + }) + .with('TEXT', () => { + return ZTextFieldMeta.safeParse(fieldMeta); + }) + .otherwise(() => { + return { success: false, data: {} }; + }); + + if (!result.success) { + throw new Error('Field meta parsing failed'); + } + const field = await prisma.field.create({ data: { documentId, @@ -97,6 +145,7 @@ export const createField = async ({ height: pageHeight, customText: '', inserted: false, + fieldMeta: advancedField ? result.data : undefined, }, include: { Recipient: true,