@saasicat/spec 1.0.0-rc.6 → 1.0.0-rc.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/admin-api.openapi.yaml +13 -14
- package/package.json +4 -2
- package/prisma-fragments/01-subscription.prisma +15 -3
- package/prisma-fragments/03-plan-versions.prisma +3 -4
- package/prisma-fragments/05-bundle.prisma +4 -5
- package/prisma-fragments/06-catalog-entries.prisma +21 -21
- package/prisma-fragments/07-promotion.prisma +2 -3
- package/prisma-fragments/08-subscription-contract.prisma +3 -4
- package/prisma-fragments/11-subscription-bundle.prisma +17 -0
- package/schemas/plan-catalog.schema.json +2 -7
- package/sql/1.0-remove-project-key.postgres.sql +203 -0
- package/sql/constraints.postgres.sql +19 -0
- package/sql/reference-schema.postgres.sql +40 -30
package/README.md
CHANGED
|
@@ -83,5 +83,6 @@ pnpm add @saasicat/spec
|
|
|
83
83
|
## Next
|
|
84
84
|
|
|
85
85
|
- [Data model](../../docs/explanation/data-model.md) — the tables the fragments declare
|
|
86
|
-
- [From capability to contract](../../docs/explanation/capability-to-contract.md) — what the schemas
|
|
86
|
+
- [From capability to contract](../../docs/explanation/capability-to-contract.md) — what the schemas
|
|
87
|
+
describe
|
|
87
88
|
- [Codegen](../../docs/explanation/adr/0006-spec-to-types-codegen.md) — how types are derived from them
|
package/admin-api.openapi.yaml
CHANGED
|
@@ -29,7 +29,7 @@ openapi: 3.1.0
|
|
|
29
29
|
|
|
30
30
|
info:
|
|
31
31
|
title: SaaS Platform SuperAdmin API
|
|
32
|
-
version: 1.0.0-rc.
|
|
32
|
+
version: 1.0.0-rc.7
|
|
33
33
|
description: |
|
|
34
34
|
Read and write operations for platform administration:
|
|
35
35
|
tenants, users, subscriptions, promo codes, audit log,
|
|
@@ -1391,10 +1391,8 @@ components:
|
|
|
1391
1391
|
# ────────── Catalog V2 (DTO = SSOT: packages/nest/src/catalog/dto/*) ──────────
|
|
1392
1392
|
CatalogPlanCreate:
|
|
1393
1393
|
type: object
|
|
1394
|
-
required: [
|
|
1394
|
+
required: [planKey, label]
|
|
1395
1395
|
properties:
|
|
1396
|
-
projectKey:
|
|
1397
|
-
{ type: string, pattern: '^[a-z][a-z0-9-]*$', description: 'kebab-case' }
|
|
1398
1396
|
planKey:
|
|
1399
1397
|
{
|
|
1400
1398
|
type: string,
|
|
@@ -1435,9 +1433,8 @@ components:
|
|
|
1435
1433
|
|
|
1436
1434
|
CatalogBundleCreate:
|
|
1437
1435
|
type: object
|
|
1438
|
-
required: [
|
|
1436
|
+
required: [bundleKey, label]
|
|
1439
1437
|
properties:
|
|
1440
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1441
1438
|
bundleKey: { type: string, pattern: '^[A-Z][A-Z0-9_]*$', description: 'no hyphen' }
|
|
1442
1439
|
label: { type: string, minLength: 1, maxLength: 120 }
|
|
1443
1440
|
description: { type: string, maxLength: 2000 }
|
|
@@ -1495,9 +1492,8 @@ components:
|
|
|
1495
1492
|
|
|
1496
1493
|
CatalogMarketingProjectionCreate:
|
|
1497
1494
|
type: object
|
|
1498
|
-
required: [
|
|
1495
|
+
required: [targetType, targetVersionId, displayLabel, description]
|
|
1499
1496
|
properties:
|
|
1500
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1501
1497
|
targetType: { type: string, enum: [PLAN, BUNDLE] }
|
|
1502
1498
|
targetVersionId: { type: string }
|
|
1503
1499
|
locale: { type: string, pattern: '^[a-z]{2}(-[A-Z]{2})?$', default: de }
|
|
@@ -1613,9 +1609,8 @@ components:
|
|
|
1613
1609
|
|
|
1614
1610
|
CatalogMarketingSettingsUpdate:
|
|
1615
1611
|
type: object
|
|
1616
|
-
required: [
|
|
1612
|
+
required: [activeLocales]
|
|
1617
1613
|
properties:
|
|
1618
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1619
1614
|
activeLocales:
|
|
1620
1615
|
type: array
|
|
1621
1616
|
uniqueItems: true
|
|
@@ -1623,9 +1618,8 @@ components:
|
|
|
1623
1618
|
|
|
1624
1619
|
CatalogPromotionCreate:
|
|
1625
1620
|
type: object
|
|
1626
|
-
required: [
|
|
1621
|
+
required: [internalLabel, type, value, validFrom, validTo]
|
|
1627
1622
|
properties:
|
|
1628
|
-
projectKey: { type: string, pattern: '^[a-z][a-z0-9-]*$' }
|
|
1629
1623
|
internalLabel: { type: string, maxLength: 120 }
|
|
1630
1624
|
type: { type: string, enum: [percent, amount, intro, freeMonths] }
|
|
1631
1625
|
value:
|
|
@@ -1698,10 +1692,15 @@ components:
|
|
|
1698
1692
|
# ────────── Plan-Catalog (mirror of plans.yaml) ──────────
|
|
1699
1693
|
PlanCatalog:
|
|
1700
1694
|
type: object
|
|
1701
|
-
required: [schemaVersion,
|
|
1695
|
+
required: [schemaVersion, app, currency, vatRate, plans]
|
|
1702
1696
|
properties:
|
|
1703
1697
|
schemaVersion: { type: integer, const: 1 }
|
|
1704
|
-
|
|
1698
|
+
app:
|
|
1699
|
+
type: object
|
|
1700
|
+
required: [name]
|
|
1701
|
+
description: 'App identity — the only place the application is named'
|
|
1702
|
+
properties:
|
|
1703
|
+
name: { type: string, minLength: 1 }
|
|
1705
1704
|
currency: { type: string, pattern: '^[A-Z]{3}$' }
|
|
1706
1705
|
vatRate: { type: number, format: decimal, description: 'in percent' }
|
|
1707
1706
|
features:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/spec",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.7",
|
|
4
4
|
"description": "Language-neutral spec of the SaaS platform: JSON Schemas, OpenAPI contract, Prisma fragments, acceptance scenarios.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"ajv": "^8.17.1",
|
|
40
40
|
"ajv-formats": "^3.0.1",
|
|
41
|
+
"pg": "^8.22.0",
|
|
41
42
|
"prisma": "^6.19.0"
|
|
42
43
|
},
|
|
43
44
|
"keywords": [
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
},
|
|
64
65
|
"scripts": {
|
|
65
66
|
"gen:sql": "node scripts/generate-reference-sql.mjs",
|
|
66
|
-
"test": "node --test tests/*.test.js"
|
|
67
|
+
"test": "node --test tests/*.test.js",
|
|
68
|
+
"test:integration": "node --test 'tests/integration/*.test.js'"
|
|
67
69
|
}
|
|
68
70
|
}
|
|
@@ -84,6 +84,19 @@ model Subscription {
|
|
|
84
84
|
// field. Readers treat null as "the period end is the answer".
|
|
85
85
|
minimumTermUntil DateTime?
|
|
86
86
|
|
|
87
|
+
// The day of the month this subscription is billed on, 1–31.
|
|
88
|
+
//
|
|
89
|
+
// Written when a period window is opened — at activation, and again at a
|
|
90
|
+
// plan change that resets the window — and never rewritten by a renewal.
|
|
91
|
+
// That is the whole point: a renewal reads it, and reading its own previous
|
|
92
|
+
// result is how the day used to drift. A subscription starting on the 31st
|
|
93
|
+
// was billed on the 28th from its first February onwards.
|
|
94
|
+
//
|
|
95
|
+
// Null on a row that predates this column, and on one with no period at
|
|
96
|
+
// all. Readers fall back to the day of whichever date opened the window,
|
|
97
|
+
// which is the old behaviour for one step and correct for that step.
|
|
98
|
+
billingAnchorDay Int?
|
|
99
|
+
|
|
87
100
|
// Plan versioning — binds to a published PlanVersion.
|
|
88
101
|
// `pendingPlanVersion*` concern a version change within the
|
|
89
102
|
// same plan; `pendingPlan` the change to a different plan. Both
|
|
@@ -188,8 +201,7 @@ model SubscriptionPaymentMethod {
|
|
|
188
201
|
// -----------------------------------------------------------------------------
|
|
189
202
|
|
|
190
203
|
model CheckoutOffer {
|
|
191
|
-
id
|
|
192
|
-
projectKey String
|
|
204
|
+
id String @id @default(uuid())
|
|
193
205
|
|
|
194
206
|
planKey String
|
|
195
207
|
planVersionId String?
|
|
@@ -214,6 +226,6 @@ model CheckoutOffer {
|
|
|
214
226
|
createdAt DateTime @default(now())
|
|
215
227
|
updatedAt DateTime @updatedAt
|
|
216
228
|
|
|
217
|
-
@@index([
|
|
229
|
+
@@index([status])
|
|
218
230
|
@@map("checkout_offers")
|
|
219
231
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// -----------------------------------------------------------------------------
|
|
21
21
|
// Plan — root identity of a plan (SPEC_V2 §11.1 M6).
|
|
22
22
|
// `planKey` is the business plan identity (STARTER / STANDARD / PROFESSIONAL),
|
|
23
|
-
// unique
|
|
23
|
+
// unique for the installation. PlanVersion.planId points **softly** to
|
|
24
24
|
// `Plan.planKey` (no DB FK — the binding is enforced via service logic in M6
|
|
25
25
|
// so that the greenfield cutover can run without a destructive schema
|
|
26
26
|
// migration).
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
|
|
33
33
|
model Plan {
|
|
34
34
|
id String @id @default(uuid())
|
|
35
|
-
projectKey String
|
|
36
35
|
planKey String
|
|
37
36
|
label String
|
|
38
37
|
description String?
|
|
@@ -43,8 +42,8 @@ model Plan {
|
|
|
43
42
|
updatedAt DateTime @updatedAt
|
|
44
43
|
deletedAt DateTime?
|
|
45
44
|
|
|
46
|
-
@@unique([
|
|
47
|
-
@@index([
|
|
45
|
+
@@unique([planKey])
|
|
46
|
+
@@index([deletedAt])
|
|
48
47
|
@@map("plans")
|
|
49
48
|
}
|
|
50
49
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Global catalog tables (no tenantId, no RLS). Write access only via the
|
|
8
8
|
// SUPER_ADMIN guard in the AdminController of the `@saasicat/nest`
|
|
9
|
-
// package.
|
|
10
|
-
//
|
|
9
|
+
// package. One installation serves one application, so `bundleKey` is the
|
|
10
|
+
// whole identity.
|
|
11
11
|
//
|
|
12
12
|
// Versioning pattern identical to PlanVersion:
|
|
13
13
|
// - at most 1 draft (publishedAt IS NULL) per identity — constraint via a
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
|
|
26
26
|
model Bundle {
|
|
27
27
|
id String @id @default(uuid())
|
|
28
|
-
projectKey String // e.g. 'clubapp' | 'demoapp' | …
|
|
29
28
|
bundleKey String // SCREAMING_SNAKE_CASE, e.g. 'SPORT', 'ACCOUNTING', 'EVERSEND_PEPPOL'
|
|
30
29
|
label String
|
|
31
30
|
description String?
|
|
@@ -41,8 +40,8 @@ model Bundle {
|
|
|
41
40
|
|
|
42
41
|
versions BundleVersion[]
|
|
43
42
|
|
|
44
|
-
@@unique([
|
|
45
|
-
@@index([
|
|
43
|
+
@@unique([bundleKey])
|
|
44
|
+
@@index([deletedAt])
|
|
46
45
|
@@map("bundles")
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
|
|
43
43
|
model CapabilityCatalogEntry {
|
|
44
44
|
id String @id @default(uuid())
|
|
45
|
-
projectKey String // e.g. 'clubapp' | 'demoapp' | …
|
|
46
45
|
capabilityKey String // 'invoice.create' | 'member.archive' | …
|
|
47
46
|
label String
|
|
48
47
|
description String?
|
|
@@ -77,9 +76,9 @@ model CapabilityCatalogEntry {
|
|
|
77
76
|
updatedAt DateTime @updatedAt
|
|
78
77
|
deletedAt DateTime?
|
|
79
78
|
|
|
80
|
-
@@unique([
|
|
81
|
-
@@index([
|
|
82
|
-
@@index([
|
|
79
|
+
@@unique([capabilityKey])
|
|
80
|
+
@@index([codeStatus])
|
|
81
|
+
@@index([featureKey])
|
|
83
82
|
@@map("capability_catalog_entries")
|
|
84
83
|
}
|
|
85
84
|
|
|
@@ -100,7 +99,6 @@ model CapabilityCatalogEntry {
|
|
|
100
99
|
|
|
101
100
|
model FeatureCatalogEntry {
|
|
102
101
|
id String @id @default(uuid())
|
|
103
|
-
projectKey String
|
|
104
102
|
featureKey String // 'INVOICE_MANAGEMENT' | 'MEMBER_LIST' | …
|
|
105
103
|
label String
|
|
106
104
|
description String?
|
|
@@ -148,9 +146,9 @@ model FeatureCatalogEntry {
|
|
|
148
146
|
updatedAt DateTime @updatedAt
|
|
149
147
|
deletedAt DateTime?
|
|
150
148
|
|
|
151
|
-
@@unique([
|
|
152
|
-
@@index([
|
|
153
|
-
@@index([
|
|
149
|
+
@@unique([featureKey])
|
|
150
|
+
@@index([discoveryStatus])
|
|
151
|
+
@@index([plannedOnly])
|
|
154
152
|
@@map("feature_catalog_entries")
|
|
155
153
|
}
|
|
156
154
|
|
|
@@ -164,7 +162,6 @@ model FeatureCatalogEntry {
|
|
|
164
162
|
|
|
165
163
|
model QuotaCatalogEntry {
|
|
166
164
|
id String @id @default(uuid())
|
|
167
|
-
projectKey String
|
|
168
165
|
quotaKey String // 'invoicesPerMonth' | 'members' | 'storageGb' | …
|
|
169
166
|
label String
|
|
170
167
|
description String?
|
|
@@ -200,8 +197,8 @@ model QuotaCatalogEntry {
|
|
|
200
197
|
updatedAt DateTime @updatedAt
|
|
201
198
|
deletedAt DateTime?
|
|
202
199
|
|
|
203
|
-
@@unique([
|
|
204
|
-
@@index([
|
|
200
|
+
@@unique([quotaKey])
|
|
201
|
+
@@index([discoveryStatus])
|
|
205
202
|
@@map("quota_catalog_entries")
|
|
206
203
|
}
|
|
207
204
|
|
|
@@ -223,8 +220,7 @@ model QuotaCatalogEntry {
|
|
|
223
220
|
// -----------------------------------------------------------------------------
|
|
224
221
|
|
|
225
222
|
model MarketingProjection {
|
|
226
|
-
id
|
|
227
|
-
projectKey String
|
|
223
|
+
id String @id @default(uuid())
|
|
228
224
|
|
|
229
225
|
// Polymorphic reference to a versioned entity.
|
|
230
226
|
targetType String // 'PLAN' | 'BUNDLE'
|
|
@@ -268,21 +264,25 @@ model MarketingProjection {
|
|
|
268
264
|
updatedAt DateTime @updatedAt
|
|
269
265
|
|
|
270
266
|
@@unique([targetType, targetVersionId, locale])
|
|
271
|
-
@@index([
|
|
267
|
+
@@index([targetType, locale, priority])
|
|
272
268
|
@@map("marketing_projections")
|
|
273
269
|
}
|
|
274
270
|
|
|
275
271
|
// -----------------------------------------------------------------------------
|
|
276
|
-
// MarketingSettings —
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
272
|
+
// MarketingSettings — installation-wide, runtime-editable marketing config.
|
|
273
|
+
// At most one row. The constant default gets a caller that omits `id` onto the
|
|
274
|
+
// right row, but it is not what holds the rule — a default does not apply to a
|
|
275
|
+
// caller that supplies one, and a primary key accepts every distinct value. The
|
|
276
|
+
// `CHECK` in sql/constraints.postgres.sql is what holds it; the Prisma DSL
|
|
277
|
+
// cannot say it here.
|
|
278
|
+
// Currently exactly one field: `activeLocales` — the subset of the
|
|
279
|
+
// `availableLocales` pool activated in the marketing catalog (the pool comes
|
|
280
|
+
// from the app config, SPEC_V2 §6.5). If the row is missing, the full pool
|
|
281
|
+
// counts as active.
|
|
281
282
|
// -----------------------------------------------------------------------------
|
|
282
283
|
|
|
283
284
|
model MarketingSettings {
|
|
284
|
-
id
|
|
285
|
-
projectKey String @unique
|
|
285
|
+
id String @id @default("marketing-settings")
|
|
286
286
|
|
|
287
287
|
activeLocales Json @default("[]") // string[]
|
|
288
288
|
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
// via the SUPER_ADMIN guard in the AdminController.
|
|
15
15
|
|
|
16
16
|
model Promotion {
|
|
17
|
-
id
|
|
18
|
-
projectKey String // e.g. 'clubapp' | 'demoapp' | …
|
|
17
|
+
id String @id @default(uuid())
|
|
19
18
|
|
|
20
19
|
// Internal label (not public).
|
|
21
20
|
internalLabel String
|
|
@@ -60,6 +59,6 @@ model Promotion {
|
|
|
60
59
|
createdAt DateTime @default(now())
|
|
61
60
|
updatedAt DateTime @updatedAt
|
|
62
61
|
|
|
63
|
-
@@index([
|
|
62
|
+
@@index([targetType, validFrom, validTo])
|
|
64
63
|
@@map("promotions")
|
|
65
64
|
}
|
|
@@ -25,9 +25,8 @@ enum ContractLineItemKind {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
model SubscriptionContract {
|
|
28
|
-
id
|
|
29
|
-
|
|
30
|
-
tenantId String
|
|
28
|
+
id String @id @default(uuid())
|
|
29
|
+
tenantId String
|
|
31
30
|
|
|
32
31
|
status SubscriptionContractStatus @default(active)
|
|
33
32
|
effectiveFrom DateTime
|
|
@@ -54,7 +53,7 @@ model SubscriptionContract {
|
|
|
54
53
|
// tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
55
54
|
|
|
56
55
|
@@index([tenantId, status, effectiveFrom])
|
|
57
|
-
@@index([
|
|
56
|
+
@@index([status])
|
|
58
57
|
@@index([originalOfferId])
|
|
59
58
|
@@map("subscription_contracts")
|
|
60
59
|
}
|
|
@@ -29,6 +29,23 @@ model SubscriptionBundle {
|
|
|
29
29
|
// only at `minimumTermEndsAt`.
|
|
30
30
|
minimumTermEndsAt DateTime?
|
|
31
31
|
|
|
32
|
+
// The rhythm this booking is billed in, and the window it is billed for.
|
|
33
|
+
//
|
|
34
|
+
// A bundle may run in a shorter rhythm than its plan but never a longer
|
|
35
|
+
// one, so a yearly bundle exists only beside a yearly plan. Its periods end
|
|
36
|
+
// on the day the plan's do: the first one is short, from the booking to the
|
|
37
|
+
// next occurrence of that day, and every one after it runs anchor to
|
|
38
|
+
// anchor. Aligned at booking rather than trimmed at the end, because a trim
|
|
39
|
+
// means somebody was committed to more than they received.
|
|
40
|
+
//
|
|
41
|
+
// Null on a booking made before these columns existed, and on one whose
|
|
42
|
+
// plan has no period at all — a trial, or a subscription awaiting sales.
|
|
43
|
+
// Readers fall back to the plan's cycle, which is what every booking used
|
|
44
|
+
// before this.
|
|
45
|
+
billingCycle String?
|
|
46
|
+
currentPeriodStart DateTime?
|
|
47
|
+
currentPeriodEnd DateTime?
|
|
48
|
+
|
|
32
49
|
// Cancellation: requested at `canceledAt`, effective at
|
|
33
50
|
// `canceledEffectiveAt` (period/term end). Both null = active booking.
|
|
34
51
|
canceledAt DateTime?
|
|
@@ -4,22 +4,17 @@
|
|
|
4
4
|
"title": "PlanCatalog",
|
|
5
5
|
"description": "Language-neutral definition of a plan catalog + app identity for a SaaS app. Maintained by the consumer as a YAML/JSON file (`config/saas.yaml`) and mirrored into platform tables at backend boot.",
|
|
6
6
|
"type": "object",
|
|
7
|
-
"required": ["schemaVersion", "
|
|
7
|
+
"required": ["schemaVersion", "app", "currency", "vatRate"],
|
|
8
8
|
"additionalProperties": false,
|
|
9
9
|
"properties": {
|
|
10
10
|
"schemaVersion": {
|
|
11
11
|
"type": "integer",
|
|
12
12
|
"const": 1
|
|
13
13
|
},
|
|
14
|
-
"projectKey": {
|
|
15
|
-
"type": "string",
|
|
16
|
-
"pattern": "^[a-z][a-z0-9-]{1,30}$",
|
|
17
|
-
"description": "Unique key of the consuming project. Cross-audit marker."
|
|
18
|
-
},
|
|
19
14
|
"app": {
|
|
20
15
|
"type": "object",
|
|
21
16
|
"additionalProperties": false,
|
|
22
|
-
"description": "App identity block (branding + version). Consumed by AdminPublicBootController + AdminManifestConfigFactory.",
|
|
17
|
+
"description": "App identity block (branding + version). One installation serves one application, so this is the only place it is named. Consumed by AdminPublicBootController + AdminManifestConfigFactory.",
|
|
23
18
|
"required": ["name"],
|
|
24
19
|
"properties": {
|
|
25
20
|
"name": {
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
-- =============================================================================
|
|
2
|
+
-- SaaSiCat 1.0 — remove `projectKey` from the catalogue tables.
|
|
3
|
+
-- =============================================================================
|
|
4
|
+
--
|
|
5
|
+
-- project-key-history: this file names the retired identifier because dropping
|
|
6
|
+
-- it is what it does.
|
|
7
|
+
--
|
|
8
|
+
-- One installation serves one application, so a plan key, a bundle key, a
|
|
9
|
+
-- feature key and a quota key are unique for the installation. The column that
|
|
10
|
+
-- suggested otherwise never had a second value to hold: nothing in the platform
|
|
11
|
+
-- could configure a second project, and `subscriptions.tenantId` is unique
|
|
12
|
+
-- installation-wide, so two applications in one database was never a shape this
|
|
13
|
+
-- model could carry.
|
|
14
|
+
--
|
|
15
|
+
-- Run this against an existing database, inside the transaction it opens. It is
|
|
16
|
+
-- a one-way door — the values are dropped, not archived — but it is safe to run
|
|
17
|
+
-- again: a table whose column has already gone is skipped, so a second run does
|
|
18
|
+
-- nothing rather than failing.
|
|
19
|
+
--
|
|
20
|
+
-- psql "$DATABASE_URL" -f 1.0-remove-project-key.postgres.sql
|
|
21
|
+
--
|
|
22
|
+
-- The first statement is a guard. If the affected tables between them hold rows
|
|
23
|
+
-- under more than one project, the migration STOPS and names the table each key
|
|
24
|
+
-- came from, rather than merging rows nobody meant to merge — two `STANDARD`
|
|
25
|
+
-- plans would otherwise collide on the new unique index, and which of them
|
|
26
|
+
-- survives is not a decision a migration should take.
|
|
27
|
+
--
|
|
28
|
+
-- The question is asked across the tables and not once per table. A per-table
|
|
29
|
+
-- count is one where `plans` holds only `alpha` and `bundles` only `beta`, and
|
|
30
|
+
-- that is a two-project installation whose evidence this migration would drop.
|
|
31
|
+
--
|
|
32
|
+
-- Applies to a database created from `reference-schema.postgres.sql` at 0.27 or
|
|
33
|
+
-- any release before 1.0.
|
|
34
|
+
|
|
35
|
+
BEGIN;
|
|
36
|
+
|
|
37
|
+
DO $$
|
|
38
|
+
DECLARE
|
|
39
|
+
affected CONSTANT text[] := ARRAY[
|
|
40
|
+
'plans',
|
|
41
|
+
'bundles',
|
|
42
|
+
'capability_catalog_entries',
|
|
43
|
+
'feature_catalog_entries',
|
|
44
|
+
'quota_catalog_entries',
|
|
45
|
+
'marketing_projections',
|
|
46
|
+
'marketing_settings',
|
|
47
|
+
'promotions',
|
|
48
|
+
'checkout_offers',
|
|
49
|
+
'subscription_contracts'
|
|
50
|
+
];
|
|
51
|
+
target text;
|
|
52
|
+
keys_here text[];
|
|
53
|
+
-- Every key seen anywhere, with the table it came from, so the message can
|
|
54
|
+
-- say where the disagreement is rather than only that there is one.
|
|
55
|
+
seen text[] := ARRAY[]::text[];
|
|
56
|
+
witnesses text[] := ARRAY[]::text[];
|
|
57
|
+
BEGIN
|
|
58
|
+
FOREACH target IN ARRAY affected LOOP
|
|
59
|
+
IF to_regclass(format('%I', target)) IS NULL THEN
|
|
60
|
+
CONTINUE; -- an installation that never adopted this fragment
|
|
61
|
+
END IF;
|
|
62
|
+
-- A table that has already been migrated says nothing, and asking it
|
|
63
|
+
-- would raise `column "projectKey" does not exist`. Everything below
|
|
64
|
+
-- this block is written with IF EXISTS / IF NOT EXISTS, so with this
|
|
65
|
+
-- check the whole file is a no-op on a second run rather than an error
|
|
66
|
+
-- — which is what a deploy that retries, or a container that restarts,
|
|
67
|
+
-- needs it to be.
|
|
68
|
+
IF NOT EXISTS (
|
|
69
|
+
SELECT 1 FROM information_schema.columns
|
|
70
|
+
WHERE table_schema = current_schema()
|
|
71
|
+
AND table_name = target
|
|
72
|
+
AND column_name = 'projectKey'
|
|
73
|
+
) THEN
|
|
74
|
+
CONTINUE;
|
|
75
|
+
END IF;
|
|
76
|
+
EXECUTE format('SELECT array_agg(DISTINCT "projectKey") FROM %I', target)
|
|
77
|
+
INTO keys_here;
|
|
78
|
+
IF keys_here IS NULL THEN
|
|
79
|
+
CONTINUE; -- an empty table says nothing about the installation
|
|
80
|
+
END IF;
|
|
81
|
+
seen := ARRAY(SELECT DISTINCT unnest(seen || keys_here) ORDER BY 1);
|
|
82
|
+
witnesses := witnesses || format('%s: %s', target, array_to_string(keys_here, ', '));
|
|
83
|
+
END LOOP;
|
|
84
|
+
|
|
85
|
+
IF array_length(seen, 1) > 1 THEN
|
|
86
|
+
RAISE EXCEPTION
|
|
87
|
+
'The catalogue holds rows under % different project keys (%). Removing the column '
|
|
88
|
+
'would merge them, and a duplicate key would then collide on the new unique index. '
|
|
89
|
+
'Per table: %. Decide which rows belong to this installation and delete the rest, '
|
|
90
|
+
'then run this migration again.',
|
|
91
|
+
array_length(seen, 1),
|
|
92
|
+
array_to_string(seen, ', '),
|
|
93
|
+
array_to_string(witnesses, ' | ');
|
|
94
|
+
END IF;
|
|
95
|
+
END $$;
|
|
96
|
+
|
|
97
|
+
-- ─── The indexes that carried the column ───
|
|
98
|
+
|
|
99
|
+
DROP INDEX IF EXISTS "checkout_offers_projectKey_status_idx";
|
|
100
|
+
DROP INDEX IF EXISTS "plans_projectKey_deletedAt_idx";
|
|
101
|
+
DROP INDEX IF EXISTS "plans_projectKey_planKey_key";
|
|
102
|
+
DROP INDEX IF EXISTS "bundles_projectKey_deletedAt_idx";
|
|
103
|
+
DROP INDEX IF EXISTS "bundles_projectKey_bundleKey_key";
|
|
104
|
+
DROP INDEX IF EXISTS "capability_catalog_entries_projectKey_codeStatus_idx";
|
|
105
|
+
DROP INDEX IF EXISTS "capability_catalog_entries_projectKey_featureKey_idx";
|
|
106
|
+
DROP INDEX IF EXISTS "capability_catalog_entries_projectKey_capabilityKey_key";
|
|
107
|
+
DROP INDEX IF EXISTS "feature_catalog_entries_projectKey_discoveryStatus_idx";
|
|
108
|
+
DROP INDEX IF EXISTS "feature_catalog_entries_projectKey_plannedOnly_idx";
|
|
109
|
+
DROP INDEX IF EXISTS "feature_catalog_entries_projectKey_featureKey_key";
|
|
110
|
+
DROP INDEX IF EXISTS "quota_catalog_entries_projectKey_discoveryStatus_idx";
|
|
111
|
+
DROP INDEX IF EXISTS "quota_catalog_entries_projectKey_quotaKey_key";
|
|
112
|
+
DROP INDEX IF EXISTS "marketing_projections_projectKey_targetType_locale_priority_idx";
|
|
113
|
+
DROP INDEX IF EXISTS "marketing_settings_projectKey_key";
|
|
114
|
+
DROP INDEX IF EXISTS "promotions_projectKey_targetType_validFrom_validTo_idx";
|
|
115
|
+
DROP INDEX IF EXISTS "subscription_contracts_projectKey_status_idx";
|
|
116
|
+
|
|
117
|
+
-- ─── The column, the tightened uniques, and the shrunken lookup indexes ───
|
|
118
|
+
--
|
|
119
|
+
-- Per table, and only where the table is there. The Prisma fragments are
|
|
120
|
+
-- adopted à la carte — `saasicat schema check` reports what an app did not take
|
|
121
|
+
-- — and an unconditional `ALTER TABLE "bundles"` raises `relation does not
|
|
122
|
+
-- exist` on an app that never adopted bundles, which rolls back everything,
|
|
123
|
+
-- including the tables it could have migrated. `IF EXISTS` on the column does
|
|
124
|
+
-- not help when it is the relation that is missing.
|
|
125
|
+
--
|
|
126
|
+
-- One list, so a table and what happens to it cannot drift apart.
|
|
127
|
+
|
|
128
|
+
DO $$
|
|
129
|
+
DECLARE
|
|
130
|
+
step record;
|
|
131
|
+
statement text;
|
|
132
|
+
BEGIN
|
|
133
|
+
FOR step IN
|
|
134
|
+
SELECT * FROM (VALUES
|
|
135
|
+
('plans', ARRAY[
|
|
136
|
+
'ALTER TABLE "plans" DROP COLUMN IF EXISTS "projectKey"',
|
|
137
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS "plans_planKey_key" ON "plans"("planKey")',
|
|
138
|
+
'CREATE INDEX IF NOT EXISTS "plans_deletedAt_idx" ON "plans"("deletedAt")'
|
|
139
|
+
]),
|
|
140
|
+
('bundles', ARRAY[
|
|
141
|
+
'ALTER TABLE "bundles" DROP COLUMN IF EXISTS "projectKey"',
|
|
142
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS "bundles_bundleKey_key" ON "bundles"("bundleKey")',
|
|
143
|
+
'CREATE INDEX IF NOT EXISTS "bundles_deletedAt_idx" ON "bundles"("deletedAt")'
|
|
144
|
+
]),
|
|
145
|
+
('capability_catalog_entries', ARRAY[
|
|
146
|
+
'ALTER TABLE "capability_catalog_entries" DROP COLUMN IF EXISTS "projectKey"',
|
|
147
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS "capability_catalog_entries_capabilityKey_key" ON "capability_catalog_entries"("capabilityKey")',
|
|
148
|
+
'CREATE INDEX IF NOT EXISTS "capability_catalog_entries_codeStatus_idx" ON "capability_catalog_entries"("codeStatus")',
|
|
149
|
+
'CREATE INDEX IF NOT EXISTS "capability_catalog_entries_featureKey_idx" ON "capability_catalog_entries"("featureKey")'
|
|
150
|
+
]),
|
|
151
|
+
('feature_catalog_entries', ARRAY[
|
|
152
|
+
'ALTER TABLE "feature_catalog_entries" DROP COLUMN IF EXISTS "projectKey"',
|
|
153
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS "feature_catalog_entries_featureKey_key" ON "feature_catalog_entries"("featureKey")',
|
|
154
|
+
'CREATE INDEX IF NOT EXISTS "feature_catalog_entries_discoveryStatus_idx" ON "feature_catalog_entries"("discoveryStatus")',
|
|
155
|
+
'CREATE INDEX IF NOT EXISTS "feature_catalog_entries_plannedOnly_idx" ON "feature_catalog_entries"("plannedOnly")'
|
|
156
|
+
]),
|
|
157
|
+
('quota_catalog_entries', ARRAY[
|
|
158
|
+
'ALTER TABLE "quota_catalog_entries" DROP COLUMN IF EXISTS "projectKey"',
|
|
159
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS "quota_catalog_entries_quotaKey_key" ON "quota_catalog_entries"("quotaKey")',
|
|
160
|
+
'CREATE INDEX IF NOT EXISTS "quota_catalog_entries_discoveryStatus_idx" ON "quota_catalog_entries"("discoveryStatus")'
|
|
161
|
+
]),
|
|
162
|
+
('marketing_projections', ARRAY[
|
|
163
|
+
'ALTER TABLE "marketing_projections" DROP COLUMN IF EXISTS "projectKey"',
|
|
164
|
+
'CREATE INDEX IF NOT EXISTS "marketing_projections_targetType_locale_priority_idx" ON "marketing_projections"("targetType", "locale", "priority")'
|
|
165
|
+
]),
|
|
166
|
+
('promotions', ARRAY[
|
|
167
|
+
'ALTER TABLE "promotions" DROP COLUMN IF EXISTS "projectKey"',
|
|
168
|
+
'CREATE INDEX IF NOT EXISTS "promotions_targetType_validFrom_validTo_idx" ON "promotions"("targetType", "validFrom", "validTo")'
|
|
169
|
+
]),
|
|
170
|
+
('checkout_offers', ARRAY[
|
|
171
|
+
'ALTER TABLE "checkout_offers" DROP COLUMN IF EXISTS "projectKey"',
|
|
172
|
+
'CREATE INDEX IF NOT EXISTS "checkout_offers_status_idx" ON "checkout_offers"("status")'
|
|
173
|
+
]),
|
|
174
|
+
('subscription_contracts', ARRAY[
|
|
175
|
+
'ALTER TABLE "subscription_contracts" DROP COLUMN IF EXISTS "projectKey"',
|
|
176
|
+
'CREATE INDEX IF NOT EXISTS "subscription_contracts_status_idx" ON "subscription_contracts"("status")'
|
|
177
|
+
]),
|
|
178
|
+
-- `marketing_settings` loses the column and gains the identity that
|
|
179
|
+
-- replaces it: one row, under a constant primary key. The CHECK is
|
|
180
|
+
-- what actually holds it. A default only applies where the caller
|
|
181
|
+
-- omits the id and a primary key accepts every distinct value, so
|
|
182
|
+
-- without it a consumer writing the row directly could make as many
|
|
183
|
+
-- as it liked — which is what the comment claiming otherwise was
|
|
184
|
+
-- worth.
|
|
185
|
+
('marketing_settings', ARRAY[
|
|
186
|
+
'ALTER TABLE "marketing_settings" DROP COLUMN IF EXISTS "projectKey"',
|
|
187
|
+
'ALTER TABLE "marketing_settings" ALTER COLUMN "id" SET DEFAULT ''marketing-settings''',
|
|
188
|
+
'UPDATE "marketing_settings" SET "id" = ''marketing-settings''',
|
|
189
|
+
'ALTER TABLE "marketing_settings" DROP CONSTRAINT IF EXISTS "marketing_settings_is_a_singleton"',
|
|
190
|
+
'ALTER TABLE "marketing_settings" ADD CONSTRAINT "marketing_settings_is_a_singleton" CHECK ("id" = ''marketing-settings'')'
|
|
191
|
+
])
|
|
192
|
+
) AS t(relname, statements)
|
|
193
|
+
LOOP
|
|
194
|
+
IF to_regclass(format('%I', step.relname)) IS NULL THEN
|
|
195
|
+
CONTINUE; -- a fragment this installation never adopted
|
|
196
|
+
END IF;
|
|
197
|
+
FOREACH statement IN ARRAY step.statements LOOP
|
|
198
|
+
EXECUTE statement;
|
|
199
|
+
END LOOP;
|
|
200
|
+
END LOOP;
|
|
201
|
+
END $$;
|
|
202
|
+
|
|
203
|
+
COMMIT;
|
|
@@ -17,3 +17,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
|
|
|
17
17
|
|
|
18
18
|
CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
|
|
19
19
|
ON bundle_versions ("bundleId") WHERE "publishedAt" IS NULL;
|
|
20
|
+
|
|
21
|
+
-- `marketing_settings` holds at most ONE row.
|
|
22
|
+
--
|
|
23
|
+
-- The Prisma DSL cannot say this, and the constant default on `id` does not: a
|
|
24
|
+
-- default applies only where the caller omits the value, and a primary key
|
|
25
|
+
-- accepts every distinct one. A consumer writing the row directly could make as
|
|
26
|
+
-- many as it liked, and the repository would then read one and ignore the rest.
|
|
27
|
+
--
|
|
28
|
+
-- `ADD CONSTRAINT` has no `IF NOT EXISTS`, and this file is applied again on
|
|
29
|
+
-- every deployment — so it is dropped first. Two plain statements rather than a
|
|
30
|
+
-- dollar-quoted `DO` block on purpose: every consumer of this file splits it at
|
|
31
|
+
-- the statement separator, and a dollar-quoted body would need each of those
|
|
32
|
+
-- splitters to become a SQL lexer. For the same reason no comment in this file
|
|
33
|
+
-- may contain that separator — one here did, and three splitters cut the
|
|
34
|
+
-- sentence in half.
|
|
35
|
+
ALTER TABLE marketing_settings
|
|
36
|
+
DROP CONSTRAINT IF EXISTS marketing_settings_is_a_singleton;
|
|
37
|
+
ALTER TABLE marketing_settings
|
|
38
|
+
ADD CONSTRAINT marketing_settings_is_a_singleton CHECK ("id" = 'marketing-settings');
|
|
@@ -64,6 +64,7 @@ CREATE TABLE "subscriptions" (
|
|
|
64
64
|
"currentPeriodStart" TIMESTAMP(3),
|
|
65
65
|
"currentPeriodEnd" TIMESTAMP(3),
|
|
66
66
|
"minimumTermUntil" TIMESTAMP(3),
|
|
67
|
+
"billingAnchorDay" INTEGER,
|
|
67
68
|
"planVersionId" TEXT NOT NULL,
|
|
68
69
|
"pendingPlanVersionId" TEXT,
|
|
69
70
|
"pendingPlanVersionEffectiveAt" TIMESTAMP(3),
|
|
@@ -113,7 +114,6 @@ CREATE TABLE "subscription_payment_methods" (
|
|
|
113
114
|
-- CreateTable
|
|
114
115
|
CREATE TABLE "checkout_offers" (
|
|
115
116
|
"id" TEXT NOT NULL,
|
|
116
|
-
"projectKey" TEXT NOT NULL,
|
|
117
117
|
"planKey" TEXT NOT NULL,
|
|
118
118
|
"planVersionId" TEXT,
|
|
119
119
|
"billingCycle" TEXT NOT NULL,
|
|
@@ -199,7 +199,6 @@ CREATE TABLE "promo_code_validation_logs" (
|
|
|
199
199
|
-- CreateTable
|
|
200
200
|
CREATE TABLE "plans" (
|
|
201
201
|
"id" TEXT NOT NULL,
|
|
202
|
-
"projectKey" TEXT NOT NULL,
|
|
203
202
|
"planKey" TEXT NOT NULL,
|
|
204
203
|
"label" TEXT NOT NULL,
|
|
205
204
|
"description" TEXT,
|
|
@@ -259,7 +258,6 @@ CREATE TABLE "audit_logs" (
|
|
|
259
258
|
-- CreateTable
|
|
260
259
|
CREATE TABLE "bundles" (
|
|
261
260
|
"id" TEXT NOT NULL,
|
|
262
|
-
"projectKey" TEXT NOT NULL,
|
|
263
261
|
"bundleKey" TEXT NOT NULL,
|
|
264
262
|
"label" TEXT NOT NULL,
|
|
265
263
|
"description" TEXT,
|
|
@@ -304,7 +302,6 @@ CREATE TABLE "bundle_versions" (
|
|
|
304
302
|
-- CreateTable
|
|
305
303
|
CREATE TABLE "capability_catalog_entries" (
|
|
306
304
|
"id" TEXT NOT NULL,
|
|
307
|
-
"projectKey" TEXT NOT NULL,
|
|
308
305
|
"capabilityKey" TEXT NOT NULL,
|
|
309
306
|
"label" TEXT NOT NULL,
|
|
310
307
|
"description" TEXT,
|
|
@@ -329,7 +326,6 @@ CREATE TABLE "capability_catalog_entries" (
|
|
|
329
326
|
-- CreateTable
|
|
330
327
|
CREATE TABLE "feature_catalog_entries" (
|
|
331
328
|
"id" TEXT NOT NULL,
|
|
332
|
-
"projectKey" TEXT NOT NULL,
|
|
333
329
|
"featureKey" TEXT NOT NULL,
|
|
334
330
|
"label" TEXT NOT NULL,
|
|
335
331
|
"description" TEXT,
|
|
@@ -358,7 +354,6 @@ CREATE TABLE "feature_catalog_entries" (
|
|
|
358
354
|
-- CreateTable
|
|
359
355
|
CREATE TABLE "quota_catalog_entries" (
|
|
360
356
|
"id" TEXT NOT NULL,
|
|
361
|
-
"projectKey" TEXT NOT NULL,
|
|
362
357
|
"quotaKey" TEXT NOT NULL,
|
|
363
358
|
"label" TEXT NOT NULL,
|
|
364
359
|
"description" TEXT,
|
|
@@ -384,7 +379,6 @@ CREATE TABLE "quota_catalog_entries" (
|
|
|
384
379
|
-- CreateTable
|
|
385
380
|
CREATE TABLE "marketing_projections" (
|
|
386
381
|
"id" TEXT NOT NULL,
|
|
387
|
-
"projectKey" TEXT NOT NULL,
|
|
388
382
|
"targetType" TEXT NOT NULL,
|
|
389
383
|
"targetVersionId" TEXT NOT NULL,
|
|
390
384
|
"locale" TEXT NOT NULL DEFAULT 'de',
|
|
@@ -407,8 +401,7 @@ CREATE TABLE "marketing_projections" (
|
|
|
407
401
|
|
|
408
402
|
-- CreateTable
|
|
409
403
|
CREATE TABLE "marketing_settings" (
|
|
410
|
-
"id" TEXT NOT NULL,
|
|
411
|
-
"projectKey" TEXT NOT NULL,
|
|
404
|
+
"id" TEXT NOT NULL DEFAULT 'marketing-settings',
|
|
412
405
|
"activeLocales" JSONB NOT NULL DEFAULT '[]',
|
|
413
406
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
414
407
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
@@ -419,7 +412,6 @@ CREATE TABLE "marketing_settings" (
|
|
|
419
412
|
-- CreateTable
|
|
420
413
|
CREATE TABLE "promotions" (
|
|
421
414
|
"id" TEXT NOT NULL,
|
|
422
|
-
"projectKey" TEXT NOT NULL,
|
|
423
415
|
"internalLabel" TEXT NOT NULL,
|
|
424
416
|
"type" TEXT NOT NULL,
|
|
425
417
|
"value" JSONB NOT NULL,
|
|
@@ -443,7 +435,6 @@ CREATE TABLE "promotions" (
|
|
|
443
435
|
-- CreateTable
|
|
444
436
|
CREATE TABLE "subscription_contracts" (
|
|
445
437
|
"id" TEXT NOT NULL,
|
|
446
|
-
"projectKey" TEXT NOT NULL,
|
|
447
438
|
"tenantId" TEXT NOT NULL,
|
|
448
439
|
"status" "SubscriptionContractStatus" NOT NULL DEFAULT 'active',
|
|
449
440
|
"effectiveFrom" TIMESTAMP(3) NOT NULL,
|
|
@@ -564,6 +555,9 @@ CREATE TABLE "subscription_bundles" (
|
|
|
564
555
|
"bundleVersionId" TEXT NOT NULL,
|
|
565
556
|
"startedAt" TIMESTAMP(3) NOT NULL,
|
|
566
557
|
"minimumTermEndsAt" TIMESTAMP(3),
|
|
558
|
+
"billingCycle" TEXT,
|
|
559
|
+
"currentPeriodStart" TIMESTAMP(3),
|
|
560
|
+
"currentPeriodEnd" TIMESTAMP(3),
|
|
567
561
|
"canceledAt" TIMESTAMP(3),
|
|
568
562
|
"canceledEffectiveAt" TIMESTAMP(3),
|
|
569
563
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -591,7 +585,7 @@ CREATE INDEX "subscriptions_currentPeriodEnd_idx" ON "subscriptions"("currentPer
|
|
|
591
585
|
CREATE UNIQUE INDEX "subscription_payment_methods_subscriptionId_key" ON "subscription_payment_methods"("subscriptionId");
|
|
592
586
|
|
|
593
587
|
-- CreateIndex
|
|
594
|
-
CREATE INDEX "
|
|
588
|
+
CREATE INDEX "checkout_offers_status_idx" ON "checkout_offers"("status");
|
|
595
589
|
|
|
596
590
|
-- CreateIndex
|
|
597
591
|
CREATE UNIQUE INDEX "promo_codes_code_key" ON "promo_codes"("code");
|
|
@@ -624,10 +618,10 @@ CREATE INDEX "promo_code_validation_logs_ipHash_createdAt_idx" ON "promo_code_va
|
|
|
624
618
|
CREATE INDEX "promo_code_validation_logs_sessionId_createdAt_idx" ON "promo_code_validation_logs"("sessionId", "createdAt");
|
|
625
619
|
|
|
626
620
|
-- CreateIndex
|
|
627
|
-
CREATE INDEX "
|
|
621
|
+
CREATE INDEX "plans_deletedAt_idx" ON "plans"("deletedAt");
|
|
628
622
|
|
|
629
623
|
-- CreateIndex
|
|
630
|
-
CREATE UNIQUE INDEX "
|
|
624
|
+
CREATE UNIQUE INDEX "plans_planKey_key" ON "plans"("planKey");
|
|
631
625
|
|
|
632
626
|
-- CreateIndex
|
|
633
627
|
CREATE INDEX "plan_versions_planId_supersededAt_idx" ON "plan_versions"("planId", "supersededAt");
|
|
@@ -654,10 +648,10 @@ CREATE INDEX "audit_logs_entity_action_createdAt_idx" ON "audit_logs"("entity",
|
|
|
654
648
|
CREATE INDEX "audit_logs_actorTag_createdAt_idx" ON "audit_logs"("actorTag", "createdAt");
|
|
655
649
|
|
|
656
650
|
-- CreateIndex
|
|
657
|
-
CREATE INDEX "
|
|
651
|
+
CREATE INDEX "bundles_deletedAt_idx" ON "bundles"("deletedAt");
|
|
658
652
|
|
|
659
653
|
-- CreateIndex
|
|
660
|
-
CREATE UNIQUE INDEX "
|
|
654
|
+
CREATE UNIQUE INDEX "bundles_bundleKey_key" ON "bundles"("bundleKey");
|
|
661
655
|
|
|
662
656
|
-- CreateIndex
|
|
663
657
|
CREATE INDEX "bundle_versions_bundleId_supersededAt_idx" ON "bundle_versions"("bundleId", "supersededAt");
|
|
@@ -672,46 +666,43 @@ CREATE INDEX "bundle_versions_bundleId_validFrom_idx" ON "bundle_versions"("bund
|
|
|
672
666
|
CREATE UNIQUE INDEX "bundle_versions_bundleId_version_key" ON "bundle_versions"("bundleId", "version");
|
|
673
667
|
|
|
674
668
|
-- CreateIndex
|
|
675
|
-
CREATE INDEX "
|
|
669
|
+
CREATE INDEX "capability_catalog_entries_codeStatus_idx" ON "capability_catalog_entries"("codeStatus");
|
|
676
670
|
|
|
677
671
|
-- CreateIndex
|
|
678
|
-
CREATE INDEX "
|
|
672
|
+
CREATE INDEX "capability_catalog_entries_featureKey_idx" ON "capability_catalog_entries"("featureKey");
|
|
679
673
|
|
|
680
674
|
-- CreateIndex
|
|
681
|
-
CREATE UNIQUE INDEX "
|
|
675
|
+
CREATE UNIQUE INDEX "capability_catalog_entries_capabilityKey_key" ON "capability_catalog_entries"("capabilityKey");
|
|
682
676
|
|
|
683
677
|
-- CreateIndex
|
|
684
|
-
CREATE INDEX "
|
|
678
|
+
CREATE INDEX "feature_catalog_entries_discoveryStatus_idx" ON "feature_catalog_entries"("discoveryStatus");
|
|
685
679
|
|
|
686
680
|
-- CreateIndex
|
|
687
|
-
CREATE INDEX "
|
|
681
|
+
CREATE INDEX "feature_catalog_entries_plannedOnly_idx" ON "feature_catalog_entries"("plannedOnly");
|
|
688
682
|
|
|
689
683
|
-- CreateIndex
|
|
690
|
-
CREATE UNIQUE INDEX "
|
|
684
|
+
CREATE UNIQUE INDEX "feature_catalog_entries_featureKey_key" ON "feature_catalog_entries"("featureKey");
|
|
691
685
|
|
|
692
686
|
-- CreateIndex
|
|
693
|
-
CREATE INDEX "
|
|
687
|
+
CREATE INDEX "quota_catalog_entries_discoveryStatus_idx" ON "quota_catalog_entries"("discoveryStatus");
|
|
694
688
|
|
|
695
689
|
-- CreateIndex
|
|
696
|
-
CREATE UNIQUE INDEX "
|
|
690
|
+
CREATE UNIQUE INDEX "quota_catalog_entries_quotaKey_key" ON "quota_catalog_entries"("quotaKey");
|
|
697
691
|
|
|
698
692
|
-- CreateIndex
|
|
699
|
-
CREATE INDEX "
|
|
693
|
+
CREATE INDEX "marketing_projections_targetType_locale_priority_idx" ON "marketing_projections"("targetType", "locale", "priority");
|
|
700
694
|
|
|
701
695
|
-- CreateIndex
|
|
702
696
|
CREATE UNIQUE INDEX "marketing_projections_targetType_targetVersionId_locale_key" ON "marketing_projections"("targetType", "targetVersionId", "locale");
|
|
703
697
|
|
|
704
698
|
-- CreateIndex
|
|
705
|
-
CREATE
|
|
706
|
-
|
|
707
|
-
-- CreateIndex
|
|
708
|
-
CREATE INDEX "promotions_projectKey_targetType_validFrom_validTo_idx" ON "promotions"("projectKey", "targetType", "validFrom", "validTo");
|
|
699
|
+
CREATE INDEX "promotions_targetType_validFrom_validTo_idx" ON "promotions"("targetType", "validFrom", "validTo");
|
|
709
700
|
|
|
710
701
|
-- CreateIndex
|
|
711
702
|
CREATE INDEX "subscription_contracts_tenantId_status_effectiveFrom_idx" ON "subscription_contracts"("tenantId", "status", "effectiveFrom");
|
|
712
703
|
|
|
713
704
|
-- CreateIndex
|
|
714
|
-
CREATE INDEX "
|
|
705
|
+
CREATE INDEX "subscription_contracts_status_idx" ON "subscription_contracts"("status");
|
|
715
706
|
|
|
716
707
|
-- CreateIndex
|
|
717
708
|
CREATE INDEX "subscription_contracts_originalOfferId_idx" ON "subscription_contracts"("originalOfferId");
|
|
@@ -810,3 +801,22 @@ CREATE UNIQUE INDEX IF NOT EXISTS plan_versions_draft_per_plan
|
|
|
810
801
|
|
|
811
802
|
CREATE UNIQUE INDEX IF NOT EXISTS bundle_versions_draft_per_bundle
|
|
812
803
|
ON bundle_versions ("bundleId") WHERE "publishedAt" IS NULL;
|
|
804
|
+
|
|
805
|
+
-- `marketing_settings` holds at most ONE row.
|
|
806
|
+
--
|
|
807
|
+
-- The Prisma DSL cannot say this, and the constant default on `id` does not: a
|
|
808
|
+
-- default applies only where the caller omits the value, and a primary key
|
|
809
|
+
-- accepts every distinct one. A consumer writing the row directly could make as
|
|
810
|
+
-- many as it liked, and the repository would then read one and ignore the rest.
|
|
811
|
+
--
|
|
812
|
+
-- `ADD CONSTRAINT` has no `IF NOT EXISTS`, and this file is applied again on
|
|
813
|
+
-- every deployment — so it is dropped first. Two plain statements rather than a
|
|
814
|
+
-- dollar-quoted `DO` block on purpose: every consumer of this file splits it at
|
|
815
|
+
-- the statement separator, and a dollar-quoted body would need each of those
|
|
816
|
+
-- splitters to become a SQL lexer. For the same reason no comment in this file
|
|
817
|
+
-- may contain that separator — one here did, and three splitters cut the
|
|
818
|
+
-- sentence in half.
|
|
819
|
+
ALTER TABLE marketing_settings
|
|
820
|
+
DROP CONSTRAINT IF EXISTS marketing_settings_is_a_singleton;
|
|
821
|
+
ALTER TABLE marketing_settings
|
|
822
|
+
ADD CONSTRAINT marketing_settings_is_a_singleton CHECK ("id" = 'marketing-settings');
|