mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
## Description
**Fixes issues with mismatching state between document steps.**
For example, editing a recipient and proceeding to the next step may not
display the updated recipient. And going back will display the old
recipient instead of the updated values.
**This PR also improves mutation and query speeds by adding logic to
bypass query invalidation.**
```ts
export const trpc = createTRPCReact<AppRouter>({
unstable_overrides: {
useMutation: {
async onSuccess(opts) {
await opts.originalFn();
// This forces mutations to wait for all the queries on the page to reload, and in
// this case one of the queries is `searchDocument` for the command overlay, which
// on average takes ~500ms. This means that every single mutation must wait for this.
await opts.queryClient.invalidateQueries();
},
},
},
});
```
I've added workarounds to allow us to bypass things such as batching and
invalidating queries. But I think we should instead remove this and
update all the mutations where a query is required for a more optimised
system.
## Example benchmarks
Using stg-app vs this preview there's an average 50% speed increase
across mutations.
**Set signer step:**
Average old speed: ~1100ms
Average new speed: ~550ms
**Set recipient step:**
Average old speed: ~1200ms
Average new speed: ~600ms
**Set fields step:**
Average old speed: ~1200ms
Average new speed: ~600ms
## Related Issue
This will resolve #470
## Changes Made
- Added ability to skip batch queries
- Added a state to store the required document data.
- Refetch the data between steps if/when required
- Optimise mutations and queries
## Checklist
- [X] I have tested these changes locally and they work as expected.
- [X] I have followed the project's coding style guidelines.
---------
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
231 lines
6.0 KiB
TypeScript
231 lines
6.0 KiB
TypeScript
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
|
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
|
import {
|
|
createDocumentAuditLogData,
|
|
diffFieldChanges,
|
|
} from '@documenso/lib/utils/document-audit-logs';
|
|
import { prisma } from '@documenso/prisma';
|
|
import type { Field, FieldType } from '@documenso/prisma/client';
|
|
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
|
|
|
export interface SetFieldsForDocumentOptions {
|
|
userId: number;
|
|
documentId: number;
|
|
fields: {
|
|
id?: number | null;
|
|
type: FieldType;
|
|
signerEmail: string;
|
|
pageNumber: number;
|
|
pageX: number;
|
|
pageY: number;
|
|
pageWidth: number;
|
|
pageHeight: number;
|
|
}[];
|
|
requestMetadata?: RequestMetadata;
|
|
}
|
|
|
|
export const setFieldsForDocument = async ({
|
|
userId,
|
|
documentId,
|
|
fields,
|
|
requestMetadata,
|
|
}: SetFieldsForDocumentOptions): Promise<Field[]> => {
|
|
const document = await prisma.document.findFirst({
|
|
where: {
|
|
id: documentId,
|
|
OR: [
|
|
{
|
|
userId,
|
|
},
|
|
{
|
|
team: {
|
|
members: {
|
|
some: {
|
|
userId,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
const user = await prisma.user.findFirstOrThrow({
|
|
where: {
|
|
id: userId,
|
|
},
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
email: true,
|
|
},
|
|
});
|
|
|
|
if (!document) {
|
|
throw new Error('Document not found');
|
|
}
|
|
|
|
if (document.completedAt) {
|
|
throw new Error('Document already complete');
|
|
}
|
|
|
|
const existingFields = await prisma.field.findMany({
|
|
where: {
|
|
documentId,
|
|
},
|
|
include: {
|
|
Recipient: true,
|
|
},
|
|
});
|
|
|
|
const removedFields = existingFields.filter(
|
|
(existingField) => !fields.find((field) => field.id === existingField.id),
|
|
);
|
|
|
|
const linkedFields = fields
|
|
.map((field) => {
|
|
const existing = existingFields.find((existingField) => existingField.id === field.id);
|
|
|
|
return {
|
|
...field,
|
|
_persisted: existing,
|
|
};
|
|
})
|
|
.filter((field) => {
|
|
return (
|
|
field._persisted?.Recipient?.sendStatus !== SendStatus.SENT &&
|
|
field._persisted?.Recipient?.signingStatus !== SigningStatus.SIGNED
|
|
);
|
|
});
|
|
|
|
const persistedFields = await prisma.$transaction(async (tx) => {
|
|
return await Promise.all(
|
|
linkedFields.map(async (field) => {
|
|
const fieldSignerEmail = field.signerEmail.toLowerCase();
|
|
|
|
const upsertedField = await tx.field.upsert({
|
|
where: {
|
|
id: field._persisted?.id ?? -1,
|
|
documentId,
|
|
},
|
|
update: {
|
|
page: field.pageNumber,
|
|
positionX: field.pageX,
|
|
positionY: field.pageY,
|
|
width: field.pageWidth,
|
|
height: field.pageHeight,
|
|
},
|
|
create: {
|
|
type: field.type,
|
|
page: field.pageNumber,
|
|
positionX: field.pageX,
|
|
positionY: field.pageY,
|
|
width: field.pageWidth,
|
|
height: field.pageHeight,
|
|
customText: '',
|
|
inserted: false,
|
|
Document: {
|
|
connect: {
|
|
id: documentId,
|
|
},
|
|
},
|
|
Recipient: {
|
|
connect: {
|
|
documentId_email: {
|
|
documentId,
|
|
email: fieldSignerEmail,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (upsertedField.recipientId === null) {
|
|
throw new Error('Not possible');
|
|
}
|
|
|
|
const baseAuditLog = {
|
|
fieldId: upsertedField.secondaryId,
|
|
fieldRecipientEmail: fieldSignerEmail,
|
|
fieldRecipientId: upsertedField.recipientId,
|
|
fieldType: upsertedField.type,
|
|
};
|
|
|
|
const changes = field._persisted ? diffFieldChanges(field._persisted, upsertedField) : [];
|
|
|
|
// Handle field updated audit log.
|
|
if (field._persisted && changes.length > 0) {
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
|
documentId: documentId,
|
|
user,
|
|
requestMetadata,
|
|
data: {
|
|
changes,
|
|
...baseAuditLog,
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
// Handle field created audit log.
|
|
if (!field._persisted) {
|
|
await tx.documentAuditLog.create({
|
|
data: createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
|
documentId: documentId,
|
|
user,
|
|
requestMetadata,
|
|
data: {
|
|
...baseAuditLog,
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
return upsertedField;
|
|
}),
|
|
);
|
|
});
|
|
|
|
if (removedFields.length > 0) {
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.field.deleteMany({
|
|
where: {
|
|
id: {
|
|
in: removedFields.map((field) => field.id),
|
|
},
|
|
},
|
|
});
|
|
|
|
await tx.documentAuditLog.createMany({
|
|
data: removedFields.map((field) =>
|
|
createDocumentAuditLogData({
|
|
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
|
|
documentId: documentId,
|
|
user,
|
|
requestMetadata,
|
|
data: {
|
|
fieldId: field.secondaryId,
|
|
fieldRecipientEmail: field.Recipient?.email ?? '',
|
|
fieldRecipientId: field.recipientId ?? -1,
|
|
fieldType: field.type,
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// Filter out fields that have been removed or have been updated.
|
|
const filteredFields = existingFields.filter((field) => {
|
|
const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
|
|
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);
|
|
|
|
return !isRemoved && !isUpdated;
|
|
});
|
|
|
|
return [...filteredFields, ...persistedFields];
|
|
};
|