@endora-commerce/mod-newsletter 0.100.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/admin/api/newsletter-client.d.ts +64 -0
- package/dist/admin/api/newsletter-client.d.ts.map +1 -0
- package/dist/admin/api/newsletter-client.js +71 -0
- package/dist/admin/api/newsletter-client.js.map +1 -0
- package/dist/admin/email-variables.d.ts +21 -0
- package/dist/admin/email-variables.d.ts.map +1 -0
- package/dist/admin/email-variables.js +48 -0
- package/dist/admin/email-variables.js.map +1 -0
- package/dist/admin/index.d.ts +44 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +149 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/AutomationBuilder.d.ts +8 -0
- package/dist/admin/pages/AutomationBuilder.d.ts.map +1 -0
- package/dist/admin/pages/AutomationBuilder.js +131 -0
- package/dist/admin/pages/AutomationBuilder.js.map +1 -0
- package/dist/admin/pages/AutomationsPage.d.ts +8 -0
- package/dist/admin/pages/AutomationsPage.d.ts.map +1 -0
- package/dist/admin/pages/AutomationsPage.js +43 -0
- package/dist/admin/pages/AutomationsPage.js.map +1 -0
- package/dist/admin/pages/BlocksPage.d.ts +8 -0
- package/dist/admin/pages/BlocksPage.d.ts.map +1 -0
- package/dist/admin/pages/BlocksPage.js +110 -0
- package/dist/admin/pages/BlocksPage.js.map +1 -0
- package/dist/admin/pages/CampaignEditor.d.ts +8 -0
- package/dist/admin/pages/CampaignEditor.d.ts.map +1 -0
- package/dist/admin/pages/CampaignEditor.js +144 -0
- package/dist/admin/pages/CampaignEditor.js.map +1 -0
- package/dist/admin/pages/CampaignStats.d.ts +8 -0
- package/dist/admin/pages/CampaignStats.d.ts.map +1 -0
- package/dist/admin/pages/CampaignStats.js +45 -0
- package/dist/admin/pages/CampaignStats.js.map +1 -0
- package/dist/admin/pages/CampaignsPage.d.ts +8 -0
- package/dist/admin/pages/CampaignsPage.d.ts.map +1 -0
- package/dist/admin/pages/CampaignsPage.js +28 -0
- package/dist/admin/pages/CampaignsPage.js.map +1 -0
- package/dist/admin/pages/ProviderSettingsPage.d.ts +8 -0
- package/dist/admin/pages/ProviderSettingsPage.d.ts.map +1 -0
- package/dist/admin/pages/ProviderSettingsPage.js +64 -0
- package/dist/admin/pages/ProviderSettingsPage.js.map +1 -0
- package/dist/admin/pages/SubscribersPage.d.ts +8 -0
- package/dist/admin/pages/SubscribersPage.d.ts.map +1 -0
- package/dist/admin/pages/SubscribersPage.js +55 -0
- package/dist/admin/pages/SubscribersPage.js.map +1 -0
- package/dist/admin/pages/TagsPage.d.ts +8 -0
- package/dist/admin/pages/TagsPage.d.ts.map +1 -0
- package/dist/admin/pages/TagsPage.js +114 -0
- package/dist/admin/pages/TagsPage.js.map +1 -0
- package/dist/backend/entities/newsletter-automation-run.entity.d.ts +19 -0
- package/dist/backend/entities/newsletter-automation-run.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-automation-run.entity.js +72 -0
- package/dist/backend/entities/newsletter-automation-run.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-automation.entity.d.ts +22 -0
- package/dist/backend/entities/newsletter-automation.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-automation.entity.js +86 -0
- package/dist/backend/entities/newsletter-automation.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts +6 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.js +30 -0
- package/dist/backend/entities/newsletter-campaign-subscriber.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-campaign.entity.d.ts +22 -0
- package/dist/backend/entities/newsletter-campaign.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-campaign.entity.js +102 -0
- package/dist/backend/entities/newsletter-campaign.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-custom-field.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-custom-field.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-custom-field.entity.js +53 -0
- package/dist/backend/entities/newsletter-custom-field.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts +7 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js +35 -0
- package/dist/backend/entities/newsletter-email-block-sales-channel.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-email-block.entity.d.ts +21 -0
- package/dist/backend/entities/newsletter-email-block.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-email-block.entity.js +78 -0
- package/dist/backend/entities/newsletter-email-block.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.js +54 -0
- package/dist/backend/entities/newsletter-engagement-event.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-send-record.entity.d.ts +23 -0
- package/dist/backend/entities/newsletter-send-record.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-send-record.entity.js +89 -0
- package/dist/backend/entities/newsletter-send-record.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts +6 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.js +31 -0
- package/dist/backend/entities/newsletter-subscriber-tag.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-subscriber.entity.d.ts +63 -0
- package/dist/backend/entities/newsletter-subscriber.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-subscriber.entity.js +147 -0
- package/dist/backend/entities/newsletter-subscriber.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-suppression.entity.d.ts +14 -0
- package/dist/backend/entities/newsletter-suppression.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-suppression.entity.js +51 -0
- package/dist/backend/entities/newsletter-suppression.entity.js.map +1 -0
- package/dist/backend/entities/newsletter-tag.entity.d.ts +12 -0
- package/dist/backend/entities/newsletter-tag.entity.d.ts.map +1 -0
- package/dist/backend/entities/newsletter-tag.entity.js +53 -0
- package/dist/backend/entities/newsletter-tag.entity.js.map +1 -0
- package/dist/backend/index.d.ts +120 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +139 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +114 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +171 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +27 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +118 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.self.d.ts +15 -0
- package/dist/backend/routes.self.d.ts.map +1 -0
- package/dist/backend/routes.self.js +28 -0
- package/dist/backend/routes.self.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +24 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +79 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/audience-resolver.d.ts +20 -0
- package/dist/backend/services/audience-resolver.d.ts.map +1 -0
- package/dist/backend/services/audience-resolver.js +41 -0
- package/dist/backend/services/audience-resolver.js.map +1 -0
- package/dist/backend/services/automation.service.d.ts +58 -0
- package/dist/backend/services/automation.service.d.ts.map +1 -0
- package/dist/backend/services/automation.service.js +301 -0
- package/dist/backend/services/automation.service.js.map +1 -0
- package/dist/backend/services/campaign-dispatch.service.d.ts +50 -0
- package/dist/backend/services/campaign-dispatch.service.d.ts.map +1 -0
- package/dist/backend/services/campaign-dispatch.service.js +130 -0
- package/dist/backend/services/campaign-dispatch.service.js.map +1 -0
- package/dist/backend/services/campaign.service.d.ts +40 -0
- package/dist/backend/services/campaign.service.d.ts.map +1 -0
- package/dist/backend/services/campaign.service.js +195 -0
- package/dist/backend/services/campaign.service.js.map +1 -0
- package/dist/backend/services/consent-block-seeder.d.ts +30 -0
- package/dist/backend/services/consent-block-seeder.d.ts.map +1 -0
- package/dist/backend/services/consent-block-seeder.js +52 -0
- package/dist/backend/services/consent-block-seeder.js.map +1 -0
- package/dist/backend/services/content.service.d.ts +52 -0
- package/dist/backend/services/content.service.d.ts.map +1 -0
- package/dist/backend/services/content.service.js +48 -0
- package/dist/backend/services/content.service.js.map +1 -0
- package/dist/backend/services/custom-field.service.d.ts +16 -0
- package/dist/backend/services/custom-field.service.d.ts.map +1 -0
- package/dist/backend/services/custom-field.service.js +67 -0
- package/dist/backend/services/custom-field.service.js.map +1 -0
- package/dist/backend/services/email-block.service.d.ts +26 -0
- package/dist/backend/services/email-block.service.d.ts.map +1 -0
- package/dist/backend/services/email-block.service.js +120 -0
- package/dist/backend/services/email-block.service.js.map +1 -0
- package/dist/backend/services/opt-in.service.d.ts +25 -0
- package/dist/backend/services/opt-in.service.d.ts.map +1 -0
- package/dist/backend/services/opt-in.service.js +46 -0
- package/dist/backend/services/opt-in.service.js.map +1 -0
- package/dist/backend/services/provider/console-provider.d.ts +32 -0
- package/dist/backend/services/provider/console-provider.d.ts.map +1 -0
- package/dist/backend/services/provider/console-provider.js +31 -0
- package/dist/backend/services/provider/console-provider.js.map +1 -0
- package/dist/backend/services/provider/provider-registry.d.ts +51 -0
- package/dist/backend/services/provider/provider-registry.d.ts.map +1 -0
- package/dist/backend/services/provider/provider-registry.js +84 -0
- package/dist/backend/services/provider/provider-registry.js.map +1 -0
- package/dist/backend/services/provider/smtp-provider.d.ts +30 -0
- package/dist/backend/services/provider/smtp-provider.d.ts.map +1 -0
- package/dist/backend/services/provider/smtp-provider.js +42 -0
- package/dist/backend/services/provider/smtp-provider.js.map +1 -0
- package/dist/backend/services/provider-admin.service.d.ts +39 -0
- package/dist/backend/services/provider-admin.service.d.ts.map +1 -0
- package/dist/backend/services/provider-admin.service.js +59 -0
- package/dist/backend/services/provider-admin.service.js.map +1 -0
- package/dist/backend/services/provider-admin.types.d.ts +6 -0
- package/dist/backend/services/provider-admin.types.d.ts.map +1 -0
- package/dist/backend/services/provider-admin.types.js +2 -0
- package/dist/backend/services/provider-admin.types.js.map +1 -0
- package/dist/backend/services/queues/newsletter-queues.d.ts +34 -0
- package/dist/backend/services/queues/newsletter-queues.d.ts.map +1 -0
- package/dist/backend/services/queues/newsletter-queues.js +58 -0
- package/dist/backend/services/queues/newsletter-queues.js.map +1 -0
- package/dist/backend/services/self.service.d.ts +17 -0
- package/dist/backend/services/self.service.d.ts.map +1 -0
- package/dist/backend/services/self.service.js +44 -0
- package/dist/backend/services/self.service.js.map +1 -0
- package/dist/backend/services/stats.service.d.ts +10 -0
- package/dist/backend/services/stats.service.d.ts.map +1 -0
- package/dist/backend/services/stats.service.js +45 -0
- package/dist/backend/services/stats.service.js.map +1 -0
- package/dist/backend/services/subscriber-admin.service.d.ts +26 -0
- package/dist/backend/services/subscriber-admin.service.d.ts.map +1 -0
- package/dist/backend/services/subscriber-admin.service.js +163 -0
- package/dist/backend/services/subscriber-admin.service.js.map +1 -0
- package/dist/backend/services/subscriber.service.d.ts +91 -0
- package/dist/backend/services/subscriber.service.d.ts.map +1 -0
- package/dist/backend/services/subscriber.service.js +280 -0
- package/dist/backend/services/subscriber.service.js.map +1 -0
- package/dist/backend/services/tag.service.d.ts +16 -0
- package/dist/backend/services/tag.service.d.ts.map +1 -0
- package/dist/backend/services/tag.service.js +79 -0
- package/dist/backend/services/tag.service.js.map +1 -0
- package/dist/backend/services/token.helper.d.ts +32 -0
- package/dist/backend/services/token.helper.d.ts.map +1 -0
- package/dist/backend/services/token.helper.js +76 -0
- package/dist/backend/services/token.helper.js.map +1 -0
- package/dist/backend/services/tracking.service.d.ts +14 -0
- package/dist/backend/services/tracking.service.d.ts.map +1 -0
- package/dist/backend/services/tracking.service.js +45 -0
- package/dist/backend/services/tracking.service.js.map +1 -0
- package/dist/manifest.d.ts +192 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +211 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260629T200954_newsletter_init.d.ts +6 -0
- package/dist/migrations/20260629T200954_newsletter_init.d.ts.map +1 -0
- package/dist/migrations/20260629T200954_newsletter_init.js +261 -0
- package/dist/migrations/20260629T200954_newsletter_init.js.map +1 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts +116 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.js +186 -0
- package/dist/migrations/20260830T212736_newsletter_organization_attribution.js.map +1 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts +6 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.d.ts.map +1 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js +58 -0
- package/dist/migrations/20260903T101752_newsletter_namespace_block_names.js.map +1 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts +23 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts.map +1 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js +27 -0
- package/dist/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.js.map +1 -0
- package/dist/migrations/index.d.ts +30 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +35 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/newsletter.md +94 -0
- package/i18n/en.json +27 -0
- package/i18n/pl.json +27 -0
- package/package.json +119 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* D-187 — `newsletter_subscribers` gains its organisation, and a row that names
|
|
4
|
+
* a customer account carries one.
|
|
5
|
+
*
|
|
6
|
+
* Feature 087 Group B, class 4 of 4 and the last of the five
|
|
7
|
+
* (`specs/087-tenant-scope-enforcement/`). `NewsletterSubscriber` is
|
|
8
|
+
* `@CustomerScoped`, and until this migration the `allowed-set` arm of
|
|
9
|
+
* `customerFilterCond` had no column to grant on: a sales representative
|
|
10
|
+
* assigned to the buyer's own organisation was shown **none** of their
|
|
11
|
+
* subscribers. The column is what turns that into an answer.
|
|
12
|
+
*
|
|
13
|
+
* ## The column and the read arrive together, on purpose
|
|
14
|
+
*
|
|
15
|
+
* `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
|
|
16
|
+
* entity carries `organizationId`, **per query**, so the entity property in
|
|
17
|
+
* this same merge request is what switches the grant on — there is no third
|
|
18
|
+
* artefact, no flag and no staging. At the same instant the
|
|
19
|
+
* `ORGANIZATION_ATTRIBUTION_PENDING` disclosure retires itself: it is keyed on
|
|
20
|
+
* the column's *absence*.
|
|
21
|
+
*
|
|
22
|
+
* That pairing is why this migration is not "column and backfill". From the
|
|
23
|
+
* moment the grant is live, a row inserted without an organisation is invisible
|
|
24
|
+
* to the representative who serves that organisation, on a screen that has just
|
|
25
|
+
* stopped explaining itself — and MikroORM applies no filter to `INSERT`
|
|
26
|
+
* (`r1-spike.md` §4, measured), so the filter cannot refuse it. The `CHECK`
|
|
27
|
+
* below is the only refusal an `INSERT` has.
|
|
28
|
+
*
|
|
29
|
+
* ## An implication, not an equivalence
|
|
30
|
+
*
|
|
31
|
+
* `customer_account_id is null or organization_id is not null` is FR-010 and
|
|
32
|
+
* FR-011 together and nothing wider: an **owned** row has an organisation, an
|
|
33
|
+
* **ownerless** row need not. This table is ownerless by construction for the
|
|
34
|
+
* storefront's sign-up form — `POST /api/v1/newsletter/subscribe` passes no
|
|
35
|
+
* account at all, so every subscriber who has never signed in is such a row,
|
|
36
|
+
* and on this table they are the majority rather than the exception. Who they
|
|
37
|
+
* belong to is R-6's open question. The equivalence would answer it in the
|
|
38
|
+
* schema, which D-187 declines to do.
|
|
39
|
+
*
|
|
40
|
+
* The shape the implication leaves open — an **ownerless** row still carrying
|
|
41
|
+
* an organisation — is unreachable here, for `inventory`'s reason rather than
|
|
42
|
+
* `pwa`'s. `subscriber.service.ts` writes `customer_account_id` in exactly two
|
|
43
|
+
* places and both of them *acquire* an owner: the `em.create` on first
|
|
44
|
+
* subscribe, and the adoption line that fills in the account when a subscriber
|
|
45
|
+
* who signed up anonymously later signs in. Nothing in this module ever clears
|
|
46
|
+
* it (grep of `packages/modules/newsletter/src` for `customerAccountId`: two
|
|
47
|
+
* writes, both assignments of a non-null id, the rest reads). So there is no
|
|
48
|
+
* de-association direction for the constraint to be silent about.
|
|
49
|
+
*
|
|
50
|
+
* ## The e-mail address is not a derivation
|
|
51
|
+
*
|
|
52
|
+
* `newsletter_subscribers.email` is globally unique and `customer_accounts`
|
|
53
|
+
* has an `email` too, so the backfill below could have matched them and
|
|
54
|
+
* attributed far more rows. It deliberately does not. That match would claim
|
|
55
|
+
* rows the schema does not link, silently and irreversibly, for whoever
|
|
56
|
+
* happened to sign up with the address their employer later registered — it is
|
|
57
|
+
* R-6 answer (2) guessed rather than decided. The only derivation is
|
|
58
|
+
* `customer_account_id`.
|
|
59
|
+
*
|
|
60
|
+
* ## No foreign key
|
|
61
|
+
*
|
|
62
|
+
* Matching `Cart`, `Comparison`, `PushSubscription` and
|
|
63
|
+
* `AvailabilityNotification`, none of which carries one. D-187 withdraws
|
|
64
|
+
* `r1-spike.md` §8's `on delete set null` recommendation as contradicting
|
|
65
|
+
* FR-011: it produces a row that names an account and no organisation, which is
|
|
66
|
+
* precisely the state this migration makes unreachable, and under the
|
|
67
|
+
* constraint below it would abort an unrelated organisation delete with a
|
|
68
|
+
* message about a table the operator was not touching. If one is ever taken
|
|
69
|
+
* here it must be `restrict`, and it is not this migration's decision.
|
|
70
|
+
*
|
|
71
|
+
* ## Derive, then count, then refuse — never delete
|
|
72
|
+
*
|
|
73
|
+
* The derivation is the owning account's own organisation. **On this table the
|
|
74
|
+
* refusal is a real branch, not a formality**: like `push_subscriptions` and
|
|
75
|
+
* `availability_notifications`, and unlike `comparisons`,
|
|
76
|
+
* `newsletter_subscribers` has **no foreign key** on `customer_account_id` —
|
|
77
|
+
* `20260629T200954_newsletter_init.ts` declares it `uuid null` and constrains
|
|
78
|
+
* nothing — so a row naming an account that is gone is representable here.
|
|
79
|
+
*
|
|
80
|
+
* It is nevertheless empty today, and the reason is worth knowing rather than
|
|
81
|
+
* hoping: a customer account is never hard-deleted in this tree.
|
|
82
|
+
* `customer-account-lifecycle-ports.ts` soft-deletes, restores and
|
|
83
|
+
* **anonymises** — the last one rewrites the e-mail and the name and keeps the
|
|
84
|
+
* row — and every one of those still satisfies `organization_id NOT NULL`
|
|
85
|
+
* (D-178), so it still derives. Change any of that and the count below is the
|
|
86
|
+
* only thing that would notice. The anonymise path is worth a second look on
|
|
87
|
+
* *this* table in particular, because it rewrites the very column an e-mail
|
|
88
|
+
* match would have keyed on; deriving from `customer_account_id` is unaffected
|
|
89
|
+
* by it.
|
|
90
|
+
*
|
|
91
|
+
* It raises with the count and up to twenty ids and deletes nothing (D-184), in
|
|
92
|
+
* the shape `20260825T141659_customer_accounts_organization_required.ts`
|
|
93
|
+
* established and the three Group B migrations before this one repeated.
|
|
94
|
+
* Deleting would be particularly wrong here: a subscriber row is a consent
|
|
95
|
+
* record, and dropping one destroys the evidence that the address may be
|
|
96
|
+
* mailed at all.
|
|
97
|
+
*
|
|
98
|
+
* ## Nothing to declare in the manifest
|
|
99
|
+
*
|
|
100
|
+
* `newsletter` declares `customers` in its manifest `dependencies`, whose
|
|
101
|
+
* transitive closure reaches `customer_accounts` and `organizations`, so the
|
|
102
|
+
* table this reads is created before this runs. No foreign key is added, so
|
|
103
|
+
* `fk-dependency-drift` has nothing to say either, and a migration naming
|
|
104
|
+
* another module's table is outside `check:module-boundary`'s population by
|
|
105
|
+
* that check's own rule.
|
|
106
|
+
*
|
|
107
|
+
* The standing guard is
|
|
108
|
+
* `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
|
|
109
|
+
* which derives its population from the ORM's metadata rather than from a list
|
|
110
|
+
* — so this class is covered by gaining the column, with no edit to that file.
|
|
111
|
+
*/
|
|
112
|
+
export class Migration20260830T212736NewsletterOrganizationAttribution extends Migration {
|
|
113
|
+
async up() {
|
|
114
|
+
// 1. The column. Nullable, because a storefront sign-up legitimately has
|
|
115
|
+
// none (FR-011) and on this table that is the ordinary case rather than
|
|
116
|
+
// the edge.
|
|
117
|
+
this.addSql(`alter table "newsletter_subscribers" add column "organization_id" uuid null;`);
|
|
118
|
+
// 2. Derive the organisation from the account that owns the subscription.
|
|
119
|
+
// Every account carries one since D-178, including a soft-deleted or
|
|
120
|
+
// anonymised one, so this is total for every row whose account is still
|
|
121
|
+
// there. Step 3 is about the rows for which it is not.
|
|
122
|
+
this.addSql(`
|
|
123
|
+
update "newsletter_subscribers" ns
|
|
124
|
+
set "organization_id" = ca."organization_id"
|
|
125
|
+
from "customer_accounts" ca
|
|
126
|
+
where ca."id" = ns."customer_account_id"
|
|
127
|
+
and ns."organization_id" is null;
|
|
128
|
+
`);
|
|
129
|
+
// 3. Count what is left and refuse. This table has no foreign key on
|
|
130
|
+
// `customer_account_id`, so a dangling account id is representable and
|
|
131
|
+
// this branch is real. It deletes nothing (D-184).
|
|
132
|
+
this.addSql(`
|
|
133
|
+
do $$
|
|
134
|
+
declare
|
|
135
|
+
remaining bigint;
|
|
136
|
+
sample text;
|
|
137
|
+
begin
|
|
138
|
+
select count(*) into remaining
|
|
139
|
+
from "newsletter_subscribers"
|
|
140
|
+
where "customer_account_id" is not null
|
|
141
|
+
and "organization_id" is null;
|
|
142
|
+
if remaining > 0 then
|
|
143
|
+
select string_agg(id::text, ', ') into sample from (
|
|
144
|
+
select "id" from "newsletter_subscribers"
|
|
145
|
+
where "customer_account_id" is not null
|
|
146
|
+
and "organization_id" is null
|
|
147
|
+
order by "id" limit 20
|
|
148
|
+
) s;
|
|
149
|
+
raise exception
|
|
150
|
+
'D-187: % newsletter_subscribers row(s) still name a customer account with organization_id IS NULL after derivation. First ids: %. Every customer account has an organization (D-178) and none is ever hard-deleted, so these rows name an account that is gone. Do not delete them - they are consent records - and do not match them by e-mail address, which is R-6 guessed. Find out how they got here.',
|
|
151
|
+
remaining, sample;
|
|
152
|
+
end if;
|
|
153
|
+
end $$;
|
|
154
|
+
`);
|
|
155
|
+
// 4. The constraint.
|
|
156
|
+
this.addSql(`
|
|
157
|
+
alter table "newsletter_subscribers"
|
|
158
|
+
add constraint "newsletter_subscribers_organization_attribution_chk"
|
|
159
|
+
check ("customer_account_id" is null or "organization_id" is not null);
|
|
160
|
+
`);
|
|
161
|
+
// 5. The index the entity property declares, named
|
|
162
|
+
// `newsletter_subscribers_organization_idx` — **this table's own style**
|
|
163
|
+
// and not the previous batch's. Every index the init migration wrote
|
|
164
|
+
// here is `newsletter_subscribers_<subject>_idx`, and the one other
|
|
165
|
+
// `*_id` column it indexes is spelled with the subject alone:
|
|
166
|
+
// `sales_channel_id` is `newsletter_subscribers_channel_idx`, not
|
|
167
|
+
// `..._sales_channel_id_index`. `comparisons` uses `idx_<table>_<col>`,
|
|
168
|
+
// `pwa` uses `<table>_<col>_idx` and `inventory` uses
|
|
169
|
+
// `<table>_<col>_index`; each followed its own table, and this follows
|
|
170
|
+
// this one. Whole rather than partial, like every index above it here.
|
|
171
|
+
this.addSql(`
|
|
172
|
+
create index "newsletter_subscribers_organization_idx"
|
|
173
|
+
on "newsletter_subscribers" ("organization_id");
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
async down() {
|
|
177
|
+
// The constraint, the index and the column. The organisations step 2
|
|
178
|
+
// derived go with the column they were written into — there is nothing to
|
|
179
|
+
// preserve, because each of them was already implied by the account its row
|
|
180
|
+
// names.
|
|
181
|
+
this.addSql(`alter table "newsletter_subscribers" drop constraint if exists "newsletter_subscribers_organization_attribution_chk";`);
|
|
182
|
+
this.addSql(`drop index if exists "newsletter_subscribers_organization_idx";`);
|
|
183
|
+
this.addSql(`alter table "newsletter_subscribers" drop column if exists "organization_id";`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=20260830T212736_newsletter_organization_attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260830T212736_newsletter_organization_attribution.js","sourceRoot":"","sources":["../../src/migrations/20260830T212736_newsletter_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AACH,MAAM,OAAO,yDAA0D,SAAQ,SAAS;IAC7E,KAAK,CAAC,EAAE;QACf,yEAAyE;QACzE,2EAA2E;QAC3E,eAAe;QACf,IAAI,CAAC,MAAM,CAAC,8EAA8E,CAAC,CAAC;QAE5F,0EAA0E;QAC1E,wEAAwE;QACxE,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,MAAM,CAAC;;;;;;KAMX,CAAC,CAAC;QAEH,qEAAqE;QACrE,0EAA0E;QAC1E,sDAAsD;QACtD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;KAsBX,CAAC,CAAC;QAEH,qBAAqB;QACrB,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;QAEH,mDAAmD;QACnD,4EAA4E;QAC5E,wEAAwE;QACxE,uEAAuE;QACvE,iEAAiE;QACjE,qEAAqE;QACrE,2EAA2E;QAC3E,yDAAyD;QACzD,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC;;;KAGX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,qEAAqE;QACrE,0EAA0E;QAC1E,4EAA4E;QAC5E,SAAS;QACT,IAAI,CAAC,MAAM,CACT,uHAAuH,CACxH,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,iEAAiE,CAAC,CAAC;QAC/E,IAAI,CAAC,MAAM,CACT,+EAA+E,CAChF,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
export declare class Migration20260903T101752NewsletterNamespaceBlockNames extends Migration {
|
|
3
|
+
up(): Promise<void>;
|
|
4
|
+
down(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=20260903T101752_newsletter_namespace_block_names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260903T101752_newsletter_namespace_block_names.d.ts","sourceRoot":"","sources":["../../src/migrations/20260903T101752_newsletter_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAmDlD,qBAAa,qDAAsD,SAAQ,SAAS;IACnE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
import { FROZEN_BLOCK_RENAMES, FROZEN_BLOCK_RENAMES_INVERSE, applyRenameFunctionSql, createRenameFunctionSql, dropRenameFunctionSql, } from '@endora-commerce/page-builder-core/migration';
|
|
3
|
+
/**
|
|
4
|
+
* Namespace the Page Builder block names stored in `newsletter`'s 2 `jsonb` columns
|
|
5
|
+
* — feature 096, T407 (`contracts/block-name-migration.md`).
|
|
6
|
+
*
|
|
7
|
+
* **The rewrite is structural, never textual.** Twelve of the 74 renamed names
|
|
8
|
+
* are ordinary English words that occur throughout shop content — `Row`,
|
|
9
|
+
* `Text`, `Image`, `Map`, `Button` among them — so a text substitution over the
|
|
10
|
+
* column would corrupt a `RawHtml` block's markup and every `alt` attribute in
|
|
11
|
+
* the shop. `pg_temp.rename_block_names_newsletter` descends the document and replaces the value of a
|
|
12
|
+
* `type` property in a **node** position and nothing else; the walk is
|
|
13
|
+
* generated from `FROZEN_BLOCK_RENAMES` by
|
|
14
|
+
* `@endora-commerce/page-builder-core/migration`, so five migrations share one
|
|
15
|
+
* definition of what a node is.
|
|
16
|
+
*
|
|
17
|
+
* **This migration belongs to `newsletter` because `newsletter` owns these tables**,
|
|
18
|
+
* not because it owns the new names (`contracts/block-name-migration.md` §2).
|
|
19
|
+
* Some of the names it writes belong to other modules; that creates no
|
|
20
|
+
* obligation on them and **no new manifest `dependencies` edge**, because a
|
|
21
|
+
* block name is a string value inside a JSONB document — not a foreign key and
|
|
22
|
+
* not a table identifier.
|
|
23
|
+
*
|
|
24
|
+
* **It cannot fail on its input.** A name the map does not hold is left
|
|
25
|
+
* byte-identical: an already-namespaced one and an unrecognised one alike,
|
|
26
|
+
* because the map's domain is bare and its codomain is dotted, so the two sets
|
|
27
|
+
* are disjoint. That is also what makes a second run rewrite nothing — FR-013
|
|
28
|
+
* is a property of the map rather than an outcome a branch has to remember to
|
|
29
|
+
* produce, and no dot test appears in this SQL.
|
|
30
|
+
*
|
|
31
|
+
* `down()` applies the inverse over the identical walk and is **exact for the
|
|
32
|
+
* frozen set**. It is partial by construction: a name with no pre-migration
|
|
33
|
+
* form — a block authored after the upgrade, a third party's `acme.Banner` —
|
|
34
|
+
* has nothing to return to and is left alone. That is correct, and it is why
|
|
35
|
+
* the operator pre-flight (§6) is a backup rather than a `down()`.
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Named per module: the five rename migrations may run on one pooled session,
|
|
39
|
+
* and a second `create function` over the same name fails. Each drops its own
|
|
40
|
+
* when it is done, and a rollback removes it with everything else — `pg_temp`
|
|
41
|
+
* DDL is transactional.
|
|
42
|
+
*/
|
|
43
|
+
const FN = 'rename_block_names_newsletter';
|
|
44
|
+
export class Migration20260903T101752NewsletterNamespaceBlockNames extends Migration {
|
|
45
|
+
async up() {
|
|
46
|
+
this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES));
|
|
47
|
+
this.addSql(applyRenameFunctionSql(FN, 'newsletter_campaigns', 'content'));
|
|
48
|
+
this.addSql(applyRenameFunctionSql(FN, 'newsletter_email_blocks', 'content'));
|
|
49
|
+
this.addSql(dropRenameFunctionSql(FN));
|
|
50
|
+
}
|
|
51
|
+
async down() {
|
|
52
|
+
this.addSql(createRenameFunctionSql(FN, FROZEN_BLOCK_RENAMES_INVERSE));
|
|
53
|
+
this.addSql(applyRenameFunctionSql(FN, 'newsletter_campaigns', 'content'));
|
|
54
|
+
this.addSql(applyRenameFunctionSql(FN, 'newsletter_email_blocks', 'content'));
|
|
55
|
+
this.addSql(dropRenameFunctionSql(FN));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=20260903T101752_newsletter_namespace_block_names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260903T101752_newsletter_namespace_block_names.js","sourceRoot":"","sources":["../../src/migrations/20260903T101752_newsletter_namespace_block_names.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,8CAA8C,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH;;;;;GAKG;AACH,MAAM,EAAE,GAAG,+BAA+B,CAAC;AAE3C,MAAM,OAAO,qDAAsD,SAAQ,SAAS;IACzE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,sBAAsB,EAAE,SAAS,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,yBAAyB,EAAE,SAAS,CAAC,CAAC,CAAC;QAC9E,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE,EAAE,4BAA4B,CAAC,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,sBAAsB,EAAE,SAAS,CAAC,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,sBAAsB,CAAC,EAAE,EAAE,yBAAyB,EAAE,SAAS,CAAC,CAAC,CAAC;QAC9E,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,CAAC;CACF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The tenant-key index on `newsletter_subscribers.customer_account_id`
|
|
4
|
+
* (feature 050, research §R5).
|
|
5
|
+
*
|
|
6
|
+
* It was created by the platform's frozen
|
|
7
|
+
* `Migration20260717T134752CoreTenantScopeIndexes` until
|
|
8
|
+
* `specs/120-migration-closure-bridge-ownership/` Phase 3. Under D-226 the
|
|
9
|
+
* platform declares no dependencies and so can never name a module's table:
|
|
10
|
+
* an instance that omits `newsletter` had a frozen corpus indexing a table
|
|
11
|
+
* nothing builds. `newsletter_subscribers` is this module's own table, so
|
|
12
|
+
* the closure here is trivial.
|
|
13
|
+
*
|
|
14
|
+
* The statement is the frozen one verbatim, `if not exists` and all. A
|
|
15
|
+
* database that has already applied the frozen migration is offered nothing
|
|
16
|
+
* from it — the storage keys on the class name and holds no checksum — and
|
|
17
|
+
* this one is the no-op it already reads as.
|
|
18
|
+
*/
|
|
19
|
+
export declare class Migration20260912T125702NewsletterSubscriberTenantScopeIndex extends Migration {
|
|
20
|
+
up(): Promise<void>;
|
|
21
|
+
down(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T125702_newsletter_subscriber_tenant_scope_index.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,4DAA6D,SAAQ,SAAS;IAC1E,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAMnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* The tenant-key index on `newsletter_subscribers.customer_account_id`
|
|
4
|
+
* (feature 050, research §R5).
|
|
5
|
+
*
|
|
6
|
+
* It was created by the platform's frozen
|
|
7
|
+
* `Migration20260717T134752CoreTenantScopeIndexes` until
|
|
8
|
+
* `specs/120-migration-closure-bridge-ownership/` Phase 3. Under D-226 the
|
|
9
|
+
* platform declares no dependencies and so can never name a module's table:
|
|
10
|
+
* an instance that omits `newsletter` had a frozen corpus indexing a table
|
|
11
|
+
* nothing builds. `newsletter_subscribers` is this module's own table, so
|
|
12
|
+
* the closure here is trivial.
|
|
13
|
+
*
|
|
14
|
+
* The statement is the frozen one verbatim, `if not exists` and all. A
|
|
15
|
+
* database that has already applied the frozen migration is offered nothing
|
|
16
|
+
* from it — the storage keys on the class name and holds no checksum — and
|
|
17
|
+
* this one is the no-op it already reads as.
|
|
18
|
+
*/
|
|
19
|
+
export class Migration20260912T125702NewsletterSubscriberTenantScopeIndex extends Migration {
|
|
20
|
+
async up() {
|
|
21
|
+
this.addSql('create index if not exists "newsletter_subscribers_customer_account_id_index" on "newsletter_subscribers" ("customer_account_id");');
|
|
22
|
+
}
|
|
23
|
+
async down() {
|
|
24
|
+
this.addSql('drop index if exists "newsletter_subscribers_customer_account_id_index";');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=20260912T125702_newsletter_subscriber_tenant_scope_index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260912T125702_newsletter_subscriber_tenant_scope_index.js","sourceRoot":"","sources":["../../src/migrations/20260912T125702_newsletter_subscriber_tenant_scope_index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,4DAA6D,SAAQ,SAAS;IAChF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CACT,oIAAoI,CACrI,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,0EAA0E,CAAC,CAAC;IAC1F,CAAC;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which is the order of this module's own
|
|
10
|
+
* migrations and of nothing else (feature 081): a manifest `dependencies` array
|
|
11
|
+
* is the only thing ordering this block against another module's.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
14
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
15
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
16
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
17
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
22
|
+
* a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260629T200954NewsletterInit } from './20260629T200954_newsletter_init.js';
|
|
25
|
+
import { Migration20260830T212736NewsletterOrganizationAttribution } from './20260830T212736_newsletter_organization_attribution.js';
|
|
26
|
+
import { Migration20260903T101752NewsletterNamespaceBlockNames } from './20260903T101752_newsletter_namespace_block_names.js';
|
|
27
|
+
import { Migration20260912T125702NewsletterSubscriberTenantScopeIndex } from './20260912T125702_newsletter_subscriber_tenant_scope_index.js';
|
|
28
|
+
export declare const migrations: (typeof Migration20260629T200954NewsletterInit)[];
|
|
29
|
+
export { Migration20260629T200954NewsletterInit, Migration20260830T212736NewsletterOrganizationAttribution, Migration20260903T101752NewsletterNamespaceBlockNames, Migration20260912T125702NewsletterSubscriberTenantScopeIndex, };
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sCAAsC,EAAE,MAAM,sCAAsC,CAAC;AAC9F,OAAO,EAAE,yDAAyD,EAAE,MAAM,0DAA0D,CAAC;AACrI,OAAO,EAAE,qDAAqD,EAAE,MAAM,uDAAuD,CAAC;AAC9H,OAAO,EAAE,4DAA4D,EAAE,MAAM,+DAA+D,CAAC;AAE7I,eAAO,MAAM,UAAU,mDAKtB,CAAC;AAEF,OAAO,EACL,sCAAsC,EACtC,yDAAyD,EACzD,qDAAqD,EACrD,4DAA4D,GAC7D,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* Listed in ascending timestamp, which is the order of this module's own
|
|
10
|
+
* migrations and of nothing else (feature 081): a manifest `dependencies` array
|
|
11
|
+
* is the only thing ordering this block against another module's.
|
|
12
|
+
*
|
|
13
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
14
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
15
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
16
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
17
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
18
|
+
* already-migrated database holds.
|
|
19
|
+
*
|
|
20
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
21
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
22
|
+
* a query against a table nobody created.
|
|
23
|
+
*/
|
|
24
|
+
import { Migration20260629T200954NewsletterInit } from './20260629T200954_newsletter_init.js';
|
|
25
|
+
import { Migration20260830T212736NewsletterOrganizationAttribution } from './20260830T212736_newsletter_organization_attribution.js';
|
|
26
|
+
import { Migration20260903T101752NewsletterNamespaceBlockNames } from './20260903T101752_newsletter_namespace_block_names.js';
|
|
27
|
+
import { Migration20260912T125702NewsletterSubscriberTenantScopeIndex } from './20260912T125702_newsletter_subscriber_tenant_scope_index.js';
|
|
28
|
+
export const migrations = [
|
|
29
|
+
Migration20260629T200954NewsletterInit,
|
|
30
|
+
Migration20260830T212736NewsletterOrganizationAttribution,
|
|
31
|
+
Migration20260903T101752NewsletterNamespaceBlockNames,
|
|
32
|
+
Migration20260912T125702NewsletterSubscriberTenantScopeIndex,
|
|
33
|
+
];
|
|
34
|
+
export { Migration20260629T200954NewsletterInit, Migration20260830T212736NewsletterOrganizationAttribution, Migration20260903T101752NewsletterNamespaceBlockNames, Migration20260912T125702NewsletterSubscriberTenantScopeIndex, };
|
|
35
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,sCAAsC,EAAE,MAAM,sCAAsC,CAAC;AAC9F,OAAO,EAAE,yDAAyD,EAAE,MAAM,0DAA0D,CAAC;AACrI,OAAO,EAAE,qDAAqD,EAAE,MAAM,uDAAuD,CAAC;AAC9H,OAAO,EAAE,4DAA4D,EAAE,MAAM,+DAA+D,CAAC;AAE7I,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,sCAAsC;IACtC,yDAAyD;IACzD,qDAAqD;IACrD,4DAA4D;CAC7D,CAAC;AAEF,OAAO,EACL,sCAAsC,EACtC,yDAAyD,EACzD,qDAAqD,EACrD,4DAA4D,GAC7D,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: newsletter
|
|
3
|
+
description: Own-infrastructure newsletter — subscriber list, tags, segments, one-off campaigns and multi-step automations with per-channel opt-in
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `newsletter`
|
|
7
|
+
|
|
8
|
+
Own-infrastructure newsletter sending. Lets operators grow a subscriber list,
|
|
9
|
+
segment it with **tags** and **custom fields**, and reach it through one-off
|
|
10
|
+
**campaigns** and multi-step **automations** (linear send/wait sequences).
|
|
11
|
+
Storefront visitors and signed-in customers subscribe with a per-Sales-Channel
|
|
12
|
+
**opt-in** model; every email carries a working unsubscribe link. Content reuses
|
|
13
|
+
the email-safe renderer and `{{var}}/{{if}}/{{for}}` directive engine from the
|
|
14
|
+
`transactional_emails` stack (`@endora-commerce/email-components`). Bulk delivery goes
|
|
15
|
+
through the module's own configurable **sending provider** (an SMTP adapter that
|
|
16
|
+
reaches Amazon SES SMTP, Mailgun, or any relay), independent of the
|
|
17
|
+
transactional-email transport. The whole module can be **enabled/disabled** so
|
|
18
|
+
it never collides with an external ESP (MailerLite, GetResponse, …).
|
|
19
|
+
|
|
20
|
+
## Concepts
|
|
21
|
+
|
|
22
|
+
- **Subscriber** — keyed by email (global identity). Status is `pending` →
|
|
23
|
+
`active` → `unsubscribed` / `deactivated`. Re-submitting an email merges tags
|
|
24
|
+
and custom fields rather than duplicating. A separate **suppression** list
|
|
25
|
+
(unsubscribe / bounce / complaint), keyed by email, survives deletion and
|
|
26
|
+
overrides all targeting.
|
|
27
|
+
- **Opt-in** — per Sales Channel via Settings `newsletter.opt_in_mode`
|
|
28
|
+
(`single` | `double`). Double opt-in issues a signed, TTL-bounded confirmation
|
|
29
|
+
link; unconfirmed `pending` subscribers expire after
|
|
30
|
+
`newsletter.confirm_ttl_hours`.
|
|
31
|
+
- **Tags & custom fields** — operator-defined; tags drive campaign targeting and
|
|
32
|
+
automation triggers, custom fields enrich subscribers (set via API, Admin UI,
|
|
33
|
+
or signup) and feed automation criteria.
|
|
34
|
+
- **Campaign** — a one-off send to `all` / a manual `group` / a `tag` / a
|
|
35
|
+
`tag_list`. Authored in the shared email Page Builder (same palette as
|
|
36
|
+
transactional emails) with subject + Puck content tree + variables; previewed
|
|
37
|
+
with sample data; sent now or scheduled.
|
|
38
|
+
- **Automation** — a linear `send` / `wait N days` sequence triggered by
|
|
39
|
+
all/tag/tag-list. Send steps use the same email Page Builder. The step model
|
|
40
|
+
is designed to extend to conditional branching later without rework.
|
|
41
|
+
- **Email blocks** — reusable email-safe fragments edited with the same Puck
|
|
42
|
+
editor and embeddable via `EmailInsertBlock` where configured.
|
|
43
|
+
- **Variables** — newsletter catalogue includes `subscriber.email`,
|
|
44
|
+
`customFields.*`, `unsubscribeUrl`, `webviewUrl`, `channel.id`, plus branding
|
|
45
|
+
keys; the admin **Insert variable** picker works on subject and content.
|
|
46
|
+
- **Provider** — selected + configured in the Admin UI; the SMTP password is
|
|
47
|
+
stored as a Settings `secret` (AES-256-GCM, write-only at the boundary).
|
|
48
|
+
|
|
49
|
+
## Delivery
|
|
50
|
+
|
|
51
|
+
Dispatch is queue-backed on Redis/BullMQ:
|
|
52
|
+
|
|
53
|
+
- `newsletter.campaign.plan` — resolves the audience and **atomically claims** a
|
|
54
|
+
`newsletter_send_records` row per recipient (`INSERT … ON CONFLICT DO
|
|
55
|
+
NOTHING`), then enqueues a send job for each freshly-claimed recipient.
|
|
56
|
+
- `newsletter.send` — renders + dispatches one recipient, idempotent on the
|
|
57
|
+
send-record id (used as the provider `messageId`), throttled by the
|
|
58
|
+
send-worker rate limiter (`newsletter.rate_limit_per_second`).
|
|
59
|
+
- `newsletter.automation.step` — executes a step; `wait` steps schedule the next
|
|
60
|
+
step as a BullMQ **delayed job**. A run self-cancels if its subscriber
|
|
61
|
+
unsubscribes mid-sequence.
|
|
62
|
+
|
|
63
|
+
Workers run under the separable `worker.ts` entrypoint and pause when the module
|
|
64
|
+
is disabled. Producers only enqueue — never inline-execute — so N≥2 workers
|
|
65
|
+
never double-send.
|
|
66
|
+
|
|
67
|
+
## Engagement
|
|
68
|
+
|
|
69
|
+
Open tracking uses a 1×1 pixel; click tracking rewrites links through a signed
|
|
70
|
+
redirect. Per-campaign counts of sent / delivered / failed / opened / clicked
|
|
71
|
+
(plus per-link clicks) are aggregated from `newsletter_send_records` and
|
|
72
|
+
`newsletter_engagement_events`. Tracking can be disabled per campaign.
|
|
73
|
+
|
|
74
|
+
## Permissions
|
|
75
|
+
|
|
76
|
+
- `newsletter:read` — view subscribers, campaigns, automations, stats.
|
|
77
|
+
- `newsletter:write` — manage subscribers, campaigns, automations, blocks, and
|
|
78
|
+
the sending provider.
|
|
79
|
+
|
|
80
|
+
## Storefront
|
|
81
|
+
|
|
82
|
+
A reusable signup component (server action, tag-attachable), a double-opt-in
|
|
83
|
+
confirmation landing, an unsubscribe page (optional reason), and an account
|
|
84
|
+
panel showing subscription status + tags with subscribe/unsubscribe actions.
|
|
85
|
+
|
|
86
|
+
## Schema
|
|
87
|
+
|
|
88
|
+
Migration `083_newsletter_init.ts` creates `newsletter_subscribers`,
|
|
89
|
+
`newsletter_tags`, `newsletter_subscriber_tags`, `newsletter_custom_fields`,
|
|
90
|
+
`newsletter_suppressions`, `newsletter_email_blocks`(+ channel bridge),
|
|
91
|
+
`newsletter_campaigns`(+ group bridge), `newsletter_send_records`,
|
|
92
|
+
`newsletter_engagement_events`, `newsletter_automations`, and
|
|
93
|
+
`newsletter_automation_runs`. Provider config + opt-in mode live in the
|
|
94
|
+
**Settings** module (no bespoke credential table).
|
package/i18n/en.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"actions.openNewsletter.label": "Newsletter",
|
|
3
|
+
"actions.openNewsletter.description": "Manage newsletter subscribers, campaigns, and automations",
|
|
4
|
+
"actions.newCampaign.label": "New newsletter campaign",
|
|
5
|
+
"actions.newCampaign.description": "Create and send a new newsletter campaign",
|
|
6
|
+
"newsletter.nav.subscribers": "Subscribers",
|
|
7
|
+
"newsletter.nav.campaigns": "Campaigns",
|
|
8
|
+
"newsletter.nav.automations": "Automations",
|
|
9
|
+
"newsletter.nav.tags": "Tags",
|
|
10
|
+
"newsletter.nav.customFields": "Custom fields",
|
|
11
|
+
"newsletter.nav.provider": "Sending provider",
|
|
12
|
+
"newsletter.editor.viewportDesktop": "Desktop mail (600px)",
|
|
13
|
+
"newsletter.editor.viewportNarrow": "Narrow (320px)",
|
|
14
|
+
"newsletter.editor.insertVariable": "Insert variable",
|
|
15
|
+
"newsletter.editor.variable": "Variable",
|
|
16
|
+
"newsletter.editor.livePreview": "Live HTML preview",
|
|
17
|
+
"actions.openCampaigns.label": "Newsletter campaigns",
|
|
18
|
+
"actions.openCampaigns.description": "Newsletter campaigns & sends",
|
|
19
|
+
"actions.openAutomations.label": "Newsletter automations",
|
|
20
|
+
"actions.openAutomations.description": "Automated newsletter workflows",
|
|
21
|
+
"nav.subscribers.label": "Subscribers",
|
|
22
|
+
"nav.campaigns.label": "Campaigns",
|
|
23
|
+
"nav.automations.label": "Automations",
|
|
24
|
+
"nav.tags.label": "Tags & Fields",
|
|
25
|
+
"nav.blocks.label": "Blocks",
|
|
26
|
+
"nav.provider.label": "Provider"
|
|
27
|
+
}
|
package/i18n/pl.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"actions.openNewsletter.label": "Newsletter",
|
|
3
|
+
"actions.openNewsletter.description": "Zarządzaj subskrybentami, kampaniami i automatyzacjami newslettera",
|
|
4
|
+
"actions.newCampaign.label": "Nowa kampania newslettera",
|
|
5
|
+
"actions.newCampaign.description": "Utwórz i wyślij nową kampanię newslettera",
|
|
6
|
+
"newsletter.nav.subscribers": "Subskrybenci",
|
|
7
|
+
"newsletter.nav.campaigns": "Kampanie",
|
|
8
|
+
"newsletter.nav.automations": "Automatyzacje",
|
|
9
|
+
"newsletter.nav.tags": "Tagi",
|
|
10
|
+
"newsletter.nav.customFields": "Pola dodatkowe",
|
|
11
|
+
"newsletter.nav.provider": "Usługa wysyłki",
|
|
12
|
+
"newsletter.editor.viewportDesktop": "Mail desktop (600px)",
|
|
13
|
+
"newsletter.editor.viewportNarrow": "Wąski (320px)",
|
|
14
|
+
"newsletter.editor.insertVariable": "Wstaw zmienną",
|
|
15
|
+
"newsletter.editor.variable": "Zmienna",
|
|
16
|
+
"newsletter.editor.livePreview": "Podgląd HTML na żywo",
|
|
17
|
+
"actions.openCampaigns.label": "Kampanie newslettera",
|
|
18
|
+
"actions.openCampaigns.description": "Kampanie newslettera i wysyłki",
|
|
19
|
+
"actions.openAutomations.label": "Automatyzacje newslettera",
|
|
20
|
+
"actions.openAutomations.description": "Automatyczne procesy newslettera",
|
|
21
|
+
"nav.subscribers.label": "Subskrybenci",
|
|
22
|
+
"nav.campaigns.label": "Kampanie",
|
|
23
|
+
"nav.automations.label": "Automatyzacje",
|
|
24
|
+
"nav.tags.label": "Tagi i pola",
|
|
25
|
+
"nav.blocks.label": "Bloki",
|
|
26
|
+
"nav.provider.label": "Usługa wysyłki"
|
|
27
|
+
}
|