mirror of
https://github.com/documenso/documenso.git
synced 2025-11-26 06:32:08 +10:00
Compare commits
1 Commits
feat/find-
...
chore/tran
| Author | SHA1 | Date | |
|---|---|---|---|
| 30e096c763 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -17,6 +17,5 @@
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import type {
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/envelope-router/envelope-recipients/create-envelope-recipients.types';
|
||||
import type { TFindEnvelopesResponse } from '@documenso/trpc/server/envelope-router/find-envelopes.types';
|
||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||
|
||||
@ -558,201 +557,4 @@ test.describe('API V2 Envelopes', () => {
|
||||
userEmail: userA.email,
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Envelope find endpoint', () => {
|
||||
const createEnvelope = async (
|
||||
request: ReturnType<typeof test.extend>['request'] extends Promise<infer R> ? R : never,
|
||||
token: string,
|
||||
payload: TCreateEnvelopePayload,
|
||||
) => {
|
||||
const formData = new FormData();
|
||||
formData.append('payload', JSON.stringify(payload));
|
||||
|
||||
const pdfData = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../../assets/field-font-alignment.pdf'),
|
||||
);
|
||||
formData.append('files', new File([pdfData], 'test.pdf', { type: 'application/pdf' }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
return (await res.json()) as TCreateEnvelopeResponse;
|
||||
};
|
||||
|
||||
test('should find envelopes with pagination', async ({ request }) => {
|
||||
// Create 3 envelopes
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document 1',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document 2',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'Template 1',
|
||||
});
|
||||
|
||||
// Find all envelopes
|
||||
const res = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.length).toBe(3);
|
||||
expect(response.count).toBe(3);
|
||||
expect(response.currentPage).toBe(1);
|
||||
expect(response.totalPages).toBe(1);
|
||||
|
||||
// Test pagination
|
||||
const paginatedRes = await request.get(`${baseUrl}/envelope?perPage=2&page=1`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(paginatedRes.ok()).toBeTruthy();
|
||||
const paginatedResponse = (await paginatedRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(paginatedResponse.data.length).toBe(2);
|
||||
expect(paginatedResponse.count).toBe(3);
|
||||
expect(paginatedResponse.totalPages).toBe(2);
|
||||
});
|
||||
|
||||
test('should filter envelopes by type', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Document Only',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
title: 'Template Only',
|
||||
});
|
||||
|
||||
// Filter by DOCUMENT type
|
||||
const documentRes = await request.get(`${baseUrl}/envelope?type=DOCUMENT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(documentRes.ok()).toBeTruthy();
|
||||
const documentResponse = (await documentRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(documentResponse.data.every((e) => e.type === EnvelopeType.DOCUMENT)).toBe(true);
|
||||
|
||||
// Filter by TEMPLATE type
|
||||
const templateRes = await request.get(`${baseUrl}/envelope?type=TEMPLATE`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(templateRes.ok()).toBeTruthy();
|
||||
const templateResponse = (await templateRes.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(templateResponse.data.every((e) => e.type === EnvelopeType.TEMPLATE)).toBe(true);
|
||||
});
|
||||
|
||||
test('should filter envelopes by status', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Draft Document',
|
||||
});
|
||||
|
||||
// Filter by DRAFT status (default for new envelopes)
|
||||
const res = await request.get(`${baseUrl}/envelope?status=DRAFT`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.every((e) => e.status === DocumentStatus.DRAFT)).toBe(true);
|
||||
});
|
||||
|
||||
test('should search envelopes by query', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Unique Searchable Title',
|
||||
});
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Another Document',
|
||||
});
|
||||
|
||||
const res = await request.get(`${baseUrl}/envelope?query=Unique%20Searchable`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(response.data.length).toBe(1);
|
||||
expect(response.data[0].title).toBe('Unique Searchable Title');
|
||||
});
|
||||
|
||||
test('should not return envelopes from other users', async ({ request }) => {
|
||||
// Create envelope for userA
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserA Document',
|
||||
});
|
||||
|
||||
// Create envelope for userB
|
||||
await createEnvelope(request, tokenB, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'UserB Document',
|
||||
});
|
||||
|
||||
// userA should only see their own envelopes
|
||||
const resA = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(resA.ok()).toBeTruthy();
|
||||
const responseA = (await resA.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(responseA.data.every((e) => e.title !== 'UserB Document')).toBe(true);
|
||||
|
||||
// userB should only see their own envelopes
|
||||
const resB = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenB}` },
|
||||
});
|
||||
|
||||
expect(resB.ok()).toBeTruthy();
|
||||
const responseB = (await resB.json()) as TFindEnvelopesResponse;
|
||||
|
||||
expect(responseB.data.every((e) => e.title !== 'UserA Document')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return envelope with expected schema fields', async ({ request }) => {
|
||||
await createEnvelope(request, tokenA, {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Schema Test Document',
|
||||
});
|
||||
|
||||
const res = await request.get(`${baseUrl}/envelope`, {
|
||||
headers: { Authorization: `Bearer ${tokenA}` },
|
||||
});
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const response = (await res.json()) as TFindEnvelopesResponse;
|
||||
|
||||
const envelope = response.data.find((e) => e.title === 'Schema Test Document');
|
||||
|
||||
expect(envelope).toBeDefined();
|
||||
expect(envelope?.id).toBeDefined();
|
||||
expect(envelope?.type).toBe(EnvelopeType.DOCUMENT);
|
||||
expect(envelope?.status).toBe(DocumentStatus.DRAFT);
|
||||
expect(envelope?.documentMeta).toBeDefined();
|
||||
expect(envelope?.recipients).toBeDefined();
|
||||
expect(envelope?.fields).toBeDefined();
|
||||
expect(envelope?.envelopeItems).toBeDefined();
|
||||
expect(envelope?.user).toBeDefined();
|
||||
expect(envelope?.team).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,223 +0,0 @@
|
||||
import type {
|
||||
DocumentSource,
|
||||
DocumentStatus,
|
||||
Envelope,
|
||||
EnvelopeType,
|
||||
Prisma,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { TEAM_DOCUMENT_VISIBILITY_MAP } from '../../constants/teams';
|
||||
import type { FindResultResponse } from '../../types/search-params';
|
||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
||||
import { getTeamById } from '../team/get-team';
|
||||
|
||||
export type FindEnvelopesOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
type?: EnvelopeType;
|
||||
templateId?: number;
|
||||
source?: DocumentSource;
|
||||
status?: DocumentStatus;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
orderBy?: {
|
||||
column: keyof Pick<Envelope, 'createdAt'>;
|
||||
direction: 'asc' | 'desc';
|
||||
};
|
||||
query?: string;
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const findEnvelopes = async ({
|
||||
userId,
|
||||
teamId,
|
||||
type,
|
||||
templateId,
|
||||
source,
|
||||
status,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
orderBy,
|
||||
query = '',
|
||||
folderId,
|
||||
}: FindEnvelopesOptions) => {
|
||||
const user = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await getTeamById({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const orderByColumn = orderBy?.column ?? 'createdAt';
|
||||
const orderByDirection = orderBy?.direction ?? 'desc';
|
||||
|
||||
const searchFilter: Prisma.EnvelopeWhereInput = query
|
||||
? {
|
||||
OR: [
|
||||
{ title: { contains: query, mode: 'insensitive' } },
|
||||
{ externalId: { contains: query, mode: 'insensitive' } },
|
||||
{ recipients: { some: { name: { contains: query, mode: 'insensitive' } } } },
|
||||
{ recipients: { some: { email: { contains: query, mode: 'insensitive' } } } },
|
||||
],
|
||||
}
|
||||
: {};
|
||||
|
||||
const visibilityFilter: Prisma.EnvelopeWhereInput = {
|
||||
visibility: {
|
||||
in: TEAM_DOCUMENT_VISIBILITY_MAP[team.currentTeamRole],
|
||||
},
|
||||
};
|
||||
|
||||
const teamEmailFilters: Prisma.EnvelopeWhereInput[] = [];
|
||||
|
||||
if (team.teamEmail) {
|
||||
teamEmailFilters.push(
|
||||
{
|
||||
user: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
{
|
||||
recipients: {
|
||||
some: {
|
||||
email: team.teamEmail.email,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause: Prisma.EnvelopeWhereInput = {
|
||||
AND: [
|
||||
searchFilter,
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
teamId: team.id,
|
||||
...visibilityFilter,
|
||||
},
|
||||
{
|
||||
userId,
|
||||
},
|
||||
...teamEmailFilters,
|
||||
],
|
||||
},
|
||||
{
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (type) {
|
||||
whereClause.type = type;
|
||||
}
|
||||
|
||||
if (templateId) {
|
||||
whereClause.templateId = templateId;
|
||||
}
|
||||
|
||||
if (source) {
|
||||
whereClause.source = source;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
whereClause.status = status;
|
||||
}
|
||||
|
||||
if (folderId !== undefined) {
|
||||
whereClause.folderId = folderId;
|
||||
} else {
|
||||
whereClause.folderId = null;
|
||||
}
|
||||
|
||||
const [data, count] = await Promise.all([
|
||||
prisma.envelope.findMany({
|
||||
where: whereClause,
|
||||
skip: Math.max(page - 1, 0) * perPage,
|
||||
take: perPage,
|
||||
orderBy: {
|
||||
[orderByColumn]: orderByDirection,
|
||||
},
|
||||
include: {
|
||||
envelopeItems: {
|
||||
select: {
|
||||
envelopeId: true,
|
||||
id: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
documentMeta: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
orderBy: {
|
||||
id: 'asc',
|
||||
},
|
||||
},
|
||||
fields: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
directLink: {
|
||||
select: {
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
id: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
const maskedData = data.map((envelope) =>
|
||||
maskRecipientTokensForDocument({
|
||||
document: envelope,
|
||||
user,
|
||||
}),
|
||||
);
|
||||
|
||||
const mappedData = maskedData.map((envelope) => ({
|
||||
...envelope,
|
||||
recipients: envelope.Recipient,
|
||||
user: {
|
||||
id: envelope.user.id,
|
||||
name: envelope.user.name || '',
|
||||
email: envelope.user.email,
|
||||
},
|
||||
}));
|
||||
|
||||
return {
|
||||
data: mappedData,
|
||||
count,
|
||||
currentPage: Math.max(page, 1),
|
||||
perPage,
|
||||
totalPages: Math.ceil(count / perPage),
|
||||
} satisfies FindResultResponse<typeof mappedData>;
|
||||
};
|
||||
@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: pl\n"
|
||||
"Project-Id-Version: documenso-app\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-11-20 02:32\n"
|
||||
"PO-Revision-Date: 2025-11-21 00:14\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: Polish\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
|
||||
@ -7584,7 +7584,7 @@ msgstr "Liczba podpisów"
|
||||
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
|
||||
#: packages/ui/components/document/document-read-only-fields.tsx
|
||||
msgid "Signed"
|
||||
msgstr "Podpisał"
|
||||
msgstr "Podpisano"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
msgctxt "Signed document (adjective)"
|
||||
|
||||
@ -1,56 +0,0 @@
|
||||
import { findEnvelopes } from '@documenso/lib/server-only/envelope/find-envelopes';
|
||||
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import {
|
||||
ZFindEnvelopesRequestSchema,
|
||||
ZFindEnvelopesResponseSchema,
|
||||
findEnvelopesMeta,
|
||||
} from './find-envelopes.types';
|
||||
|
||||
export const findEnvelopesRoute = authenticatedProcedure
|
||||
.meta(findEnvelopesMeta)
|
||||
.input(ZFindEnvelopesRequestSchema)
|
||||
.output(ZFindEnvelopesResponseSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { user, teamId } = ctx;
|
||||
|
||||
const {
|
||||
query,
|
||||
type,
|
||||
templateId,
|
||||
page,
|
||||
perPage,
|
||||
orderByDirection,
|
||||
orderByColumn,
|
||||
source,
|
||||
status,
|
||||
folderId,
|
||||
} = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: {
|
||||
query,
|
||||
type,
|
||||
templateId,
|
||||
source,
|
||||
status,
|
||||
folderId,
|
||||
page,
|
||||
perPage,
|
||||
},
|
||||
});
|
||||
|
||||
return await findEnvelopes({
|
||||
userId: user.id,
|
||||
teamId,
|
||||
type,
|
||||
templateId,
|
||||
query,
|
||||
source,
|
||||
status,
|
||||
page,
|
||||
perPage,
|
||||
folderId,
|
||||
orderBy: orderByColumn ? { column: orderByColumn, direction: orderByDirection } : undefined,
|
||||
});
|
||||
});
|
||||
@ -1,46 +0,0 @@
|
||||
import { DocumentSource, DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ZEnvelopeSchema } from '@documenso/lib/types/envelope';
|
||||
import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
|
||||
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
|
||||
export const findEnvelopesMeta: TrpcRouteMeta = {
|
||||
openapi: {
|
||||
method: 'GET',
|
||||
path: '/envelope',
|
||||
summary: 'Find envelopes',
|
||||
description: 'Find envelopes based on search criteria',
|
||||
tags: ['Envelope'],
|
||||
},
|
||||
};
|
||||
|
||||
export const ZFindEnvelopesRequestSchema = ZFindSearchParamsSchema.extend({
|
||||
type: z
|
||||
.nativeEnum(EnvelopeType)
|
||||
.describe('Filter envelopes by type (DOCUMENT or TEMPLATE).')
|
||||
.optional(),
|
||||
templateId: z
|
||||
.number()
|
||||
.describe('Filter envelopes by the template ID used to create it.')
|
||||
.optional(),
|
||||
source: z
|
||||
.nativeEnum(DocumentSource)
|
||||
.describe('Filter envelopes by how it was created.')
|
||||
.optional(),
|
||||
status: z
|
||||
.nativeEnum(DocumentStatus)
|
||||
.describe('Filter envelopes by the current status.')
|
||||
.optional(),
|
||||
folderId: z.string().describe('Filter envelopes by folder ID.').optional(),
|
||||
orderByColumn: z.enum(['createdAt']).optional(),
|
||||
orderByDirection: z.enum(['asc', 'desc']).describe('Sort direction.').default('desc'),
|
||||
});
|
||||
|
||||
export const ZFindEnvelopesResponseSchema = ZFindResultResponse.extend({
|
||||
data: ZEnvelopeSchema.array(),
|
||||
});
|
||||
|
||||
export type TFindEnvelopesRequest = z.infer<typeof ZFindEnvelopesRequestSchema>;
|
||||
export type TFindEnvelopesResponse = z.infer<typeof ZFindEnvelopesResponseSchema>;
|
||||
@ -18,7 +18,6 @@ import { createEnvelopeRecipientsRoute } from './envelope-recipients/create-enve
|
||||
import { deleteEnvelopeRecipientRoute } from './envelope-recipients/delete-envelope-recipient';
|
||||
import { getEnvelopeRecipientRoute } from './envelope-recipients/get-envelope-recipient';
|
||||
import { updateEnvelopeRecipientsRoute } from './envelope-recipients/update-envelope-recipients';
|
||||
import { findEnvelopesRoute } from './find-envelopes';
|
||||
import { getEnvelopeRoute } from './get-envelope';
|
||||
import { getEnvelopeItemsRoute } from './get-envelope-items';
|
||||
import { getEnvelopeItemsByTokenRoute } from './get-envelope-items-by-token';
|
||||
@ -66,7 +65,6 @@ export const envelopeRouter = router({
|
||||
set: setEnvelopeFieldsRoute,
|
||||
sign: signEnvelopeFieldRoute,
|
||||
},
|
||||
find: findEnvelopesRoute,
|
||||
get: getEnvelopeRoute,
|
||||
create: createEnvelopeRoute,
|
||||
use: useEnvelopeRoute,
|
||||
|
||||
Reference in New Issue
Block a user