Skip to main content

Forms Controller

πŸ“– Overview​

Controllers/forms.js is the heart of the Forms module. It creates and updates form documents, manages per-account taxonomies (categories/tags), distributes fill requests to clients, and drives the dashboard widgets. The controller orchestrates multiple utilitiesβ€”short-link generation, email/SMS senders, campaign automation guards, and recursive aggregationsβ€”to keep authored content, pending requests, and submissions in sync.

πŸ—„οΈ Collections touched​

CollectionPurpose
formsStores the draft/published form document, layout JSON, status, and metadata.
forms.sent.requestsTracks every request link sent to a contact, including status transitions and resend history.
forms.userresponseRead in widget aggregations to compute submission counts.
forms-user-categories / forms-user-tagsPer-account taxonomies surfaced in the UI filters.
campaign.dataLinked campaigns that should be soft-deactivated when a form is deleted.
crm.contactsResolves recipient details for email/SMS payloads.
_usersUsed indirectly when pending requests target internal reps rather than CRM contacts.

πŸ” Middleware & validation​

All routes in Routes/forms.js run through the shared authorization stack:

  • verifyAuthorization() – extracts auth context and JWT.
  • verifyAccessAndStatus({ accountIDRequired: true }) – blocks suspended or orphaned sub-accounts.
  • verifyScope(['forms', ...]) – fine-grained permission slices (create/update/delete/read buckets).
  • validateLimit – standard pagination limits, applied to list endpoints.
  • validateRequestSchemaV2(validators) – Joi map in validators/index.js validates params, body, and query strings. Highlights:
    • Accepts either ObjectId or UUID for :formid, enabling public share links to hit the same controller.
    • Bulk delete requires total to match server-side counts when all=true; mismatches throw FORMS_COUNT_MISMATCH.
    • POST /forms supports cloning via ?isclone=true&form_id=<FormId> and enforces Instasites/Instareports payload shape when form_type is set.

πŸ›£οΈ Route map​

MethodPath (internal service)Controller methodNotes
POST/formssaveFormCreates drafts or clones; requires forms.create scope.
GET/formsgetFormsLists published forms owned by the account.
GET/forms/getclientformsgetClientFormsLists published forms for child accounts (uses parent_account).
PUT/forms/:formidupdateFormUpserts by form_secret_id; automatically publishes if status missing.
GET/forms/:formidgetFormByIdAccepts ObjectId or form_secret_id; validates request invite status.
DELETE/forms/:formiddeleteFormSingle delete + campaign soft-delete.
DELETE/formsdeleteFormsBulk delete with count safeguard.
GET/forms/formswidgetgetformsWidgetDataReturns submissions vs pending request counts.
POST/forms/:formid/sendrequestsendRequestIssues invite links and optional notifications.
POST/forms/resendrequestreSendRequestResends existing invites with custom contact info.
POST/forms/:formid/userresponseuserresponseController.saveResponsePublic submission entry point (included here for completeness).
GET/forms/usercategoriesgetUserCategoriesPagination + search across account categories.
POST/forms/usercategoriesuserCategoriesCreates a category for the account.
PUT/forms/usercategories/:categoryidupdateUserCategoriesHandles duplicate key error -> CATEGORY_ALREADY_EXIST.
DELETE/forms/usercategories/:categoryiddeleteUserCategoriesHard delete category.
GET/forms/usertagsgetUserTagsTag listing.
POST/forms/usertagsuserTagsTag creation.
PUT/forms/usertags/:tagidupdateUserTagsTag rename.
DELETE/forms/usertags/:tagiddeleteUserTagsTag delete.

✏️ Form lifecycle​

saveForm​

  • Generates a new form_secret_id (UUID v4) and stores owner/account IDs from req.auth.
  • When cloning (?isclone=true&form_id=<id>), copies layout, styling, thank-you copy, and status from the source form, then resolves a unique name through getFormCopyName. The helper recursively appends "copy", "copy 1", etc., while skipping the current form ID when renaming clones of clones.
  • Returns { formdata, domain }, where domain comes from utilities.getActiveDomain({ req, proto: true }) to help the UI render public links immediately.

getForms & getClientForms​

  • Both delegate to formsModel.getAllForms, which handles text search, date filtering, category/tag filters, sort mapping, aggregation, and pagination.
  • Account owner lists (getForms) filter by account_id and default to status: 'published'. Client lists (getClientForms) use parent_account to collect sub-account-ready forms.
  • formsModel.getAllForms augments each document with response counts, pending request counts, and iscampaignConnected by looking up campaign.data.

