@aglyn/plugins-crm 1.0.0-beta.220 → 1.0.0-beta.222
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +13 -13
- package/src/lib/components/activity-list.js +12 -5
- package/src/lib/components/activity-list.js.map +1 -1
- package/src/lib/components/companies-bulk-bar.d.ts +1 -4
- package/src/lib/components/companies-bulk-bar.js +7 -20
- package/src/lib/components/companies-bulk-bar.js.map +1 -1
- package/src/lib/components/companies-section.d.ts +3 -3
- package/src/lib/components/companies-section.js +149 -46
- package/src/lib/components/companies-section.js.map +1 -1
- package/src/lib/components/company-edit-drawer.js +281 -20
- package/src/lib/components/company-edit-drawer.js.map +1 -1
- package/src/lib/components/company-properties-card.js +99 -8
- package/src/lib/components/company-properties-card.js.map +1 -1
- package/src/lib/components/contact-detail-page.js +3 -1
- package/src/lib/components/contact-detail-page.js.map +1 -1
- package/src/lib/components/contact-merge-dialog.js +33 -10
- package/src/lib/components/contact-merge-dialog.js.map +1 -1
- package/src/lib/components/contact-properties-card.d.ts +0 -69
- package/src/lib/components/contact-properties-card.js +350 -60
- package/src/lib/components/contact-properties-card.js.map +1 -1
- package/src/lib/components/contact-reports-to-field.d.ts +46 -0
- package/src/lib/components/contact-reports-to-field.js +67 -0
- package/src/lib/components/contact-reports-to-field.js.map +1 -0
- package/src/lib/components/contacts-bulk-bar.d.ts +1 -8
- package/src/lib/components/contacts-bulk-bar.js +7 -19
- package/src/lib/components/contacts-bulk-bar.js.map +1 -1
- package/src/lib/components/contacts-section.js +46 -33
- package/src/lib/components/contacts-section.js.map +1 -1
- package/src/lib/components/crm-call-actions.d.ts +10 -0
- package/src/lib/components/crm-call-actions.js +26 -9
- package/src/lib/components/crm-call-actions.js.map +1 -1
- package/src/lib/components/crm-record-picker.d.ts +15 -0
- package/src/lib/components/crm-record-picker.js +16 -10
- package/src/lib/components/crm-record-picker.js.map +1 -1
- package/src/lib/components/crm-transfer-buttons.d.ts +54 -0
- package/src/lib/components/crm-transfer-buttons.js +167 -0
- package/src/lib/components/crm-transfer-buttons.js.map +1 -0
- package/src/lib/components/deal-contact-roles-card.d.ts +46 -0
- package/src/lib/components/deal-contact-roles-card.js +368 -0
- package/src/lib/components/deal-contact-roles-card.js.map +1 -0
- package/src/lib/components/deal-detail-page.d.ts +3 -2
- package/src/lib/components/deal-detail-page.js +25 -6
- package/src/lib/components/deal-detail-page.js.map +1 -1
- package/src/lib/components/deal-edit-drawer.d.ts +10 -0
- package/src/lib/components/deal-edit-drawer.js +219 -10
- package/src/lib/components/deal-edit-drawer.js.map +1 -1
- package/src/lib/components/deal-properties-card.d.ts +7 -0
- package/src/lib/components/deal-properties-card.js +34 -3
- package/src/lib/components/deal-properties-card.js.map +1 -1
- package/src/lib/components/deal-stage-card.d.ts +4 -0
- package/src/lib/components/deal-stage-card.js +10 -2
- package/src/lib/components/deal-stage-card.js.map +1 -1
- package/src/lib/components/deals-bulk-bar.d.ts +0 -3
- package/src/lib/components/deals-bulk-bar.js +7 -20
- package/src/lib/components/deals-bulk-bar.js.map +1 -1
- package/src/lib/components/deals-section.d.ts +3 -3
- package/src/lib/components/deals-section.js +123 -63
- package/src/lib/components/deals-section.js.map +1 -1
- package/src/lib/components/fields-section.js +31 -17
- package/src/lib/components/fields-section.js.map +1 -1
- package/src/lib/components/lead-convert-dialog.d.ts +4 -0
- package/src/lib/components/lead-convert-dialog.js +23 -1
- package/src/lib/components/lead-convert-dialog.js.map +1 -1
- package/src/lib/components/lead-detail-page.js +8 -2
- package/src/lib/components/lead-detail-page.js.map +1 -1
- package/src/lib/components/lead-profile-fields.d.ts +71 -0
- package/src/lib/components/lead-profile-fields.js +386 -0
- package/src/lib/components/lead-profile-fields.js.map +1 -0
- package/src/lib/components/lead-properties-card.d.ts +2 -1
- package/src/lib/components/lead-properties-card.js +85 -176
- package/src/lib/components/lead-properties-card.js.map +1 -1
- package/src/lib/components/lead-source-select.d.ts +4 -23
- package/src/lib/components/lead-source-select.js +9 -73
- package/src/lib/components/lead-source-select.js.map +1 -1
- package/src/lib/components/lead-status-chip.d.ts +9 -3
- package/src/lib/components/lead-status-chip.js +8 -4
- package/src/lib/components/lead-status-chip.js.map +1 -1
- package/src/lib/components/lead-status-options.d.ts +20 -0
- package/src/lib/components/lead-status-options.js +51 -0
- package/src/lib/components/lead-status-options.js.map +1 -0
- package/src/lib/components/lead-unqualify-dialog.d.ts +5 -0
- package/src/lib/components/lead-unqualify-dialog.js +50 -18
- package/src/lib/components/lead-unqualify-dialog.js.map +1 -1
- package/src/lib/components/leads-bulk-bar.d.ts +0 -3
- package/src/lib/components/leads-bulk-bar.js +29 -29
- package/src/lib/components/leads-bulk-bar.js.map +1 -1
- package/src/lib/components/leads-section.js +120 -68
- package/src/lib/components/leads-section.js.map +1 -1
- package/src/lib/components/linked-deals-card.d.ts +4 -0
- package/src/lib/components/linked-deals-card.js +47 -3
- package/src/lib/components/linked-deals-card.js.map +1 -1
- package/src/lib/components/log-activity-dialog.js +43 -3
- package/src/lib/components/log-activity-dialog.js.map +1 -1
- package/src/lib/components/new-contact-drawer.d.ts +8 -0
- package/src/lib/components/new-contact-drawer.js +108 -6
- package/src/lib/components/new-contact-drawer.js.map +1 -1
- package/src/lib/components/new-lead-drawer.d.ts +9 -2
- package/src/lib/components/new-lead-drawer.js +88 -121
- package/src/lib/components/new-lead-drawer.js.map +1 -1
- package/src/lib/components/picklist-select.d.ts +36 -0
- package/src/lib/components/picklist-select.js +114 -0
- package/src/lib/components/picklist-select.js.map +1 -0
- package/src/lib/components/picklist-values-card.d.ts +34 -0
- package/src/lib/components/{lead-source-values-card.js → picklist-values-card.js} +241 -58
- package/src/lib/components/picklist-values-card.js.map +1 -0
- package/src/lib/components/pipeline-stages-dialog.d.ts +7 -0
- package/src/lib/components/pipeline-stages-dialog.js +26 -4
- package/src/lib/components/pipeline-stages-dialog.js.map +1 -1
- package/src/lib/components/pipelines-dialog.js +15 -6
- package/src/lib/components/pipelines-dialog.js.map +1 -1
- package/src/lib/components/recent-activity-feed.js +20 -5
- package/src/lib/components/recent-activity-feed.js.map +1 -1
- package/src/lib/components/recipes-card.js +4 -2
- package/src/lib/components/recipes-card.js.map +1 -1
- package/src/lib/components/record-tasks-card.js +15 -8
- package/src/lib/components/record-tasks-card.js.map +1 -1
- package/src/lib/components/reports/forecast-card.d.ts +6 -0
- package/src/lib/components/reports/forecast-card.js +101 -3
- package/src/lib/components/reports/forecast-card.js.map +1 -1
- package/src/lib/components/reports/lead-funnel-card.d.ts +12 -0
- package/src/lib/components/reports/lead-funnel-card.js +9 -5
- package/src/lib/components/reports/lead-funnel-card.js.map +1 -1
- package/src/lib/components/reports/lead-source-card.d.ts +23 -0
- package/src/lib/components/reports/lead-source-card.js +318 -0
- package/src/lib/components/reports/lead-source-card.js.map +1 -0
- package/src/lib/components/reports/report-export.js +1 -1
- package/src/lib/components/reports/report-export.js.map +1 -1
- package/src/lib/components/reports-section.js +8 -0
- package/src/lib/components/reports-section.js.map +1 -1
- package/src/lib/components/sharing-rule-drawer.d.ts +5 -0
- package/src/lib/components/sharing-rule-drawer.js +108 -19
- package/src/lib/components/sharing-rule-drawer.js.map +1 -1
- package/src/lib/components/sharing-rules-card.js +11 -3
- package/src/lib/components/sharing-rules-card.js.map +1 -1
- package/src/lib/components/task-cells.d.ts +9 -1
- package/src/lib/components/task-cells.js +9 -4
- package/src/lib/components/task-cells.js.map +1 -1
- package/src/lib/components/task-edit-drawer.js +111 -43
- package/src/lib/components/task-edit-drawer.js.map +1 -1
- package/src/lib/components/tasks-bulk-bar.d.ts +0 -3
- package/src/lib/components/tasks-bulk-bar.js +7 -20
- package/src/lib/components/tasks-bulk-bar.js.map +1 -1
- package/src/lib/components/tasks-calendar.d.ts +3 -0
- package/src/lib/components/tasks-calendar.js +8 -1
- package/src/lib/components/tasks-calendar.js.map +1 -1
- package/src/lib/components/tasks-section.d.ts +3 -3
- package/src/lib/components/tasks-section.js +58 -43
- package/src/lib/components/tasks-section.js.map +1 -1
- package/src/lib/constants/api-routes.d.ts +2 -9
- package/src/lib/constants/api-routes.js +1 -7
- package/src/lib/constants/api-routes.js.map +1 -1
- package/src/lib/constants/bundle-common.d.ts +6 -0
- package/src/lib/constants/bundle-common.js +11 -0
- package/src/lib/constants/bundle-common.js.map +1 -1
- package/src/lib/constants/company-filters.d.ts +14 -0
- package/src/lib/constants/company-filters.js +51 -1
- package/src/lib/constants/company-filters.js.map +1 -1
- package/src/lib/constants/contact-filters.d.ts +2 -0
- package/src/lib/constants/contact-filters.js +18 -1
- package/src/lib/constants/contact-filters.js.map +1 -1
- package/src/lib/constants/deal-filters.d.ts +22 -1
- package/src/lib/constants/deal-filters.js +67 -4
- package/src/lib/constants/deal-filters.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +3 -0
- package/src/lib/declarations.console-server.js +35 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +35 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/declarations.server.d.ts +11 -0
- package/src/lib/declarations.server.js +92 -1
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/hooks/use-company-picklists.d.ts +15 -0
- package/src/lib/hooks/use-company-picklists.js +55 -0
- package/src/lib/hooks/use-company-picklists.js.map +1 -0
- package/src/lib/hooks/use-crm-picklist.d.ts +25 -0
- package/src/lib/hooks/use-crm-picklist.js +53 -0
- package/src/lib/hooks/use-crm-picklist.js.map +1 -0
- package/src/lib/hooks/use-crm-record-names.d.ts +16 -0
- package/src/lib/hooks/use-crm-record-names.js +45 -19
- package/src/lib/hooks/use-crm-record-names.js.map +1 -1
- package/src/lib/hooks/use-crm-task-picklists.d.ts +11 -0
- package/src/lib/hooks/use-crm-task-picklists.js +45 -0
- package/src/lib/hooks/use-crm-task-picklists.js.map +1 -0
- package/src/lib/hooks/use-deals.d.ts +14 -0
- package/src/lib/hooks/use-deals.js +29 -0
- package/src/lib/hooks/use-deals.js.map +1 -1
- package/src/lib/hooks/use-lead-picklists.d.ts +14 -0
- package/src/lib/hooks/use-lead-picklists.js +46 -0
- package/src/lib/hooks/use-lead-picklists.js.map +1 -0
- package/src/lib/hooks/use-lead-source-picklist.d.ts +5 -19
- package/src/lib/hooks/use-lead-source-picklist.js +12 -22
- package/src/lib/hooks/use-lead-source-picklist.js.map +1 -1
- package/src/lib/hooks/use-lead-status-picklist.d.ts +9 -0
- package/src/lib/hooks/use-lead-status-picklist.js +40 -0
- package/src/lib/hooks/use-lead-status-picklist.js.map +1 -0
- package/src/lib/model/companies.d.ts +32 -3
- package/src/lib/model/companies.js +133 -16
- package/src/lib/model/companies.js.map +1 -1
- package/src/lib/model/contact-merge.d.ts +27 -3
- package/src/lib/model/contact-merge.js +81 -7
- package/src/lib/model/contact-merge.js.map +1 -1
- package/src/lib/model/contact-record.d.ts +17 -0
- package/src/lib/model/contact-record.js +15 -1
- package/src/lib/model/contact-record.js.map +1 -1
- package/src/lib/model/contact-update.d.ts +31 -2
- package/src/lib/model/contact-update.js +14 -0
- package/src/lib/model/contact-update.js.map +1 -1
- package/src/lib/model/crm-list-query.d.ts +5 -0
- package/src/lib/model/crm-list-query.js +8 -0
- package/src/lib/model/crm-list-query.js.map +1 -1
- package/src/lib/model/crm-recipes.d.ts +40 -0
- package/src/lib/model/crm-recipes.js +244 -0
- package/src/lib/model/crm-recipes.js.map +1 -0
- package/src/lib/model/crm-record-counts.d.ts +36 -0
- package/src/lib/model/crm-record-counts.js +54 -0
- package/src/lib/model/crm-record-counts.js.map +1 -0
- package/src/lib/model/crm-record-lists.d.ts +23 -0
- package/src/lib/model/crm-record-lists.js +161 -0
- package/src/lib/model/crm-record-lists.js.map +1 -0
- package/src/lib/model/crm-reports.d.ts +73 -3
- package/src/lib/model/crm-reports.js +120 -1
- package/src/lib/model/crm-reports.js.map +1 -1
- package/src/lib/model/crm-sharing.d.ts +21 -3
- package/src/lib/model/crm-sharing.js +27 -6
- package/src/lib/model/crm-sharing.js.map +1 -1
- package/src/lib/model/deal-board-model.d.ts +8 -2
- package/src/lib/model/deal-board-model.js +15 -2
- package/src/lib/model/deal-board-model.js.map +1 -1
- package/src/lib/model/deal-contact-roles.d.ts +45 -0
- package/src/lib/model/deal-contact-roles.js +73 -0
- package/src/lib/model/deal-contact-roles.js.map +1 -0
- package/src/lib/model/deal-form-model.d.ts +56 -0
- package/src/lib/model/deal-form-model.js +79 -7
- package/src/lib/model/deal-form-model.js.map +1 -1
- package/src/lib/model/download-text-file.d.ts +23 -0
- package/src/lib/model/download-text-file.js +34 -0
- package/src/lib/model/download-text-file.js.map +1 -0
- package/src/lib/model/lead-filters.d.ts +36 -2
- package/src/lib/model/lead-filters.js +123 -2
- package/src/lib/model/lead-filters.js.map +1 -1
- package/src/lib/model/lead-source-direction.d.ts +38 -0
- package/src/lib/model/lead-source-direction.js +52 -0
- package/src/lib/model/lead-source-direction.js.map +1 -0
- package/src/lib/model/picklist-values.d.ts +119 -0
- package/src/lib/model/picklist-values.js +256 -0
- package/src/lib/model/picklist-values.js.map +1 -0
- package/src/lib/model/record-facts-disclosed.d.ts +81 -0
- package/src/lib/model/record-facts-disclosed.js +281 -0
- package/src/lib/model/record-facts-disclosed.js.map +1 -0
- package/src/lib/model/record-facts.d.ts +215 -34
- package/src/lib/model/record-facts.js +307 -128
- package/src/lib/model/record-facts.js.map +1 -1
- package/src/lib/model/task-picklists.d.ts +52 -0
- package/src/lib/model/task-picklists.js +83 -0
- package/src/lib/model/task-picklists.js.map +1 -0
- package/src/lib/model/task-routes.d.ts +13 -0
- package/src/lib/model/task-routes.js +23 -4
- package/src/lib/model/task-routes.js.map +1 -1
- package/src/lib/plan-entitlements.js +6 -4
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +17 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/contacts.js +220 -14
- package/src/lib/server/api-v1/contacts.js.map +1 -1
- package/src/lib/server/api-v1/crm-activities.js +14 -1
- package/src/lib/server/api-v1/crm-activities.js.map +1 -1
- package/src/lib/server/api-v1/crm-companies.js +123 -14
- package/src/lib/server/api-v1/crm-companies.js.map +1 -1
- package/src/lib/server/api-v1/crm-deals.js +291 -26
- package/src/lib/server/api-v1/crm-deals.js.map +1 -1
- package/src/lib/server/api-v1/crm-leads.js +144 -38
- package/src/lib/server/api-v1/crm-leads.js.map +1 -1
- package/src/lib/server/api-v1/crm-pipelines.d.ts +6 -0
- package/src/lib/server/api-v1/crm-pipelines.js +8 -2
- package/src/lib/server/api-v1/crm-pipelines.js.map +1 -1
- package/src/lib/server/api-v1/crm-tasks.js +75 -33
- package/src/lib/server/api-v1/crm-tasks.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +0 -11
- package/src/lib/server/api-v1/openapi.js +171 -16
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/assign-contact-owner.d.ts +162 -0
- package/src/lib/server/assign-contact-owner.js +340 -0
- package/src/lib/server/assign-contact-owner.js.map +1 -0
- package/src/lib/server/associate-company-by-domain.d.ts +70 -0
- package/src/lib/server/associate-company-by-domain.js +153 -0
- package/src/lib/server/associate-company-by-domain.js.map +1 -0
- package/src/lib/server/automation-steps.d.ts +139 -0
- package/src/lib/server/automation-steps.js +472 -0
- package/src/lib/server/automation-steps.js.map +1 -0
- package/src/lib/server/capture-contact.js +69 -22
- package/src/lib/server/capture-contact.js.map +1 -1
- package/src/lib/server/capture-host-contact.d.ts +104 -0
- package/src/lib/server/capture-host-contact.js +151 -0
- package/src/lib/server/capture-host-contact.js.map +1 -0
- package/src/lib/server/company-children.d.ts +25 -0
- package/src/lib/server/company-children.js +42 -0
- package/src/lib/server/company-children.js.map +1 -0
- package/src/lib/server/company-delete.d.ts +18 -0
- package/src/lib/server/company-delete.js +44 -21
- package/src/lib/server/company-delete.js.map +1 -1
- package/src/lib/server/contact-email-history.js +21 -19
- package/src/lib/server/contact-email-history.js.map +1 -1
- package/src/lib/server/contact-remove.js +27 -2
- package/src/lib/server/contact-remove.js.map +1 -1
- package/src/lib/server/contact-reports-to.d.ts +61 -0
- package/src/lib/server/contact-reports-to.js +114 -0
- package/src/lib/server/contact-reports-to.js.map +1 -0
- package/src/lib/server/contact-update.d.ts +42 -1
- package/src/lib/server/contact-update.js +204 -23
- package/src/lib/server/contact-update.js.map +1 -1
- package/src/lib/server/convert-host-lead.d.ts +46 -1
- package/src/lib/server/convert-host-lead.js +173 -15
- package/src/lib/server/convert-host-lead.js.map +1 -1
- package/src/lib/server/convert-open-lead.js +12 -5
- package/src/lib/server/convert-open-lead.js.map +1 -1
- package/src/lib/server/crm-record-indexes.d.ts +45 -0
- package/src/lib/server/crm-record-indexes.js +78 -0
- package/src/lib/server/crm-record-indexes.js.map +1 -0
- package/src/lib/server/crm-sharing.d.ts +9 -1
- package/src/lib/server/crm-sharing.js +23 -3
- package/src/lib/server/crm-sharing.js.map +1 -1
- package/src/lib/server/deal-contact-roles.d.ts +27 -0
- package/src/lib/server/deal-contact-roles.js +89 -0
- package/src/lib/server/deal-contact-roles.js.map +1 -0
- package/src/lib/server/lead-convert.d.ts +5 -0
- package/src/lib/server/lead-convert.js +19 -3
- package/src/lib/server/lead-convert.js.map +1 -1
- package/src/lib/server/lead-create.d.ts +17 -2
- package/src/lib/server/lead-create.js +46 -14
- package/src/lib/server/lead-create.js.map +1 -1
- package/src/lib/server/lead-nurturing.d.ts +6 -0
- package/src/lib/server/lead-nurturing.js +57 -20
- package/src/lib/server/lead-nurturing.js.map +1 -1
- package/src/lib/server/lead-picklists.d.ts +35 -0
- package/src/lib/server/lead-picklists.js +52 -0
- package/src/lib/server/lead-picklists.js.map +1 -0
- package/src/lib/server/lead-source-picklist.d.ts +1 -13
- package/src/lib/server/lead-source-picklist.js +6 -33
- package/src/lib/server/lead-source-picklist.js.map +1 -1
- package/src/lib/server/merge-contacts.js +22 -4
- package/src/lib/server/merge-contacts.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +53 -0
- package/src/lib/server/person-eraser.js +149 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/person-records.d.ts +69 -0
- package/src/lib/server/person-records.js +317 -0
- package/src/lib/server/person-records.js.map +1 -0
- package/src/lib/server/person-refund.d.ts +97 -0
- package/src/lib/server/person-refund.js +221 -0
- package/src/lib/server/person-refund.js.map +1 -0
- package/src/lib/server/picklist-values.d.ts +21 -0
- package/src/lib/server/picklist-values.js +308 -0
- package/src/lib/server/picklist-values.js.map +1 -0
- package/src/lib/server/read-picklist.d.ts +44 -0
- package/src/lib/server/read-picklist.js +65 -0
- package/src/lib/server/read-picklist.js.map +1 -0
- package/src/lib/server/recipe-routes.js +15 -7
- package/src/lib/server/recipe-routes.js.map +1 -1
- package/src/lib/server/record-email-state.js +7 -1
- package/src/lib/server/record-email-state.js.map +1 -1
- package/src/lib/server/record-facts.d.ts +3 -0
- package/src/lib/server/record-facts.js +233 -31
- package/src/lib/server/record-facts.js.map +1 -1
- package/src/lib/server/record-origin.d.ts +27 -0
- package/src/lib/server/record-origin.js +104 -0
- package/src/lib/server/record-origin.js.map +1 -0
- package/src/lib/server/record-timeline.js +15 -2
- package/src/lib/server/record-timeline.js.map +1 -1
- package/src/lib/server/suite-gate.js +4 -3
- package/src/lib/server/suite-gate.js.map +1 -1
- package/src/lib/server/task-routes.js +30 -16
- package/src/lib/server/task-routes.js.map +1 -1
- package/src/lib/server-deal-stage.d.ts +5 -1
- package/src/lib/server-deal-stage.js +10 -5
- package/src/lib/server-deal-stage.js.map +1 -1
- package/src/lib/server.d.ts +8 -3
- package/src/lib/server.js +47 -43
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +55 -0
- package/src/lib/testing/memory-firestore.js +335 -0
- package/src/lib/testing/memory-firestore.js.map +1 -0
- package/src/lib/testing/stand-in-email-send-index.d.ts +25 -0
- package/src/lib/testing/stand-in-email-send-index.js +49 -0
- package/src/lib/testing/stand-in-email-send-index.js.map +1 -0
- package/src/lib/transfer/activities.d.ts +30 -0
- package/src/lib/transfer/activities.js +132 -0
- package/src/lib/transfer/activities.js.map +1 -0
- package/src/lib/transfer/aliases.d.ts +22 -0
- package/src/lib/transfer/aliases.js +1342 -0
- package/src/lib/transfer/aliases.js.map +1 -0
- package/src/lib/transfer/common.d.ts +222 -0
- package/src/lib/transfer/common.js +832 -0
- package/src/lib/transfer/common.js.map +1 -0
- package/src/lib/transfer/companies.d.ts +43 -0
- package/src/lib/transfer/companies.js +549 -0
- package/src/lib/transfer/companies.js.map +1 -0
- package/src/lib/transfer/contacts.d.ts +31 -0
- package/src/lib/transfer/contacts.js +727 -0
- package/src/lib/transfer/contacts.js.map +1 -0
- package/src/lib/transfer/deals.d.ts +66 -0
- package/src/lib/transfer/deals.js +698 -0
- package/src/lib/transfer/deals.js.map +1 -0
- package/src/lib/transfer/email-templates-package.d.ts +40 -0
- package/src/lib/transfer/email-templates-package.js +76 -0
- package/src/lib/transfer/email-templates-package.js.map +1 -0
- package/src/lib/transfer/email-templates-package.server.d.ts +32 -0
- package/src/lib/transfer/email-templates-package.server.js +176 -0
- package/src/lib/transfer/email-templates-package.server.js.map +1 -0
- package/src/lib/transfer/fields.d.ts +102 -0
- package/src/lib/transfer/fields.js +1755 -0
- package/src/lib/transfer/fields.js.map +1 -0
- package/src/lib/transfer/leads.d.ts +30 -0
- package/src/lib/transfer/leads.js +582 -0
- package/src/lib/transfer/leads.js.map +1 -0
- package/src/lib/transfer/pipelines.d.ts +21 -0
- package/src/lib/transfer/pipelines.js +172 -0
- package/src/lib/transfer/pipelines.js.map +1 -0
- package/src/lib/transfer/register.d.ts +47 -0
- package/src/lib/transfer/register.js +277 -0
- package/src/lib/transfer/register.js.map +1 -0
- package/src/lib/transfer/tasks.d.ts +30 -0
- package/src/lib/transfer/tasks.js +418 -0
- package/src/lib/transfer/tasks.js.map +1 -0
- package/src/lib/transfer/ui.d.ts +20 -0
- package/src/lib/transfer/ui.js +42 -0
- package/src/lib/transfer/ui.js.map +1 -0
- package/src/lib/components/company-import-drawer.d.ts +0 -42
- package/src/lib/components/company-import-drawer.js +0 -99
- package/src/lib/components/company-import-drawer.js.map +0 -1
- package/src/lib/components/contact-import-drawer.d.ts +0 -46
- package/src/lib/components/contact-import-drawer.js +0 -149
- package/src/lib/components/contact-import-drawer.js.map +0 -1
- package/src/lib/components/crm-export-all-button.d.ts +0 -39
- package/src/lib/components/crm-export-all-button.js +0 -98
- package/src/lib/components/crm-export-all-button.js.map +0 -1
- package/src/lib/components/csv-import-drawer.d.ts +0 -139
- package/src/lib/components/csv-import-drawer.js +0 -757
- package/src/lib/components/csv-import-drawer.js.map +0 -1
- package/src/lib/components/deal-import-drawer.d.ts +0 -24
- package/src/lib/components/deal-import-drawer.js +0 -83
- package/src/lib/components/deal-import-drawer.js.map +0 -1
- package/src/lib/components/lead-import-drawer.d.ts +0 -27
- package/src/lib/components/lead-import-drawer.js +0 -118
- package/src/lib/components/lead-import-drawer.js.map +0 -1
- package/src/lib/components/lead-source-values-card.d.ts +0 -27
- package/src/lib/components/lead-source-values-card.js.map +0 -1
- package/src/lib/components/task-import-drawer.d.ts +0 -24
- package/src/lib/components/task-import-drawer.js +0 -82
- package/src/lib/components/task-import-drawer.js.map +0 -1
- package/src/lib/model/companies-csv.d.ts +0 -36
- package/src/lib/model/companies-csv.js +0 -38
- package/src/lib/model/companies-csv.js.map +0 -1
- package/src/lib/model/contacts-csv.d.ts +0 -54
- package/src/lib/model/contacts-csv.js +0 -67
- package/src/lib/model/contacts-csv.js.map +0 -1
- package/src/lib/model/crm-company-import.d.ts +0 -154
- package/src/lib/model/crm-company-import.js +0 -331
- package/src/lib/model/crm-company-import.js.map +0 -1
- package/src/lib/model/crm-csv.d.ts +0 -239
- package/src/lib/model/crm-csv.js +0 -467
- package/src/lib/model/crm-csv.js.map +0 -1
- package/src/lib/model/crm-deal-import.d.ts +0 -112
- package/src/lib/model/crm-deal-import.js +0 -263
- package/src/lib/model/crm-deal-import.js.map +0 -1
- package/src/lib/model/crm-import.d.ts +0 -250
- package/src/lib/model/crm-import.js +0 -467
- package/src/lib/model/crm-import.js.map +0 -1
- package/src/lib/model/crm-lead-import.d.ts +0 -138
- package/src/lib/model/crm-lead-import.js +0 -446
- package/src/lib/model/crm-lead-import.js.map +0 -1
- package/src/lib/model/crm-task-import.d.ts +0 -94
- package/src/lib/model/crm-task-import.js +0 -275
- package/src/lib/model/crm-task-import.js.map +0 -1
- package/src/lib/model/deals-csv.d.ts +0 -32
- package/src/lib/model/deals-csv.js +0 -36
- package/src/lib/model/deals-csv.js.map +0 -1
- package/src/lib/model/import-column-shapes.d.ts +0 -36
- package/src/lib/model/import-column-shapes.js +0 -76
- package/src/lib/model/import-column-shapes.js.map +0 -1
- package/src/lib/model/lead-source-values.d.ts +0 -82
- package/src/lib/model/lead-source-values.js +0 -149
- package/src/lib/model/lead-source-values.js.map +0 -1
- package/src/lib/model/leads-csv.d.ts +0 -40
- package/src/lib/model/leads-csv.js +0 -40
- package/src/lib/model/leads-csv.js.map +0 -1
- package/src/lib/model/tasks-csv.d.ts +0 -28
- package/src/lib/model/tasks-csv.js +0 -28
- package/src/lib/model/tasks-csv.js.map +0 -1
- package/src/lib/server/companies-import.d.ts +0 -57
- package/src/lib/server/companies-import.js +0 -242
- package/src/lib/server/companies-import.js.map +0 -1
- package/src/lib/server/contacts-import.d.ts +0 -109
- package/src/lib/server/contacts-import.js +0 -393
- package/src/lib/server/contacts-import.js.map +0 -1
- package/src/lib/server/deals-import.d.ts +0 -76
- package/src/lib/server/deals-import.js +0 -248
- package/src/lib/server/deals-import.js.map +0 -1
- package/src/lib/server/export-route.d.ts +0 -60
- package/src/lib/server/export-route.js +0 -447
- package/src/lib/server/export-route.js.map +0 -1
- package/src/lib/server/import-context.d.ts +0 -87
- package/src/lib/server/import-context.js +0 -182
- package/src/lib/server/import-context.js.map +0 -1
- package/src/lib/server/lead-source-values.d.ts +0 -50
- package/src/lib/server/lead-source-values.js +0 -191
- package/src/lib/server/lead-source-values.js.map +0 -1
- package/src/lib/server/leads-import.d.ts +0 -84
- package/src/lib/server/leads-import.js +0 -367
- package/src/lib/server/leads-import.js.map +0 -1
- package/src/lib/server/tasks-import.d.ts +0 -54
- package/src/lib/server/tasks-import.js +0 -160
- package/src/lib/server/tasks-import.js.map +0 -1
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
/**
|
|
3
|
-
* @license
|
|
4
|
-
* Copyright 2026 Aglyn LLC
|
|
5
|
-
*
|
|
6
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
-
* you may not use this file except in compliance with the License.
|
|
8
|
-
* You may obtain a copy of the License at
|
|
9
|
-
*
|
|
10
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
-
*
|
|
12
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
-
* See the License for the specific language governing permissions and
|
|
16
|
-
* limitations under the License.
|
|
17
|
-
*/ import { checkEntitlement, consentGroupForHost, CRM_COLLECTIONS, csvCell, hostScopeToken, isOrgWideMember, MAX_SCOPE_HOSTS, memberCanSee, memberScopeTokens, scopeTokensForHost, planLabelGrantingFeature, pluginRequestFromWeb } from "@aglyn/aglyn/server";
|
|
18
|
-
import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession, isServerReleaseFlagOnForOrg, lockdownRefusal, memberHasOrgPermission, resolveOrgMembership } from "@aglyn/tenant-data-admin";
|
|
19
|
-
import { FieldPath, FieldValue } from "firebase-admin/firestore";
|
|
20
|
-
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
21
|
-
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
22
|
-
import { contactCsvRowFromDoc, crmExportCells, crmExportHeader, isCrmExportResource } from "../model/crm-csv.js";
|
|
23
|
-
import { contactPrimaryGroup } from "../model/contact-holder.js";
|
|
24
|
-
/**
|
|
25
|
-
* Documents read per round trip. Not a cap — the stream keeps paging until
|
|
26
|
-
* the collection is exhausted. It bounds what is held at once, which is the
|
|
27
|
-
* only thing a page size should ever bound.
|
|
28
|
-
*/ const PAGE_SIZE = 500;
|
|
29
|
-
/** Sites read per page when the leads file spans an organization. */ const HOST_PAGE = 100;
|
|
30
|
-
/**
|
|
31
|
-
* The most sites one organization-level leads file reads from.
|
|
32
|
-
*
|
|
33
|
-
* A ceiling rather than an unbounded sweep, and it is REPORTED: the file's
|
|
34
|
-
* row header counts only what was read, so a short file is provably short
|
|
35
|
-
* rather than quietly partial.
|
|
36
|
-
*/ const HOST_CEILING = 200;
|
|
37
|
-
/** The most linked records a tasks file resolves names for before writing ids. */ const NAME_CACHE_CEILING = 2000;
|
|
38
|
-
/** Header naming the row count the server undertook to send. */ export const EXPORT_ROWS_HEADER = 'X-Aglyn-Export-Rows';
|
|
39
|
-
const json = (body, status)=>Response.json(body, {
|
|
40
|
-
status
|
|
41
|
-
});
|
|
42
|
-
/** Which collection under the org root each resource is, bar leads. */ const ORG_COLLECTION = {
|
|
43
|
-
contacts: 'contacts',
|
|
44
|
-
companies: CRM_COLLECTIONS.companies,
|
|
45
|
-
deals: CRM_COLLECTIONS.deals,
|
|
46
|
-
tasks: CRM_COLLECTIONS.tasks
|
|
47
|
-
};
|
|
48
|
-
/** The file's name, before the date stamp. */ const FILE_BASE = {
|
|
49
|
-
contacts: 'contacts',
|
|
50
|
-
companies: 'companies',
|
|
51
|
-
deals: 'deals',
|
|
52
|
-
tasks: 'tasks',
|
|
53
|
-
leads: 'leads'
|
|
54
|
-
};
|
|
55
|
-
/**
|
|
56
|
-
* THE WHOLE COLLECTION, STREAMED (AGL-2662).
|
|
57
|
-
*
|
|
58
|
-
* ## What this fixes
|
|
59
|
-
*
|
|
60
|
-
* Every CRM export until now wrote the rows the LIST had loaded — a
|
|
61
|
-
* `limit(1000)` window for contacts, a `limit(200)` view for tasks — and
|
|
62
|
-
* called the file `contacts.csv`. A merchant with 40,000 contacts got 1,000
|
|
63
|
-
* of them and nothing on screen or in the file said so. `docs/bulk-actions.md`
|
|
64
|
-
* said it in prose, which is not where somebody opening a spreadsheet looks.
|
|
65
|
-
* This is the same file over every row there is, and it carries
|
|
66
|
-
* `X-Aglyn-Export-Rows` so the client can prove what it received is whole —
|
|
67
|
-
* a stream that dies halfway yields a perfectly well-formed shorter file, and
|
|
68
|
-
* nothing about the bytes says they are short.
|
|
69
|
-
*
|
|
70
|
-
* ## Why it is a route and not a bigger client read
|
|
71
|
-
*
|
|
72
|
-
* A browser cannot hold an unbounded collection, and the tier that most
|
|
73
|
-
* needs a complete export is the tier with the most rows. Cursor-paged and
|
|
74
|
-
* streamed, completeness costs neither a hang nor a buffer.
|
|
75
|
-
*
|
|
76
|
-
* ## The columns are the client's columns
|
|
77
|
-
*
|
|
78
|
-
* `crmExportHeader` and `crmExportCells` (`model/crm-csv.ts`) are the
|
|
79
|
-
* writers each section's Export button uses. One feature, one file format.
|
|
80
|
-
*
|
|
81
|
-
* ## The order is by document id, deliberately
|
|
82
|
-
*
|
|
83
|
-
* Firestore DROPS every document missing the ordered field, so ordering by
|
|
84
|
-
* `updatedAt` or `dueAtMs` would silently omit rows written before that
|
|
85
|
-
* field existed — the exact silent loss an export must not have. A document
|
|
86
|
-
* id is on every document and is a total order.
|
|
87
|
-
*
|
|
88
|
-
* ## Scope is enforced here, because nothing else can
|
|
89
|
-
*
|
|
90
|
-
* The Admin SDK bypasses the rules, so `memberScopeTokens` IS the
|
|
91
|
-
* enforcement: an org-wide member reads every row, and a scoped
|
|
92
|
-
* collaborator's query carries their own tokens as the `array-contains-any`
|
|
93
|
-
* the rules would have evaluated. Leads are host data by path, so the
|
|
94
|
-
* equivalent check is whether the caller reaches the site.
|
|
95
|
-
*/ export async function crmExportRoute(request) {
|
|
96
|
-
var _headers_authorization, _ref, _ref1, _ref2;
|
|
97
|
-
const { method, query, headers: rawHeaders } = await pluginRequestFromWeb(request);
|
|
98
|
-
const headers = rawHeaders;
|
|
99
|
-
if (method !== 'GET') return json({
|
|
100
|
-
error: 'Method not allowed'
|
|
101
|
-
}, 405);
|
|
102
|
-
const authorization = (_headers_authorization = headers.authorization) != null ? _headers_authorization : '';
|
|
103
|
-
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
104
|
-
if (!idToken) return json({
|
|
105
|
-
error: 'Unauthenticated'
|
|
106
|
-
}, 401);
|
|
107
|
-
const orgId = String((_ref = query == null ? void 0 : query['orgId']) != null ? _ref : '');
|
|
108
|
-
const resource = String((_ref1 = query == null ? void 0 : query['resource']) != null ? _ref1 : '');
|
|
109
|
-
const hostId = String((_ref2 = query == null ? void 0 : query['hostId']) != null ? _ref2 : '');
|
|
110
|
-
if (!orgId || !isCrmExportResource(resource)) {
|
|
111
|
-
return json({
|
|
112
|
-
error: 'Missing orgId or resource'
|
|
113
|
-
}, 400);
|
|
114
|
-
}
|
|
115
|
-
try {
|
|
116
|
-
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
117
|
-
if (!decoded.email_verified && !isImpersonationSession(decoded)) {
|
|
118
|
-
return emailUnverifiedResponse();
|
|
119
|
-
}
|
|
120
|
-
const staff = decoded['staff'] === true;
|
|
121
|
-
const membership = await resolveOrgMembership(decoded.uid, orgId);
|
|
122
|
-
const member = membership == null ? void 0 : membership.member;
|
|
123
|
-
if (!member && !staff) return json({
|
|
124
|
-
error: 'Not found'
|
|
125
|
-
}, 404);
|
|
126
|
-
/*
|
|
127
|
-
* THE PEOPLE FILES ARE EVERY WORKSPACE'S (AGL-2839, AGL-2851).
|
|
128
|
-
*
|
|
129
|
-
* Exporting the contacts and leads a workspace holds is an obligation,
|
|
130
|
-
* not a CRM feature — a Free workspace takes them from its Privacy
|
|
131
|
-
* settings — so neither the release flag nor the plan is asked of those
|
|
132
|
-
* two. The CRM's own records, companies, deals and tasks, are asked
|
|
133
|
-
* both: the flag here, and the plan once the org has been read.
|
|
134
|
-
*/ const peopleFile = resource === 'contacts' || resource === 'leads';
|
|
135
|
-
if (!peopleFile && !staff && !await isServerReleaseFlagOnForOrg('release_crm', orgId)) {
|
|
136
|
-
return json({
|
|
137
|
-
error: 'Not available'
|
|
138
|
-
}, 404);
|
|
139
|
-
}
|
|
140
|
-
// The permission the CRM's own rules read for. Reading is not writing,
|
|
141
|
-
// but these records ARE the audience: `data.manage` is what admits a
|
|
142
|
-
// person to the list this file copies.
|
|
143
|
-
if (!staff && !await memberHasOrgPermission(orgId, member, 'data.manage')) {
|
|
144
|
-
return json({
|
|
145
|
-
error: 'Not found'
|
|
146
|
-
}, 404);
|
|
147
|
-
}
|
|
148
|
-
const firestore = firebaseAdmin.app().firestore();
|
|
149
|
-
const orgRef = firestore.collection('orgs').doc(orgId);
|
|
150
|
-
const orgSnapshot = await orgRef.get();
|
|
151
|
-
if (!orgSnapshot.exists) return json({
|
|
152
|
-
error: 'Unknown organization'
|
|
153
|
-
}, 404);
|
|
154
|
-
const org = orgSnapshot.data();
|
|
155
|
-
const locked = await lockdownRefusal({
|
|
156
|
-
request,
|
|
157
|
-
staff,
|
|
158
|
-
uid: decoded.uid,
|
|
159
|
-
org
|
|
160
|
-
});
|
|
161
|
-
if (locked) return locked;
|
|
162
|
-
// The CRM's own records on a plan without the CRM, staff included: the
|
|
163
|
-
// refusal every `crm/*` route gives, after the caller is known.
|
|
164
|
-
if (!peopleFile && !checkEntitlement(org, 'crm')) {
|
|
165
|
-
const plan = planLabelGrantingFeature('crm');
|
|
166
|
-
return json({
|
|
167
|
-
error: `Exporting ${resource} is part of the CRM, which is not included in ` + 'your current plan. Manage your plan and add-ons from Billing.' + (plan ? ` Included from ${plan}.` : ''),
|
|
168
|
-
reason: 'plan_required',
|
|
169
|
-
code: 'crm'
|
|
170
|
-
}, 403);
|
|
171
|
-
}
|
|
172
|
-
const orgWide = staff || isOrgWideMember(member);
|
|
173
|
-
// `array-contains-any` takes at most thirty values, which is also the
|
|
174
|
-
// widest a consent group can be — a reader past that is the org-wide
|
|
175
|
-
// case and never reaches here.
|
|
176
|
-
const tokens = memberScopeTokens(member).slice(0, MAX_SCOPE_HOSTS);
|
|
177
|
-
if (!orgWide && !tokens.length) return json({
|
|
178
|
-
error: 'Not found'
|
|
179
|
-
}, 404);
|
|
180
|
-
/* The roster, once, so every owner and assignee cell is an address. */ const ownerEmails = new Map();
|
|
181
|
-
for (const entry of (await orgRef.collection('members').get()).docs){
|
|
182
|
-
var _ref3;
|
|
183
|
-
var _entry_data;
|
|
184
|
-
const email = String((_ref3 = (_entry_data = entry.data()) == null ? void 0 : _entry_data['email']) != null ? _ref3 : '');
|
|
185
|
-
if (email) ownerEmails.set(entry.id, email);
|
|
186
|
-
}
|
|
187
|
-
const ownerEmail = (uid)=>{
|
|
188
|
-
var _ownerEmails_get;
|
|
189
|
-
return (_ownerEmails_get = ownerEmails.get(uid)) != null ? _ownerEmails_get : uid;
|
|
190
|
-
};
|
|
191
|
-
const options = {
|
|
192
|
-
ownerEmail,
|
|
193
|
-
assigneeEmail: ownerEmail
|
|
194
|
-
};
|
|
195
|
-
if (resource === 'contacts') {
|
|
196
|
-
/*
|
|
197
|
-
* A contact's fields live in one holder's FACET, so the file has to
|
|
198
|
-
* say which. Under a site that is the site's own group, for every
|
|
199
|
-
* row; at the organization level it is each row's PRIMARY holder —
|
|
200
|
-
* the same answer the org-level Contacts list flattens by, so the
|
|
201
|
-
* file and the table read one profile per person. What a file must
|
|
202
|
-
* never do is read the top of the document, which is where the
|
|
203
|
-
* pre-facet migration left one holder's fields.
|
|
204
|
-
*/ if (!hostId && !orgWide) return json({
|
|
205
|
-
error: 'Missing hostId'
|
|
206
|
-
}, 400);
|
|
207
|
-
const fields = await orgRef.collection(CRM_COLLECTIONS.contactFields).get();
|
|
208
|
-
options.customFields = fields.docs.map((entry)=>{
|
|
209
|
-
var _ref, _ref1;
|
|
210
|
-
var _entry_data, _entry_data1, _entry_data2;
|
|
211
|
-
return {
|
|
212
|
-
key: String((_ref = (_entry_data = entry.data()) == null ? void 0 : _entry_data['key']) != null ? _ref : entry.id),
|
|
213
|
-
label: String((_ref1 = (_entry_data1 = entry.data()) == null ? void 0 : _entry_data1['label']) != null ? _ref1 : entry.id),
|
|
214
|
-
archivedAt: (_entry_data2 = entry.data()) == null ? void 0 : _entry_data2['archivedAt']
|
|
215
|
-
};
|
|
216
|
-
}).filter((field)=>!field.archivedAt).map(({ key, label })=>({
|
|
217
|
-
key,
|
|
218
|
-
label
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
if (resource === 'deals') {
|
|
222
|
-
const pipelines = await orgRef.collection(CRM_COLLECTIONS.pipelines).get();
|
|
223
|
-
const names = new Map();
|
|
224
|
-
const stages = new Map();
|
|
225
|
-
for (const entry of pipelines.docs){
|
|
226
|
-
var _entry_data1, _data_name, _data_stages;
|
|
227
|
-
const data = (_entry_data1 = entry.data()) != null ? _entry_data1 : {};
|
|
228
|
-
names.set(entry.id, String((_data_name = data['name']) != null ? _data_name : entry.id));
|
|
229
|
-
for (const stage of (_data_stages = data['stages']) != null ? _data_stages : []){
|
|
230
|
-
var _ref4;
|
|
231
|
-
const stageId = String((_ref4 = stage == null ? void 0 : stage['id']) != null ? _ref4 : '');
|
|
232
|
-
if (stageId) {
|
|
233
|
-
var _ref5;
|
|
234
|
-
stages.set(`${entry.id}/${stageId}`, String((_ref5 = stage == null ? void 0 : stage['name']) != null ? _ref5 : stageId));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
options.pipelineName = (id)=>names.get(id);
|
|
239
|
-
options.stageName = (pipelineId, stageId)=>stages.get(`${pipelineId}/${stageId}`);
|
|
240
|
-
}
|
|
241
|
-
/*
|
|
242
|
-
* A task names its contact, company and deal by ID, and a spreadsheet
|
|
243
|
-
* cannot read one. The list resolves them from rows it already holds;
|
|
244
|
-
* the server looks them up as it meets them — batched per page,
|
|
245
|
-
* remembered, and capped, so a tasks file costs reads in proportion to
|
|
246
|
-
* the records it actually references rather than to the org. Past the
|
|
247
|
-
* cap the id is written, which is the documented degradation everywhere
|
|
248
|
-
* else in these files.
|
|
249
|
-
*/ const recordNames = new Map();
|
|
250
|
-
const resolveNames = async (wanted)=>{
|
|
251
|
-
const missing = wanted.filter((entry)=>entry.id && !recordNames.has(`${entry.kind}:${entry.id}`) && recordNames.size < NAME_CACHE_CEILING);
|
|
252
|
-
if (!missing.length) return;
|
|
253
|
-
const refs = missing.map((entry)=>orgRef.collection(entry.kind === 'contact' ? 'contacts' : entry.kind === 'company' ? CRM_COLLECTIONS.companies : CRM_COLLECTIONS.deals).doc(entry.id));
|
|
254
|
-
const snapshots = await firestore.getAll(...refs);
|
|
255
|
-
snapshots.forEach((snapshot, index)=>{
|
|
256
|
-
var _snapshot_data, _data_title, _ref, _data_name;
|
|
257
|
-
const entry = missing[index];
|
|
258
|
-
const data = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
259
|
-
const name = entry.kind === 'deal' ? String((_data_title = data['title']) != null ? _data_title : '') : String((_ref = (_data_name = data['name']) != null ? _data_name : data['email']) != null ? _ref : '');
|
|
260
|
-
recordNames.set(`${entry.kind}:${entry.id}`, name || entry.id);
|
|
261
|
-
});
|
|
262
|
-
};
|
|
263
|
-
if (resource === 'tasks') {
|
|
264
|
-
options.recordName = (kind, id)=>recordNames.get(`${kind}:${id}`);
|
|
265
|
-
}
|
|
266
|
-
/*
|
|
267
|
-
* LEADS ARE HOST DATA BY PATH. Under one site the file is that site's;
|
|
268
|
-
* with no site named, an org-wide member takes every site's leads in one
|
|
269
|
-
* file with a `Site` column — the same file the organization-level Leads
|
|
270
|
-
* list writes. A scoped collaborator must name a site they reach.
|
|
271
|
-
*/ const siteNames = new Map();
|
|
272
|
-
let leadHostIds = [];
|
|
273
|
-
if (resource === 'leads') {
|
|
274
|
-
if (hostId) {
|
|
275
|
-
if (!orgWide && !memberCanSee(member, [
|
|
276
|
-
hostScopeToken(hostId)
|
|
277
|
-
])) {
|
|
278
|
-
return json({
|
|
279
|
-
error: 'Not found'
|
|
280
|
-
}, 404);
|
|
281
|
-
}
|
|
282
|
-
leadHostIds = [
|
|
283
|
-
hostId
|
|
284
|
-
];
|
|
285
|
-
} else {
|
|
286
|
-
if (!orgWide) return json({
|
|
287
|
-
error: 'Missing hostId'
|
|
288
|
-
}, 400);
|
|
289
|
-
let cursor = null;
|
|
290
|
-
for(;;){
|
|
291
|
-
let page = firestore.collection('hosts').where('orgId', '==', orgId).orderBy(FieldPath.documentId()).limit(HOST_PAGE);
|
|
292
|
-
if (cursor) page = page.startAfter(firestore.collection('hosts').doc(cursor));
|
|
293
|
-
const snapshot = await page.get();
|
|
294
|
-
for (const entry of snapshot.docs){
|
|
295
|
-
var _ref6, _ref7;
|
|
296
|
-
var _entry_data2, _entry_data3;
|
|
297
|
-
leadHostIds.push(entry.id);
|
|
298
|
-
siteNames.set(entry.id, String((_ref6 = (_ref7 = (_entry_data2 = entry.data()) == null ? void 0 : _entry_data2['displayName']) != null ? _ref7 : (_entry_data3 = entry.data()) == null ? void 0 : _entry_data3['subdomain']) != null ? _ref6 : entry.id));
|
|
299
|
-
}
|
|
300
|
-
if (snapshot.docs.length < HOST_PAGE) break;
|
|
301
|
-
if (leadHostIds.length >= HOST_CEILING) break;
|
|
302
|
-
cursor = snapshot.docs[snapshot.docs.length - 1].id;
|
|
303
|
-
}
|
|
304
|
-
options.siteName = (id)=>siteNames.get(id);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
/** One resource's rows, as a query per collection the file spans. */ const sources = resource === 'leads' ? leadHostIds.map((id)=>({
|
|
308
|
-
hostId: id,
|
|
309
|
-
/*
|
|
310
|
-
* The org collection, narrowed per SITE (AGL-3275).
|
|
311
|
-
*
|
|
312
|
-
* One source per site is still right — the file names the site a
|
|
313
|
-
* row came from — but a site's leads are selected by `visibleTo`
|
|
314
|
-
* now rather than by the collection's parent, so the narrowing
|
|
315
|
-
* moves onto the reference itself. It cannot use the `scoped`
|
|
316
|
-
* flag below: that applies the MEMBER's tokens, and here each
|
|
317
|
-
* source needs the tokens of its own site.
|
|
318
|
-
*/ reference: orgRef.collection('leads').where('visibleTo', 'array-contains-any', scopeTokensForHost(id)),
|
|
319
|
-
scoped: false
|
|
320
|
-
})) : [
|
|
321
|
-
{
|
|
322
|
-
hostId: '',
|
|
323
|
-
reference: orgRef.collection(ORG_COLLECTION[resource]),
|
|
324
|
-
scoped: !orgWide
|
|
325
|
-
}
|
|
326
|
-
];
|
|
327
|
-
// Taken BEFORE the first page, so it describes the collections the
|
|
328
|
-
// export started from. A `count()` aggregate bills one read per 1,000
|
|
329
|
-
// documents — cheap next to the export it is describing, and the only
|
|
330
|
-
// way the client can tell a complete file from a truncated one.
|
|
331
|
-
let total = 0;
|
|
332
|
-
for (const source of sources){
|
|
333
|
-
var _data_count;
|
|
334
|
-
const counted = source.scoped ? source.reference.where('visibleTo', 'array-contains-any', tokens) : source.reference;
|
|
335
|
-
total += Number((_data_count = (await counted.count().get()).data().count) != null ? _data_count : 0);
|
|
336
|
-
}
|
|
337
|
-
/** The holder one contact is written through, resolved per level. */ const holderFor = (document)=>{
|
|
338
|
-
const group = hostId ? consentGroupForHost(org, hostId) : contactPrimaryGroup(document, org);
|
|
339
|
-
return {
|
|
340
|
-
groupId: group.groupId,
|
|
341
|
-
hostIds: group.hostIds
|
|
342
|
-
};
|
|
343
|
-
};
|
|
344
|
-
const encoder = new TextEncoder();
|
|
345
|
-
const encode = (text)=>encoder.encode(text);
|
|
346
|
-
const line = (cells)=>cells.map(csvCell).join(',');
|
|
347
|
-
let sourceIndex = 0;
|
|
348
|
-
let cursor = null;
|
|
349
|
-
let phase = 'head';
|
|
350
|
-
const stream = new ReadableStream({
|
|
351
|
-
async pull (controller) {
|
|
352
|
-
try {
|
|
353
|
-
if (phase === 'head') {
|
|
354
|
-
phase = sources.length ? 'body' : 'tail';
|
|
355
|
-
controller.enqueue(encode(line(crmExportHeader(resource, options))));
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
if (phase === 'body') {
|
|
359
|
-
var _snapshot_docs_;
|
|
360
|
-
const source = sources[sourceIndex];
|
|
361
|
-
let page = source.reference;
|
|
362
|
-
if (source.scoped) {
|
|
363
|
-
page = page.where('visibleTo', 'array-contains-any', tokens);
|
|
364
|
-
}
|
|
365
|
-
page = page.orderBy(FieldPath.documentId()).limit(PAGE_SIZE);
|
|
366
|
-
if (cursor) page = page.startAfter(cursor);
|
|
367
|
-
const snapshot = await page.get();
|
|
368
|
-
const exhausted = snapshot.docs.length < PAGE_SIZE;
|
|
369
|
-
cursor = exhausted ? null : (_snapshot_docs_ = snapshot.docs[snapshot.docs.length - 1]) != null ? _snapshot_docs_ : null;
|
|
370
|
-
if (exhausted) {
|
|
371
|
-
sourceIndex += 1;
|
|
372
|
-
if (sourceIndex >= sources.length) phase = 'tail';
|
|
373
|
-
}
|
|
374
|
-
if (!snapshot.empty) {
|
|
375
|
-
const documents = snapshot.docs.map((entry)=>{
|
|
376
|
-
var _entry_data;
|
|
377
|
-
return _extends({}, (_entry_data = entry.data()) != null ? _entry_data : {}, {
|
|
378
|
-
$id: entry.id
|
|
379
|
-
}, source.hostId ? {
|
|
380
|
-
hostId: source.hostId
|
|
381
|
-
} : {});
|
|
382
|
-
});
|
|
383
|
-
if (resource === 'tasks') {
|
|
384
|
-
await resolveNames(documents.flatMap((document)=>[
|
|
385
|
-
'contact',
|
|
386
|
-
'company',
|
|
387
|
-
'deal'
|
|
388
|
-
].map((kind)=>{
|
|
389
|
-
var _document_;
|
|
390
|
-
return {
|
|
391
|
-
kind,
|
|
392
|
-
id: String((_document_ = document[kind === 'contact' ? 'contactId' : kind === 'company' ? 'companyId' : 'dealId']) != null ? _document_ : '')
|
|
393
|
-
};
|
|
394
|
-
})));
|
|
395
|
-
}
|
|
396
|
-
const lines = documents.map((document)=>line(crmExportCells(resource, resource === 'contacts' ? contactCsvRowFromDoc(document, holderFor(document)) : document, options)));
|
|
397
|
-
controller.enqueue(encode(`\n${lines.join('\n')}`));
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
if (phase === 'body') return;
|
|
401
|
-
}
|
|
402
|
-
controller.enqueue(encode('\n'));
|
|
403
|
-
controller.close();
|
|
404
|
-
} catch (error) {
|
|
405
|
-
// Erroring the stream truncates the download rather than
|
|
406
|
-
// completing it short — a half-written file the browser reports
|
|
407
|
-
// as a failed transfer, not a quiet partial handover.
|
|
408
|
-
controller.error(error);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
// Ids and counts only, never content: a full copy of an audience
|
|
413
|
-
// leaving the platform is worth a row; who was in it is not ours to log.
|
|
414
|
-
void addAdminAudit(firestore, {
|
|
415
|
-
actorUid: decoded.uid,
|
|
416
|
-
action: 'crm.exported',
|
|
417
|
-
target: `orgs/${orgId}/${resource}`,
|
|
418
|
-
before: null,
|
|
419
|
-
after: {
|
|
420
|
-
resource,
|
|
421
|
-
rows: total,
|
|
422
|
-
hostId: hostId || null
|
|
423
|
-
},
|
|
424
|
-
at: FieldValue.serverTimestamp()
|
|
425
|
-
}).catch(()=>undefined);
|
|
426
|
-
const stamp = new Date().toISOString().slice(0, 10);
|
|
427
|
-
return new Response(stream, {
|
|
428
|
-
status: 200,
|
|
429
|
-
headers: {
|
|
430
|
-
'Content-Type': 'text/csv; charset=utf-8',
|
|
431
|
-
'Content-Disposition': `attachment; filename="${FILE_BASE[resource]}-${stamp}.csv"`,
|
|
432
|
-
[EXPORT_ROWS_HEADER]: String(total),
|
|
433
|
-
'Cache-Control': 'no-store, private'
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
} catch (error) {
|
|
437
|
-
// A refused credential is a 401, not a fault of ours (AGL-1993). Null
|
|
438
|
-
// for anything else, so a real failure keeps the answer below.
|
|
439
|
-
const unauthenticated = invalidIdTokenResponse(error);
|
|
440
|
-
if (unauthenticated) return unauthenticated;
|
|
441
|
-
return json({
|
|
442
|
-
error: 'Export failed'
|
|
443
|
-
}, 500);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
//# sourceMappingURL=export-route.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/crm/src/lib/server/export-route.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n checkEntitlement,\n consentGroupForHost,\n CRM_COLLECTIONS,\n csvCell,\n hostScopeToken,\n isOrgWideMember,\n MAX_SCOPE_HOSTS,\n memberCanSee,\n memberScopeTokens,\n scopeTokensForHost,\n planLabelGrantingFeature,\n pluginRequestFromWeb,\n} from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n isServerReleaseFlagOnForOrg,\n lockdownRefusal,\n memberHasOrgPermission,\n resolveOrgMembership,\n} from '@aglyn/tenant-data-admin'\nimport { FieldPath, FieldValue } from 'firebase-admin/firestore'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n contactCsvRowFromDoc,\n crmExportCells,\n crmExportHeader,\n type CrmExportOptions,\n type CrmExportResource,\n isCrmExportResource,\n} from '../model/crm-csv'\nimport { contactPrimaryGroup } from '../model/contact-holder'\n\n/**\n * Documents read per round trip. Not a cap — the stream keeps paging until\n * the collection is exhausted. It bounds what is held at once, which is the\n * only thing a page size should ever bound.\n */\nconst PAGE_SIZE = 500\n\n/** Sites read per page when the leads file spans an organization. */\nconst HOST_PAGE = 100\n\n/**\n * The most sites one organization-level leads file reads from.\n *\n * A ceiling rather than an unbounded sweep, and it is REPORTED: the file's\n * row header counts only what was read, so a short file is provably short\n * rather than quietly partial.\n */\nconst HOST_CEILING = 200\n\n/** The most linked records a tasks file resolves names for before writing ids. */\nconst NAME_CACHE_CEILING = 2000\n\n/** Header naming the row count the server undertook to send. */\nexport const EXPORT_ROWS_HEADER = 'X-Aglyn-Export-Rows'\n\nconst json = (body: unknown, status: number) => Response.json(body, { status })\n\n/** Which collection under the org root each resource is, bar leads. */\nconst ORG_COLLECTION: Record<Exclude<CrmExportResource, 'leads'>, string> = {\n contacts: 'contacts',\n companies: CRM_COLLECTIONS.companies,\n deals: CRM_COLLECTIONS.deals,\n tasks: CRM_COLLECTIONS.tasks,\n}\n\n/** The file's name, before the date stamp. */\nconst FILE_BASE: Record<CrmExportResource, string> = {\n contacts: 'contacts',\n companies: 'companies',\n deals: 'deals',\n tasks: 'tasks',\n leads: 'leads',\n}\n\n/**\n * THE WHOLE COLLECTION, STREAMED (AGL-2662).\n *\n * ## What this fixes\n *\n * Every CRM export until now wrote the rows the LIST had loaded — a\n * `limit(1000)` window for contacts, a `limit(200)` view for tasks — and\n * called the file `contacts.csv`. A merchant with 40,000 contacts got 1,000\n * of them and nothing on screen or in the file said so. `docs/bulk-actions.md`\n * said it in prose, which is not where somebody opening a spreadsheet looks.\n * This is the same file over every row there is, and it carries\n * `X-Aglyn-Export-Rows` so the client can prove what it received is whole —\n * a stream that dies halfway yields a perfectly well-formed shorter file, and\n * nothing about the bytes says they are short.\n *\n * ## Why it is a route and not a bigger client read\n *\n * A browser cannot hold an unbounded collection, and the tier that most\n * needs a complete export is the tier with the most rows. Cursor-paged and\n * streamed, completeness costs neither a hang nor a buffer.\n *\n * ## The columns are the client's columns\n *\n * `crmExportHeader` and `crmExportCells` (`model/crm-csv.ts`) are the\n * writers each section's Export button uses. One feature, one file format.\n *\n * ## The order is by document id, deliberately\n *\n * Firestore DROPS every document missing the ordered field, so ordering by\n * `updatedAt` or `dueAtMs` would silently omit rows written before that\n * field existed — the exact silent loss an export must not have. A document\n * id is on every document and is a total order.\n *\n * ## Scope is enforced here, because nothing else can\n *\n * The Admin SDK bypasses the rules, so `memberScopeTokens` IS the\n * enforcement: an org-wide member reads every row, and a scoped\n * collaborator's query carries their own tokens as the `array-contains-any`\n * the rules would have evaluated. Leads are host data by path, so the\n * equivalent check is whether the caller reaches the site.\n */\nexport async function crmExportRoute(request: Request): Promise<Response> {\n const { method, query, headers: rawHeaders } =\n await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n if (method !== 'GET') return json({ error: 'Method not allowed' }, 405)\n\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n\n const orgId = String(query?.['orgId'] ?? '')\n const resource = String(query?.['resource'] ?? '')\n const hostId = String(query?.['hostId'] ?? '')\n if (!orgId || !isCrmExportResource(resource)) {\n return json({ error: 'Missing orgId or resource' }, 400)\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n return emailUnverifiedResponse()\n }\n const staff = decoded['staff'] === true\n const membership = await resolveOrgMembership(decoded.uid, orgId)\n const member = membership?.member\n if (!member && !staff) return json({ error: 'Not found' }, 404)\n\n /*\n * THE PEOPLE FILES ARE EVERY WORKSPACE'S (AGL-2839, AGL-2851).\n *\n * Exporting the contacts and leads a workspace holds is an obligation,\n * not a CRM feature — a Free workspace takes them from its Privacy\n * settings — so neither the release flag nor the plan is asked of those\n * two. The CRM's own records, companies, deals and tasks, are asked\n * both: the flag here, and the plan once the org has been read.\n */\n const peopleFile = resource === 'contacts' || resource === 'leads'\n if (!peopleFile && !staff && !(await isServerReleaseFlagOnForOrg('release_crm', orgId))) {\n return json({ error: 'Not available' }, 404)\n }\n // The permission the CRM's own rules read for. Reading is not writing,\n // but these records ARE the audience: `data.manage` is what admits a\n // person to the list this file copies.\n if (!staff && !(await memberHasOrgPermission(orgId, member, 'data.manage'))) {\n return json({ error: 'Not found' }, 404)\n }\n\n const firestore = firebaseAdmin.app().firestore()\n const orgRef = firestore.collection('orgs').doc(orgId)\n const orgSnapshot = await orgRef.get()\n if (!orgSnapshot.exists) return json({ error: 'Unknown organization' }, 404)\n const org = orgSnapshot.data()\n\n const locked = await lockdownRefusal({\n request,\n staff,\n uid: decoded.uid,\n org,\n })\n if (locked) return locked\n\n // The CRM's own records on a plan without the CRM, staff included: the\n // refusal every `crm/*` route gives, after the caller is known.\n if (!peopleFile && !checkEntitlement(org as never, 'crm')) {\n const plan = planLabelGrantingFeature('crm')\n return json(\n {\n error:\n `Exporting ${resource} is part of the CRM, which is not included in ` +\n 'your current plan. Manage your plan and add-ons from Billing.' +\n (plan ? ` Included from ${plan}.` : ''),\n reason: 'plan_required',\n code: 'crm',\n },\n 403,\n )\n }\n\n const orgWide = staff || isOrgWideMember(member)\n // `array-contains-any` takes at most thirty values, which is also the\n // widest a consent group can be — a reader past that is the org-wide\n // case and never reaches here.\n const tokens = memberScopeTokens(member).slice(0, MAX_SCOPE_HOSTS)\n if (!orgWide && !tokens.length) return json({ error: 'Not found' }, 404)\n\n /* The roster, once, so every owner and assignee cell is an address. */\n const ownerEmails = new Map<string, string>()\n for (const entry of (await orgRef.collection('members').get()).docs) {\n const email = String(entry.data()?.['email'] ?? '')\n if (email) ownerEmails.set(entry.id, email)\n }\n const ownerEmail = (uid: string) => ownerEmails.get(uid) ?? uid\n\n const options: CrmExportOptions = { ownerEmail, assigneeEmail: ownerEmail }\n\n if (resource === 'contacts') {\n /*\n * A contact's fields live in one holder's FACET, so the file has to\n * say which. Under a site that is the site's own group, for every\n * row; at the organization level it is each row's PRIMARY holder —\n * the same answer the org-level Contacts list flattens by, so the\n * file and the table read one profile per person. What a file must\n * never do is read the top of the document, which is where the\n * pre-facet migration left one holder's fields.\n */\n if (!hostId && !orgWide) return json({ error: 'Missing hostId' }, 400)\n const fields = await orgRef.collection(CRM_COLLECTIONS.contactFields).get()\n options.customFields = fields.docs\n .map((entry) => ({\n key: String(entry.data()?.['key'] ?? entry.id),\n label: String(entry.data()?.['label'] ?? entry.id),\n archivedAt: entry.data()?.['archivedAt'],\n }))\n .filter((field) => !field.archivedAt)\n .map(({ key, label }) => ({ key, label }))\n }\n\n if (resource === 'deals') {\n const pipelines = await orgRef.collection(CRM_COLLECTIONS.pipelines).get()\n const names = new Map<string, string>()\n const stages = new Map<string, string>()\n for (const entry of pipelines.docs) {\n const data = entry.data() ?? {}\n names.set(entry.id, String(data['name'] ?? entry.id))\n for (const stage of (data['stages'] ?? []) as Array<Record<string, unknown>>) {\n const stageId = String(stage?.['id'] ?? '')\n if (stageId) {\n stages.set(`${entry.id}/${stageId}`, String(stage?.['name'] ?? stageId))\n }\n }\n }\n options.pipelineName = (id) => names.get(id)\n options.stageName = (pipelineId, stageId) =>\n stages.get(`${pipelineId}/${stageId}`)\n }\n\n /*\n * A task names its contact, company and deal by ID, and a spreadsheet\n * cannot read one. The list resolves them from rows it already holds;\n * the server looks them up as it meets them — batched per page,\n * remembered, and capped, so a tasks file costs reads in proportion to\n * the records it actually references rather than to the org. Past the\n * cap the id is written, which is the documented degradation everywhere\n * else in these files.\n */\n const recordNames = new Map<string, string>()\n const resolveNames = async (\n wanted: ReadonlyArray<{ kind: 'contact' | 'company' | 'deal'; id: string }>,\n ) => {\n const missing = wanted.filter(\n (entry) =>\n entry.id &&\n !recordNames.has(`${entry.kind}:${entry.id}`) &&\n recordNames.size < NAME_CACHE_CEILING,\n )\n if (!missing.length) return\n const refs = missing.map((entry) =>\n orgRef\n .collection(\n entry.kind === 'contact'\n ? 'contacts'\n : entry.kind === 'company'\n ? CRM_COLLECTIONS.companies\n : CRM_COLLECTIONS.deals,\n )\n .doc(entry.id),\n )\n const snapshots = await firestore.getAll(...refs)\n snapshots.forEach((snapshot, index) => {\n const entry = missing[index]\n const data = snapshot.data() ?? {}\n const name =\n entry.kind === 'deal'\n ? String(data['title'] ?? '')\n : String(data['name'] ?? data['email'] ?? '')\n recordNames.set(`${entry.kind}:${entry.id}`, name || entry.id)\n })\n }\n if (resource === 'tasks') {\n options.recordName = (kind, id) => recordNames.get(`${kind}:${id}`)\n }\n\n /*\n * LEADS ARE HOST DATA BY PATH. Under one site the file is that site's;\n * with no site named, an org-wide member takes every site's leads in one\n * file with a `Site` column — the same file the organization-level Leads\n * list writes. A scoped collaborator must name a site they reach.\n */\n const siteNames = new Map<string, string>()\n let leadHostIds: string[] = []\n if (resource === 'leads') {\n if (hostId) {\n if (!orgWide && !memberCanSee(member, [hostScopeToken(hostId)])) {\n return json({ error: 'Not found' }, 404)\n }\n leadHostIds = [hostId]\n } else {\n if (!orgWide) return json({ error: 'Missing hostId' }, 400)\n let cursor: string | null = null\n for (;;) {\n let page = firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .orderBy(FieldPath.documentId())\n .limit(HOST_PAGE)\n if (cursor) page = page.startAfter(firestore.collection('hosts').doc(cursor))\n const snapshot = await page.get()\n for (const entry of snapshot.docs) {\n leadHostIds.push(entry.id)\n siteNames.set(\n entry.id,\n String(\n entry.data()?.['displayName'] ??\n entry.data()?.['subdomain'] ??\n entry.id,\n ),\n )\n }\n if (snapshot.docs.length < HOST_PAGE) break\n if (leadHostIds.length >= HOST_CEILING) break\n cursor = snapshot.docs[snapshot.docs.length - 1].id\n }\n options.siteName = (id) => siteNames.get(id)\n }\n }\n\n /** One resource's rows, as a query per collection the file spans. */\n const sources =\n resource === 'leads'\n ? leadHostIds.map((id) => ({\n hostId: id,\n /*\n * The org collection, narrowed per SITE (AGL-3275).\n *\n * One source per site is still right — the file names the site a\n * row came from — but a site's leads are selected by `visibleTo`\n * now rather than by the collection's parent, so the narrowing\n * moves onto the reference itself. It cannot use the `scoped`\n * flag below: that applies the MEMBER's tokens, and here each\n * source needs the tokens of its own site.\n */\n reference: orgRef\n .collection('leads')\n .where('visibleTo', 'array-contains-any', scopeTokensForHost(id)),\n scoped: false,\n }))\n : [\n {\n hostId: '',\n reference: orgRef.collection(\n ORG_COLLECTION[resource as Exclude<CrmExportResource, 'leads'>],\n ),\n scoped: !orgWide,\n },\n ]\n\n // Taken BEFORE the first page, so it describes the collections the\n // export started from. A `count()` aggregate bills one read per 1,000\n // documents — cheap next to the export it is describing, and the only\n // way the client can tell a complete file from a truncated one.\n let total = 0\n for (const source of sources) {\n const counted = source.scoped\n ? source.reference.where('visibleTo', 'array-contains-any', tokens)\n : source.reference\n total += Number((await counted.count().get()).data().count ?? 0)\n }\n\n /** The holder one contact is written through, resolved per level. */\n const holderFor = (document: Record<string, unknown>) => {\n const group = hostId\n ? consentGroupForHost(org as Record<string, unknown>, hostId)\n : contactPrimaryGroup(document, org as Record<string, unknown>)\n return { groupId: group.groupId, hostIds: group.hostIds }\n }\n\n const encoder = new TextEncoder()\n const encode = (text: string) => encoder.encode(text)\n const line = (cells: readonly unknown[]) => cells.map(csvCell).join(',')\n let sourceIndex = 0\n let cursor: FirebaseFirestore.QueryDocumentSnapshot | null = null\n let phase: 'head' | 'body' | 'tail' = 'head'\n\n const stream = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n if (phase === 'head') {\n phase = sources.length ? 'body' : 'tail'\n controller.enqueue(encode(line(crmExportHeader(resource, options))))\n return\n }\n if (phase === 'body') {\n const source = sources[sourceIndex]\n let page: FirebaseFirestore.Query = source.reference\n if (source.scoped) {\n page = page.where('visibleTo', 'array-contains-any', tokens)\n }\n page = page.orderBy(FieldPath.documentId()).limit(PAGE_SIZE)\n if (cursor) page = page.startAfter(cursor)\n const snapshot = await page.get()\n const exhausted = snapshot.docs.length < PAGE_SIZE\n cursor = exhausted\n ? null\n : (snapshot.docs[snapshot.docs.length - 1] ?? null)\n if (exhausted) {\n sourceIndex += 1\n if (sourceIndex >= sources.length) phase = 'tail'\n }\n if (!snapshot.empty) {\n const documents = snapshot.docs.map((entry) => ({\n ...(entry.data() ?? {}),\n $id: entry.id,\n ...(source.hostId ? { hostId: source.hostId } : {}),\n }))\n if (resource === 'tasks') {\n await resolveNames(\n documents.flatMap((document) =>\n (['contact', 'company', 'deal'] as const).map((kind) => ({\n kind,\n id: String(\n document[\n kind === 'contact'\n ? 'contactId'\n : kind === 'company'\n ? 'companyId'\n : 'dealId'\n ] ?? '',\n ),\n })),\n ),\n )\n }\n const lines = documents.map((document) =>\n line(\n crmExportCells(\n resource,\n resource === 'contacts'\n ? (contactCsvRowFromDoc(\n document,\n holderFor(document),\n ) as Record<string, unknown>)\n : document,\n options,\n ),\n ),\n )\n controller.enqueue(encode(`\\n${lines.join('\\n')}`))\n return\n }\n if (phase === 'body') return\n }\n controller.enqueue(encode('\\n'))\n controller.close()\n } catch (error) {\n // Erroring the stream truncates the download rather than\n // completing it short — a half-written file the browser reports\n // as a failed transfer, not a quiet partial handover.\n controller.error(error)\n }\n },\n })\n\n // Ids and counts only, never content: a full copy of an audience\n // leaving the platform is worth a row; who was in it is not ours to log.\n void addAdminAudit(firestore, {\n actorUid: decoded.uid,\n action: 'crm.exported',\n target: `orgs/${orgId}/${resource}`,\n before: null,\n after: { resource, rows: total, hostId: hostId || null },\n at: FieldValue.serverTimestamp(),\n })\n .catch(() => undefined)\n\n const stamp = new Date().toISOString().slice(0, 10)\n return new Response(stream, {\n status: 200,\n headers: {\n 'Content-Type': 'text/csv; charset=utf-8',\n 'Content-Disposition': `attachment; filename=\"${FILE_BASE[resource]}-${stamp}.csv\"`,\n [EXPORT_ROWS_HEADER]: String(total),\n 'Cache-Control': 'no-store, private',\n },\n })\n } catch (error) {\n // A refused credential is a 401, not a fault of ours (AGL-1993). Null\n // for anything else, so a real failure keeps the answer below.\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n return json({ error: 'Export failed' }, 500)\n }\n}\n\n"],"names":["checkEntitlement","consentGroupForHost","CRM_COLLECTIONS","csvCell","hostScopeToken","isOrgWideMember","MAX_SCOPE_HOSTS","memberCanSee","memberScopeTokens","scopeTokensForHost","planLabelGrantingFeature","pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","isServerReleaseFlagOnForOrg","lockdownRefusal","memberHasOrgPermission","resolveOrgMembership","FieldPath","FieldValue","addAdminAudit","invalidIdTokenResponse","contactCsvRowFromDoc","crmExportCells","crmExportHeader","isCrmExportResource","contactPrimaryGroup","PAGE_SIZE","HOST_PAGE","HOST_CEILING","NAME_CACHE_CEILING","EXPORT_ROWS_HEADER","json","body","status","Response","ORG_COLLECTION","contacts","companies","deals","tasks","FILE_BASE","leads","crmExportRoute","request","headers","method","query","rawHeaders","error","authorization","idToken","startsWith","slice","length","undefined","orgId","String","resource","hostId","decoded","app","auth","verifyIdToken","email_verified","staff","membership","uid","member","peopleFile","firestore","orgRef","collection","doc","orgSnapshot","get","exists","org","data","locked","plan","reason","code","orgWide","tokens","ownerEmails","Map","entry","docs","email","set","id","ownerEmail","options","assigneeEmail","fields","contactFields","customFields","map","key","label","archivedAt","filter","field","pipelines","names","stages","stage","stageId","pipelineName","stageName","pipelineId","recordNames","resolveNames","wanted","missing","has","kind","size","refs","snapshots","getAll","forEach","snapshot","index","name","recordName","siteNames","leadHostIds","cursor","page","where","orderBy","documentId","limit","startAfter","push","siteName","sources","reference","scoped","total","source","counted","Number","count","holderFor","document","group","groupId","hostIds","encoder","TextEncoder","encode","text","line","cells","join","sourceIndex","phase","stream","ReadableStream","pull","controller","enqueue","exhausted","empty","documents","$id","flatMap","lines","close","actorUid","action","target","before","after","rows","at","serverTimestamp","catch","stamp","Date","toISOString","unauthenticated"],"mappings":"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"}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Aglyn LLC
|
|
4
|
-
*
|
|
5
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
* you may not use this file except in compliance with the License.
|
|
7
|
-
* You may obtain a copy of the License at
|
|
8
|
-
*
|
|
9
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
*
|
|
11
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
* See the License for the specific language governing permissions and
|
|
15
|
-
* limitations under the License.
|
|
16
|
-
*/
|
|
17
|
-
/**
|
|
18
|
-
* WHAT EVERY CSV IMPORT ROUTE ASKS BEFORE IT READS A ROW (AGL-2602, shared
|
|
19
|
-
* by the companies import since AGL-2621).
|
|
20
|
-
*
|
|
21
|
-
* Who is asking and on whose behalf, whether the request is the size a
|
|
22
|
-
* chunk is allowed to be, and — when a row names an owner — which member
|
|
23
|
-
* of the org that address is. The contacts route settled each answer; the
|
|
24
|
-
* companies route needs the same three and must not spell them
|
|
25
|
-
* differently, because two gates for one permission is how one of them
|
|
26
|
-
* comes to be wider.
|
|
27
|
-
*
|
|
28
|
-
* ## Who may call an import
|
|
29
|
-
*
|
|
30
|
-
* The same two gates the rules put on every CRM collection: a site role
|
|
31
|
-
* on the host the request names, AND the organization's `data.manage`
|
|
32
|
-
* permission resolved through the member's custom role and overrides. The
|
|
33
|
-
* Admin SDK evaluates no rules, so this is the enforcement rather than an
|
|
34
|
-
* echo of it — and `data.manage` rather than the raw role because an owner
|
|
35
|
-
* who unticked "Manage data" on a custom role meant it. A staff token
|
|
36
|
-
* passes both gates the way it does on every console route: support
|
|
37
|
-
* importing a customer's file for them is the support act this exists for.
|
|
38
|
-
*/
|
|
39
|
-
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
40
|
-
/** Everything an import needs about who is asking, or the refusal to send back. */
|
|
41
|
-
export type ImportContext = {
|
|
42
|
-
ok: true;
|
|
43
|
-
uid: string;
|
|
44
|
-
/** The importer's address, for who set off the runs it causes (AGL-3376). */
|
|
45
|
-
email: string | null;
|
|
46
|
-
hostId: string;
|
|
47
|
-
orgId: string;
|
|
48
|
-
org: Record<string, unknown>;
|
|
49
|
-
/**
|
|
50
|
-
* The stamp a record CREATED by this import carries — the org, when
|
|
51
|
-
* the org widened its default, and otherwise the capturing group.
|
|
52
|
-
*/
|
|
53
|
-
scopeTokens: string[];
|
|
54
|
-
/**
|
|
55
|
-
* The tokens a record must overlap to be SEEN from this site: the
|
|
56
|
-
* group's own, plus `org`, because an org-wide record is visible to
|
|
57
|
-
* every site in the account. The reader's set is wider than the
|
|
58
|
-
* creator's stamp, exactly as it is on the collection listeners.
|
|
59
|
-
*/
|
|
60
|
-
readTokens: string[];
|
|
61
|
-
} | {
|
|
62
|
-
ok: false;
|
|
63
|
-
status: number;
|
|
64
|
-
body: Record<string, unknown>;
|
|
65
|
-
};
|
|
66
|
-
export type ImportRequest = Parameters<PluginApiHandler>[0];
|
|
67
|
-
/** Who is asking, and on whose behalf. */
|
|
68
|
-
export declare function resolveImportContext(req: ImportRequest): Promise<ImportContext>;
|
|
69
|
-
/** The rows a request may carry, or the refusal to send back. */
|
|
70
|
-
export declare function readImportRows<R>(req: ImportRequest, limits: {
|
|
71
|
-
maxBodyBytes: number;
|
|
72
|
-
chunkSize: number;
|
|
73
|
-
}): {
|
|
74
|
-
rows: R[];
|
|
75
|
-
} | {
|
|
76
|
-
status: number;
|
|
77
|
-
error: string;
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* The org's members by address, read only when a row names an owner.
|
|
81
|
-
*
|
|
82
|
-
* A file with no owner column costs no roster read; a file with one costs
|
|
83
|
-
* exactly one, however many rows it has.
|
|
84
|
-
*/
|
|
85
|
-
export declare function ownerDirectory(orgId: string, rows: readonly {
|
|
86
|
-
ownerEmail?: string;
|
|
87
|
-
}[]): Promise<Map<string, string>>;
|