@saasicat/adapter-prisma 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,8 @@ Options:
37
37
  `core.superAdminProvisioning` (setup wizard / `user create-super-admin`).
38
38
  - `rlsIntegration: true` — declare the `rowLevelSecurity` capability once
39
39
  your Prisma middleware really applies the bypass (see below).
40
+ - `schema` — explicit plan identity, delegate and optional-field capabilities
41
+ for schemas that differ from the 0.6 canonical layout.
40
42
 
41
43
  The bundle also ships `planCatalogReadSink` for DB hydration. To use it,
42
44
  omit `planCatalog` and pass the identity the database cannot provide:
@@ -56,27 +58,36 @@ PromoCodesModule.forRoot({
56
58
 
57
59
  ## Shipped adapters
58
60
 
59
- | Class | Implements port | Tables |
60
- | ---------------------------------------- | ---------------------------------- | --------------------------------------------------- |
61
- | `PrismaTransactionRunner` | `TransactionRunner` | — (`$transaction`) |
62
- | `PrismaMfaAdapter` | `MfaPort` | `super_admin_mfa` |
63
- | `PrismaAuditAdapter` | `AuditPort` | `audit_logs` |
64
- | `PrismaAuditQueryAdapter` | `AuditQueryPort` | `audit_logs` |
65
- | `PrismaAuditStatsAdapter` | `AuditStatsPort` | `audit_logs` |
66
- | `AsyncLocalRlsBypassAdapter` | `RlsBypassPort` | (no DB access) |
67
- | `PrismaSubscriptionRepository` | `SubscriptionRepository` | `subscriptions`, `plan_versions` |
68
- | `PrismaPlanVersionRepository` | `PlanVersionRepository` | `plan_versions` |
69
- | `PrismaPromoCodeRepository` | `PromoCodeRepository` | `promo_codes` |
70
- | `PrismaPromoCodeRedemptionRepository` | `PromoCodeRedemptionRepository` | `promo_code_redemptions` |
71
- | `PrismaPromoCodeValidationLogRepository` | `PromoCodeValidationLogRepository` | `promo_code_validation_logs` |
72
- | `PrismaPromoSubscriptionLookup` | `PromoSubscriptionLookup` | `subscriptions` |
73
- | `ZeroPromoRevenueDeductionAggregator` | `PromoRevenueDeductionAggregator` | — (constant `'0.00'`) |
74
- | `PrismaSuperAdminBootstrapAdapter` | `SuperAdminProvisioningPort` | `super_admin_users` |
75
- | `PrismaPlanCatalogReadSink` | `PlanCatalogReadSink` | `plans`, `plan_versions`, `feature_catalog_entries` |
76
- | `PrismaPlanCatalogImportSink` | `PlanCatalogImportSink` | same |
77
-
78
- Not shipped (custom adapters stay yours): subscription contracts, bundle
79
- bookings, registration, tenant-billing write ports, `FirstTimeCustomerCheck`.
61
+ | Class | Implements port | Tables |
62
+ | ---------------------------------------- | ---------------------------------- | ------------------------------------------------------------------- |
63
+ | `PrismaTransactionRunner` | `TransactionRunner` | — (`$transaction`) |
64
+ | `PrismaMfaAdapter` | `MfaPort` | `super_admin_mfa` |
65
+ | `PrismaAuditAdapter` | `AuditPort` | `audit_logs` |
66
+ | `PrismaAuditQueryAdapter` | `AuditQueryPort` | `audit_logs` |
67
+ | `PrismaAuditStatsAdapter` | `AuditStatsPort` | `audit_logs` |
68
+ | `AsyncLocalRlsBypassAdapter` | `RlsBypassPort` | (no DB access) |
69
+ | `PrismaSubscriptionRepository` | `SubscriptionRepository` | `subscriptions`, `plan_versions`, optionally `subscription_bundles` |
70
+ | `PrismaSubscriptionBundleRepository` | `SubscriptionBundleRepository` | `subscription_bundles` |
71
+ | `PrismaTenantSubscriptionWriteAdapter` | `TenantSubscriptionWritePort` | `subscriptions`, `plans`, `plan_versions` |
72
+ | `PrismaPlanVersionRepository` | `PlanVersionRepository` | `plan_versions` |
73
+ | `PrismaPromoCodeRepository` | `PromoCodeRepository` | `promo_codes` |
74
+ | `PrismaPromoCodeRedemptionRepository` | `PromoCodeRedemptionRepository` | `promo_code_redemptions` |
75
+ | `PrismaPromoCodeValidationLogRepository` | `PromoCodeValidationLogRepository` | `promo_code_validation_logs` |
76
+ | `PrismaPromoSubscriptionLookup` | `PromoSubscriptionLookup` | `subscriptions` |
77
+ | `ZeroPromoRevenueDeductionAggregator` | `PromoRevenueDeductionAggregator` | (constant `'0.00'`) |
78
+ | `PrismaSuperAdminBootstrapAdapter` | `SuperAdminProvisioningPort` | `super_admin_users` |
79
+ | `PrismaPlanCatalogReadSink` | `PlanCatalogReadSink` | `plans`, `plan_versions`, `feature_catalog_entries` |
80
+ | `PrismaPlanCatalogImportSink` | `PlanCatalogImportSink` | same |
81
+ | `PrismaPlanRepository` | `PlanRepository` | `plans`, `plan_versions` |
82
+ | `PrismaBundleRepository` | `BundleRepository` | `bundles`, `bundle_versions` |
83
+ | `PrismaCatalogEntryRepository` | `CatalogEntryRepository` | capability, feature and quota catalog tables |
84
+ | `PrismaMarketingProjectionRepository` | `MarketingProjectionRepository` | `marketing_projections` |
85
+ | `PrismaMarketingSettingsRepository` | `MarketingSettingsRepository` | `marketing_settings` |
86
+ | `PrismaPromotionRepository` | `PromotionRepository` | `promotions` |
87
+ | `PrismaSubscriptionContractRepository` | `SubscriptionContractRepository` | `subscription_contracts`, `contract_line_items` |
88
+
89
+ Not shipped (custom adapters stay yours): registration persistence,
90
+ consumer-specific payment/invoice integrations, and `FirstTimeCustomerCheck`.
80
91
  Absent optional repository methods degrade fail-closed as documented on the
81
92
  ports (e.g. `countByBundleVersionId`).
82
93
 
@@ -104,8 +115,121 @@ The canonical schema: copy the models from
104
115
  `@saasicat/spec/sql/reference-schema.postgres.sql`) **plus**
105
116
  `sql/constraints.postgres.sql` — the partial unique indexes and the
106
117
  subscription CHECK are part of the contract, `claimSlot`/`findByTenantIdLocked`
107
- rely on real PostgreSQL semantics. If your schema differs: write your own
108
- adapters the platform ports stay identical.
118
+ rely on real PostgreSQL semantics. Use the explicit schema options below for
119
+ supported differences; for other shapes, override only the affected adapter
120
+ the platform ports stay identical.
121
+
122
+ ### Plan identity and split PlanVersion delegates
123
+
124
+ The default is the SaaSiCat 0.6 layout: `PlanVersion.planId` stores the
125
+ semantic `planKey`, both catalog and entitlement reads use the `planVersion`
126
+ delegate, and optional validity columns are not queried. There is no schema
127
+ auto-detection.
128
+
129
+ An app with a normalized UUID foreign key opts in explicitly. Port inputs and
130
+ outputs still use the semantic key:
131
+
132
+ ```ts
133
+ const schema = {
134
+ planBinding: {
135
+ mode: 'normalized-plan-id',
136
+ projectKey: 'vereinsfux',
137
+ },
138
+ planVersionFields: {
139
+ validityWindows: true,
140
+ endsAt: true,
141
+ },
142
+ tenantSubscription: {
143
+ subscriptionBundleDelegate: 'subscriptionBundle',
144
+ synchronizePlanVersion: true,
145
+ atomicOnboardingSelection: true,
146
+ activeVersionSelection: 'validity-window',
147
+ withEndsAt: true,
148
+ },
149
+ } satisfies PrismaSchemaOptions;
150
+
151
+ const persistence = prismaPersistence({ client: PrismaService, schema });
152
+ const plans = new PrismaPlanRepository(prisma, schema);
153
+ const subscriptionWrites = new PrismaTenantSubscriptionWriteAdapter(prisma, schema);
154
+ ```
155
+
156
+ For direct Nest registration, bind the same object once:
157
+
158
+ ```ts
159
+ providers: [
160
+ { provide: PRISMA_CLIENT_TOKEN, useExisting: PrismaService },
161
+ { provide: PRISMA_SCHEMA_OPTIONS_TOKEN, useValue: schema },
162
+ PrismaPlanRepository,
163
+ PrismaPlanVersionRepository,
164
+ PrismaTenantSubscriptionWriteAdapter,
165
+ ];
166
+ ```
167
+
168
+ Split schemas can name catalog and entitlement delegates independently. This
169
+ keeps, for example, `catalogPlanVersion` with validity columns separate from a
170
+ legacy billing `planVersion`:
171
+
172
+ ```ts
173
+ const schema = {
174
+ delegates: {
175
+ catalogPlanVersion: 'catalogPlanVersion',
176
+ entitlementPlanVersion: 'planVersion',
177
+ },
178
+ planVersionFields: {
179
+ catalog: { validityWindows: true },
180
+ entitlement: { validityWindows: false },
181
+ },
182
+ } satisfies PrismaSchemaOptions;
183
+ ```
184
+
185
+ Delegate selection is field-level and backwards-compatible; an app whose
186
+ billing version stores fixed quota columns instead of JSON can continue to
187
+ override only the entitlement repository.
188
+
189
+ ### Atomic tenant plan binding
190
+
191
+ `PrismaTenantSubscriptionWriteAdapter` exposes the optional
192
+ `applyOnboardingSelection` capability only when
193
+ `tenantSubscription.atomicOnboardingSelection: true`. The default is `false`,
194
+ preserving the 0.6 sequential fallback. When enabled, the subscription update
195
+ and optional promo callback share one Prisma transaction. With
196
+ `tenantSubscription.synchronizePlanVersion: true`, immediate changes and
197
+ onboarding also resolve the target PlanVersion and update `plan`,
198
+ `planVersionId`, cycle and stale pending-version fields atomically. The default
199
+ is `false`, preserving the 0.6 plan-only write until an app opts in.
200
+
201
+ `tenantSubscription.delegate` selects the Prisma model delegate used for all
202
+ subscription ORM operations, including the read that follows a row lock.
203
+ `findByTenantIdLocked` deliberately locks the canonical physical
204
+ `subscriptions` table with raw SQL, so a differently named Prisma model must
205
+ map to that table via `@@map("subscriptions")`.
206
+
207
+ When `tenantSubscription.subscriptionBundleDelegate` names the app's
208
+ SubscriptionBundle delegate, `PrismaSubscriptionRepository` also exposes
209
+ `countByBundleVersionId`. This keeps published-but-future BundleVersion
210
+ editability aligned with real active bookings. Without the option the method
211
+ is absent and the catalog service remains fail-closed.
212
+
213
+ ### Bundle validity windows
214
+
215
+ `PrismaBundleRepository` keeps its 0.6-compatible behavior by default and does
216
+ not require `bundle_versions.validFrom` / `validUntil`. After applying the
217
+ additive columns from the current `@saasicat/spec` bundle fragment, enable them
218
+ explicitly:
219
+
220
+ ```ts
221
+ const bundles = new PrismaBundleRepository(prisma, {
222
+ validityWindows: true,
223
+ });
224
+ ```
225
+
226
+ The enabled mode persists and returns both dates. Publishing also sets the
227
+ predecessor's `validUntil` to one UTC calendar day before the successor starts,
228
+ and wraps supersede + publish in a transaction when the caller did not already
229
+ provide one. It also exposes the optional
230
+ `BundleRepository.findActiveBundleVersion(bundleId, asOf?)` capability, using
231
+ inclusive UTC-day boundaries and preferring the highest `validFrom`, then
232
+ `version`. In the default legacy mode that optional capability is `undefined`.
109
233
 
110
234
  ## RLS bypass
111
235