updateForm​

  • Resolves by form_secret_id + account to avoid exposing Mongo IDs in public share links.
  • Enforces status: 'published' if the payload omitted a status, ensuring the form becomes visible on first save from the builder.
  • Returns the updated document (lean, _id remapped to id).

getFormById​

  • Accepts either the UUID secret or a raw ObjectId; uses regex to detect the format.
  • When a rid (recipient secret) is provided, verifies the invite is still active. If the request is already submitted or cancelled, the controller short-circuits with a 400 error and descriptive message (ALREADY_SUBMITTED, REQUEST_CANCELLED).
  • Returns the hydrated form array from formsModel.getForms (which already injects campaign connection metadata).

Deletion (deleteForm, deleteForms)​

  • Single delete blocks if the form does not belong to the current account.
  • Bulk delete opens a Mongo session/transaction, recalculates the expected count server-side via formsModel.getFormCount, and throws FORMS_COUNT_MISMATCH if the client-supplied total no longer matches. When all=true, it first enumerates matching form IDs before calling deleteMany.
  • Both paths mark any linked campaign.data records as is_deleted=true to prevent future inbound automation from firing against a missing form.

Widgets (getformsWidgetData)​

  • Aggregates published forms for the account to compute:
    • pendingrequests: count of invites still in status: 'sent'.
    • allsubmissions: count of forms.userresponse entries with is_deleted=false.
  • Used in dashboard widgets to provide quick stats without running the heavier getForms aggregation.

🏷️ Categories & tags​

userCategories / userTags​

  • Attach account_id and owner directly from the auth context before saving.
  • Insert operations bubble Mongo duplicate key errors; categories convert duplicates into a custom error with code CATEGORY_ALREADY_EXIST, tags leave the raw Mongo message DUPLICATE_TAGS for the client to translate.

getUserCategories / getUserTags​

  • Support search via case-insensitive regex (name field).
  • Aggregation adds a count property by $lookup-ing into forms to show how many forms currently reference the category/tag.
  • Pagination uses limit/skip semantics consistent with other list endpoints.

updateUserCategories / updateUserTags​

  • Run findOneAndUpdate with { new: true } so the API returns the mutated document (with _id remapped to id).

deleteUserCategories / deleteUserTags​

  • Hard deletes the taxonomy entry. The controller does not cascade to forms; existing forms retain the stale ID. The UI should surface these as "(deleted)" until updated.

βœ‰οΈ Request distribution​

sendRequest​

  • Ensures sendrequest payload exists; otherwise returns a 400 with info is required!.
  • Loads the form by form_secret_id (must be published).
  • Generates a unique recipient_secret_id for each contact, persists a forms.sent.requests document with status: 'sent', and composes the share URL:
    1. ${activeDomain}/${share_path}?rid=<recipientSecret>&rqid=<requestId>
    2. Passes the URL to the internal URL shortener (POST http://localhost:${PORT}/v1/url) using the same JWT + account headers.
    3. Replaces with process.env.SHORT_DOMAIN/<code> for final delivery.
  • Based on notification_type, triggers sendMail and/or sendSMS to notify the recipient.

reSendRequest​

  • Accepts a request ID (rqid), recipient secret (receipientSecretId), optional override email/phone, and share path.
  • Reconstructs the link, shortens it, and replays the chosen channels. Uses the already stored recipient_id to look up contact details when overrides are missing.

Email helper (sendMail)​

  • Loads the CRM contact for personalization. Falls back to deriving a name from the email prefix when missing.
  • Sends through the shared Mail utility with origin forms so analytics/notifications stay grouped.
  • Supports overriding the recipient email via custom_email (used during resend).

SMS helper (sendSMS)​

  • Requires the account to own a Twilio number (TwillioNumber collection). Throws NO_NUMBER_PURCHASED otherwise.
  • Falls back to the contact record when custom_number is empty.
  • Uses the shared SMS utility and labels the origin forms.

shortenUrl​

  • Internal helper that reuses the caller's JWT (req.authToken) and X-Account-Id headers. Any failure bubbles up so the request endpoint responds with a 500.

⚠️ Edge cases & safeguards​

  • Invite replay – getFormById protects against multiple submissions from the same invite.
  • Clone naming – getFormCopyName prevents infinite loops by checking copy suffixes with numeric increments.
  • Bulk delete drift – Abort the transaction if the client-side total no longer matches the recalculated count to avoid surprise data loss.
  • Notification personalization – Email content greets recipients by first name when it can split the CRM contact's name field.
  • Domain resolution – All send/resend flows rely on utilities.getActiveDomain so links stay accurate for sub-accounts or white-labeled domains.
πŸ’¬

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM