@kelpie/server 0.5.2 → 0.7.0
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 +5 -2
- package/dist/app.d.ts +13 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +9 -0
- package/dist/app.js.map +1 -1
- package/dist/boot.d.ts +42 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +57 -0
- package/dist/boot.js.map +1 -0
- package/dist/index.d.ts +20 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/actor.d.ts +9 -1
- package/dist/lib/actor.d.ts.map +1 -1
- package/dist/lib/actor.js +13 -0
- package/dist/lib/actor.js.map +1 -1
- package/dist/lib/config.d.ts +51 -6
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +11 -5
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/email.d.ts +15 -63
- package/dist/lib/email.d.ts.map +1 -1
- package/dist/lib/email.js +13 -65
- package/dist/lib/email.js.map +1 -1
- package/dist/lib/fromEnv.d.ts +60 -0
- package/dist/lib/fromEnv.d.ts.map +1 -0
- package/dist/lib/fromEnv.js +94 -0
- package/dist/lib/fromEnv.js.map +1 -0
- package/dist/lib/ids.d.ts +6 -0
- package/dist/lib/ids.d.ts.map +1 -1
- package/dist/lib/ids.js +6 -0
- package/dist/lib/ids.js.map +1 -1
- package/dist/lib/kelpieConfigFile.d.ts +129 -0
- package/dist/lib/kelpieConfigFile.d.ts.map +1 -0
- package/dist/lib/kelpieConfigFile.js +112 -0
- package/dist/lib/kelpieConfigFile.js.map +1 -0
- package/dist/lib/logger.d.ts +38 -7
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +58 -20
- package/dist/lib/logger.js.map +1 -1
- package/dist/modules/agent-tasks/index.d.ts.map +1 -1
- package/dist/modules/agent-tasks/index.js +4 -1
- package/dist/modules/agent-tasks/index.js.map +1 -1
- package/dist/modules/agent-tasks/prompt.d.ts +36 -6
- package/dist/modules/agent-tasks/prompt.d.ts.map +1 -1
- package/dist/modules/agent-tasks/prompt.js +33 -4
- package/dist/modules/agent-tasks/prompt.js.map +1 -1
- package/dist/modules/agent-tasks/service.d.ts.map +1 -1
- package/dist/modules/agent-tasks/service.js +3 -2
- package/dist/modules/agent-tasks/service.js.map +1 -1
- package/dist/modules/agent-tasks/wire.d.ts +11 -0
- package/dist/modules/agent-tasks/wire.d.ts.map +1 -1
- package/dist/modules/agent-tasks/wire.js +1 -0
- package/dist/modules/agent-tasks/wire.js.map +1 -1
- package/dist/modules/auth/index.d.ts.map +1 -1
- package/dist/modules/auth/index.js +8 -7
- package/dist/modules/auth/index.js.map +1 -1
- package/dist/modules/companies/events.d.ts +22 -0
- package/dist/modules/companies/events.d.ts.map +1 -0
- package/dist/modules/companies/events.js +8 -0
- package/dist/modules/companies/events.js.map +1 -0
- package/dist/modules/companies/index.d.ts.map +1 -1
- package/dist/modules/companies/index.js +2 -0
- package/dist/modules/companies/index.js.map +1 -1
- package/dist/modules/companies/service.d.ts +1 -0
- package/dist/modules/companies/service.d.ts.map +1 -1
- package/dist/modules/companies/service.js +8 -11
- package/dist/modules/companies/service.js.map +1 -1
- package/dist/modules/core.d.ts.map +1 -1
- package/dist/modules/core.js +4 -0
- package/dist/modules/core.js.map +1 -1
- package/dist/modules/deals/events.d.ts +31 -0
- package/dist/modules/deals/events.d.ts.map +1 -0
- package/dist/modules/deals/events.js +12 -0
- package/dist/modules/deals/events.js.map +1 -0
- package/dist/modules/deals/index.d.ts.map +1 -1
- package/dist/modules/deals/index.js +2 -0
- package/dist/modules/deals/index.js.map +1 -1
- package/dist/modules/deals/service.d.ts +1 -0
- package/dist/modules/deals/service.d.ts.map +1 -1
- package/dist/modules/deals/service.js +9 -18
- package/dist/modules/deals/service.js.map +1 -1
- package/dist/modules/decisions/events.d.ts +21 -0
- package/dist/modules/decisions/events.d.ts.map +1 -0
- package/dist/modules/decisions/events.js +11 -0
- package/dist/modules/decisions/events.js.map +1 -0
- package/dist/modules/decisions/index.d.ts.map +1 -1
- package/dist/modules/decisions/index.js +2 -0
- package/dist/modules/decisions/index.js.map +1 -1
- package/dist/modules/decisions/service.d.ts +1 -0
- package/dist/modules/decisions/service.d.ts.map +1 -1
- package/dist/modules/decisions/service.js +4 -7
- package/dist/modules/decisions/service.js.map +1 -1
- package/dist/modules/eventCatalogs.d.ts +35 -0
- package/dist/modules/eventCatalogs.d.ts.map +1 -0
- package/dist/modules/eventCatalogs.js +35 -0
- package/dist/modules/eventCatalogs.js.map +1 -0
- package/dist/modules/forms/events.d.ts +37 -0
- package/dist/modules/forms/events.d.ts.map +1 -0
- package/dist/modules/forms/events.js +15 -0
- package/dist/modules/forms/events.js.map +1 -0
- package/dist/modules/forms/index.d.ts.map +1 -1
- package/dist/modules/forms/index.js +2 -0
- package/dist/modules/forms/index.js.map +1 -1
- package/dist/modules/forms/service.d.ts +1 -0
- package/dist/modules/forms/service.d.ts.map +1 -1
- package/dist/modules/forms/service.js +8 -11
- package/dist/modules/forms/service.js.map +1 -1
- package/dist/modules/forms/submission.d.ts +5 -0
- package/dist/modules/forms/submission.d.ts.map +1 -1
- package/dist/modules/forms/submission.js +54 -28
- package/dist/modules/forms/submission.js.map +1 -1
- package/dist/modules/handbook/events.d.ts +28 -0
- package/dist/modules/handbook/events.d.ts.map +1 -0
- package/dist/modules/handbook/events.js +14 -0
- package/dist/modules/handbook/events.js.map +1 -0
- package/dist/modules/handbook/index.d.ts.map +1 -1
- package/dist/modules/handbook/index.js +2 -0
- package/dist/modules/handbook/index.js.map +1 -1
- package/dist/modules/handbook/service.d.ts +1 -0
- package/dist/modules/handbook/service.d.ts.map +1 -1
- package/dist/modules/handbook/service.js +8 -15
- package/dist/modules/handbook/service.js.map +1 -1
- package/dist/modules/hiring/candidates.d.ts +1 -0
- package/dist/modules/hiring/candidates.d.ts.map +1 -1
- package/dist/modules/hiring/candidates.js +8 -11
- package/dist/modules/hiring/candidates.js.map +1 -1
- package/dist/modules/hiring/events.d.ts +35 -0
- package/dist/modules/hiring/events.d.ts.map +1 -0
- package/dist/modules/hiring/events.js +11 -0
- package/dist/modules/hiring/events.js.map +1 -0
- package/dist/modules/hiring/index.d.ts.map +1 -1
- package/dist/modules/hiring/index.js +2 -0
- package/dist/modules/hiring/index.js.map +1 -1
- package/dist/modules/hiring/roles.d.ts +1 -0
- package/dist/modules/hiring/roles.d.ts.map +1 -1
- package/dist/modules/hiring/roles.js +8 -11
- package/dist/modules/hiring/roles.js.map +1 -1
- package/dist/modules/import-export/drafts.d.ts +9 -0
- package/dist/modules/import-export/drafts.d.ts.map +1 -1
- package/dist/modules/import-export/drafts.js +25 -0
- package/dist/modules/import-export/drafts.js.map +1 -1
- package/dist/modules/import-export/events.d.ts +22 -0
- package/dist/modules/import-export/events.d.ts.map +1 -0
- package/dist/modules/import-export/events.js +12 -0
- package/dist/modules/import-export/events.js.map +1 -0
- package/dist/modules/import-export/exportRows.d.ts +9 -0
- package/dist/modules/import-export/exportRows.d.ts.map +1 -1
- package/dist/modules/import-export/exportRows.js +10 -1
- package/dist/modules/import-export/exportRows.js.map +1 -1
- package/dist/modules/import-export/index.d.ts.map +1 -1
- package/dist/modules/import-export/index.js +2 -0
- package/dist/modules/import-export/index.js.map +1 -1
- package/dist/modules/import-export/plan.d.ts +23 -1
- package/dist/modules/import-export/plan.d.ts.map +1 -1
- package/dist/modules/import-export/plan.js +62 -3
- package/dist/modules/import-export/plan.js.map +1 -1
- package/dist/modules/import-export/presets.d.ts.map +1 -1
- package/dist/modules/import-export/presets.js +25 -0
- package/dist/modules/import-export/presets.js.map +1 -1
- package/dist/modules/import-export/repository.d.ts +17 -0
- package/dist/modules/import-export/repository.d.ts.map +1 -1
- package/dist/modules/import-export/repository.js +21 -1
- package/dist/modules/import-export/repository.js.map +1 -1
- package/dist/modules/import-export/routes.d.ts.map +1 -1
- package/dist/modules/import-export/routes.js +5 -1
- package/dist/modules/import-export/routes.js.map +1 -1
- package/dist/modules/import-export/schema.d.ts +55 -0
- package/dist/modules/import-export/schema.d.ts.map +1 -1
- package/dist/modules/import-export/schema.js +13 -1
- package/dist/modules/import-export/schema.js.map +1 -1
- package/dist/modules/import-export/service.d.ts +9 -1
- package/dist/modules/import-export/service.d.ts.map +1 -1
- package/dist/modules/import-export/service.js +104 -17
- package/dist/modules/import-export/service.js.map +1 -1
- package/dist/modules/import-export/tools.d.ts.map +1 -1
- package/dist/modules/import-export/tools.js +7 -1
- package/dist/modules/import-export/tools.js.map +1 -1
- package/dist/modules/import-export/writes.d.ts +16 -0
- package/dist/modules/import-export/writes.d.ts.map +1 -1
- package/dist/modules/import-export/writes.js +77 -2
- package/dist/modules/import-export/writes.js.map +1 -1
- package/dist/modules/notes/events.d.ts +22 -0
- package/dist/modules/notes/events.d.ts.map +1 -0
- package/dist/modules/notes/events.js +12 -0
- package/dist/modules/notes/events.js.map +1 -0
- package/dist/modules/notes/index.d.ts.map +1 -1
- package/dist/modules/notes/index.js +2 -0
- package/dist/modules/notes/index.js.map +1 -1
- package/dist/modules/notes/service.d.ts +1 -0
- package/dist/modules/notes/service.d.ts.map +1 -1
- package/dist/modules/notes/service.js +4 -7
- package/dist/modules/notes/service.js.map +1 -1
- package/dist/modules/opportunities/events.d.ts +31 -0
- package/dist/modules/opportunities/events.d.ts.map +1 -0
- package/dist/modules/opportunities/events.js +12 -0
- package/dist/modules/opportunities/events.js.map +1 -0
- package/dist/modules/opportunities/index.d.ts.map +1 -1
- package/dist/modules/opportunities/index.js +2 -0
- package/dist/modules/opportunities/index.js.map +1 -1
- package/dist/modules/opportunities/service.d.ts +1 -0
- package/dist/modules/opportunities/service.d.ts.map +1 -1
- package/dist/modules/opportunities/service.js +9 -18
- package/dist/modules/opportunities/service.js.map +1 -1
- package/dist/modules/partnerships/events.d.ts +31 -0
- package/dist/modules/partnerships/events.d.ts.map +1 -0
- package/dist/modules/partnerships/events.js +12 -0
- package/dist/modules/partnerships/events.js.map +1 -0
- package/dist/modules/partnerships/index.d.ts.map +1 -1
- package/dist/modules/partnerships/index.js +2 -0
- package/dist/modules/partnerships/index.js.map +1 -1
- package/dist/modules/partnerships/service.d.ts +1 -0
- package/dist/modules/partnerships/service.d.ts.map +1 -1
- package/dist/modules/partnerships/service.js +9 -18
- package/dist/modules/partnerships/service.js.map +1 -1
- package/dist/modules/people/events.d.ts +28 -0
- package/dist/modules/people/events.d.ts.map +1 -0
- package/dist/modules/people/events.js +18 -0
- package/dist/modules/people/events.js.map +1 -0
- package/dist/modules/people/index.d.ts.map +1 -1
- package/dist/modules/people/index.js +2 -0
- package/dist/modules/people/index.js.map +1 -1
- package/dist/modules/people/service.d.ts +1 -0
- package/dist/modules/people/service.d.ts.map +1 -1
- package/dist/modules/people/service.js +8 -11
- package/dist/modules/people/service.js.map +1 -1
- package/dist/modules/pipelines/service.d.ts +4 -0
- package/dist/modules/pipelines/service.d.ts.map +1 -1
- package/dist/modules/pipelines/service.js +31 -8
- package/dist/modules/pipelines/service.js.map +1 -1
- package/dist/modules/plans/events.d.ts +21 -0
- package/dist/modules/plans/events.d.ts.map +1 -0
- package/dist/modules/plans/events.js +11 -0
- package/dist/modules/plans/events.js.map +1 -0
- package/dist/modules/plans/index.d.ts.map +1 -1
- package/dist/modules/plans/index.js +2 -0
- package/dist/modules/plans/index.js.map +1 -1
- package/dist/modules/plans/service.d.ts +1 -0
- package/dist/modules/plans/service.d.ts.map +1 -1
- package/dist/modules/plans/service.js +6 -14
- package/dist/modules/plans/service.js.map +1 -1
- package/dist/modules/positions/events.d.ts +22 -0
- package/dist/modules/positions/events.d.ts.map +1 -0
- package/dist/modules/positions/events.js +8 -0
- package/dist/modules/positions/events.js.map +1 -0
- package/dist/modules/positions/index.d.ts.map +1 -1
- package/dist/modules/positions/index.js +2 -0
- package/dist/modules/positions/index.js.map +1 -1
- package/dist/modules/positions/service.d.ts +1 -0
- package/dist/modules/positions/service.d.ts.map +1 -1
- package/dist/modules/positions/service.js +8 -11
- package/dist/modules/positions/service.js.map +1 -1
- package/dist/modules/raises/events.d.ts +31 -0
- package/dist/modules/raises/events.d.ts.map +1 -0
- package/dist/modules/raises/events.js +12 -0
- package/dist/modules/raises/events.js.map +1 -0
- package/dist/modules/raises/index.d.ts.map +1 -1
- package/dist/modules/raises/index.js +2 -0
- package/dist/modules/raises/index.js.map +1 -1
- package/dist/modules/raises/service.d.ts +1 -0
- package/dist/modules/raises/service.d.ts.map +1 -1
- package/dist/modules/raises/service.js +9 -18
- package/dist/modules/raises/service.js.map +1 -1
- package/dist/modules/reseal.d.ts +55 -0
- package/dist/modules/reseal.d.ts.map +1 -1
- package/dist/modules/reseal.js +90 -1
- package/dist/modules/reseal.js.map +1 -1
- package/dist/modules/smtp-email/index.d.ts +69 -0
- package/dist/modules/smtp-email/index.d.ts.map +1 -0
- package/dist/modules/smtp-email/index.js +82 -0
- package/dist/modules/smtp-email/index.js.map +1 -0
- package/dist/modules/webhooks/index.d.ts.map +1 -1
- package/dist/modules/webhooks/index.js +5 -1
- package/dist/modules/webhooks/index.js.map +1 -1
- package/dist/modules/webhooks/payloads.d.ts +22 -7
- package/dist/modules/webhooks/payloads.d.ts.map +1 -1
- package/dist/modules/webhooks/payloads.js +63 -44
- package/dist/modules/webhooks/payloads.js.map +1 -1
- package/dist/modules/workspace/events.d.ts +52 -0
- package/dist/modules/workspace/events.d.ts.map +1 -0
- package/dist/modules/workspace/events.js +16 -0
- package/dist/modules/workspace/events.js.map +1 -0
- package/dist/modules/workspace/index.d.ts.map +1 -1
- package/dist/modules/workspace/index.js +6 -2
- package/dist/modules/workspace/index.js.map +1 -1
- package/dist/modules/workspace/service.d.ts +1 -0
- package/dist/modules/workspace/service.d.ts.map +1 -1
- package/dist/modules/workspace/service.js +13 -15
- package/dist/modules/workspace/service.js.map +1 -1
- package/dist/runtime/events.d.ts +86 -108
- package/dist/runtime/events.d.ts.map +1 -1
- package/dist/runtime/events.js +123 -52
- package/dist/runtime/events.js.map +1 -1
- package/dist/runtime/module.d.ts +54 -2
- package/dist/runtime/module.d.ts.map +1 -1
- package/dist/runtime/registry.d.ts +23 -0
- package/dist/runtime/registry.d.ts.map +1 -1
- package/dist/runtime/registry.js +102 -2
- package/dist/runtime/registry.js.map +1 -1
- package/dist/runtime/transaction.d.ts +29 -4
- package/dist/runtime/transaction.d.ts.map +1 -1
- package/dist/runtime/transaction.js +48 -7
- package/dist/runtime/transaction.js.map +1 -1
- package/dist/testing/app.d.ts +5 -1
- package/dist/testing/app.d.ts.map +1 -1
- package/dist/testing/app.js +13 -3
- package/dist/testing/app.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +6 -3
- package/dist/testing/database.js.map +1 -1
- package/dist/testing/services.d.ts +21 -1
- package/dist/testing/services.d.ts.map +1 -1
- package/dist/testing/services.js +20 -5
- package/dist/testing/services.js.map +1 -1
- package/migrations/0019_sharp_red_skull.sql +4 -0
- package/migrations/0020_early_nemesis.sql +2 -0
- package/migrations/meta/0019_snapshot.json +5138 -0
- package/migrations/meta/0020_snapshot.json +5138 -0
- package/migrations/meta/_journal.json +14 -0
- package/package.json +5 -2
- package/src/app.ts +26 -0
- package/src/boot.ts +89 -0
- package/src/index.ts +52 -9
- package/src/lib/actor.ts +15 -1
- package/src/lib/config.ts +65 -7
- package/src/lib/email.ts +22 -92
- package/src/lib/fromEnv.ts +143 -0
- package/src/lib/ids.ts +6 -0
- package/src/lib/kelpieConfigFile.ts +291 -0
- package/src/lib/logger.ts +92 -34
- package/src/modules/agent-tasks/index.ts +4 -1
- package/src/modules/agent-tasks/prompt.ts +57 -9
- package/src/modules/agent-tasks/service.ts +3 -2
- package/src/modules/agent-tasks/wire.ts +12 -0
- package/src/modules/auth/index.ts +11 -9
- package/src/modules/companies/events.ts +25 -0
- package/src/modules/companies/index.ts +2 -0
- package/src/modules/companies/service.ts +8 -11
- package/src/modules/core.ts +4 -0
- package/src/modules/deals/events.ts +34 -0
- package/src/modules/deals/index.ts +2 -0
- package/src/modules/deals/service.ts +17 -18
- package/src/modules/decisions/events.ts +24 -0
- package/src/modules/decisions/index.ts +2 -0
- package/src/modules/decisions/service.ts +8 -7
- package/src/modules/eventCatalogs.ts +35 -0
- package/src/modules/forms/events.ts +37 -0
- package/src/modules/forms/index.ts +2 -0
- package/src/modules/forms/service.ts +12 -11
- package/src/modules/forms/submission.ts +80 -29
- package/src/modules/handbook/events.ts +31 -0
- package/src/modules/handbook/index.ts +2 -0
- package/src/modules/handbook/service.ts +8 -15
- package/src/modules/hiring/candidates.ts +8 -11
- package/src/modules/hiring/events.ts +36 -0
- package/src/modules/hiring/index.ts +2 -0
- package/src/modules/hiring/roles.ts +8 -11
- package/src/modules/import-export/drafts.ts +27 -0
- package/src/modules/import-export/events.ts +25 -0
- package/src/modules/import-export/exportRows.ts +10 -1
- package/src/modules/import-export/index.ts +2 -0
- package/src/modules/import-export/plan.ts +107 -5
- package/src/modules/import-export/presets.ts +25 -0
- package/src/modules/import-export/repository.ts +35 -1
- package/src/modules/import-export/routes.ts +5 -0
- package/src/modules/import-export/schema.ts +13 -0
- package/src/modules/import-export/service.ts +153 -19
- package/src/modules/import-export/tools.ts +9 -0
- package/src/modules/import-export/writes.ts +116 -3
- package/src/modules/notes/events.ts +25 -0
- package/src/modules/notes/index.ts +2 -0
- package/src/modules/notes/service.ts +8 -7
- package/src/modules/opportunities/events.ts +34 -0
- package/src/modules/opportunities/index.ts +2 -0
- package/src/modules/opportunities/service.ts +17 -18
- package/src/modules/partnerships/events.ts +34 -0
- package/src/modules/partnerships/index.ts +2 -0
- package/src/modules/partnerships/service.ts +17 -18
- package/src/modules/people/events.ts +36 -0
- package/src/modules/people/index.ts +2 -0
- package/src/modules/people/service.ts +97 -87
- package/src/modules/pipelines/service.ts +47 -8
- package/src/modules/plans/events.ts +24 -0
- package/src/modules/plans/index.ts +2 -0
- package/src/modules/plans/service.ts +14 -14
- package/src/modules/positions/events.ts +25 -0
- package/src/modules/positions/index.ts +2 -0
- package/src/modules/positions/service.ts +8 -11
- package/src/modules/raises/events.ts +34 -0
- package/src/modules/raises/index.ts +2 -0
- package/src/modules/raises/service.ts +17 -18
- package/src/modules/reseal.ts +144 -1
- package/src/modules/smtp-email/index.ts +119 -0
- package/src/modules/webhooks/index.ts +5 -1
- package/src/modules/webhooks/payloads.ts +94 -48
- package/src/modules/workspace/events.ts +46 -0
- package/src/modules/workspace/index.ts +6 -2
- package/src/modules/workspace/service.ts +37 -15
- package/src/runtime/events.ts +268 -107
- package/src/runtime/module.ts +55 -2
- package/src/runtime/registry.ts +169 -1
- package/src/runtime/transaction.ts +94 -17
- package/src/testing/app.ts +18 -4
- package/src/testing/database.ts +6 -3
- package/src/testing/services.ts +34 -5
|
@@ -3,10 +3,11 @@ import type {
|
|
|
3
3
|
ImportCounts,
|
|
4
4
|
ImportObject,
|
|
5
5
|
MatchKeyOption,
|
|
6
|
+
OnMissingCompany,
|
|
6
7
|
} from '@kelpie/schemas'
|
|
7
8
|
|
|
8
9
|
import { normaliseDomain, normaliseEmail } from '../../lib/normalisation.ts'
|
|
9
|
-
import { companyDraft, dealFieldsDraft, personDraft } from './drafts.ts'
|
|
10
|
+
import { affiliationCompanyDraft, companyDraft, dealFieldsDraft, personDraft } from './drafts.ts'
|
|
10
11
|
import type { CompanyDraft, DealFieldsDraft, PersonDraft } from './drafts.ts'
|
|
11
12
|
import { buildMatchKey, splitList } from './mapping.ts'
|
|
12
13
|
import { aliasedStageSlug } from './presets.ts'
|
|
@@ -29,6 +30,8 @@ export interface ImportLookups {
|
|
|
29
30
|
readonly existing: ReadonlyMap<string, string>
|
|
30
31
|
readonly personIdByEmail: ReadonlyMap<string, string>
|
|
31
32
|
readonly companyIdByDomain: ReadonlyMap<string, string>
|
|
33
|
+
/** Company id by its folded name, for a People affiliation matched by name. */
|
|
34
|
+
readonly companyIdByName: ReadonlyMap<string, string>
|
|
32
35
|
/** Workspace member id by the address of the user behind it. */
|
|
33
36
|
readonly memberIdByEmail: ReadonlyMap<string, string>
|
|
34
37
|
/** Deal stage id, keyed by both its slug and its folded label. */
|
|
@@ -39,13 +42,28 @@ export interface PlanContext {
|
|
|
39
42
|
readonly object: ImportObject
|
|
40
43
|
readonly matchKey: MatchKeyOption
|
|
41
44
|
readonly conflictMode: ImportConflictMode
|
|
45
|
+
/** What a People row does with an absent company. Ignored by the other objects. */
|
|
46
|
+
readonly onMissingCompany: OnMissingCompany
|
|
42
47
|
readonly lookups: ImportLookups
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* A company affiliation a People row asks for: the position to write, and
|
|
52
|
+
* whether the company must be created before it can be linked.
|
|
53
|
+
*/
|
|
54
|
+
export type PlannedAffiliation =
|
|
55
|
+
| { readonly kind: 'link'; readonly companyId: string; readonly title: string }
|
|
56
|
+
| { readonly kind: 'create'; readonly company: CompanyDraft; readonly title: string }
|
|
57
|
+
|
|
45
58
|
/** The values a write applies, once every reference in the row has been resolved. */
|
|
46
59
|
export type ImportWrite =
|
|
47
60
|
| { readonly object: 'companies'; readonly draft: CompanyDraft }
|
|
48
|
-
| {
|
|
61
|
+
| {
|
|
62
|
+
readonly object: 'people'
|
|
63
|
+
readonly draft: PersonDraft
|
|
64
|
+
/** A position to upsert alongside the person, when the row named a company and a title. */
|
|
65
|
+
readonly affiliation?: PlannedAffiliation
|
|
66
|
+
}
|
|
49
67
|
| {
|
|
50
68
|
readonly object: 'positions'
|
|
51
69
|
readonly personId: string
|
|
@@ -78,12 +96,19 @@ export type RowPlan =
|
|
|
78
96
|
* reaches the row the earlier one has been written, so it resolves to an id.
|
|
79
97
|
*/
|
|
80
98
|
| { readonly action: 'skip'; readonly key: string; readonly targetId: string | null }
|
|
81
|
-
| {
|
|
99
|
+
| {
|
|
100
|
+
readonly action: 'create'
|
|
101
|
+
readonly key: string
|
|
102
|
+
readonly write: ImportWrite
|
|
103
|
+
/** Non-fatal notes about the applied row, e.g. a People affiliation left unlinked. */
|
|
104
|
+
readonly warnings?: readonly StoredRowError[]
|
|
105
|
+
}
|
|
82
106
|
| {
|
|
83
107
|
readonly action: 'update'
|
|
84
108
|
readonly key: string
|
|
85
109
|
readonly targetId: string | null
|
|
86
110
|
readonly write: ImportWrite
|
|
111
|
+
readonly warnings?: readonly StoredRowError[]
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
export interface PlannedRow {
|
|
@@ -233,6 +258,80 @@ function resolveWrite(context: PlanContext, mapped: Readonly<Record<string, stri
|
|
|
233
258
|
}
|
|
234
259
|
}
|
|
235
260
|
|
|
261
|
+
/**
|
|
262
|
+
* The company affiliation a People row asks for, if any, and any note about it.
|
|
263
|
+
*
|
|
264
|
+
* A row states an affiliation only when it carries both a title and a company
|
|
265
|
+
* identity, so a blank optional cell still says nothing, the additive rule every
|
|
266
|
+
* column follows. The company is matched by domain when the row has one and by
|
|
267
|
+
* name otherwise. A named company that is not here follows `on_missing_company`:
|
|
268
|
+
* `create` invents it from the row, `skip` imports the person alone and returns
|
|
269
|
+
* a warning.
|
|
270
|
+
*/
|
|
271
|
+
function planAffiliation(
|
|
272
|
+
context: PlanContext,
|
|
273
|
+
mapped: Readonly<Record<string, string>>,
|
|
274
|
+
): { readonly affiliation?: PlannedAffiliation; readonly warnings: readonly StoredRowError[] } {
|
|
275
|
+
const title = (mapped.title ?? '').trim()
|
|
276
|
+
const domainRaw = (mapped.company_domain ?? '').trim()
|
|
277
|
+
const nameRaw = (mapped.company_name ?? '').trim()
|
|
278
|
+
|
|
279
|
+
if (title.length === 0 || (domainRaw.length === 0 && nameRaw.length === 0)) {
|
|
280
|
+
return { warnings: [] }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const domain = normaliseDomain(domainRaw)
|
|
284
|
+
const companyId =
|
|
285
|
+
domainRaw.length > 0
|
|
286
|
+
? domain === null
|
|
287
|
+
? undefined
|
|
288
|
+
: context.lookups.companyIdByDomain.get(domain)
|
|
289
|
+
: context.lookups.companyIdByName.get(nameRaw.toLowerCase())
|
|
290
|
+
|
|
291
|
+
if (companyId !== undefined) {
|
|
292
|
+
return { affiliation: { kind: 'link', companyId, title }, warnings: [] }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (context.onMissingCompany === 'create') {
|
|
296
|
+
return {
|
|
297
|
+
affiliation: { kind: 'create', company: affiliationCompanyDraft(mapped), title },
|
|
298
|
+
warnings: [],
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const named = domainRaw.length > 0 ? domainRaw : nameRaw
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
warnings: [
|
|
306
|
+
{
|
|
307
|
+
field: domainRaw.length > 0 ? 'company_domain' : 'company_name',
|
|
308
|
+
message: `No company here matches "${named}", so the person imported without a position`,
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Folds a People row's affiliation into its create or update plan.
|
|
316
|
+
*
|
|
317
|
+
* A no-op for every other object, and for a person the row said nothing about a
|
|
318
|
+
* company for. The affiliation rides on the write; any note rides on the plan.
|
|
319
|
+
*/
|
|
320
|
+
function withAffiliation(
|
|
321
|
+
context: PlanContext,
|
|
322
|
+
mapped: Readonly<Record<string, string>>,
|
|
323
|
+
plan: Extract<RowPlan, { action: 'create' } | { action: 'update' }>,
|
|
324
|
+
): RowPlan {
|
|
325
|
+
if (context.object !== 'people' || plan.write.object !== 'people') {
|
|
326
|
+
return plan
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const { affiliation, warnings } = planAffiliation(context, mapped)
|
|
330
|
+
const write = affiliation === undefined ? plan.write : { ...plan.write, affiliation }
|
|
331
|
+
|
|
332
|
+
return { ...plan, write, ...(warnings.length > 0 ? { warnings } : {}) }
|
|
333
|
+
}
|
|
334
|
+
|
|
236
335
|
/**
|
|
237
336
|
* @param mapped The row's cells by Kelpie column, from `mapRow`.
|
|
238
337
|
* @returns What this row would do. `create` when its key matches nothing;
|
|
@@ -265,15 +364,18 @@ export function planRow(context: PlanContext, mapped: Readonly<Record<string, st
|
|
|
265
364
|
const targetId = context.lookups.existing.get(key)
|
|
266
365
|
|
|
267
366
|
if (targetId === undefined) {
|
|
268
|
-
return { action: 'create', key, write }
|
|
367
|
+
return withAffiliation(context, mapped, { action: 'create', key, write })
|
|
269
368
|
}
|
|
270
369
|
|
|
271
370
|
// An empty string is the placeholder an in-file match carries: the record it
|
|
272
371
|
// matched has not been written yet, so there is no id to report.
|
|
273
372
|
const resolved = targetId.length === 0 ? null : targetId
|
|
274
373
|
|
|
374
|
+
// A skipped row is left entirely alone, affiliation included. A caller who
|
|
375
|
+
// wants an existing person's position updated runs the job in `update` mode,
|
|
376
|
+
// which is where the rename-in-place lives.
|
|
275
377
|
return context.conflictMode === 'update'
|
|
276
|
-
? { action: 'update', key, targetId: resolved, write }
|
|
378
|
+
? withAffiliation(context, mapped, { action: 'update', key, targetId: resolved, write })
|
|
277
379
|
: { action: 'skip', key, targetId: resolved }
|
|
278
380
|
}
|
|
279
381
|
|
|
@@ -31,6 +31,8 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
|
|
|
31
31
|
email: 'Email',
|
|
32
32
|
phones: 'Phone Number',
|
|
33
33
|
location: 'City',
|
|
34
|
+
company_domain: 'Company Domain Name',
|
|
35
|
+
title: 'Job Title',
|
|
34
36
|
},
|
|
35
37
|
positions: {
|
|
36
38
|
person_email: 'Email',
|
|
@@ -60,6 +62,8 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
|
|
|
60
62
|
email: 'Email',
|
|
61
63
|
phones: 'Phone',
|
|
62
64
|
location: 'Mailing City',
|
|
65
|
+
company_name: 'Account Name',
|
|
66
|
+
title: 'Title',
|
|
63
67
|
},
|
|
64
68
|
positions: {
|
|
65
69
|
person_email: 'Email',
|
|
@@ -76,6 +80,27 @@ export const SOURCE_PRESETS: Readonly<Record<ImportSource, SourcePreset>> = {
|
|
|
76
80
|
external_id: 'Opportunity ID',
|
|
77
81
|
},
|
|
78
82
|
},
|
|
83
|
+
attio: {
|
|
84
|
+
// Attio's export writes one column per attribute, and a linked or nested
|
|
85
|
+
// attribute as `Parent > Child`. Only Companies and People are mapped: the
|
|
86
|
+
// sample export carries no Deals or Positions file, and an Attio People row
|
|
87
|
+
// names its company but not the company's domain, so a Position could not be
|
|
88
|
+
// keyed from it. Those objects fall back to `custom`.
|
|
89
|
+
companies: {
|
|
90
|
+
name: 'Record',
|
|
91
|
+
domain: 'Domains',
|
|
92
|
+
industry: 'Categories',
|
|
93
|
+
description: 'Description',
|
|
94
|
+
hq: 'Primary location > Country',
|
|
95
|
+
},
|
|
96
|
+
people: {
|
|
97
|
+
name: 'Record',
|
|
98
|
+
email: 'Email addresses',
|
|
99
|
+
location: 'Primary location > Country',
|
|
100
|
+
company_name: 'Company > Name',
|
|
101
|
+
title: 'Job title',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
/**
|
|
@@ -175,6 +175,8 @@ export interface RowOutcome {
|
|
|
175
175
|
readonly rowNumber: number
|
|
176
176
|
readonly action: SettledRowAction
|
|
177
177
|
readonly errors: readonly { readonly field: string; readonly message: string }[]
|
|
178
|
+
/** Non-fatal notes about a row that was applied anyway, e.g. an unlinked position. */
|
|
179
|
+
readonly warnings: readonly { readonly field: string; readonly message: string }[]
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
/**
|
|
@@ -202,12 +204,18 @@ export async function recordRowOutcome(
|
|
|
202
204
|
values,
|
|
203
205
|
action: outcome.action,
|
|
204
206
|
errors: outcome.errors,
|
|
207
|
+
warnings: outcome.warnings,
|
|
205
208
|
createdAt: now,
|
|
206
209
|
updatedAt: now,
|
|
207
210
|
})
|
|
208
211
|
.onConflictDoUpdate({
|
|
209
212
|
target: [importJobRows.jobId, importJobRows.rowNumber],
|
|
210
|
-
set: {
|
|
213
|
+
set: {
|
|
214
|
+
action: outcome.action,
|
|
215
|
+
errors: outcome.errors,
|
|
216
|
+
warnings: outcome.warnings,
|
|
217
|
+
updatedAt: now,
|
|
218
|
+
},
|
|
211
219
|
})
|
|
212
220
|
}
|
|
213
221
|
|
|
@@ -381,6 +389,32 @@ export function findCompanyIdsByDomain(
|
|
|
381
389
|
)
|
|
382
390
|
}
|
|
383
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Company ids by folded name, for a People affiliation matched on a company
|
|
394
|
+
* name rather than a domain.
|
|
395
|
+
*
|
|
396
|
+
* `lower(name) in (…)` rather than `ilike`, so a `%` inside a caller-supplied
|
|
397
|
+
* name is a literal, not a wildcard. The same rule `findCompanyKeys` follows.
|
|
398
|
+
* The caller passes names already lowercased.
|
|
399
|
+
*/
|
|
400
|
+
export function findCompanyIdsByName(
|
|
401
|
+
db: Queryable,
|
|
402
|
+
workspaceId: string,
|
|
403
|
+
names: readonly string[],
|
|
404
|
+
): Promise<{ name: string; id: string }[]> {
|
|
405
|
+
return overChunks(names, (chunk) =>
|
|
406
|
+
db
|
|
407
|
+
.select({ id: companies.id, name: companies.name })
|
|
408
|
+
.from(companies)
|
|
409
|
+
.where(
|
|
410
|
+
and(
|
|
411
|
+
eq(companies.workspaceId, workspaceId),
|
|
412
|
+
inArray(sql<string>`lower(${companies.name})`, [...chunk]),
|
|
413
|
+
),
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
|
|
384
418
|
/** Every member of the workspace, by the address of the user behind them. */
|
|
385
419
|
export function listMemberEmails(
|
|
386
420
|
db: Queryable,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
IMPORT_OBJECTS,
|
|
4
4
|
IMPORT_SOURCES,
|
|
5
5
|
MAX_IMPORT_FILE_BYTES,
|
|
6
|
+
ON_MISSING_COMPANY,
|
|
6
7
|
defaultMatchKeyId,
|
|
7
8
|
} from '@kelpie/schemas'
|
|
8
9
|
import type { ImportColumnMap, ImportObject } from '@kelpie/schemas'
|
|
@@ -42,6 +43,7 @@ const createJobForm = z.object({
|
|
|
42
43
|
source: z.enum(IMPORT_SOURCES),
|
|
43
44
|
object: z.enum(IMPORT_OBJECTS),
|
|
44
45
|
conflict_mode: z.enum(IMPORT_CONFLICT_MODES).default('skip'),
|
|
46
|
+
on_missing_company: z.enum(ON_MISSING_COMPANY).default('skip'),
|
|
45
47
|
match_key: z.string().min(1).optional(),
|
|
46
48
|
column_map: z.string().optional(),
|
|
47
49
|
dry_run: z.enum(['true', 'false']).default('true'),
|
|
@@ -58,12 +60,14 @@ export function importJobResponse(job: ImportJobView): Record<string, unknown> {
|
|
|
58
60
|
object: job.object,
|
|
59
61
|
status: job.status,
|
|
60
62
|
conflict_mode: job.conflictMode,
|
|
63
|
+
on_missing_company: job.onMissingCompany,
|
|
61
64
|
match_key: job.matchKey,
|
|
62
65
|
column_map: job.columnMap,
|
|
63
66
|
source_headers: job.sourceHeaders,
|
|
64
67
|
file_name: job.fileName,
|
|
65
68
|
counts: job.counts,
|
|
66
69
|
errors: job.errors,
|
|
70
|
+
warnings: job.warnings,
|
|
67
71
|
preview: job.preview,
|
|
68
72
|
created_at: job.createdAt.toISOString(),
|
|
69
73
|
updated_at: job.updatedAt.toISOString(),
|
|
@@ -230,6 +234,7 @@ export function mountImportExportRoutes(
|
|
|
230
234
|
source: parsed.data.source,
|
|
231
235
|
object: parsed.data.object,
|
|
232
236
|
conflictMode: parsed.data.conflict_mode,
|
|
237
|
+
onMissingCompany: parsed.data.on_missing_company,
|
|
233
238
|
matchKeyId: parsed.data.match_key ?? defaultMatchKeyId(parsed.data.object),
|
|
234
239
|
columnMap: readColumnMap(parsed.data.column_map),
|
|
235
240
|
fileName: file.name.length === 0 ? null : file.name,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
IMPORT_OBJECTS,
|
|
5
5
|
IMPORT_ROW_ACTIONS,
|
|
6
6
|
IMPORT_SOURCES,
|
|
7
|
+
ON_MISSING_COMPANY,
|
|
7
8
|
} from '@kelpie/schemas'
|
|
8
9
|
import type {
|
|
9
10
|
ImportColumnMap,
|
|
@@ -68,12 +69,21 @@ export const importJobs = pgTable(
|
|
|
68
69
|
object: text('object').notNull(),
|
|
69
70
|
status: text('status').notNull(),
|
|
70
71
|
conflictMode: text('conflict_mode').notNull(),
|
|
72
|
+
/**
|
|
73
|
+
* What a People import does with a row naming a company that is not here
|
|
74
|
+
* yet: `skip` leaves the affiliation unlinked and warns, `create` invents
|
|
75
|
+
* the company. Defaulted so a job predating this reads as `skip`, which is
|
|
76
|
+
* the behaviour every other object already has.
|
|
77
|
+
*/
|
|
78
|
+
onMissingCompany: text('on_missing_company').notNull().default('skip'),
|
|
71
79
|
matchKey: text('match_key').notNull(),
|
|
72
80
|
columnMap: jsonb('column_map').$type<ImportColumnMap>().notNull().default({}),
|
|
73
81
|
sourceHeaders: jsonb('source_headers').$type<readonly string[]>().notNull().default([]),
|
|
74
82
|
counts: jsonb('counts').$type<ImportCounts>().notNull(),
|
|
75
83
|
/** The first `IMPORT_REPORTED_ERRORS` failing rows. `counts.error` is the true number. */
|
|
76
84
|
errors: jsonb('errors').$type<readonly ImportRowError[]>().notNull().default([]),
|
|
85
|
+
/** The first `IMPORT_REPORTED_ERRORS` non-fatal notes, e.g. a person imported with the position skipped. */
|
|
86
|
+
warnings: jsonb('warnings').$type<readonly ImportRowError[]>().notNull().default([]),
|
|
77
87
|
/** The first `IMPORT_PREVIEW_ROWS` rows, mapped as Kelpie read them. */
|
|
78
88
|
preview: jsonb('preview').$type<readonly ImportPreviewRow[]>().notNull().default([]),
|
|
79
89
|
/**
|
|
@@ -97,6 +107,7 @@ export const importJobs = pgTable(
|
|
|
97
107
|
checkOneOf('import_jobs_source_check', table.source, IMPORT_SOURCES),
|
|
98
108
|
checkOneOf('import_jobs_object_check', table.object, IMPORT_OBJECTS),
|
|
99
109
|
checkOneOf('import_jobs_conflict_mode_check', table.conflictMode, IMPORT_CONFLICT_MODES),
|
|
110
|
+
checkOneOf('import_jobs_on_missing_company_check', table.onMissingCompany, ON_MISSING_COMPANY),
|
|
100
111
|
checkOneOf('import_jobs_status_check', table.status, IMPORT_JOB_STATUSES),
|
|
101
112
|
],
|
|
102
113
|
)
|
|
@@ -128,6 +139,8 @@ export const importJobRows = pgTable(
|
|
|
128
139
|
values: jsonb('values').$type<Readonly<Record<string, string>>>().notNull().default({}),
|
|
129
140
|
action: text('action').notNull(),
|
|
130
141
|
errors: jsonb('errors').$type<readonly StoredRowError[]>().notNull().default([]),
|
|
142
|
+
/** Non-fatal notes about a row that was applied anyway, e.g. an unlinked position. */
|
|
143
|
+
warnings: jsonb('warnings').$type<readonly StoredRowError[]>().notNull().default([]),
|
|
131
144
|
createdAt: createdAt(),
|
|
132
145
|
updatedAt: updatedAt(),
|
|
133
146
|
},
|
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
ImportRowError,
|
|
18
18
|
ImportSource,
|
|
19
19
|
MatchKeyOption,
|
|
20
|
+
OnMissingCompany,
|
|
20
21
|
} from '@kelpie/schemas'
|
|
21
22
|
|
|
22
23
|
import type { Database } from '../../lib/database.ts'
|
|
@@ -24,10 +25,78 @@ import { AppError, describeThrown } from '../../lib/errors.ts'
|
|
|
24
25
|
import type { IdFactory } from '../../lib/ids.ts'
|
|
25
26
|
import type { Logger } from '../../lib/logger.ts'
|
|
26
27
|
import { normaliseDomain, normaliseEmail } from '../../lib/normalisation.ts'
|
|
27
|
-
import type { Queryable, TransactionScope } from '../../runtime/transaction.ts'
|
|
28
|
+
import type { BufferedEvents, Queryable, TransactionScope } from '../../runtime/transaction.ts'
|
|
28
29
|
import type { ActivityRecorder } from '../activities/recorder.ts'
|
|
30
|
+
import { toEventActor } from '../../lib/actor.ts'
|
|
31
|
+
import type { RecordObjectType } from '../../runtime/events.ts'
|
|
29
32
|
import type { Actor } from '../auth/actor.ts'
|
|
30
33
|
import { requireWorkspaceId } from '../auth/actor.ts'
|
|
34
|
+
import '../companies/events.ts'
|
|
35
|
+
import '../deals/events.ts'
|
|
36
|
+
import '../people/events.ts'
|
|
37
|
+
import '../positions/events.ts'
|
|
38
|
+
import './events.ts'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Fires the created event owned by the record's module for one imported row.
|
|
42
|
+
* Split from the loop so TypeScript can correlate the object type with its
|
|
43
|
+
* typed event name at each call site.
|
|
44
|
+
*/
|
|
45
|
+
function emitImportedRecordCreated(
|
|
46
|
+
events: BufferedEvents,
|
|
47
|
+
objectType: RecordObjectType,
|
|
48
|
+
recordId: string,
|
|
49
|
+
): void {
|
|
50
|
+
switch (objectType) {
|
|
51
|
+
case 'person':
|
|
52
|
+
events.emit('people.person.created', { type: 'person', id: recordId }, {})
|
|
53
|
+
return
|
|
54
|
+
case 'company':
|
|
55
|
+
events.emit('companies.company.created', { type: 'company', id: recordId }, {})
|
|
56
|
+
return
|
|
57
|
+
case 'position':
|
|
58
|
+
events.emit('positions.position.created', { type: 'position', id: recordId }, {})
|
|
59
|
+
return
|
|
60
|
+
case 'deal':
|
|
61
|
+
events.emit('deals.deal.created', { type: 'deal', id: recordId }, {})
|
|
62
|
+
return
|
|
63
|
+
// The remaining `RecordObjectType` values are not produced by an import.
|
|
64
|
+
default:
|
|
65
|
+
throw new Error(`import produced an unexpected object type: ${objectType}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function emitImportedRecordUpdated(
|
|
70
|
+
events: BufferedEvents,
|
|
71
|
+
objectType: RecordObjectType,
|
|
72
|
+
recordId: string,
|
|
73
|
+
changed: readonly string[],
|
|
74
|
+
): void {
|
|
75
|
+
switch (objectType) {
|
|
76
|
+
case 'person':
|
|
77
|
+
events.emit('people.person.updated', { type: 'person', id: recordId }, { changed })
|
|
78
|
+
return
|
|
79
|
+
case 'company':
|
|
80
|
+
events.emit(
|
|
81
|
+
'companies.company.updated',
|
|
82
|
+
{ type: 'company', id: recordId },
|
|
83
|
+
{ changed },
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
case 'position':
|
|
87
|
+
events.emit(
|
|
88
|
+
'positions.position.updated',
|
|
89
|
+
{ type: 'position', id: recordId },
|
|
90
|
+
{ changed },
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
case 'deal':
|
|
94
|
+
events.emit('deals.deal.updated', { type: 'deal', id: recordId }, { changed })
|
|
95
|
+
return
|
|
96
|
+
default:
|
|
97
|
+
throw new Error(`import produced an unexpected object type: ${objectType}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
31
100
|
import { CsvFormatError, csvLine, fileDigest, parseCsv } from './csv.ts'
|
|
32
101
|
import type { CsvRow, ParsedCsv } from './csv.ts'
|
|
33
102
|
import { headersFor } from './exportRows.ts'
|
|
@@ -74,12 +143,14 @@ export interface ImportJobView {
|
|
|
74
143
|
readonly object: ImportObject
|
|
75
144
|
readonly status: string
|
|
76
145
|
readonly conflictMode: ImportConflictMode
|
|
146
|
+
readonly onMissingCompany: OnMissingCompany
|
|
77
147
|
readonly matchKey: string
|
|
78
148
|
readonly columnMap: ImportColumnMap
|
|
79
149
|
readonly sourceHeaders: readonly string[]
|
|
80
150
|
readonly fileName: string | null
|
|
81
151
|
readonly counts: ImportCounts
|
|
82
152
|
readonly errors: readonly ImportRowError[]
|
|
153
|
+
readonly warnings: readonly ImportRowError[]
|
|
83
154
|
readonly preview: readonly ImportPreviewRow[]
|
|
84
155
|
readonly createdAt: Date
|
|
85
156
|
readonly updatedAt: Date
|
|
@@ -89,6 +160,7 @@ export interface CreateImportJobInput {
|
|
|
89
160
|
readonly source: ImportSource
|
|
90
161
|
readonly object: ImportObject
|
|
91
162
|
readonly conflictMode: ImportConflictMode
|
|
163
|
+
readonly onMissingCompany: OnMissingCompany
|
|
92
164
|
readonly matchKeyId: string
|
|
93
165
|
/** Absent means "derive one from the source preset and the file's own headers". */
|
|
94
166
|
readonly columnMap: ImportColumnMap | undefined
|
|
@@ -145,6 +217,7 @@ function toView(job: ImportJobRecord): ImportJobView {
|
|
|
145
217
|
source: rest.source as ImportSource,
|
|
146
218
|
object: rest.object as ImportObject,
|
|
147
219
|
conflictMode: rest.conflictMode as ImportConflictMode,
|
|
220
|
+
onMissingCompany: rest.onMissingCompany as OnMissingCompany,
|
|
148
221
|
}
|
|
149
222
|
}
|
|
150
223
|
|
|
@@ -290,11 +363,37 @@ export function createImportExportService(
|
|
|
290
363
|
): Promise<ImportLookups> {
|
|
291
364
|
const existing = await findExistingKeys(db, workspaceId, object, matchKey, rows)
|
|
292
365
|
|
|
293
|
-
if (object === 'companies'
|
|
366
|
+
if (object === 'companies') {
|
|
294
367
|
return {
|
|
295
368
|
existing,
|
|
296
369
|
personIdByEmail: new Map(),
|
|
297
370
|
companyIdByDomain: new Map(),
|
|
371
|
+
companyIdByName: new Map(),
|
|
372
|
+
memberIdByEmail: new Map(),
|
|
373
|
+
dealStageIdByName: new Map(),
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// People only need companies, and only when a row carries an affiliation.
|
|
378
|
+
// Both a domain map and a name map, because a row may identify its company
|
|
379
|
+
// either way.
|
|
380
|
+
if (object === 'people') {
|
|
381
|
+
const domains = distinct(rows.map((row) => normaliseDomain(row.mapped.company_domain ?? '')))
|
|
382
|
+
const names = distinct(
|
|
383
|
+
rows.map((row) => (row.mapped.company_name ?? '').trim().toLowerCase() || null),
|
|
384
|
+
)
|
|
385
|
+
const [byDomain, byName] = await Promise.all([
|
|
386
|
+
repository.findCompanyIdsByDomain(db, workspaceId, domains),
|
|
387
|
+
repository.findCompanyIdsByName(db, workspaceId, names),
|
|
388
|
+
])
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
existing,
|
|
392
|
+
personIdByEmail: new Map(),
|
|
393
|
+
companyIdByDomain: new Map(
|
|
394
|
+
byDomain.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
|
|
395
|
+
),
|
|
396
|
+
companyIdByName: new Map(byName.map((row) => [row.name.toLowerCase(), row.id] as const)),
|
|
298
397
|
memberIdByEmail: new Map(),
|
|
299
398
|
dealStageIdByName: new Map(),
|
|
300
399
|
}
|
|
@@ -321,6 +420,7 @@ export function createImportExportService(
|
|
|
321
420
|
companyIdByDomain: new Map(
|
|
322
421
|
companyRows.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
|
|
323
422
|
),
|
|
423
|
+
companyIdByName: new Map(),
|
|
324
424
|
memberIdByEmail: new Map(),
|
|
325
425
|
dealStageIdByName: new Map(),
|
|
326
426
|
}
|
|
@@ -348,6 +448,7 @@ export function createImportExportService(
|
|
|
348
448
|
companyIdByDomain: new Map(
|
|
349
449
|
companyRows.flatMap((row) => (row.domain === null ? [] : [[row.domain, row.id] as const])),
|
|
350
450
|
),
|
|
451
|
+
companyIdByName: new Map(),
|
|
351
452
|
memberIdByEmail: new Map(members.map((member) => [member.email.toLowerCase(), member.id])),
|
|
352
453
|
dealStageIdByName: stagesByName,
|
|
353
454
|
}
|
|
@@ -358,6 +459,7 @@ export function createImportExportService(
|
|
|
358
459
|
object: job.object as ImportObject,
|
|
359
460
|
matchKey: requireMatchKey(job.object as ImportObject, job.matchKey),
|
|
360
461
|
conflictMode: job.conflictMode as ImportConflictMode,
|
|
462
|
+
onMissingCompany: job.onMissingCompany as OnMissingCompany,
|
|
361
463
|
lookups,
|
|
362
464
|
}
|
|
363
465
|
}
|
|
@@ -401,10 +503,13 @@ export function createImportExportService(
|
|
|
401
503
|
}
|
|
402
504
|
|
|
403
505
|
function toOutcome(planned: PlannedRow): repository.RowOutcome {
|
|
506
|
+
const { plan } = planned
|
|
507
|
+
|
|
404
508
|
return {
|
|
405
509
|
rowNumber: planned.row,
|
|
406
|
-
action:
|
|
407
|
-
errors:
|
|
510
|
+
action: plan.action,
|
|
511
|
+
errors: plan.action === 'error' ? plan.errors : [],
|
|
512
|
+
warnings: plan.action === 'create' || plan.action === 'update' ? (plan.warnings ?? []) : [],
|
|
408
513
|
}
|
|
409
514
|
}
|
|
410
515
|
|
|
@@ -418,7 +523,11 @@ export function createImportExportService(
|
|
|
418
523
|
function reportOf(
|
|
419
524
|
outcomes: readonly repository.RowOutcome[],
|
|
420
525
|
mapped: readonly MappedRow[],
|
|
421
|
-
): {
|
|
526
|
+
): {
|
|
527
|
+
errors: readonly ImportRowError[]
|
|
528
|
+
warnings: readonly ImportRowError[]
|
|
529
|
+
preview: readonly ImportPreviewRow[]
|
|
530
|
+
} {
|
|
422
531
|
const byRow = new Map(mapped.map((row) => [row.row, row.mapped]))
|
|
423
532
|
|
|
424
533
|
return {
|
|
@@ -432,6 +541,16 @@ export function createImportExportService(
|
|
|
432
541
|
message: problem.message,
|
|
433
542
|
})),
|
|
434
543
|
),
|
|
544
|
+
warnings: outcomes
|
|
545
|
+
.filter((outcome) => outcome.warnings.length > 0)
|
|
546
|
+
.slice(0, IMPORT_REPORTED_ERRORS)
|
|
547
|
+
.flatMap((outcome) =>
|
|
548
|
+
outcome.warnings.map((problem) => ({
|
|
549
|
+
row: outcome.rowNumber,
|
|
550
|
+
field: problem.field,
|
|
551
|
+
message: problem.message,
|
|
552
|
+
})),
|
|
553
|
+
),
|
|
435
554
|
preview: outcomes.slice(0, IMPORT_PREVIEW_ROWS).map((outcome) => ({
|
|
436
555
|
row: outcome.rowNumber,
|
|
437
556
|
action: outcome.action,
|
|
@@ -533,6 +652,8 @@ export function createImportExportService(
|
|
|
533
652
|
rowNumber: line.number,
|
|
534
653
|
action: plan.action,
|
|
535
654
|
errors: plan.action === 'error' ? plan.errors : [],
|
|
655
|
+
warnings:
|
|
656
|
+
plan.action === 'create' || plan.action === 'update' ? (plan.warnings ?? []) : [],
|
|
536
657
|
}
|
|
537
658
|
|
|
538
659
|
if (plan.action !== 'error' && plan.action !== 'skip') {
|
|
@@ -550,20 +671,26 @@ export function createImportExportService(
|
|
|
550
671
|
)
|
|
551
672
|
|
|
552
673
|
if (plan.action === 'create') {
|
|
553
|
-
events.
|
|
554
|
-
workspaceId,
|
|
555
|
-
objectType: written.objectType,
|
|
556
|
-
recordId: written.recordId,
|
|
557
|
-
})
|
|
674
|
+
emitImportedRecordCreated(events, written.objectType, written.recordId)
|
|
558
675
|
} else if (written.changedFields.length > 0) {
|
|
559
676
|
// An update that moved nothing publishes nothing. Re-running the same
|
|
560
677
|
// file must not wake every consumer watching for a change.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
678
|
+
emitImportedRecordUpdated(
|
|
679
|
+
events,
|
|
680
|
+
written.objectType,
|
|
681
|
+
written.recordId,
|
|
682
|
+
written.changedFields,
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// A People affiliation makes a Position, and maybe a Company. Each is its
|
|
687
|
+
// own record with its own module event, whatever the person row did.
|
|
688
|
+
for (const side of written.sideRecords ?? []) {
|
|
689
|
+
if (side.created) {
|
|
690
|
+
emitImportedRecordCreated(events, side.objectType, side.recordId)
|
|
691
|
+
} else if (side.changedFields.length > 0) {
|
|
692
|
+
emitImportedRecordUpdated(events, side.objectType, side.recordId, side.changedFields)
|
|
693
|
+
}
|
|
567
694
|
}
|
|
568
695
|
}
|
|
569
696
|
|
|
@@ -579,7 +706,7 @@ export function createImportExportService(
|
|
|
579
706
|
)
|
|
580
707
|
|
|
581
708
|
return outcome
|
|
582
|
-
})
|
|
709
|
+
}, { workspaceId, actor: toEventActor(actor) })
|
|
583
710
|
}
|
|
584
711
|
|
|
585
712
|
/**
|
|
@@ -618,6 +745,7 @@ export function createImportExportService(
|
|
|
618
745
|
rowNumber: row.row,
|
|
619
746
|
action: 'error' as const,
|
|
620
747
|
errors: [{ field: '', message: describeThrown(error) }],
|
|
748
|
+
warnings: [],
|
|
621
749
|
}
|
|
622
750
|
|
|
623
751
|
// Its own statement: the transaction that would have carried this one
|
|
@@ -658,10 +786,14 @@ export function createImportExportService(
|
|
|
658
786
|
})
|
|
659
787
|
|
|
660
788
|
await dependencies.transaction(({ events }) => {
|
|
661
|
-
events.emit(
|
|
789
|
+
events.emit(
|
|
790
|
+
'imports.job.completed',
|
|
791
|
+
{ type: 'import_job', id: jobId },
|
|
792
|
+
{ object: job.object },
|
|
793
|
+
)
|
|
662
794
|
|
|
663
795
|
return Promise.resolve()
|
|
664
|
-
})
|
|
796
|
+
}, { workspaceId, actor: toEventActor(actor) })
|
|
665
797
|
}
|
|
666
798
|
|
|
667
799
|
/**
|
|
@@ -718,11 +850,13 @@ export function createImportExportService(
|
|
|
718
850
|
object: input.object,
|
|
719
851
|
status: 'pending',
|
|
720
852
|
conflictMode: input.conflictMode,
|
|
853
|
+
onMissingCompany: input.onMissingCompany,
|
|
721
854
|
matchKey: matchKey.id,
|
|
722
855
|
columnMap,
|
|
723
856
|
sourceHeaders: parsed.headers,
|
|
724
857
|
counts: { ...EMPTY_COUNTS, total: parsed.rows.length },
|
|
725
858
|
errors: [],
|
|
859
|
+
warnings: [],
|
|
726
860
|
preview: [],
|
|
727
861
|
fileSha256: fileDigest(input.csv),
|
|
728
862
|
fileName: input.fileName,
|