In this chapter, I’m going to create the Contracts module using SaasRock’s Entity Code Generator, customize the generated code, and implement the Dropbox Sign API using their official SDK for Node.
Using SaasRock’s entity builder, we can quickly get basic CRUD functionality to get our MVP running. Here’s how I’m going to create the entity “contract”:
And I’ll create some properties: Name (text), Type (select), Description (optional text with rows), Document (pdf), Document signed (optional pdf), Attachments (optional files), Estimated Amount (number), Real Amount (optional number), Active (boolean), Estimated Completion Date (date), and Real Completion Date (optional date).
And by the way, I’m going to copy an SVG icon from icons8, and add a property “fill=’currentColor’” like this (if you’re curious I’m using this one):
With this model, I now have a full no-code CRUD functionality for contracts, check out the quick video demo here: https://www.loom.com/share/3a1b96fb691a4bfd9dd3c7a812cfe535
Now that I’m happy with the autogenerated CRUD, I’m going to use the new Code Generator feature to download 23 files for my Contracts module.
Quick video demo of downloading the generated code or visit the URL of the generated files here: https://www.loom.com/share/cd3be70caf574bff9db0460abbcf5bfc
These files handle our model properties for using a typed interface in both server and client code.
These API Routes are basically Remix Loader and Action functions to handle data loading for the client and to perform actions on the server.
These files use their corresponding API to render the data and to submit actions (i.e. the Edit view loads a row, and can submit an “edit” action).
Finally, for each route (Index, New, Edit, Activity, Share, and Tags) there’s a route that orchestrates the API with its View, like in the following image:
After generating the code, your git changes should look like in the following image. By the way, before generating code, commit any pending changes so you can roll back unwanted code.
Before committing the generated code, run npm run prettier
, this way you’ll have your prettier settings applied.
Each contract needs to be signed by a registered user, so I need to override my contracts module to require a list of signers on every created contract.
The first thing that I need to create is a “ContractSignersForm.tsx” component that allows adding signers (Email, Name, and Role):
I’ll add this component at the bottom of the “ContractForm.tsx”:
Then, for read-only purposes a “ContractSignersList.tsx” component:
And this component will go in the “ContractRoutesEditView.tsx” autogenerated view component:
And this component will render like in the following image:
A database model is needed for saving each Contract signer. At the bottom of the “schema.prisma” file, I’ll add the following model:
+ model Signer {
+ id String @id @default(cuid())
+ rowId String
+ row Row @relation(fields: [rowId], references: [id], onDelete: Cascade)
+ email String
+ name String
+ role String
+ signedAt DateTime?
+}
And the Row relationship needs to be set on the Row model (each contract is basically a row):
model Row {
id String @id @default(cuid())
...
+ signers Signer[]
}
You can either run npx prisma migrate dev --name signers_model
, or npx prisma db push
.
Now a new “_signers_” property is required:
export type ContractCreateDto = {
name: string;
type: string;
description: string | undefined;
document: MediaDto;
attachments: MediaDto[] | undefined;
estimatedAmount: number;
active: boolean;
estimatedCompletionDate: Date;
+ signers: { email: string; name: string; role: string; }[]
};
Before calling the “ContractService.create(…)” function, let’s grab the signers from the form, and throw an error if no signers were set:
export namespace ContractRoutesNewApi {
...
export const action: ActionFunction = async ({ request, params }) => {
...
if (estimatedCompletionDate === undefined) throw new Error(t("Estimated Completion Date") + " is required");
+ const signers: { email: string; name: string; role: string; }[] = form.getAll("signers[]").map((f: FormDataEntryValue) => {
+ return JSON.parse(f.toString());
+ });
+ if (signers.filter((f) => f.role === "signer").length === 0) {
+ throw new Error("At least one signer is required");
+ }
+ const invalidSigners = signers.filter((f) => f.email === "" || f.name === "" || f.role === "");
+ if (invalidSigners.length > 0) {
+ throw new Error("Signer email, name and role are required");
+ }
const item = await ContractService.create(
...
Now that the validation is set, is time to save on the database inside the “ContractService.create(…)” implementation:
export namespace ContractService {
...
export async function create(data: ContractCreateDto, session: { tenantId: string | null; userId?: string }): Promise<ContractDto> {
...
+ await Promise.all(
+ data.signers.map((signer) => {
+ return db.signer.create({
+ data: {
+ rowId: item.id,
+ email: signer.email,
+ name: signer.name,
+ role: signer.role,
+ },
+ });
+ })
+ );
return ContractHelpers.rowToDto({ entity, row: item });
}
...
}
After these modifications, signers should be saved into the database when creating a contract at “_/admin/entities/code-generator/tests/contracts/new_”. But now let’s display the signers on our Edit view.
Same as I did with the “_ContractCreateDto_” but with the “_id_” and “_signedAt_” properties:
export type ContractCreateDto = {
...
+ signers: { id: string; email: string; name: string; role: string; signedAt: Date | null; }[]
};
Now let’s load the signers in every row.
Since signers are basically a row property, let’s modify the interface of RowWithDetails at the “_app/utils/db/entities/rows.db.server.ts_” file:
import { ...,
+ Signer
} from "@prisma/client";
...
export type RowWithDetails = Row & {
createdByUser: UserSimple | null;
...
+ signers: Signer[];
};
...
export const includeRowDetails = {
...
permissions: true,
sampleCustomEntity: true,
+ signers: true,
};
Every row will now have its signers, but we need to load them into the Dto object:
...
function rowToDto({ entity, row }: { entity: EntityWithDetails; row: RowWithDetails }): ContractDto {
return {
row,
...
realCompletionDate: RowValueHelper.getDate({ entity, row, name: "realCompletionDate" }), // optional
+ signers: row.signers.map((s) => {
+ return {
+ id: s.id,
+ email: s.email,
+ name: s.name,
+ role: s.role,
+ signedAt: s.signedAt,
+ };
+ }),
};
}
...
This new property (signers) needs to be set on our components <ContractSignersList items={data.item.signers} />
in “ContractRoutes.Edit.View.tsx” and <ContractSignersForm items={item?.signers} />
in “ContractForm.tsx”.
Up to this point, I’ve modified 9 autogenerated files, and 2 existing ones (the prisma schema and the RowWithDetails interface) to add signers functionality. And you can test it here.
If you’re a SaasRock Enterprise subscriber, you can download this progress here: github.com/AlexandroMtzG/saasrock-delega/releases/tag/part-2.
I’m going to use the Dropbox Sign (formerly HelloSign) Node.js SDK.
I’ve already implemented the API at tools.saasrock.com (ask for access to the repo if you’re a SaasRock subscriber) so I don’t waste your time explaining custom implementations, you only need to know that in order to create signable contracts, you need to specify:
a Title — The title of the contract or Subject of the sent email a Message — Contract details so signers know what they’ll sign a list of Signers — A list of email addresses and names and the Files — A list of contracts to be signed
Check out a quick demo here: https://www.loom.com/share/1fde3e46457d41abbabca60cf515c757
I’m going to create a file named “DropboxSignService.ts” inside my module folder (in my case app/modules/codeGeneratorTests/contracts/services
), and paste the content of this public gist: gist.github.com/AlexandroMtzG/c934727cbd3d214c7ac8991b2ae5c409.
Now I need to install the SDK:
npm install hellosign-sdk hellosign-embedded
npm install -D @types/hellosign-sdk @types/hellosign-embedded
And set two new required .env variables:
DROPBOX_SIGN_APP_ID="..."
DROPBOX_SIGN_API_KEY="..."
Let’s think about the new requirements:
I’m going to visit “_/admin/entities/contracts/properties/new_” and create a property “_signatureRequestId_” of type TEXT, which is not required and hidden.
Then, I’m going to add the property to my “ContractDto” interface:
export type ContractCreateDto = {
...
signers: { email: string; name: string; role: string; }[]
+ signatureRequestId: string | undefined;
};
Map the new value inside the “ContractHelpers.rowToDto” function:
function rowToDto({ entity, row }: { entity: EntityWithDetails; row: RowWithDetails }): ContractDto {
return {
...
+ signatureRequestId: RowValueHelper.getText({ entity, row, name: "signatureRequestId" }) ?? "",
};
}
Before creating the row itself, I’ll create the signable document to get the signatureRequestId value because I don’t want contracts that could not be created using the Dropbox Sign API. And in order to create one, first I have to save the PDF locally using the base64 “data.document” content.
...
+ import DropboxSignService from "./DropboxSignService";
+ import fs from "fs";
export namespace ContractService {
...
export async function create(data: ContractCreateDto, session: { tenantId: string | null; userId?: string }): Promise<ContractDto> {
+ const randomId = Math.random().toString(36).substring(2, 15);
+ const fileDirectory = "/tmp/pdfs/files";
+ const filePath = `${fileDirectory}/${randomId}.pdf`;
+ if (!fs.existsSync(fileDirectory)) {
+ fs.mkdirSync(fileDirectory, { recursive: true });
+ }
+ fs.writeFileSync(filePath, data.document.file.replace(/^data:application\/pdf;base64,/, ""), "base64");
+ const document = await DropboxSignService.create({
+ embedded: true,
+ subject: data.name,
+ message: data.description ?? "",
+ signers: data.signers
+ .filter((f) => f.role === "signer")
+ .map((signer) => {
+ return { email_address: signer.email, name: signer.name };
+ }),
+ files: [filePath],
+ });
+ fs.unlinkSync(filePath);
...
const rowValues = RowHelper.getRowPropertiesFromForm({
entity,
values: [
...
+ { name: "signatureRequestId", value: document.signature_request_id },
],
});
...
return ContractHelpers.rowToDto({ entity, row: item });
}
}
In the Loader function of the “ContractRoutes.Edit.Api.tsx” file, I’ll add find the embedded sign URL for the current user (if it’s a signer):
...
+ import DropboxSignService, { DropboxSignatureRequestDto } from "../../services/DropboxSignService";
export namespace ContractRoutesEditApi {
export type LoaderData = {
...
+ signableDocument: {
+ clientId: string;
+ embeddedSignUrl?: string;
+ item?: DropboxSignatureRequestDto;
+ };
};
...
export let loader: LoaderFunction = async ({ request, params }) => {
...
+ let embeddedSignUrl = "";
+ if (item.signatureRequestId) {
+ const dropboxDocument = await DropboxSignService.get(item.signatureRequestId);
+ const currentUser = await getUser(userId);
+ const signer = dropboxDocument.signatures.find((x) => x.signer_email_address === currentUser!.email);
+ const contractSigner = item.signers.find((f) => f.email === currentUser!.email);
+ if (signer && !contractSigner?.signedAt) {
+ embeddedSignUrl = await DropboxSignService.getSignUrl(signer.signature_id);
+ }
+ }
const data: LoaderData = {
...
+ embeddedSignUrl,
};
return json(data);
};
...
Now, if “signableDocument” is not undefined, that means we have a signer viewing the contract, so I need to add a “Sign” button that triggers the Dropbox Sign widget in the “ContractRoutes.Edit.View.tsx” file:
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
export default function ContractRoutesEditView() {
...
+ function onSign() {
+ // @ts-ignore
+ import("hellosign-embedded")
+ .then(({ default: HelloSign }) => {
+ return new HelloSign({
+ allowCancel: false,
+ clientId: data.signableDocument?.clientId,
+ skipDomainVerification: true,
+ testMode: true,
+ });
+ })
+ .then((client) => {
+ client.open(data.signableDocument?.embeddedSignUrl ?? "");
+ client.on("sign", () => {
+ alert("The document has been signed");
+ });
+ });
+ }
return (
<EditPageLayout...>
<div className="relative items-center justify-between space-y-2 border-b border-gray-200 pb-4 sm:flex sm:space-y-0">
...
<div className="flex space-x-2">
...
{canUpdate() && (
<ButtonSecondary onClick={() => { ... }}>
<PencilIcon className="h-4 w-4 text-gray-500" />
</ButtonSecondary>
)}
+ {data.signableDocument && (
+ <ButtonPrimary onClick={onSign} className="bg-teal-600 py-1.5 text-white hover:bg-teal-700">
+ Sign
+ </ButtonPrimary>
)}
...
Up to this point, you can see how it’s working now: https://www.loom.com/share/03f55a6b189b48218a8fee556a1e5660
Dropbox Sign is working correctly now, but I need to update in my database the “signer.signedAt” field so my “ContractSignersList” component renders accordingly.
First, I’ll submit an action “_signed_” when the widget tells me it has been signed at “ContractRoutes.Edit.View”:
...
export default function ContractRoutesEditView() {
...
function onSign() {
// @ts-ignore
import("hellosign-embedded")
.then(({ default: HelloSign }) => { ... })
.then((client) => {
client.open(data.signableDocument?.embeddedSignUrl ?? "");
client.on("sign", () => {
+ const form = new FormData();
+ form.set("action", "signed");
+ submit(form, {
+ method: "post",
+ });
});
});
}
...
And add this new action in the “ContractRoutes.Edit.Api” Action function:
...
+ import { db } from "~/utils/db.server";
export namespace ContractRoutesEditApi {
...
export const action: ActionFunction = async ({ request, params }) => {
...
+ } else if (action === "signed") {
+ const item = await ContractService.get(params.id!, {
+ tenantId,
+ userId,
+ });
+ const signer = item?.signers.find((f) => f.email === user?.email);
+ if (!signer) {
+ return json({ error: t("shared.unauthorized") }, { status: 400 });
+ } else if (signer.signedAt) {
+ return json({ error: "Already signed" }, { status: 400 });
+ }
+ await db.signer.update({
+ where: { id: signer.id },
+ data: { signedAt: new Date() },
+ });
+ return json({ success: t("shared.updated") });
+ }
...
}
}
And there you go:
You can test the Contracts simple module at delega.saasrock.com/admin/entities/code-generator/tests/contracts.
And if you’re a SaasRock Enterprise subscriber, you can download this code in this release: github.com/AlexandroMtzG/saasrock-delega/releases/tag/part-2-dropbox-sign.
In chapter 3, I’ll improve some functionality for the Contracts module:
And many more improvements…
You can now get an idea of how quick and easy is to build SaaS applications with SaasRock 😀.
Follow me & SaasRock or subscribe to my newsletter to stay tuned!
We respect your privacy. We respect your privacy.
TLDR: We use cookies for language selection, theme, and analytics. Learn more. TLDR: We use cookies for language selection, theme, and analytics. Learn more