@endora-commerce/mod-pwa 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 +63 -0
- package/dist/admin/api/pwa-client.d.ts +15 -0
- package/dist/admin/api/pwa-client.d.ts.map +1 -0
- package/dist/admin/api/pwa-client.js +56 -0
- package/dist/admin/api/pwa-client.js.map +1 -0
- package/dist/admin/components/PushAudienceRuleBuilder.d.ts +19 -0
- package/dist/admin/components/PushAudienceRuleBuilder.d.ts.map +1 -0
- package/dist/admin/components/PushAudienceRuleBuilder.js +211 -0
- package/dist/admin/components/PushAudienceRuleBuilder.js.map +1 -0
- package/dist/admin/index.d.ts +39 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +61 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/PwaPage.d.ts +16 -0
- package/dist/admin/pages/PwaPage.d.ts.map +1 -0
- package/dist/admin/pages/PwaPage.js +207 -0
- package/dist/admin/pages/PwaPage.js.map +1 -0
- package/dist/backend/entities/push-message-delivery.entity.d.ts +18 -0
- package/dist/backend/entities/push-message-delivery.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-message-delivery.entity.js +68 -0
- package/dist/backend/entities/push-message-delivery.entity.js.map +1 -0
- package/dist/backend/entities/push-message.entity.d.ts +26 -0
- package/dist/backend/entities/push-message.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-message.entity.js +102 -0
- package/dist/backend/entities/push-message.entity.js.map +1 -0
- package/dist/backend/entities/push-subscription.entity.d.ts +49 -0
- package/dist/backend/entities/push-subscription.entity.d.ts.map +1 -0
- package/dist/backend/entities/push-subscription.entity.js +117 -0
- package/dist/backend/entities/push-subscription.entity.js.map +1 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.d.ts +21 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.d.ts.map +1 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.js +69 -0
- package/dist/backend/entities/pwa-icon-rendition.entity.js.map +1 -0
- package/dist/backend/index.d.ts +100 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +107 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +119 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +109 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/request-actor.d.ts +24 -0
- package/dist/backend/request-actor.d.ts.map +1 -0
- package/dist/backend/request-actor.js +29 -0
- package/dist/backend/request-actor.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +52 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +222 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +29 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +66 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/cross-module-context.d.ts +90 -0
- package/dist/backend/services/cross-module-context.d.ts.map +1 -0
- package/dist/backend/services/cross-module-context.js +84 -0
- package/dist/backend/services/cross-module-context.js.map +1 -0
- package/dist/backend/services/providers/web-push-provider.d.ts +31 -0
- package/dist/backend/services/providers/web-push-provider.d.ts.map +1 -0
- package/dist/backend/services/providers/web-push-provider.js +72 -0
- package/dist/backend/services/providers/web-push-provider.js.map +1 -0
- package/dist/backend/services/push-audience-evaluator.d.ts +24 -0
- package/dist/backend/services/push-audience-evaluator.d.ts.map +1 -0
- package/dist/backend/services/push-audience-evaluator.js +29 -0
- package/dist/backend/services/push-audience-evaluator.js.map +1 -0
- package/dist/backend/services/push-delivery-queue.d.ts +14 -0
- package/dist/backend/services/push-delivery-queue.d.ts.map +1 -0
- package/dist/backend/services/push-delivery-queue.js +30 -0
- package/dist/backend/services/push-delivery-queue.js.map +1 -0
- package/dist/backend/services/push-event-subscriber.d.ts +86 -0
- package/dist/backend/services/push-event-subscriber.d.ts.map +1 -0
- package/dist/backend/services/push-event-subscriber.js +109 -0
- package/dist/backend/services/push-event-subscriber.js.map +1 -0
- package/dist/backend/services/push-message-service.d.ts +58 -0
- package/dist/backend/services/push-message-service.d.ts.map +1 -0
- package/dist/backend/services/push-message-service.js +145 -0
- package/dist/backend/services/push-message-service.js.map +1 -0
- package/dist/backend/services/push-provider-registry.d.ts +17 -0
- package/dist/backend/services/push-provider-registry.d.ts.map +1 -0
- package/dist/backend/services/push-provider-registry.js +32 -0
- package/dist/backend/services/push-provider-registry.js.map +1 -0
- package/dist/backend/services/push-subscription-service.d.ts +67 -0
- package/dist/backend/services/push-subscription-service.d.ts.map +1 -0
- package/dist/backend/services/push-subscription-service.js +165 -0
- package/dist/backend/services/push-subscription-service.js.map +1 -0
- package/dist/backend/services/pwa-config-resolver.d.ts +29 -0
- package/dist/backend/services/pwa-config-resolver.d.ts.map +1 -0
- package/dist/backend/services/pwa-config-resolver.js +92 -0
- package/dist/backend/services/pwa-config-resolver.js.map +1 -0
- package/dist/backend/services/pwa-icon-service.d.ts +44 -0
- package/dist/backend/services/pwa-icon-service.d.ts.map +1 -0
- package/dist/backend/services/pwa-icon-service.js +105 -0
- package/dist/backend/services/pwa-icon-service.js.map +1 -0
- package/dist/backend/workers/push-delivery-worker.d.ts +17 -0
- package/dist/backend/workers/push-delivery-worker.d.ts.map +1 -0
- package/dist/backend/workers/push-delivery-worker.js +105 -0
- package/dist/backend/workers/push-delivery-worker.js.map +1 -0
- package/dist/manifest.d.ts +184 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +207 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260625T144228_pwa_init.d.ts +18 -0
- package/dist/migrations/20260625T144228_pwa_init.d.ts.map +1 -0
- package/dist/migrations/20260625T144228_pwa_init.js +105 -0
- package/dist/migrations/20260625T144228_pwa_init.js.map +1 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.d.ts +102 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.d.ts.map +1 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.js +169 -0
- package/dist/migrations/20260830T172022_pwa_organization_attribution.js.map +1 -0
- package/dist/migrations/index.d.ts +28 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +31 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/pwa.md +93 -0
- package/i18n/en.json +43 -0
- package/i18n/pl.json +43 -0
- package/package.json +105 -0
- package/tailwind.css +14 -0
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { defineModuleManifest, defineModuleSettingsManifest, PWA_PERMISSIONS, PWA_SETTING_CODES, } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* PWA module — feature 046.
|
|
4
|
+
*
|
|
5
|
+
* Adds Progressive-Web-App capabilities to the storefront and admin: home-screen
|
|
6
|
+
* installability, a Settings-driven per-channel installable identity (icon, name,
|
|
7
|
+
* theme), opt-in static-asset caching, a controlled service-worker update path,
|
|
8
|
+
* and opt-in push notifications behind a provider-agnostic abstraction (default
|
|
9
|
+
* backend: Web-Push/VAPID; FCM/OneSignal are drop-in providers).
|
|
10
|
+
*
|
|
11
|
+
* Configuration lives in the Settings module (group `pwa`) and resolves per Sales
|
|
12
|
+
* Channel via the standard global + override chain (FR-011). VAPID private key and
|
|
13
|
+
* the optional FCM service-account JSON are `secret` value types (encrypted at rest).
|
|
14
|
+
*/
|
|
15
|
+
// Every PWA setting is managed exclusively from the dedicated PWA settings
|
|
16
|
+
// page (/settings/pwa). Marking them `hidden: true` (applied below) keeps them
|
|
17
|
+
// out of the generic Settings screen so each value lives in exactly one place.
|
|
18
|
+
const pwaSettingEntries = [
|
|
19
|
+
{
|
|
20
|
+
code: PWA_SETTING_CODES.APP_NAME,
|
|
21
|
+
name: 'App name',
|
|
22
|
+
description: 'Installed-app name shown on the device home screen (manifest `name`).',
|
|
23
|
+
groupCode: 'pwa',
|
|
24
|
+
valueType: 'string',
|
|
25
|
+
defaultValue: 'B2B Platform',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: PWA_SETTING_CODES.SHORT_NAME,
|
|
29
|
+
name: 'Short name',
|
|
30
|
+
description: 'Short installed-app name used where space is constrained (manifest `short_name`).',
|
|
31
|
+
groupCode: 'pwa',
|
|
32
|
+
valueType: 'string',
|
|
33
|
+
defaultValue: 'B2B',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
code: PWA_SETTING_CODES.THEME_COLOR,
|
|
37
|
+
name: 'Theme color',
|
|
38
|
+
description: 'Hex theme color applied to the installed-app UI (manifest `theme_color`).',
|
|
39
|
+
groupCode: 'pwa',
|
|
40
|
+
valueType: 'string',
|
|
41
|
+
defaultValue: '#1d4ed8',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
code: PWA_SETTING_CODES.BACKGROUND_COLOR,
|
|
45
|
+
name: 'Background color',
|
|
46
|
+
description: 'Hex background color shown on the splash screen (manifest `background_color`).',
|
|
47
|
+
groupCode: 'pwa',
|
|
48
|
+
valueType: 'string',
|
|
49
|
+
defaultValue: '#fafafa',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: PWA_SETTING_CODES.DISPLAY_MODE,
|
|
53
|
+
name: 'Display mode',
|
|
54
|
+
description: "How the installed app launches: 'standalone', 'fullscreen' or 'minimal-ui'.",
|
|
55
|
+
groupCode: 'pwa',
|
|
56
|
+
valueType: 'string',
|
|
57
|
+
defaultValue: 'standalone',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: PWA_SETTING_CODES.ICON_ASSET_ID,
|
|
61
|
+
name: 'Icon source asset id',
|
|
62
|
+
description: 'Assets-library id of the uploaded source icon. Empty falls back to bundled placeholder icons.',
|
|
63
|
+
groupCode: 'pwa',
|
|
64
|
+
valueType: 'string',
|
|
65
|
+
defaultValue: '',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
code: PWA_SETTING_CODES.CACHING_ENABLED,
|
|
69
|
+
name: 'Static-asset caching enabled',
|
|
70
|
+
description: 'When on, the storefront service worker caches static assets for faster repeat visits and an offline shell.',
|
|
71
|
+
groupCode: 'pwa',
|
|
72
|
+
valueType: 'boolean',
|
|
73
|
+
defaultValue: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
code: PWA_SETTING_CODES.PUSH_ENABLED,
|
|
77
|
+
name: 'Push notifications enabled',
|
|
78
|
+
description: 'When on, the storefront offers a push opt-in and the platform can deliver notifications to subscribed devices.',
|
|
79
|
+
groupCode: 'pwa',
|
|
80
|
+
valueType: 'boolean',
|
|
81
|
+
defaultValue: false,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: PWA_SETTING_CODES.VAPID_PUBLIC_KEY,
|
|
85
|
+
name: 'VAPID public key',
|
|
86
|
+
description: 'Public VAPID key served to the storefront subscribe flow. Generated together with the private key.',
|
|
87
|
+
groupCode: 'pwa',
|
|
88
|
+
valueType: 'string',
|
|
89
|
+
defaultValue: '',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
code: PWA_SETTING_CODES.VAPID_PRIVATE_KEY,
|
|
93
|
+
name: 'VAPID private key',
|
|
94
|
+
description: 'Private VAPID key used to sign Web-Push messages. Write-only: encrypted at rest and never returned after saving.',
|
|
95
|
+
groupCode: 'pwa',
|
|
96
|
+
valueType: 'secret',
|
|
97
|
+
defaultValue: '',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
code: PWA_SETTING_CODES.FCM_SERVICE_ACCOUNT,
|
|
101
|
+
name: 'FCM service account (JSON)',
|
|
102
|
+
description: 'Optional Firebase service-account JSON for the token-based FCM provider. Write-only: encrypted at rest.',
|
|
103
|
+
groupCode: 'pwa',
|
|
104
|
+
valueType: 'secret',
|
|
105
|
+
defaultValue: '',
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const settings = defineModuleSettingsManifest({
|
|
109
|
+
moduleCode: 'pwa',
|
|
110
|
+
groups: [{ code: 'pwa', name: 'Progressive Web App' }],
|
|
111
|
+
settings: [
|
|
112
|
+
...pwaSettingEntries.map((entry) => ({ ...entry, hidden: true })),
|
|
113
|
+
{
|
|
114
|
+
// Feature 073 — the operator's activation control. Deliberately NOT
|
|
115
|
+
// hidden: every other setting here is internal plumbing the PWA screen
|
|
116
|
+
// manages, while this one is the operator's own switch and belongs on
|
|
117
|
+
// `/platform/modules`.
|
|
118
|
+
code: 'pwa.enabled',
|
|
119
|
+
name: 'Progressive Web App enabled',
|
|
120
|
+
description: 'Switches the installable-app manifest, the push-subscription registry and push delivery on or off. Nothing is dropped: subscriptions and icons stay in the database, and pushes resume for the same subscribers when you switch it back on.',
|
|
121
|
+
groupCode: 'pwa',
|
|
122
|
+
valueType: 'boolean',
|
|
123
|
+
defaultValue: true,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
/** Settings-only export consumed by the boot-time ManifestReconciler list. */
|
|
128
|
+
export const pwaSettingsManifest = settings;
|
|
129
|
+
export const manifest = defineModuleManifest({
|
|
130
|
+
id: 'pwa',
|
|
131
|
+
name: 'Progressive Web App',
|
|
132
|
+
description: 'Installability, static-asset caching, controlled service-worker updates, and provider-agnostic push notifications for the storefront and admin.',
|
|
133
|
+
version: '1.0.0',
|
|
134
|
+
/**
|
|
135
|
+
* What this module needs from the environment (`specs/117-instance-bring-up/`
|
|
136
|
+
* FR-002). Only what it **owns**: its reads of platform-owned names are
|
|
137
|
+
* satisfied by `packages/platform/src/env/index.ts`.
|
|
138
|
+
*
|
|
139
|
+
* Why each of these is not a Setting is its entry in
|
|
140
|
+
* `backend/scripts/ledgers/module-environment-inputs/pwa.ts`.
|
|
141
|
+
*/
|
|
142
|
+
env: [
|
|
143
|
+
{
|
|
144
|
+
name: 'PWA_VAPID_SUBJECT',
|
|
145
|
+
describes: {
|
|
146
|
+
en: 'The contact address push services are given for this shop, so a provider can reach somebody about its notifications.',
|
|
147
|
+
pl: 'Adres kontaktowy przekazywany usługom push dla tego sklepu, aby dostawca miał się z kim skontaktować w sprawie powiadomień.',
|
|
148
|
+
},
|
|
149
|
+
requirement: {
|
|
150
|
+
kind: 'optional',
|
|
151
|
+
without: {
|
|
152
|
+
en: 'Push services are given the platform’s placeholder contact rather than this shop’s, so a provider with a delivery problem has nobody to write to.',
|
|
153
|
+
pl: 'Usługi push dostają zastępczy kontakt platformy zamiast kontaktu tego sklepu, więc dostawca mający problem z doręczeniem nie ma do kogo napisać.',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
secret: false,
|
|
157
|
+
generable: false,
|
|
158
|
+
owner: { kind: 'module', moduleId: 'pwa' },
|
|
159
|
+
consumers: ['backend'],
|
|
160
|
+
addressOf: null,
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
// `auth` owns the `requireAdmin` port the admin routes are gated by; feature
|
|
164
|
+
// 072 made it a container resolution rather than a constructor argument.
|
|
165
|
+
//
|
|
166
|
+
// `customer_accounts` and `organizations` arrived with feature 075 Phase C:
|
|
167
|
+
// the push Rule Builder's three target pickers and the rule-audience
|
|
168
|
+
// expansion read rows those modules own, and they used to read them off the
|
|
169
|
+
// tables directly. Both are **binding**: a picker that silently lists nothing
|
|
170
|
+
// and an audience that silently resolves to nobody both read as legitimate
|
|
171
|
+
// answers, so the seam fails closed. `price_lists` was the third, for
|
|
172
|
+
// `customerGroupReadPort`; feature 076 (D-79) moved that port to
|
|
173
|
+
// `customer_accounts`, which this module already declares, and the port check
|
|
174
|
+
// confirms nothing else here resolves out of `price_lists`.
|
|
175
|
+
//
|
|
176
|
+
// `orders` arrived with `specs/110-instance-repository/` T118c: the FR-024
|
|
177
|
+
// order-status auto-trigger reads the order it is announcing over
|
|
178
|
+
// `orderReadPort`, which a composition root used to read on this module's
|
|
179
|
+
// behalf inside `pwaBridge`. It is **binding**, and it costs no operator a
|
|
180
|
+
// control, derived rather than waived: `orders` is `nonDeactivatable`.
|
|
181
|
+
// `assets_library` and `sales_channels` are `nonDeactivatable` as well, and
|
|
182
|
+
// both were already declared, so the two ports that drained beside it owe no
|
|
183
|
+
// `refuses-without` sentence either. The kernel's
|
|
184
|
+
// `salesChannelResolutionPort` is a platform name and puts nothing here at
|
|
185
|
+
// all.
|
|
186
|
+
dependencies: [
|
|
187
|
+
'_i18n',
|
|
188
|
+
'_lifecycle',
|
|
189
|
+
'assets_library',
|
|
190
|
+
'orders',
|
|
191
|
+
'sales_channels',
|
|
192
|
+
'settings',
|
|
193
|
+
'auth',
|
|
194
|
+
'customer_accounts',
|
|
195
|
+
'organizations',
|
|
196
|
+
],
|
|
197
|
+
settings,
|
|
198
|
+
i18n: { bundlesDir: 'i18n' },
|
|
199
|
+
docs: { dir: 'docs' },
|
|
200
|
+
permissions: [
|
|
201
|
+
{ code: PWA_PERMISSIONS.READ, label: 'View PWA settings' },
|
|
202
|
+
{ code: PWA_PERMISSIONS.WRITE, label: 'Configure PWA' },
|
|
203
|
+
{ code: PWA_PERMISSIONS.SEND_PUSH, label: 'Send push notifications' },
|
|
204
|
+
],
|
|
205
|
+
activation: { settingCode: 'pwa.enabled', default: true },
|
|
206
|
+
});
|
|
207
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* PWA module init (feature 046).
|
|
4
|
+
*
|
|
5
|
+
* Four tables owned by the `pwa` module:
|
|
6
|
+
* - push_subscriptions — one subscribed device (Web-Push), unique endpoint
|
|
7
|
+
* - push_messages — a notification (admin- or event-triggered)
|
|
8
|
+
* - push_message_deliveries — (message × subscription) delivery, idempotency unit
|
|
9
|
+
* - pwa_icon_renditions — derived icon sizes per channel (global = null channel)
|
|
10
|
+
*
|
|
11
|
+
* PWA identity/toggles are NOT a table — they live in the Settings module
|
|
12
|
+
* (group `pwa`, seeded by the manifest reconciler).
|
|
13
|
+
*/
|
|
14
|
+
export declare class Migration20260625T144228PwaInit extends Migration {
|
|
15
|
+
up(): Promise<void>;
|
|
16
|
+
down(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=20260625T144228_pwa_init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260625T144228_pwa_init.d.ts","sourceRoot":"","sources":["../../src/migrations/20260625T144228_pwa_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,qBAAa,+BAAgC,SAAQ,SAAS;IAC7C,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IA6GnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAMrC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* PWA module init (feature 046).
|
|
4
|
+
*
|
|
5
|
+
* Four tables owned by the `pwa` module:
|
|
6
|
+
* - push_subscriptions — one subscribed device (Web-Push), unique endpoint
|
|
7
|
+
* - push_messages — a notification (admin- or event-triggered)
|
|
8
|
+
* - push_message_deliveries — (message × subscription) delivery, idempotency unit
|
|
9
|
+
* - pwa_icon_renditions — derived icon sizes per channel (global = null channel)
|
|
10
|
+
*
|
|
11
|
+
* PWA identity/toggles are NOT a table — they live in the Settings module
|
|
12
|
+
* (group `pwa`, seeded by the manifest reconciler).
|
|
13
|
+
*/
|
|
14
|
+
export class Migration20260625T144228PwaInit extends Migration {
|
|
15
|
+
async up() {
|
|
16
|
+
this.addSql(`
|
|
17
|
+
create table "push_subscriptions" (
|
|
18
|
+
"id" uuid not null,
|
|
19
|
+
"sales_channel_id" uuid not null,
|
|
20
|
+
"customer_account_id" uuid null,
|
|
21
|
+
"endpoint" text not null,
|
|
22
|
+
"p256dh" text not null,
|
|
23
|
+
"auth" text not null,
|
|
24
|
+
"provider" varchar(32) not null default 'web_push',
|
|
25
|
+
"status" varchar(16) not null default 'active',
|
|
26
|
+
"user_agent" text null,
|
|
27
|
+
"created_at" timestamptz not null,
|
|
28
|
+
"last_seen_at" timestamptz not null,
|
|
29
|
+
constraint "push_subscriptions_pkey" primary key ("id"),
|
|
30
|
+
constraint "push_subscriptions_status_check" check ("status" in ('active', 'invalid'))
|
|
31
|
+
);
|
|
32
|
+
`);
|
|
33
|
+
this.addSql(`alter table "push_subscriptions" add constraint "push_subscriptions_endpoint_unique" unique ("endpoint");`);
|
|
34
|
+
this.addSql(`create index "push_subscriptions_channel_status_idx" on "push_subscriptions" ("sales_channel_id", "status");`);
|
|
35
|
+
this.addSql(`create index "push_subscriptions_customer_idx" on "push_subscriptions" ("customer_account_id") where "customer_account_id" is not null;`);
|
|
36
|
+
this.addSql(`
|
|
37
|
+
create table "push_messages" (
|
|
38
|
+
"id" uuid not null,
|
|
39
|
+
"sales_channel_id" uuid not null,
|
|
40
|
+
"title" varchar(120) not null,
|
|
41
|
+
"body" varchar(500) not null,
|
|
42
|
+
"icon_url" text null,
|
|
43
|
+
"url" text null,
|
|
44
|
+
"audience" jsonb not null,
|
|
45
|
+
"trigger" varchar(32) not null,
|
|
46
|
+
"source_event_id" varchar(128) null,
|
|
47
|
+
"status" varchar(16) not null default 'queued',
|
|
48
|
+
"sent_count" int not null default 0,
|
|
49
|
+
"failed_count" int not null default 0,
|
|
50
|
+
"created_by_admin_user_id" uuid null,
|
|
51
|
+
"created_at" timestamptz not null,
|
|
52
|
+
"sent_at" timestamptz null,
|
|
53
|
+
constraint "push_messages_pkey" primary key ("id"),
|
|
54
|
+
constraint "push_messages_trigger_check" check ("trigger" in ('admin', 'order_status', 'quote_request')),
|
|
55
|
+
constraint "push_messages_status_check" check ("status" in ('queued', 'sending', 'sent', 'failed'))
|
|
56
|
+
);
|
|
57
|
+
`);
|
|
58
|
+
this.addSql(`create index "push_messages_channel_created_idx" on "push_messages" ("sales_channel_id", "created_at" desc);`);
|
|
59
|
+
this.addSql(`create unique index "push_messages_trigger_event_uniq" on "push_messages" ("trigger", "source_event_id") where "source_event_id" is not null;`);
|
|
60
|
+
this.addSql(`
|
|
61
|
+
create table "push_message_deliveries" (
|
|
62
|
+
"id" uuid not null,
|
|
63
|
+
"message_id" uuid not null,
|
|
64
|
+
"subscription_id" uuid not null,
|
|
65
|
+
"status" varchar(16) not null default 'pending',
|
|
66
|
+
"attempts" int not null default 0,
|
|
67
|
+
"last_error" text null,
|
|
68
|
+
"attempted_at" timestamptz null,
|
|
69
|
+
"created_at" timestamptz not null,
|
|
70
|
+
constraint "push_message_deliveries_pkey" primary key ("id"),
|
|
71
|
+
constraint "push_message_deliveries_status_check" check ("status" in ('pending', 'sent', 'failed', 'pruned'))
|
|
72
|
+
);
|
|
73
|
+
`);
|
|
74
|
+
this.addSql(`alter table "push_message_deliveries" add constraint "push_message_deliveries_message_id_foreign" foreign key ("message_id") references "push_messages" ("id") on update cascade on delete cascade;`);
|
|
75
|
+
this.addSql(`alter table "push_message_deliveries" add constraint "push_message_deliveries_subscription_id_foreign" foreign key ("subscription_id") references "push_subscriptions" ("id") on update cascade on delete cascade;`);
|
|
76
|
+
this.addSql(`alter table "push_message_deliveries" add constraint "push_message_deliveries_msg_sub_unique" unique ("message_id", "subscription_id");`);
|
|
77
|
+
this.addSql(`create index "push_message_deliveries_status_idx" on "push_message_deliveries" ("status");`);
|
|
78
|
+
this.addSql(`
|
|
79
|
+
create table "pwa_icon_renditions" (
|
|
80
|
+
"id" uuid not null,
|
|
81
|
+
"sales_channel_id" uuid null,
|
|
82
|
+
"source_asset_id" uuid not null,
|
|
83
|
+
"size" int not null,
|
|
84
|
+
"purpose" varchar(16) not null,
|
|
85
|
+
"asset_id" uuid not null,
|
|
86
|
+
"content_hash" varchar(64) not null,
|
|
87
|
+
"created_at" timestamptz not null,
|
|
88
|
+
constraint "pwa_icon_renditions_pkey" primary key ("id"),
|
|
89
|
+
constraint "pwa_icon_renditions_purpose_check" check ("purpose" in ('any', 'maskable'))
|
|
90
|
+
);
|
|
91
|
+
`);
|
|
92
|
+
// One rendition per (size, purpose) per scope. Two partial unique indexes so
|
|
93
|
+
// the rule holds for both per-channel rows and the single global (null-channel)
|
|
94
|
+
// row without relying on PG15-only NULLS NOT DISTINCT.
|
|
95
|
+
this.addSql(`create unique index "pwa_icon_renditions_channel_size_purpose_uniq" on "pwa_icon_renditions" ("sales_channel_id", "size", "purpose") where "sales_channel_id" is not null;`);
|
|
96
|
+
this.addSql(`create unique index "pwa_icon_renditions_global_size_purpose_uniq" on "pwa_icon_renditions" ("size", "purpose") where "sales_channel_id" is null;`);
|
|
97
|
+
}
|
|
98
|
+
async down() {
|
|
99
|
+
this.addSql(`drop table if exists "pwa_icon_renditions" cascade;`);
|
|
100
|
+
this.addSql(`drop table if exists "push_message_deliveries" cascade;`);
|
|
101
|
+
this.addSql(`drop table if exists "push_messages" cascade;`);
|
|
102
|
+
this.addSql(`drop table if exists "push_subscriptions" cascade;`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=20260625T144228_pwa_init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260625T144228_pwa_init.js","sourceRoot":"","sources":["../../src/migrations/20260625T144228_pwa_init.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,+BAAgC,SAAQ,SAAS;IACnD,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;KAgBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,2GAA2G,CAC5G,CAAC;QACF,IAAI,CAAC,MAAM,CACT,8GAA8G,CAC/G,CAAC;QACF,IAAI,CAAC,MAAM,CACT,yIAAyI,CAC1I,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;KAqBX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,8GAA8G,CAC/G,CAAC;QACF,IAAI,CAAC,MAAM,CACT,+IAA+I,CAChJ,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,qMAAqM,CACtM,CAAC;QACF,IAAI,CAAC,MAAM,CACT,oNAAoN,CACrN,CAAC;QACF,IAAI,CAAC,MAAM,CACT,yIAAyI,CAC1I,CAAC;QACF,IAAI,CAAC,MAAM,CACT,4FAA4F,CAC7F,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;KAaX,CAAC,CAAC;QACH,6EAA6E;QAC7E,gFAAgF;QAChF,uDAAuD;QACvD,IAAI,CAAC,MAAM,CACT,4KAA4K,CAC7K,CAAC;QACF,IAAI,CAAC,MAAM,CACT,mJAAmJ,CACpJ,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,qDAAqD,CAAC,CAAC;QACnE,IAAI,CAAC,MAAM,CAAC,yDAAyD,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,+CAA+C,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,CAAC,oDAAoD,CAAC,CAAC;IACpE,CAAC;CACF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* D-187 — `push_subscriptions` gains its organisation, and a row that names a
|
|
4
|
+
* customer account carries one.
|
|
5
|
+
*
|
|
6
|
+
* Feature 087 Group B, class 2 of 4 (`specs/087-tenant-scope-enforcement/`).
|
|
7
|
+
* `PushSubscription` is `@CustomerScoped`, and until this migration the
|
|
8
|
+
* `allowed-set` arm of `customerFilterCond` had no column to grant on: a sales
|
|
9
|
+
* representative assigned to the buyer's own organisation was shown **none** of
|
|
10
|
+
* their subscribed devices and told so, through the
|
|
11
|
+
* `ORGANIZATION_ATTRIBUTION_PENDING` notice the refusing arm records. The
|
|
12
|
+
* column is what turns that into an answer.
|
|
13
|
+
*
|
|
14
|
+
* ## The column and the read arrive together, on purpose
|
|
15
|
+
*
|
|
16
|
+
* `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
|
|
17
|
+
* entity carries `organizationId`, **per query**, so the entity property in
|
|
18
|
+
* this same merge request is what switches the grant on — there is no third
|
|
19
|
+
* artefact, no flag and no staging. At the same instant the notice retires
|
|
20
|
+
* itself: it is keyed on 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 every
|
|
34
|
+
* device that subscribes before anybody signs in (FR-023 — `customer_account_id`
|
|
35
|
+
* has been nullable since `20260625T144228_pwa_init.ts`), and who such a device
|
|
36
|
+
* belongs to is R-6's open question. The equivalence would answer it in the
|
|
37
|
+
* schema, which D-187 declines to do.
|
|
38
|
+
*
|
|
39
|
+
* **What that deliberately leaves open, and where it is held instead.** The
|
|
40
|
+
* equivalence would also close the opposite shape — an **ownerless** row still
|
|
41
|
+
* carrying an organisation — and this table is the one in Group B that can
|
|
42
|
+
* produce it. Its write is an upsert on `endpoint`, so re-subscribing a
|
|
43
|
+
* previously owned device anonymously clears the account, and an implication
|
|
44
|
+
* says nothing about the organisation left behind. That row would be visible to
|
|
45
|
+
* a representative of an organisation it no longer belongs to. It is closed at
|
|
46
|
+
* the write instead of in the schema: `PushSubscriptionService` writes both
|
|
47
|
+
* columns from one resolved owner value, so there is no branch in which one
|
|
48
|
+
* moves without the other, and `admin-and-storefront.contract.test.ts` asserts
|
|
49
|
+
* the de-association end to end. A constraint here would have bought the same
|
|
50
|
+
* guarantee at the price of deciding R-6.
|
|
51
|
+
*
|
|
52
|
+
* ## No foreign key
|
|
53
|
+
*
|
|
54
|
+
* Matching `Cart` and `Comparison`, neither of which carries one. D-187
|
|
55
|
+
* withdraws `r1-spike.md` §8's `on delete set null` recommendation as
|
|
56
|
+
* contradicting FR-011: it produces a row that names an account and no
|
|
57
|
+
* organisation, which is precisely the state this migration makes unreachable,
|
|
58
|
+
* and under the constraint below it would abort an unrelated organisation
|
|
59
|
+
* delete with a message about a table the operator was not touching. If one is
|
|
60
|
+
* ever taken here it must be `restrict`, and it is not this migration's
|
|
61
|
+
* decision.
|
|
62
|
+
*
|
|
63
|
+
* ## Derive, then count, then refuse — never delete
|
|
64
|
+
*
|
|
65
|
+
* The derivation is the owning account's own organisation. **On this table the
|
|
66
|
+
* refusal is a real branch, not a formality**, and that is why it is written
|
|
67
|
+
* with as much care as the derivation: unlike `comparisons`, `push_subscriptions`
|
|
68
|
+
* has **no foreign key** on `customer_account_id` (the init migration declares
|
|
69
|
+
* the column `uuid null` and constrains nothing), so a row naming an account
|
|
70
|
+
* that is gone is representable here.
|
|
71
|
+
*
|
|
72
|
+
* It is nevertheless empty today, and the reason is worth knowing rather than
|
|
73
|
+
* hoping: a customer account is never hard-deleted in this tree.
|
|
74
|
+
* `customer-account-lifecycle-ports.ts` soft-deletes, restores and
|
|
75
|
+
* **anonymises** — the last one rewrites the e-mail and the name and keeps the
|
|
76
|
+
* row — and every one of those still satisfies `organization_id NOT NULL`
|
|
77
|
+
* (D-178), so it still derives. Change any of that and the count below is the
|
|
78
|
+
* only thing that would notice.
|
|
79
|
+
*
|
|
80
|
+
* It raises with the count and up to twenty ids and deletes nothing (D-184), in
|
|
81
|
+
* the shape `20260825T141659_customer_accounts_organization_required.ts`
|
|
82
|
+
* established and `20260830T112911_carts_organization_attribution_check.ts`
|
|
83
|
+
* repeated.
|
|
84
|
+
*
|
|
85
|
+
* ## Nothing to declare in the manifest
|
|
86
|
+
*
|
|
87
|
+
* `pwa` already declares both `customer_accounts` and `organizations` in its
|
|
88
|
+
* manifest `dependencies`, so the table this reads is created before this runs.
|
|
89
|
+
* No foreign key is added, so `fk-dependency-drift` has nothing to say either,
|
|
90
|
+
* and a migration naming another module's table is outside
|
|
91
|
+
* `check:module-boundary`'s population by that check's own rule.
|
|
92
|
+
*
|
|
93
|
+
* The standing guard is
|
|
94
|
+
* `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
|
|
95
|
+
* which derives its population from the ORM's metadata rather than from a list
|
|
96
|
+
* — so this class is covered by gaining the column, with no edit to that file.
|
|
97
|
+
*/
|
|
98
|
+
export declare class Migration20260830T172022PwaOrganizationAttribution extends Migration {
|
|
99
|
+
up(): Promise<void>;
|
|
100
|
+
down(): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=20260830T172022_pwa_organization_attribution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260830T172022_pwa_organization_attribution.d.ts","sourceRoot":"","sources":["../../src/migrations/20260830T172022_pwa_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FG;AACH,qBAAa,kDAAmD,SAAQ,SAAS;IAChE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAiEnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAWrC"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* D-187 — `push_subscriptions` gains its organisation, and a row that names a
|
|
4
|
+
* customer account carries one.
|
|
5
|
+
*
|
|
6
|
+
* Feature 087 Group B, class 2 of 4 (`specs/087-tenant-scope-enforcement/`).
|
|
7
|
+
* `PushSubscription` is `@CustomerScoped`, and until this migration the
|
|
8
|
+
* `allowed-set` arm of `customerFilterCond` had no column to grant on: a sales
|
|
9
|
+
* representative assigned to the buyer's own organisation was shown **none** of
|
|
10
|
+
* their subscribed devices and told so, through the
|
|
11
|
+
* `ORGANIZATION_ATTRIBUTION_PENDING` notice the refusing arm records. The
|
|
12
|
+
* column is what turns that into an answer.
|
|
13
|
+
*
|
|
14
|
+
* ## The column and the read arrive together, on purpose
|
|
15
|
+
*
|
|
16
|
+
* `customerOrganizationColumn` asks the ORM's own metadata whether the filtered
|
|
17
|
+
* entity carries `organizationId`, **per query**, so the entity property in
|
|
18
|
+
* this same merge request is what switches the grant on — there is no third
|
|
19
|
+
* artefact, no flag and no staging. At the same instant the notice retires
|
|
20
|
+
* itself: it is keyed on 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 every
|
|
34
|
+
* device that subscribes before anybody signs in (FR-023 — `customer_account_id`
|
|
35
|
+
* has been nullable since `20260625T144228_pwa_init.ts`), and who such a device
|
|
36
|
+
* belongs to is R-6's open question. The equivalence would answer it in the
|
|
37
|
+
* schema, which D-187 declines to do.
|
|
38
|
+
*
|
|
39
|
+
* **What that deliberately leaves open, and where it is held instead.** The
|
|
40
|
+
* equivalence would also close the opposite shape — an **ownerless** row still
|
|
41
|
+
* carrying an organisation — and this table is the one in Group B that can
|
|
42
|
+
* produce it. Its write is an upsert on `endpoint`, so re-subscribing a
|
|
43
|
+
* previously owned device anonymously clears the account, and an implication
|
|
44
|
+
* says nothing about the organisation left behind. That row would be visible to
|
|
45
|
+
* a representative of an organisation it no longer belongs to. It is closed at
|
|
46
|
+
* the write instead of in the schema: `PushSubscriptionService` writes both
|
|
47
|
+
* columns from one resolved owner value, so there is no branch in which one
|
|
48
|
+
* moves without the other, and `admin-and-storefront.contract.test.ts` asserts
|
|
49
|
+
* the de-association end to end. A constraint here would have bought the same
|
|
50
|
+
* guarantee at the price of deciding R-6.
|
|
51
|
+
*
|
|
52
|
+
* ## No foreign key
|
|
53
|
+
*
|
|
54
|
+
* Matching `Cart` and `Comparison`, neither of which carries one. D-187
|
|
55
|
+
* withdraws `r1-spike.md` §8's `on delete set null` recommendation as
|
|
56
|
+
* contradicting FR-011: it produces a row that names an account and no
|
|
57
|
+
* organisation, which is precisely the state this migration makes unreachable,
|
|
58
|
+
* and under the constraint below it would abort an unrelated organisation
|
|
59
|
+
* delete with a message about a table the operator was not touching. If one is
|
|
60
|
+
* ever taken here it must be `restrict`, and it is not this migration's
|
|
61
|
+
* decision.
|
|
62
|
+
*
|
|
63
|
+
* ## Derive, then count, then refuse — never delete
|
|
64
|
+
*
|
|
65
|
+
* The derivation is the owning account's own organisation. **On this table the
|
|
66
|
+
* refusal is a real branch, not a formality**, and that is why it is written
|
|
67
|
+
* with as much care as the derivation: unlike `comparisons`, `push_subscriptions`
|
|
68
|
+
* has **no foreign key** on `customer_account_id` (the init migration declares
|
|
69
|
+
* the column `uuid null` and constrains nothing), so a row naming an account
|
|
70
|
+
* that is gone is representable here.
|
|
71
|
+
*
|
|
72
|
+
* It is nevertheless empty today, and the reason is worth knowing rather than
|
|
73
|
+
* hoping: a customer account is never hard-deleted in this tree.
|
|
74
|
+
* `customer-account-lifecycle-ports.ts` soft-deletes, restores and
|
|
75
|
+
* **anonymises** — the last one rewrites the e-mail and the name and keeps the
|
|
76
|
+
* row — and every one of those still satisfies `organization_id NOT NULL`
|
|
77
|
+
* (D-178), so it still derives. Change any of that and the count below is the
|
|
78
|
+
* only thing that would notice.
|
|
79
|
+
*
|
|
80
|
+
* It raises with the count and up to twenty ids and deletes nothing (D-184), in
|
|
81
|
+
* the shape `20260825T141659_customer_accounts_organization_required.ts`
|
|
82
|
+
* established and `20260830T112911_carts_organization_attribution_check.ts`
|
|
83
|
+
* repeated.
|
|
84
|
+
*
|
|
85
|
+
* ## Nothing to declare in the manifest
|
|
86
|
+
*
|
|
87
|
+
* `pwa` already declares both `customer_accounts` and `organizations` in its
|
|
88
|
+
* manifest `dependencies`, so the table this reads is created before this runs.
|
|
89
|
+
* No foreign key is added, so `fk-dependency-drift` has nothing to say either,
|
|
90
|
+
* and a migration naming another module's table is outside
|
|
91
|
+
* `check:module-boundary`'s population by that check's own rule.
|
|
92
|
+
*
|
|
93
|
+
* The standing guard is
|
|
94
|
+
* `backend/test/integration/tenancy/customer-scoped-organization-completeness.test.ts`,
|
|
95
|
+
* which derives its population from the ORM's metadata rather than from a list
|
|
96
|
+
* — so this class is covered by gaining the column, with no edit to that file.
|
|
97
|
+
*/
|
|
98
|
+
export class Migration20260830T172022PwaOrganizationAttribution extends Migration {
|
|
99
|
+
async up() {
|
|
100
|
+
// 1. The column. Nullable, because an anonymous device legitimately has
|
|
101
|
+
// none (FR-011/FR-023) and this table is mostly anonymous in practice.
|
|
102
|
+
this.addSql(`alter table "push_subscriptions" add column "organization_id" uuid null;`);
|
|
103
|
+
// 2. Derive the organisation from the account that owns the subscription.
|
|
104
|
+
// Every account carries one since D-178, including a soft-deleted or
|
|
105
|
+
// anonymised one, so this is total for every row whose account is still
|
|
106
|
+
// there. Step 3 is about the rows for which it is not.
|
|
107
|
+
this.addSql(`
|
|
108
|
+
update "push_subscriptions" ps
|
|
109
|
+
set "organization_id" = ca."organization_id"
|
|
110
|
+
from "customer_accounts" ca
|
|
111
|
+
where ca."id" = ps."customer_account_id"
|
|
112
|
+
and ps."organization_id" is null;
|
|
113
|
+
`);
|
|
114
|
+
// 3. Count what is left and refuse. This table has no foreign key on
|
|
115
|
+
// `customer_account_id`, so a dangling account id is representable and
|
|
116
|
+
// this branch is real. It deletes nothing (D-184): a subscribed device
|
|
117
|
+
// is a customer's, and a migration is not the place to decide that one
|
|
118
|
+
// of them should stop receiving notifications.
|
|
119
|
+
this.addSql(`
|
|
120
|
+
do $$
|
|
121
|
+
declare
|
|
122
|
+
remaining bigint;
|
|
123
|
+
sample text;
|
|
124
|
+
begin
|
|
125
|
+
select count(*) into remaining
|
|
126
|
+
from "push_subscriptions"
|
|
127
|
+
where "customer_account_id" is not null
|
|
128
|
+
and "organization_id" is null;
|
|
129
|
+
if remaining > 0 then
|
|
130
|
+
select string_agg(id::text, ', ') into sample from (
|
|
131
|
+
select "id" from "push_subscriptions"
|
|
132
|
+
where "customer_account_id" is not null
|
|
133
|
+
and "organization_id" is null
|
|
134
|
+
order by "id" limit 20
|
|
135
|
+
) s;
|
|
136
|
+
raise exception
|
|
137
|
+
'D-187: % push_subscriptions 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 - find out how they got here.',
|
|
138
|
+
remaining, sample;
|
|
139
|
+
end if;
|
|
140
|
+
end $$;
|
|
141
|
+
`);
|
|
142
|
+
// 4. The constraint.
|
|
143
|
+
this.addSql(`
|
|
144
|
+
alter table "push_subscriptions"
|
|
145
|
+
add constraint "push_subscriptions_organization_attribution_chk"
|
|
146
|
+
check ("customer_account_id" is null or "organization_id" is not null);
|
|
147
|
+
`);
|
|
148
|
+
// 5. The index the entity property declares. Partial, matching this table's
|
|
149
|
+
// own owner index — `push_subscriptions_customer_idx` is
|
|
150
|
+
// `("customer_account_id") where "customer_account_id" is not null` —
|
|
151
|
+
// for the same reason: the column is null on every anonymous device, and
|
|
152
|
+
// the reader is an organisation-scoped list.
|
|
153
|
+
this.addSql(`
|
|
154
|
+
create index "push_subscriptions_organization_idx"
|
|
155
|
+
on "push_subscriptions" ("organization_id")
|
|
156
|
+
where "organization_id" is not null;
|
|
157
|
+
`);
|
|
158
|
+
}
|
|
159
|
+
async down() {
|
|
160
|
+
// The constraint, the index and the column. The organisations step 2
|
|
161
|
+
// derived go with the column they were written into — there is nothing to
|
|
162
|
+
// preserve, because each of them was already implied by the account its row
|
|
163
|
+
// names.
|
|
164
|
+
this.addSql(`alter table "push_subscriptions" drop constraint if exists "push_subscriptions_organization_attribution_chk";`);
|
|
165
|
+
this.addSql(`drop index if exists "push_subscriptions_organization_idx";`);
|
|
166
|
+
this.addSql(`alter table "push_subscriptions" drop column if exists "organization_id";`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=20260830T172022_pwa_organization_attribution.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260830T172022_pwa_organization_attribution.js","sourceRoot":"","sources":["../../src/migrations/20260830T172022_pwa_organization_attribution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+FG;AACH,MAAM,OAAO,kDAAmD,SAAQ,SAAS;IACtE,KAAK,CAAC,EAAE;QACf,wEAAwE;QACxE,0EAA0E;QAC1E,IAAI,CAAC,MAAM,CAAC,0EAA0E,CAAC,CAAC;QAExF,0EAA0E;QAC1E,wEAAwE;QACxE,2EAA2E;QAC3E,0DAA0D;QAC1D,IAAI,CAAC,MAAM,CAAC;;;;;;KAMX,CAAC,CAAC;QAEH,qEAAqE;QACrE,0EAA0E;QAC1E,0EAA0E;QAC1E,0EAA0E;QAC1E,kDAAkD;QAClD,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;KAsBX,CAAC,CAAC;QAEH,qBAAqB;QACrB,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;QAEH,4EAA4E;QAC5E,4DAA4D;QAC5D,yEAAyE;QACzE,4EAA4E;QAC5E,gDAAgD;QAChD,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,qEAAqE;QACrE,0EAA0E;QAC1E,4EAA4E;QAC5E,SAAS;QACT,IAAI,CAAC,MAAM,CACT,+GAA+G,CAChH,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,6DAA6D,CAAC,CAAC;QAC3E,IAAI,CAAC,MAAM,CAAC,2EAA2E,CAAC,CAAC;IAC3F,CAAC;CACF"}
|