@saasicat/spec 1.0.0-rc.2 → 1.0.0-rc.4
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 +28 -0
- package/admin-api.openapi.yaml +212 -64
- package/package.json +1 -1
- package/prisma-fragments/README.md +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
# @saasicat/spec
|
|
2
2
|
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
3
5
|
Language-neutral spec of the SaaS platform — JSON Schemas, OpenAPI and
|
|
4
6
|
acceptance test scenarios.
|
|
5
7
|
|
|
8
|
+
## What this is not
|
|
9
|
+
|
|
10
|
+
Not runnable code. JSON Schemas, an OpenAPI document, Prisma fragments and SQL
|
|
11
|
+
constraints — a contract any language can implement, which is why the reference
|
|
12
|
+
implementation is a separate package.
|
|
13
|
+
|
|
14
|
+
Not a migration tool. The fragments are merged into your own `schema.prisma`
|
|
15
|
+
by `saasicat schema apply`, and your Prisma or Drizzle migration flow takes it
|
|
16
|
+
from there.
|
|
17
|
+
|
|
18
|
+
## Entry points
|
|
19
|
+
|
|
20
|
+
| Entry | What is in it |
|
|
21
|
+
| ----------------------------- | ---------------------------------------------------------------- |
|
|
22
|
+
| `.` | The hand-written index: constants and helpers over the schemas. |
|
|
23
|
+
| `./schemas/*.json` | The JSON Schemas — the source of truth for every wire format. |
|
|
24
|
+
| `./prisma-fragments/*.prisma` | One fragment per table group, merged by `saasicat schema apply`. |
|
|
25
|
+
| `./prisma-fragments/` | The directory itself, for tooling that enumerates the fragments. |
|
|
26
|
+
| `./sql/*.sql` | The constraints Prisma cannot express, and the reference schema. |
|
|
27
|
+
|
|
6
28
|
## Contents
|
|
7
29
|
|
|
8
30
|
| File | Purpose |
|
|
@@ -57,3 +79,9 @@ pnpm add @saasicat/spec
|
|
|
57
79
|
live in the DB, curated via the SuperAdmin UI.
|
|
58
80
|
- **`schemaVersion` is required** in every top-level schema. Major bumps
|
|
59
81
|
break — that is allowed, but must be documented in a migration note.
|
|
82
|
+
|
|
83
|
+
## Next
|
|
84
|
+
|
|
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 describe
|
|
87
|
+
- [Codegen](../../docs/explanation/adr/0006-spec-to-types-codegen.md) — how types are derived from them
|
package/admin-api.openapi.yaml
CHANGED
|
@@ -17,10 +17,19 @@ openapi: 3.1.0
|
|
|
17
17
|
# - Tenant end-customer API (self-service plan change) — separate file
|
|
18
18
|
# - Onboarding API — separate file (uses PromoCode preview)
|
|
19
19
|
# - X-Rechnung — its own package, its own OpenAPI file
|
|
20
|
+
#
|
|
21
|
+
# Who serves what: an operation marked `x-served-by: app` is part of this
|
|
22
|
+
# contract but NOT shipped by @saasicat/nest — the tenant model, platform user
|
|
23
|
+
# administration and the promo-code detail view reach into data the application
|
|
24
|
+
# owns, so the application serves those routes and the admin UI calls them.
|
|
25
|
+
# Everything else is served by the reference implementation.
|
|
26
|
+
# `tests/openapi-covers-the-implementation.test.js` holds both directions: no
|
|
27
|
+
# undocumented route, and no documented operation without an implementation or
|
|
28
|
+
# that marker.
|
|
20
29
|
|
|
21
30
|
info:
|
|
22
31
|
title: SaaS Platform SuperAdmin API
|
|
23
|
-
version:
|
|
32
|
+
version: 1.0.0-rc.4
|
|
24
33
|
description: |
|
|
25
34
|
Read and write operations for platform administration:
|
|
26
35
|
tenants, users, subscriptions, promo codes, audit log,
|
|
@@ -141,7 +150,7 @@ paths:
|
|
|
141
150
|
# ────────────────────────────────────────────────────────
|
|
142
151
|
# Dashboard
|
|
143
152
|
# ────────────────────────────────────────────────────────
|
|
144
|
-
/dashboard
|
|
153
|
+
/stats/dashboard:
|
|
145
154
|
get:
|
|
146
155
|
tags: [dashboard]
|
|
147
156
|
summary: Global platform metrics
|
|
@@ -153,53 +162,6 @@ paths:
|
|
|
153
162
|
schema:
|
|
154
163
|
$ref: '#/components/schemas/DashboardStats'
|
|
155
164
|
|
|
156
|
-
# ────────────────────────────────────────────────────────
|
|
157
|
-
# Plan catalog (read-only — maintained via file + reload endpoint)
|
|
158
|
-
# ────────────────────────────────────────────────────────
|
|
159
|
-
/plans:
|
|
160
|
-
get:
|
|
161
|
-
tags: [plans]
|
|
162
|
-
summary: Currently loaded plan catalog
|
|
163
|
-
responses:
|
|
164
|
-
'200':
|
|
165
|
-
description: Catalog
|
|
166
|
-
content:
|
|
167
|
-
application/json:
|
|
168
|
-
schema:
|
|
169
|
-
$ref: '#/components/schemas/PlanCatalog'
|
|
170
|
-
|
|
171
|
-
/plans/reload:
|
|
172
|
-
post:
|
|
173
|
-
tags: [plans]
|
|
174
|
-
summary: Reload the plan catalog from file (for admin CLI / hot-reload dev)
|
|
175
|
-
x-mfa-required: true
|
|
176
|
-
responses:
|
|
177
|
-
'200':
|
|
178
|
-
description: Loaded
|
|
179
|
-
content:
|
|
180
|
-
application/json:
|
|
181
|
-
schema:
|
|
182
|
-
type: object
|
|
183
|
-
properties:
|
|
184
|
-
loadedAt: { type: string, format: date-time }
|
|
185
|
-
planCount: { type: integer }
|
|
186
|
-
source: { type: string, description: Path or URL }
|
|
187
|
-
|
|
188
|
-
/plans/last-update:
|
|
189
|
-
get:
|
|
190
|
-
tags: [plans]
|
|
191
|
-
summary: When the catalog was last loaded + hash
|
|
192
|
-
responses:
|
|
193
|
-
'200':
|
|
194
|
-
description: Meta
|
|
195
|
-
content:
|
|
196
|
-
application/json:
|
|
197
|
-
schema:
|
|
198
|
-
type: object
|
|
199
|
-
properties:
|
|
200
|
-
loadedAt: { type: string, format: date-time }
|
|
201
|
-
hash: { type: string, description: SHA-256 of the YAML content }
|
|
202
|
-
|
|
203
165
|
# ────────────────────────────────────────────────────────
|
|
204
166
|
# Tenants
|
|
205
167
|
# ────────────────────────────────────────────────────────
|
|
@@ -234,6 +196,7 @@ paths:
|
|
|
234
196
|
|
|
235
197
|
post:
|
|
236
198
|
tags: [tenants]
|
|
199
|
+
x-served-by: app # the tenant model is the application's
|
|
237
200
|
summary: Create a tenant manually (bypasses the onboarding flow)
|
|
238
201
|
x-mfa-required: true
|
|
239
202
|
requestBody:
|
|
@@ -262,6 +225,7 @@ paths:
|
|
|
262
225
|
schema: { $ref: '#/components/schemas/AdminTenantDetail' }
|
|
263
226
|
delete:
|
|
264
227
|
tags: [tenants]
|
|
228
|
+
x-served-by: app # the tenant model is the application's
|
|
265
229
|
summary: Soft-delete a tenant (set deletedAt)
|
|
266
230
|
x-mfa-required: true
|
|
267
231
|
requestBody:
|
|
@@ -315,6 +279,7 @@ paths:
|
|
|
315
279
|
- $ref: '#/components/parameters/TenantSlug'
|
|
316
280
|
post:
|
|
317
281
|
tags: [tenants]
|
|
282
|
+
x-served-by: app # the tenant model is the application's
|
|
318
283
|
summary: Short-lived JWT as TENANT_ADMIN for support insight
|
|
319
284
|
x-mfa-required: true
|
|
320
285
|
requestBody:
|
|
@@ -344,6 +309,7 @@ paths:
|
|
|
344
309
|
- $ref: '#/components/parameters/TenantSlug'
|
|
345
310
|
get:
|
|
346
311
|
tags: [tenants]
|
|
312
|
+
x-served-by: app # the tenant model is the application's
|
|
347
313
|
summary: GDPR / audit export of all tenant data as a ZIP
|
|
348
314
|
x-mfa-required: true
|
|
349
315
|
responses:
|
|
@@ -361,6 +327,7 @@ paths:
|
|
|
361
327
|
- $ref: '#/components/parameters/TenantSlug'
|
|
362
328
|
get:
|
|
363
329
|
tags: [subscriptions]
|
|
330
|
+
x-served-by: app # no admin-side subscription route ships
|
|
364
331
|
summary: Current subscription + add-ons + effective limits
|
|
365
332
|
responses:
|
|
366
333
|
'200':
|
|
@@ -371,6 +338,7 @@ paths:
|
|
|
371
338
|
|
|
372
339
|
patch:
|
|
373
340
|
tags: [subscriptions]
|
|
341
|
+
x-served-by: app # no admin-side subscription route ships
|
|
374
342
|
summary: Change plan, custom limits, or pilot flag
|
|
375
343
|
x-mfa-required: true
|
|
376
344
|
requestBody:
|
|
@@ -435,6 +403,7 @@ paths:
|
|
|
435
403
|
schema: { type: string }
|
|
436
404
|
post:
|
|
437
405
|
tags: [users]
|
|
406
|
+
x-served-by: app # platform users live in the application's identity store
|
|
438
407
|
summary: Trigger a password-reset email (sends no plaintext passwords)
|
|
439
408
|
x-mfa-required: true
|
|
440
409
|
requestBody:
|
|
@@ -457,6 +426,7 @@ paths:
|
|
|
457
426
|
schema: { type: string }
|
|
458
427
|
post:
|
|
459
428
|
tags: [users]
|
|
429
|
+
x-served-by: app # platform users live in the application's identity store
|
|
460
430
|
summary: Deactivate a user (block login)
|
|
461
431
|
x-mfa-required: true
|
|
462
432
|
requestBody:
|
|
@@ -476,6 +446,7 @@ paths:
|
|
|
476
446
|
- $ref: '#/components/parameters/TenantSlug'
|
|
477
447
|
post:
|
|
478
448
|
tags: [users]
|
|
449
|
+
x-served-by: app # the tenant model is the application's
|
|
479
450
|
summary: Transfer the TENANT_ADMIN role to another user
|
|
480
451
|
x-mfa-required: true
|
|
481
452
|
requestBody:
|
|
@@ -546,6 +517,7 @@ paths:
|
|
|
546
517
|
schema: { type: string, format: uuid }
|
|
547
518
|
get:
|
|
548
519
|
tags: [promo-codes]
|
|
520
|
+
x-served-by: app # the detail view joins application data
|
|
549
521
|
responses:
|
|
550
522
|
'200':
|
|
551
523
|
description: Detail
|
|
@@ -622,41 +594,217 @@ paths:
|
|
|
622
594
|
items: { $ref: '#/components/schemas/AuditEntryDto' }
|
|
623
595
|
|
|
624
596
|
# ────────────────────────────────────────────────────────
|
|
625
|
-
#
|
|
597
|
+
# First-run setup (SetupModule, packages/nest/src/setup/*)
|
|
598
|
+
#
|
|
599
|
+
# Public by design: before the first SUPER_ADMIN there is no session to
|
|
600
|
+
# authenticate. The window is open only while zero SUPER_ADMIN exist and
|
|
601
|
+
# is additionally gated by the operator-set SETUP_TOKEN.
|
|
626
602
|
# ────────────────────────────────────────────────────────
|
|
627
|
-
/
|
|
628
|
-
|
|
629
|
-
tags: [
|
|
630
|
-
|
|
603
|
+
/setup/status:
|
|
604
|
+
get:
|
|
605
|
+
tags: [setup]
|
|
606
|
+
security: [] # PUBLIC — answers whether the window is still open
|
|
607
|
+
summary: Whether first-run setup is still open
|
|
631
608
|
responses:
|
|
632
609
|
'200':
|
|
633
|
-
description:
|
|
610
|
+
description: Status
|
|
611
|
+
content:
|
|
612
|
+
application/json:
|
|
613
|
+
schema:
|
|
614
|
+
type: object
|
|
615
|
+
required: [needsSetup]
|
|
616
|
+
properties:
|
|
617
|
+
needsSetup:
|
|
618
|
+
type: boolean
|
|
619
|
+
description: true as long as no SUPER_ADMIN exists
|
|
620
|
+
|
|
621
|
+
/setup:
|
|
622
|
+
post:
|
|
623
|
+
tags: [setup]
|
|
624
|
+
security: [] # PUBLIC — see the section note
|
|
625
|
+
summary: Create the first SUPER_ADMIN and start its MFA enrolment
|
|
626
|
+
requestBody:
|
|
627
|
+
required: true
|
|
628
|
+
content:
|
|
629
|
+
application/json:
|
|
630
|
+
schema:
|
|
631
|
+
type: object
|
|
632
|
+
required: [token, email]
|
|
633
|
+
properties:
|
|
634
|
+
token: { type: string, description: Must match SETUP_TOKEN }
|
|
635
|
+
email: { type: string, format: email }
|
|
636
|
+
password:
|
|
637
|
+
type: string
|
|
638
|
+
minLength: 8
|
|
639
|
+
description: Generated by the server when omitted
|
|
640
|
+
responses:
|
|
641
|
+
'201':
|
|
642
|
+
description: Created — the response carries the enrolment secret once
|
|
634
643
|
content:
|
|
635
644
|
application/json:
|
|
636
645
|
schema:
|
|
637
646
|
type: object
|
|
647
|
+
required: [userId, email, otpauthUri, qrDataUrl, secret]
|
|
638
648
|
properties:
|
|
649
|
+
userId: { type: string }
|
|
650
|
+
email: { type: string, format: email }
|
|
639
651
|
otpauthUri: { type: string }
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
652
|
+
qrDataUrl: { type: string, description: PNG data URL }
|
|
653
|
+
secret: { type: string, description: Base32 TOTP secret }
|
|
654
|
+
generatedPassword:
|
|
655
|
+
type: string
|
|
656
|
+
description: Present only when the server generated it
|
|
657
|
+
'400':
|
|
658
|
+
description: The email is not an address the platform accepts
|
|
659
|
+
content:
|
|
660
|
+
application/json:
|
|
661
|
+
schema:
|
|
662
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
663
|
+
'401':
|
|
664
|
+
description: The token does not match SETUP_TOKEN
|
|
665
|
+
content:
|
|
666
|
+
application/json:
|
|
667
|
+
schema:
|
|
668
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
669
|
+
'403':
|
|
670
|
+
description: SETUP_TOKEN is not set — setup is disabled
|
|
671
|
+
content:
|
|
672
|
+
application/json:
|
|
673
|
+
schema:
|
|
674
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
675
|
+
'409':
|
|
676
|
+
description: >-
|
|
677
|
+
Either the window has closed itself because a SUPER_ADMIN already
|
|
678
|
+
exists (`SETUP_ALREADY_DONE`), or the address is taken by a platform
|
|
679
|
+
user (`EMAIL_EXISTS`). The `code` tells them apart.
|
|
680
|
+
content:
|
|
681
|
+
application/json:
|
|
682
|
+
schema:
|
|
683
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
644
684
|
|
|
645
|
-
/
|
|
685
|
+
/setup/confirm-mfa:
|
|
646
686
|
post:
|
|
647
|
-
tags: [
|
|
648
|
-
|
|
687
|
+
tags: [setup]
|
|
688
|
+
security: [] # PUBLIC — see the section note
|
|
689
|
+
summary: Confirm the enrolment with the first TOTP code, closing the window
|
|
649
690
|
requestBody:
|
|
650
691
|
required: true
|
|
651
692
|
content:
|
|
652
693
|
application/json:
|
|
653
694
|
schema:
|
|
654
695
|
type: object
|
|
655
|
-
required: [code]
|
|
696
|
+
required: [token, userId, code]
|
|
656
697
|
properties:
|
|
698
|
+
token: { type: string }
|
|
699
|
+
userId: { type: string }
|
|
657
700
|
code: { type: string, pattern: '^[0-9]{6}$' }
|
|
658
701
|
responses:
|
|
659
|
-
'200':
|
|
702
|
+
'200':
|
|
703
|
+
description: Activated
|
|
704
|
+
content:
|
|
705
|
+
application/json:
|
|
706
|
+
schema:
|
|
707
|
+
type: object
|
|
708
|
+
required: [ok]
|
|
709
|
+
properties:
|
|
710
|
+
ok: { type: boolean }
|
|
711
|
+
'401':
|
|
712
|
+
description: The token does not match SETUP_TOKEN
|
|
713
|
+
content:
|
|
714
|
+
application/json:
|
|
715
|
+
schema:
|
|
716
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
717
|
+
'403':
|
|
718
|
+
description: SETUP_TOKEN is not set — setup is disabled
|
|
719
|
+
content:
|
|
720
|
+
application/json:
|
|
721
|
+
schema:
|
|
722
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
723
|
+
|
|
724
|
+
# ────────────────────────────────────────────────────────
|
|
725
|
+
# Plan catalog import (PlanCatalogImporterModule)
|
|
726
|
+
# ────────────────────────────────────────────────────────
|
|
727
|
+
/billing/plan-catalog/import:
|
|
728
|
+
post:
|
|
729
|
+
tags: [plans]
|
|
730
|
+
summary: Import a plan catalog YAML into the catalog tables
|
|
731
|
+
requestBody:
|
|
732
|
+
required: true
|
|
733
|
+
content:
|
|
734
|
+
application/json:
|
|
735
|
+
schema:
|
|
736
|
+
type: object
|
|
737
|
+
required: [yamlContent]
|
|
738
|
+
properties:
|
|
739
|
+
yamlContent: { type: string, description: Contents of saas.yaml }
|
|
740
|
+
crossFieldChecks:
|
|
741
|
+
type: boolean
|
|
742
|
+
default: true
|
|
743
|
+
description: Validate references between plans, features and quotas
|
|
744
|
+
responses:
|
|
745
|
+
'201':
|
|
746
|
+
description: Import report — the counters say what was new and what already matched
|
|
747
|
+
content:
|
|
748
|
+
application/json:
|
|
749
|
+
schema:
|
|
750
|
+
type: object
|
|
751
|
+
required:
|
|
752
|
+
- plansCreated
|
|
753
|
+
- plansSkipped
|
|
754
|
+
- planVersionsCreated
|
|
755
|
+
- planVersionsSkipped
|
|
756
|
+
- featureEntriesCreated
|
|
757
|
+
- featureEntriesSkipped
|
|
758
|
+
- warnings
|
|
759
|
+
properties:
|
|
760
|
+
plansCreated: { type: integer }
|
|
761
|
+
plansSkipped: { type: integer }
|
|
762
|
+
planVersionsCreated: { type: integer }
|
|
763
|
+
planVersionsSkipped: { type: integer }
|
|
764
|
+
featureEntriesCreated: { type: integer }
|
|
765
|
+
featureEntriesSkipped: { type: integer }
|
|
766
|
+
warnings: { type: array, items: { type: string } }
|
|
767
|
+
'400':
|
|
768
|
+
description: The YAML failed schema validation
|
|
769
|
+
content:
|
|
770
|
+
application/json:
|
|
771
|
+
schema:
|
|
772
|
+
$ref: '#/components/schemas/ErrorResponse'
|
|
773
|
+
|
|
774
|
+
# ────────────────────────────────────────────────────────
|
|
775
|
+
# Subscriptions
|
|
776
|
+
# ────────────────────────────────────────────────────────
|
|
777
|
+
/subscriptions:
|
|
778
|
+
get:
|
|
779
|
+
tags: [subscriptions]
|
|
780
|
+
summary: Every tenant subscription, one row each
|
|
781
|
+
responses:
|
|
782
|
+
'200':
|
|
783
|
+
description: Rows
|
|
784
|
+
content:
|
|
785
|
+
application/json:
|
|
786
|
+
schema:
|
|
787
|
+
type: array
|
|
788
|
+
items:
|
|
789
|
+
type: object
|
|
790
|
+
required: [id, tenant, plan, status, billingCycle]
|
|
791
|
+
properties:
|
|
792
|
+
id: { type: string }
|
|
793
|
+
tenant:
|
|
794
|
+
type: object
|
|
795
|
+
required: [slug, name]
|
|
796
|
+
properties:
|
|
797
|
+
slug: { type: string }
|
|
798
|
+
name: { type: string }
|
|
799
|
+
plan: { type: string }
|
|
800
|
+
status: { type: string }
|
|
801
|
+
billingCycle: { type: string }
|
|
802
|
+
periodEndsAt:
|
|
803
|
+
type: string
|
|
804
|
+
format: date-time
|
|
805
|
+
nullable: true
|
|
806
|
+
monthlyNet: { type: string, nullable: true }
|
|
807
|
+
additionalProperties: true
|
|
660
808
|
|
|
661
809
|
# ────────────────────────────────────────────────────────
|
|
662
810
|
# Catalog V2 — Plan/Bundle/Marketing/Promotion + Discovery
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ related:
|
|
|
11
11
|
# Prisma Fragments
|
|
12
12
|
|
|
13
13
|
Prisma-DSL rendering of the SaaSiCat data model, **derived from the normative
|
|
14
|
-
sources**: the logical data model in [`docs/data-model.md`](../../../docs/data-model.md)
|
|
14
|
+
sources**: the logical data model in [`docs/explanation/data-model.md`](../../../docs/explanation/data-model.md)
|
|
15
15
|
and the PostgreSQL artifacts in [`../sql/`](../sql/)
|
|
16
16
|
(`reference-schema.postgres.sql` is generated FROM these fragments via
|
|
17
17
|
`pnpm run gen:sql`; `constraints.postgres.sql` carries the invariants Prisma
|