Compare commits

...

1 Commits

Author SHA1 Message Date
e7e2aa9bd8 fix: migrate template metadata 2025-08-21 17:56:04 +10:00
15 changed files with 93 additions and 70 deletions

View File

@ -3,7 +3,7 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, Recipient, Signature } from '@prisma/client';
import { type DocumentData, type Field, FieldType } from '@prisma/client'; import { type DocumentData, type Field, FieldType } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react'; import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -48,7 +48,7 @@ export type EmbedDirectTemplateClientPageProps = {
documentData: DocumentData; documentData: DocumentData;
recipient: Recipient; recipient: Recipient;
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | null;
hidePoweredBy?: boolean; hidePoweredBy?: boolean;
allowWhiteLabelling?: boolean; allowWhiteLabelling?: boolean;
}; };

View File

@ -1,4 +1,4 @@
import type { DocumentMeta, TemplateMeta } from '@prisma/client'; import type { DocumentMeta } from '@prisma/client';
import { type Field, FieldType } from '@prisma/client'; import { type Field, FieldType } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -33,7 +33,7 @@ import { DocumentSigningTextField } from '~/components/general/document-signing/
export type EmbedDocumentFieldsProps = { export type EmbedDocumentFieldsProps = {
fields: Field[]; fields: Field[];
metadata?: Pick< metadata?: Pick<
DocumentMeta | TemplateMeta, DocumentMeta,
| 'timezone' | 'timezone'
| 'dateFormat' | 'dateFormat'
| 'typedSignatureEnabled' | 'typedSignatureEnabled'

View File

@ -3,7 +3,7 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, TemplateMeta } from '@prisma/client'; import type { DocumentMeta } from '@prisma/client';
import { import {
type DocumentData, type DocumentData,
type Field, type Field,
@ -50,7 +50,7 @@ export type EmbedSignDocumentClientPageProps = {
recipient: RecipientWithFields; recipient: RecipientWithFields;
fields: Field[]; fields: Field[];
completedFields: DocumentField[]; completedFields: DocumentField[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | null;
isCompleted?: boolean; isCompleted?: boolean;
hidePoweredBy?: boolean; hidePoweredBy?: boolean;
allowWhitelabelling?: boolean; allowWhitelabelling?: boolean;

View File

@ -554,6 +554,12 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
status: 200, status: 200,
body: { body: {
...template, ...template,
templateMeta: template.templateMeta
? {
...template.templateMeta,
templateId: template.id,
}
: null,
Field: template.fields.map((field) => ({ Field: template.fields.map((field) => ({
...field, ...field,
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null, fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null,

View File

@ -1,4 +1,4 @@
import type { DocumentVisibility, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, DocumentVisibility } from '@prisma/client';
import { import {
DocumentSource, DocumentSource,
RecipientRole, RecipientRole,
@ -46,7 +46,7 @@ export type CreateDocumentOptions = {
formValues?: TDocumentFormValues; formValues?: TDocumentFormValues;
recipients: TCreateDocumentV2Request['recipients']; recipients: TCreateDocumentV2Request['recipients'];
}; };
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>; meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
}; };

View File

@ -1,13 +0,0 @@
import { prisma } from '@documenso/prisma';
export interface GetDocumentMetaByDocumentIdOptions {
id: number;
}
export const getDocumentMetaByDocumentId = async ({ id }: GetDocumentMetaByDocumentIdOptions) => {
return await prisma.documentMeta.findFirstOrThrow({
where: {
documentId: id,
},
});
};

View File

@ -1,4 +1,4 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import type { z } from 'zod'; import type { z } from 'zod';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -26,7 +26,7 @@ export type CreateTemplateOptions = {
publicDescription?: string; publicDescription?: string;
type?: Template['type']; type?: Template['type'];
}; };
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>; meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
}; };
export const ZCreateTemplateResponseSchema = TemplateSchema; export const ZCreateTemplateResponseSchema = TemplateSchema;

View File

@ -1,4 +1,4 @@
import type { DocumentVisibility, Template, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, DocumentVisibility, Template } from '@prisma/client';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -22,7 +22,7 @@ export type UpdateTemplateOptions = {
type?: Template['type']; type?: Template['type'];
useLegacyFieldInsertion?: boolean; useLegacyFieldInsertion?: boolean;
}; };
meta?: Partial<Omit<TemplateMeta, 'id' | 'templateId'>>; meta?: Partial<Omit<DocumentMeta, 'id' | 'templateId'>>;
}; };
export const updateTemplate = async ({ export const updateTemplate = async ({

View File

@ -1,10 +1,10 @@
import type { z } from 'zod'; import type { z } from 'zod';
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema'; import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema'; import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema'; import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema'; import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
import { TemplateMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateMetaSchema';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema'; import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema/TemplateSchema';
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema'; import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
@ -39,7 +39,7 @@ export const ZTemplateSchema = TemplateSchema.pick({
data: true, data: true,
initialData: true, initialData: true,
}), }),
templateMeta: TemplateMetaSchema.pick({ templateMeta: DocumentMetaSchema.pick({
id: true, id: true,
subject: true, subject: true,
message: true, message: true,
@ -129,7 +129,7 @@ export const ZTemplateManySchema = TemplateSchema.pick({
}).nullable(), }).nullable(),
fields: ZFieldSchema.array(), fields: ZFieldSchema.array(),
recipients: ZRecipientLiteSchema.array(), recipients: ZRecipientLiteSchema.array(),
templateMeta: TemplateMetaSchema.pick({ templateMeta: DocumentMetaSchema.pick({
signingOrder: true, signingOrder: true,
distributionMethod: true, distributionMethod: true,
}).nullable(), }).nullable(),

View File

@ -1,9 +1,4 @@
import type { import type { Document, DocumentMeta, OrganisationGlobalSettings } from '@prisma/client';
Document,
DocumentMeta,
OrganisationGlobalSettings,
TemplateMeta,
} from '@prisma/client';
import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client'; import { DocumentDistributionMethod, DocumentSigningOrder, DocumentStatus } from '@prisma/client';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '../constants/time-zones';
@ -29,7 +24,7 @@ export const isDocumentCompleted = (document: Pick<Document, 'status'> | Documen
*/ */
export const extractDerivedDocumentMeta = ( export const extractDerivedDocumentMeta = (
settings: Omit<OrganisationGlobalSettings, 'id'>, settings: Omit<OrganisationGlobalSettings, 'id'>,
overrideMeta: Partial<DocumentMeta | TemplateMeta> | undefined | null, overrideMeta: Partial<DocumentMeta> | undefined | null,
) => { ) => {
const meta = overrideMeta ?? {}; const meta = overrideMeta ?? {};
@ -58,5 +53,5 @@ export const extractDerivedDocumentMeta = (
emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo, emailReplyTo: meta.emailReplyTo ?? settings.emailReplyTo,
emailSettings: emailSettings:
meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS, meta.emailSettings || settings.emailDocumentSettings || DEFAULT_DOCUMENT_EMAIL_SETTINGS,
} satisfies Omit<DocumentMeta, 'id' | 'documentId'>; } satisfies Omit<DocumentMeta, 'id' | 'documentId' | 'templateId'>;
}; };

View File

@ -0,0 +1,57 @@
-- DropForeignKey
ALTER TABLE "TemplateMeta" DROP CONSTRAINT "TemplateMeta_templateId_fkey";
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "templateId" INTEGER,
ALTER COLUMN "documentId" DROP NOT NULL;
-- [CUSTOM_CHANGE] Migrate existing TemplateMeta to DocumentMeta
INSERT INTO "DocumentMeta" (
"id",
"subject",
"message",
"timezone",
"password",
"dateFormat",
"redirectUrl",
"signingOrder",
"allowDictateNextSigner",
"typedSignatureEnabled",
"uploadSignatureEnabled",
"drawSignatureEnabled",
"language",
"distributionMethod",
"emailSettings",
"emailReplyTo",
"emailId",
"templateId"
)
SELECT
gen_random_uuid()::text, -- Generate new CUID-like IDs to avoid collisions
"subject",
"message",
"timezone",
"password",
"dateFormat",
"redirectUrl",
"signingOrder",
"allowDictateNextSigner",
"typedSignatureEnabled",
"uploadSignatureEnabled",
"drawSignatureEnabled",
"language",
"distributionMethod",
"emailSettings",
"emailReplyTo",
"emailId",
"templateId"
FROM "TemplateMeta";
-- DropTable
DROP TABLE "TemplateMeta";
-- CreateIndex
CREATE UNIQUE INDEX "DocumentMeta_templateId_key" ON "DocumentMeta"("templateId");
-- AddForeignKey
ALTER TABLE "DocumentMeta" ADD CONSTRAINT "DocumentMeta_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -456,8 +456,6 @@ model DocumentMeta {
timezone String? @default("Etc/UTC") @db.Text timezone String? @default("Etc/UTC") @db.Text
password String? password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String? redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL) signingOrder DocumentSigningOrder @default(PARALLEL)
allowDictateNextSigner Boolean @default(false) allowDictateNextSigner Boolean @default(false)
@ -472,6 +470,12 @@ model DocumentMeta {
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema) emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
emailReplyTo String? emailReplyTo String?
emailId String? emailId String?
documentId Int? @unique
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
templateId Int? @unique
template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
} }
enum ReadStatus { enum ReadStatus {
@ -842,32 +846,6 @@ enum TemplateType {
PRIVATE PRIVATE
} }
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
model TemplateMeta {
id String @id @default(cuid())
subject String?
message String?
timezone String? @default("Etc/UTC") @db.Text
password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
signingOrder DocumentSigningOrder? @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
distributionMethod DocumentDistributionMethod @default(EMAIL)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
templateId Int @unique
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
redirectUrl String?
language String @default("en")
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
emailReplyTo String?
emailId String?
}
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';"]) /// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';"])
model Template { model Template {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
@ -876,7 +854,7 @@ model Template {
title String title String
visibility DocumentVisibility @default(EVERYONE) visibility DocumentVisibility @default(EVERYONE)
authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema) authOptions Json? /// [DocumentAuthOptions] @zod.custom.use(ZDocumentAuthOptionsSchema)
templateMeta TemplateMeta? templateMeta DocumentMeta?
templateDocumentDataId String templateDocumentDataId String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt

View File

@ -94,7 +94,7 @@ export const createEmbeddingTemplateRoute = procedure
emailSettings: meta.emailSettings, emailSettings: meta.emailSettings,
}; };
await prisma.templateMeta.upsert({ await prisma.documentMeta.upsert({
where: { where: {
templateId: template.id, templateId: template.id,
}, },

View File

@ -2,7 +2,7 @@ import { useState } from 'react';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Field, Recipient, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, Field, Recipient } from '@prisma/client';
import { SigningStatus } from '@prisma/client'; import { SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react'; import { Clock, EyeOffIcon } from 'lucide-react';
@ -36,7 +36,7 @@ const getRecipientDisplayText = (recipient: { name: string; email: string }) =>
export type DocumentReadOnlyFieldsProps = { export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[]; fields: DocumentField[];
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>; documentMeta?: Pick<DocumentMeta, 'dateFormat'>;
showFieldStatus?: boolean; showFieldStatus?: boolean;

View File

@ -1,5 +1,5 @@
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import type { DocumentMeta, Signature, TemplateMeta } from '@prisma/client'; import type { DocumentMeta, Signature } from '@prisma/client';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { ChevronDown } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
@ -27,7 +27,7 @@ type FieldIconProps = {
fieldMeta?: TFieldMetaSchema | null; fieldMeta?: TFieldMetaSchema | null;
signature?: Signature | null; signature?: Signature | null;
}; };
documentMeta?: Pick<DocumentMeta | TemplateMeta, 'dateFormat'>; documentMeta?: Pick<DocumentMeta, 'dateFormat'>;
}; };
/** /**