@contractspec/example.saas-boilerplate 0.0.0-canary-20260113170453

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.
Files changed (226) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +188 -0
  2. package/.turbo/turbo-build.log +189 -0
  3. package/CHANGELOG.md +440 -0
  4. package/LICENSE +21 -0
  5. package/README.md +155 -0
  6. package/dist/billing/billing.entity.d.ts +61 -0
  7. package/dist/billing/billing.entity.d.ts.map +1 -0
  8. package/dist/billing/billing.entity.js +122 -0
  9. package/dist/billing/billing.entity.js.map +1 -0
  10. package/dist/billing/billing.enum.d.ts +16 -0
  11. package/dist/billing/billing.enum.d.ts.map +1 -0
  12. package/dist/billing/billing.enum.js +27 -0
  13. package/dist/billing/billing.enum.js.map +1 -0
  14. package/dist/billing/billing.event.d.ts +86 -0
  15. package/dist/billing/billing.event.d.ts.map +1 -0
  16. package/dist/billing/billing.event.js +153 -0
  17. package/dist/billing/billing.event.js.map +1 -0
  18. package/dist/billing/billing.handler.d.ts +82 -0
  19. package/dist/billing/billing.handler.d.ts.map +1 -0
  20. package/dist/billing/billing.handler.js +58 -0
  21. package/dist/billing/billing.handler.js.map +1 -0
  22. package/dist/billing/billing.operations.d.ts +166 -0
  23. package/dist/billing/billing.operations.d.ts.map +1 -0
  24. package/dist/billing/billing.operations.js +181 -0
  25. package/dist/billing/billing.operations.js.map +1 -0
  26. package/dist/billing/billing.presentation.d.ts +14 -0
  27. package/dist/billing/billing.presentation.d.ts.map +1 -0
  28. package/dist/billing/billing.presentation.js +59 -0
  29. package/dist/billing/billing.presentation.js.map +1 -0
  30. package/dist/billing/billing.schema.d.ts +201 -0
  31. package/dist/billing/billing.schema.d.ts.map +1 -0
  32. package/dist/billing/billing.schema.js +214 -0
  33. package/dist/billing/billing.schema.js.map +1 -0
  34. package/dist/billing/index.d.ts +8 -0
  35. package/dist/billing/index.js +9 -0
  36. package/dist/dashboard/dashboard.presentation.d.ts +14 -0
  37. package/dist/dashboard/dashboard.presentation.d.ts.map +1 -0
  38. package/dist/dashboard/dashboard.presentation.js +55 -0
  39. package/dist/dashboard/dashboard.presentation.js.map +1 -0
  40. package/dist/dashboard/index.d.ts +2 -0
  41. package/dist/dashboard/index.js +3 -0
  42. package/dist/docs/index.d.ts +1 -0
  43. package/dist/docs/index.js +1 -0
  44. package/dist/docs/saas-boilerplate.docblock.d.ts +1 -0
  45. package/dist/docs/saas-boilerplate.docblock.js +100 -0
  46. package/dist/docs/saas-boilerplate.docblock.js.map +1 -0
  47. package/dist/example.d.ts +7 -0
  48. package/dist/example.d.ts.map +1 -0
  49. package/dist/example.js +53 -0
  50. package/dist/example.js.map +1 -0
  51. package/dist/handlers/index.d.ts +4 -0
  52. package/dist/handlers/index.js +5 -0
  53. package/dist/handlers/saas.handlers.d.ts +68 -0
  54. package/dist/handlers/saas.handlers.d.ts.map +1 -0
  55. package/dist/handlers/saas.handlers.js +148 -0
  56. package/dist/handlers/saas.handlers.js.map +1 -0
  57. package/dist/index.d.ts +54 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +81 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/presentations/index.d.ts +17 -0
  62. package/dist/presentations/index.d.ts.map +1 -0
  63. package/dist/presentations/index.js +17 -0
  64. package/dist/presentations/index.js.map +1 -0
  65. package/dist/project/index.d.ts +8 -0
  66. package/dist/project/index.js +9 -0
  67. package/dist/project/project.entity.d.ts +40 -0
  68. package/dist/project/project.entity.d.ts.map +1 -0
  69. package/dist/project/project.entity.js +85 -0
  70. package/dist/project/project.entity.js.map +1 -0
  71. package/dist/project/project.enum.d.ts +16 -0
  72. package/dist/project/project.enum.d.ts.map +1 -0
  73. package/dist/project/project.enum.js +26 -0
  74. package/dist/project/project.enum.js.map +1 -0
  75. package/dist/project/project.event.d.ts +92 -0
  76. package/dist/project/project.event.d.ts.map +1 -0
  77. package/dist/project/project.event.js +165 -0
  78. package/dist/project/project.event.js.map +1 -0
  79. package/dist/project/project.handler.d.ts +72 -0
  80. package/dist/project/project.handler.d.ts.map +1 -0
  81. package/dist/project/project.handler.js +82 -0
  82. package/dist/project/project.handler.js.map +1 -0
  83. package/dist/project/project.operations.d.ts +419 -0
  84. package/dist/project/project.operations.d.ts.map +1 -0
  85. package/dist/project/project.operations.js +260 -0
  86. package/dist/project/project.operations.js.map +1 -0
  87. package/dist/project/project.presentation.d.ts +14 -0
  88. package/dist/project/project.presentation.d.ts.map +1 -0
  89. package/dist/project/project.presentation.js +65 -0
  90. package/dist/project/project.presentation.js.map +1 -0
  91. package/dist/project/project.schema.d.ts +235 -0
  92. package/dist/project/project.schema.d.ts.map +1 -0
  93. package/dist/project/project.schema.js +215 -0
  94. package/dist/project/project.schema.js.map +1 -0
  95. package/dist/saas-boilerplate.feature.d.ts +12 -0
  96. package/dist/saas-boilerplate.feature.d.ts.map +1 -0
  97. package/dist/saas-boilerplate.feature.js +208 -0
  98. package/dist/saas-boilerplate.feature.js.map +1 -0
  99. package/dist/seeders/index.d.ts +10 -0
  100. package/dist/seeders/index.d.ts.map +1 -0
  101. package/dist/seeders/index.js +19 -0
  102. package/dist/seeders/index.js.map +1 -0
  103. package/dist/settings/index.d.ts +3 -0
  104. package/dist/settings/index.js +4 -0
  105. package/dist/settings/settings.entity.d.ts +37 -0
  106. package/dist/settings/settings.entity.d.ts.map +1 -0
  107. package/dist/settings/settings.entity.js +78 -0
  108. package/dist/settings/settings.entity.js.map +1 -0
  109. package/dist/settings/settings.enum.d.ts +10 -0
  110. package/dist/settings/settings.enum.d.ts.map +1 -0
  111. package/dist/settings/settings.enum.js +21 -0
  112. package/dist/settings/settings.enum.js.map +1 -0
  113. package/dist/shared/mock-data.d.ts +86 -0
  114. package/dist/shared/mock-data.d.ts.map +1 -0
  115. package/dist/shared/mock-data.js +138 -0
  116. package/dist/shared/mock-data.js.map +1 -0
  117. package/dist/shared/overlay-types.d.ts +34 -0
  118. package/dist/shared/overlay-types.d.ts.map +1 -0
  119. package/dist/shared/overlay-types.js +0 -0
  120. package/dist/tests/operations.test-spec.d.ts +10 -0
  121. package/dist/tests/operations.test-spec.d.ts.map +1 -0
  122. package/dist/tests/operations.test-spec.js +123 -0
  123. package/dist/tests/operations.test-spec.js.map +1 -0
  124. package/dist/ui/SaasDashboard.d.ts +7 -0
  125. package/dist/ui/SaasDashboard.d.ts.map +1 -0
  126. package/dist/ui/SaasDashboard.js +298 -0
  127. package/dist/ui/SaasDashboard.js.map +1 -0
  128. package/dist/ui/SaasProjectList.d.ts +14 -0
  129. package/dist/ui/SaasProjectList.d.ts.map +1 -0
  130. package/dist/ui/SaasProjectList.js +76 -0
  131. package/dist/ui/SaasProjectList.js.map +1 -0
  132. package/dist/ui/SaasSettingsPanel.d.ts +7 -0
  133. package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
  134. package/dist/ui/SaasSettingsPanel.js +138 -0
  135. package/dist/ui/SaasSettingsPanel.js.map +1 -0
  136. package/dist/ui/hooks/index.d.ts +3 -0
  137. package/dist/ui/hooks/index.js +6 -0
  138. package/dist/ui/hooks/useProjectList.d.ts +34 -0
  139. package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
  140. package/dist/ui/hooks/useProjectList.js +75 -0
  141. package/dist/ui/hooks/useProjectList.js.map +1 -0
  142. package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
  143. package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
  144. package/dist/ui/hooks/useProjectMutations.js +146 -0
  145. package/dist/ui/hooks/useProjectMutations.js.map +1 -0
  146. package/dist/ui/index.d.ts +14 -0
  147. package/dist/ui/index.js +15 -0
  148. package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
  149. package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
  150. package/dist/ui/modals/CreateProjectModal.js +139 -0
  151. package/dist/ui/modals/CreateProjectModal.js.map +1 -0
  152. package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
  153. package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
  154. package/dist/ui/modals/ProjectActionsModal.js +292 -0
  155. package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
  156. package/dist/ui/modals/index.d.ts +3 -0
  157. package/dist/ui/modals/index.js +4 -0
  158. package/dist/ui/overlays/demo-overlays.d.ts +19 -0
  159. package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
  160. package/dist/ui/overlays/demo-overlays.js +70 -0
  161. package/dist/ui/overlays/demo-overlays.js.map +1 -0
  162. package/dist/ui/overlays/index.d.ts +2 -0
  163. package/dist/ui/overlays/index.js +3 -0
  164. package/dist/ui/renderers/index.d.ts +3 -0
  165. package/dist/ui/renderers/index.js +4 -0
  166. package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
  167. package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
  168. package/dist/ui/renderers/project-list.markdown.js +148 -0
  169. package/dist/ui/renderers/project-list.markdown.js.map +1 -0
  170. package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
  171. package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
  172. package/dist/ui/renderers/project-list.renderer.js +17 -0
  173. package/dist/ui/renderers/project-list.renderer.js.map +1 -0
  174. package/example.ts +1 -0
  175. package/package.json +135 -0
  176. package/src/billing/billing.entity.ts +158 -0
  177. package/src/billing/billing.enum.ts +23 -0
  178. package/src/billing/billing.event.ts +108 -0
  179. package/src/billing/billing.handler.ts +137 -0
  180. package/src/billing/billing.operations.ts +187 -0
  181. package/src/billing/billing.presentation.ts +56 -0
  182. package/src/billing/billing.schema.ts +133 -0
  183. package/src/billing/index.ts +64 -0
  184. package/src/dashboard/dashboard.presentation.ts +56 -0
  185. package/src/dashboard/index.ts +8 -0
  186. package/src/docs/index.ts +1 -0
  187. package/src/docs/saas-boilerplate.docblock.ts +98 -0
  188. package/src/example.ts +38 -0
  189. package/src/handlers/index.ts +23 -0
  190. package/src/handlers/saas.handlers.ts +300 -0
  191. package/src/index.ts +76 -0
  192. package/src/presentations/index.ts +36 -0
  193. package/src/project/index.ts +66 -0
  194. package/src/project/project.entity.ts +93 -0
  195. package/src/project/project.enum.ts +22 -0
  196. package/src/project/project.event.ts +128 -0
  197. package/src/project/project.handler.ts +168 -0
  198. package/src/project/project.operations.ts +272 -0
  199. package/src/project/project.presentation.ts +58 -0
  200. package/src/project/project.schema.ts +147 -0
  201. package/src/saas-boilerplate.feature.ts +113 -0
  202. package/src/seeders/index.ts +28 -0
  203. package/src/settings/index.ts +9 -0
  204. package/src/settings/settings.entity.ts +89 -0
  205. package/src/settings/settings.enum.ts +11 -0
  206. package/src/shared/mock-data.ts +110 -0
  207. package/src/shared/overlay-types.ts +39 -0
  208. package/src/tests/operations.test-spec.ts +109 -0
  209. package/src/ui/SaasDashboard.tsx +325 -0
  210. package/src/ui/SaasProjectList.tsx +113 -0
  211. package/src/ui/SaasSettingsPanel.tsx +96 -0
  212. package/src/ui/hooks/index.ts +10 -0
  213. package/src/ui/hooks/useProjectList.ts +95 -0
  214. package/src/ui/hooks/useProjectMutations.ts +166 -0
  215. package/src/ui/index.ts +18 -0
  216. package/src/ui/modals/CreateProjectModal.tsx +176 -0
  217. package/src/ui/modals/ProjectActionsModal.tsx +346 -0
  218. package/src/ui/modals/index.ts +2 -0
  219. package/src/ui/overlays/demo-overlays.ts +74 -0
  220. package/src/ui/overlays/index.ts +1 -0
  221. package/src/ui/renderers/index.ts +7 -0
  222. package/src/ui/renderers/project-list.markdown.ts +239 -0
  223. package/src/ui/renderers/project-list.renderer.tsx +22 -0
  224. package/tsconfig.json +10 -0
  225. package/tsconfig.tsbuildinfo +1 -0
  226. package/tsdown.config.js +7 -0
@@ -0,0 +1,56 @@
1
+ import { definePresentation, StabilityEnum } from '@contractspec/lib.contracts';
2
+
3
+ /**
4
+ * Main dashboard presentation for the SaaS application.
5
+ */
6
+ export const SaasDashboardPresentation = definePresentation({
7
+ meta: {
8
+ key: 'saas.dashboard',
9
+ version: '1.0.0',
10
+ title: 'SaaS Dashboard',
11
+ description:
12
+ 'Main SaaS dashboard with project overview, usage stats, and quick actions',
13
+ domain: 'saas-boilerplate',
14
+ owners: ['@saas-team'],
15
+ tags: ['dashboard', 'overview'],
16
+ stability: StabilityEnum.Beta,
17
+ goal: 'Overview of SaaS activity and metrics',
18
+ context: 'Main dashboard',
19
+ },
20
+ source: {
21
+ type: 'component',
22
+ framework: 'react',
23
+ componentKey: 'SaasDashboard',
24
+ },
25
+ targets: ['react', 'markdown'],
26
+ policy: {
27
+ flags: ['saas.enabled'],
28
+ },
29
+ });
30
+
31
+ /**
32
+ * Settings panel presentation.
33
+ */
34
+ export const SettingsPanelPresentation = definePresentation({
35
+ meta: {
36
+ key: 'saas.settings',
37
+ version: '1.0.0',
38
+ title: 'Settings Panel',
39
+ description: 'Organization and user settings panel',
40
+ domain: 'saas-boilerplate',
41
+ owners: ['@saas-team'],
42
+ tags: ['settings', 'config'],
43
+ stability: StabilityEnum.Beta,
44
+ goal: 'Configure organization and user settings',
45
+ context: 'Settings section',
46
+ },
47
+ source: {
48
+ type: 'component',
49
+ framework: 'react',
50
+ componentKey: 'SettingsPanel',
51
+ },
52
+ targets: ['react'],
53
+ policy: {
54
+ flags: ['saas.enabled'],
55
+ },
56
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Dashboard domain - main SaaS dashboard and settings panels.
3
+ */
4
+
5
+ export {
6
+ SaasDashboardPresentation,
7
+ SettingsPanelPresentation,
8
+ } from './dashboard.presentation';
@@ -0,0 +1 @@
1
+ import './saas-boilerplate.docblock';
@@ -0,0 +1,98 @@
1
+ import type { DocBlock } from '@contractspec/lib.contracts/docs';
2
+ import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
3
+
4
+ const saasBoilerplateDocBlocks: DocBlock[] = [
5
+ {
6
+ id: 'docs.examples.saas-boilerplate.goal',
7
+ title: 'SaaS Boilerplate — Goal',
8
+ summary:
9
+ 'Multi-tenant SaaS foundation with orgs, members, projects, settings, and usage.',
10
+ kind: 'goal',
11
+ visibility: 'public',
12
+ route: '/docs/examples/saas-boilerplate/goal',
13
+ tags: ['saas', 'goal'],
14
+ body: `## Why it matters
15
+ - Provides a regenerable SaaS base: orgs, members, projects, settings, usage/billing.
16
+ - Avoids drift across identity, settings, and usage capture.
17
+
18
+ ## Business/Product goal
19
+ - Ship SaaS faster with tenant isolation, RBAC, and usage metering baked in.
20
+ - Keep audit/notifications ready for compliance and customer comms.
21
+
22
+ ## Success criteria
23
+ - Spec changes to org/project/settings/usage regenerate UI/API/events cleanly.
24
+ - Tenant isolation and RBAC stay enforced; usage data is captured with PII scopes.`,
25
+ },
26
+ {
27
+ id: 'docs.examples.saas-boilerplate.usage',
28
+ title: 'SaaS Boilerplate — Usage',
29
+ summary: 'How to seed, extend, and regenerate the SaaS base.',
30
+ kind: 'usage',
31
+ visibility: 'public',
32
+ route: '/docs/examples/saas-boilerplate/usage',
33
+ tags: ['saas', 'usage'],
34
+ body: `## Setup
35
+ 1) Seed (if available) or create orgs, members, and projects via UI.
36
+ 2) Configure Notifications for invites and project events; set policy.pii for sensitive fields.
37
+
38
+ ## Extend & regenerate
39
+ 1) Adjust schemas (project metadata, settings, usage records) in spec.
40
+ 2) Regenerate to sync UI/API/events and usage metering.
41
+ 3) Use Feature Flags to roll out new settings or billing fields gradually.
42
+
43
+ ## Guardrails
44
+ - Keep tenant/role context explicit in contracts and presentations.
45
+ - Emit events for invites, project changes, and usage records; log in Audit Trail.
46
+ - Redact sensitive user/org data in markdown/JSON outputs.`,
47
+ },
48
+ {
49
+ id: 'docs.examples.saas-boilerplate.reference',
50
+ title: 'SaaS Boilerplate — Reference',
51
+ summary:
52
+ 'Entities, contracts, events, and presentations for the SaaS starter.',
53
+ kind: 'reference',
54
+ visibility: 'public',
55
+ route: '/docs/examples/saas-boilerplate',
56
+ tags: ['saas', 'reference'],
57
+ body: `## Entities
58
+ - Organization, Member, Role, Project, AppSettings, UserSettings, BillingUsage.
59
+
60
+ ## Contracts
61
+ - org/project CRUD, invites, role assignment, usage recording.
62
+
63
+ ## Events
64
+ - org.created, member.invited/accepted, project.created/updated, usage.recorded.
65
+
66
+ ## Presentations
67
+ - Org/project dashboards, member management, settings screens, usage views.
68
+
69
+ ## Notes
70
+ - Tenant isolation is mandatory; enforce via RBAC/policies.
71
+ - Usage/Metering drives billing/limits; keep units explicit.`,
72
+ },
73
+ {
74
+ id: 'docs.examples.saas-boilerplate.constraints',
75
+ title: 'SaaS Boilerplate — Constraints & Safety',
76
+ summary:
77
+ 'Internal guardrails for tenancy, RBAC, usage metering, and regeneration.',
78
+ kind: 'reference',
79
+ visibility: 'internal',
80
+ route: '/docs/examples/saas-boilerplate/constraints',
81
+ tags: ['saas', 'constraints', 'internal'],
82
+ body: `## Constraints
83
+ - Tenant isolation and RBAC must remain explicit in spec; no implicit defaults in code.
84
+ - Events to emit: org.created, member.invited/accepted, project.created/updated, usage.recorded.
85
+ - Regeneration must not change billing/usage semantics without spec diffs.
86
+
87
+ ## PII & Settings
88
+ - Mark PII (user emails, names) for redaction; keep settings scoped to org/member.
89
+ - Avoid leaking secrets/config in markdown/JSON presentations.
90
+
91
+ ## Verification
92
+ - Add fixtures for usage recording and role changes.
93
+ - Ensure Audit/Notifications remain wired for invites/project updates.
94
+ - Use Feature Flags for new settings/billing fields; default safe/off.`,
95
+ },
96
+ ];
97
+
98
+ registerDocBlocks(saasBoilerplateDocBlocks);
package/src/example.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { defineExample } from '@contractspec/lib.contracts';
2
+
3
+ const example = defineExample({
4
+ meta: {
5
+ key: 'saas-boilerplate',
6
+ version: '1.0.0',
7
+ title: 'SaaS Boilerplate',
8
+ description:
9
+ 'Multi-tenant SaaS foundation with orgs, projects, settings, billing usage, and RBAC.',
10
+ kind: 'template',
11
+ visibility: 'public',
12
+ stability: 'experimental',
13
+ owners: ['@platform.core'],
14
+ tags: ['saas', 'multi-tenant', 'billing', 'rbac'],
15
+ },
16
+ docs: {
17
+ rootDocId: 'docs.examples.saas-boilerplate',
18
+ },
19
+ entrypoints: {
20
+ packageName: '@contractspec/example.saas-boilerplate',
21
+ feature: './feature',
22
+ contracts: './contracts',
23
+ presentations: './presentations',
24
+ handlers: './handlers',
25
+ docs: './docs',
26
+ },
27
+ surfaces: {
28
+ templates: true,
29
+ sandbox: {
30
+ enabled: true,
31
+ modes: ['playground', 'specs', 'builder', 'markdown', 'evolution'],
32
+ },
33
+ studio: { enabled: true, installable: true },
34
+ mcp: { enabled: true },
35
+ },
36
+ });
37
+
38
+ export default example;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * SaaS Boilerplate Handlers - re-exports from domain modules for backward compatibility.
3
+ */
4
+
5
+ // Billing handlers
6
+ export {
7
+ mockGetSubscriptionHandler,
8
+ mockRecordUsageHandler,
9
+ mockGetUsageSummaryHandler,
10
+ mockCheckFeatureAccessHandler,
11
+ } from '../billing/billing.handler';
12
+
13
+ // Project handlers
14
+ export {
15
+ mockCreateProjectHandler,
16
+ mockGetProjectHandler,
17
+ mockListProjectsHandler,
18
+ mockUpdateProjectHandler,
19
+ mockDeleteProjectHandler,
20
+ } from '../project/project.handler';
21
+
22
+ // Runtime handlers (PGLite)
23
+ export * from './saas.handlers';
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Runtime-local SaaS Boilerplate handlers
3
+ *
4
+ * Database-backed handlers for project and billing management.
5
+ */
6
+ import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
7
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
8
+ import { web } from '@contractspec/lib.runtime-sandbox';
9
+ const { generateId } = web;
10
+
11
+ // ============ Types ============
12
+
13
+ export interface Project {
14
+ id: string;
15
+ projectId: string;
16
+ organizationId: string;
17
+ name: string;
18
+ description?: string;
19
+ status: 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
20
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
21
+ createdAt: Date;
22
+ updatedAt: Date;
23
+ }
24
+
25
+ export interface Subscription {
26
+ id: string;
27
+ projectId: string;
28
+ organizationId: string;
29
+ plan: 'FREE' | 'PRO' | 'ENTERPRISE';
30
+ status: 'ACTIVE' | 'PAST_DUE' | 'CANCELED';
31
+ billingCycle: 'MONTHLY' | 'YEARLY';
32
+ currentPeriodStart: Date;
33
+ currentPeriodEnd: Date;
34
+ cancelAtPeriodEnd: boolean;
35
+ }
36
+
37
+ export interface ListProjectsInput {
38
+ projectId: string;
39
+ organizationId?: string;
40
+ status?: Project['status'] | 'all';
41
+ search?: string;
42
+ limit?: number;
43
+ offset?: number;
44
+ }
45
+
46
+ export interface ListProjectsOutput {
47
+ items: Project[];
48
+ total: number;
49
+ hasMore: boolean;
50
+ }
51
+
52
+ export interface CreateProjectInput {
53
+ name: string;
54
+ description?: string;
55
+ tier?: Project['tier'];
56
+ }
57
+
58
+ export interface UpdateProjectInput {
59
+ id: string;
60
+ name?: string;
61
+ description?: string;
62
+ status?: Project['status'];
63
+ }
64
+
65
+ // ============ Row Types ============
66
+
67
+ interface ProjectRow extends Record<string, unknown> {
68
+ id: string;
69
+ projectId: string;
70
+ organizationId: string;
71
+ name: string;
72
+ description: string | null;
73
+ status: string;
74
+ tier: string;
75
+ createdAt: string;
76
+ updatedAt: string;
77
+ }
78
+
79
+ interface SubscriptionRow extends Record<string, unknown> {
80
+ id: string;
81
+ projectId: string;
82
+ organizationId: string;
83
+ plan: string;
84
+ status: string;
85
+ billingCycle: string;
86
+ currentPeriodStart: string;
87
+ currentPeriodEnd: string;
88
+ cancelAtPeriodEnd: number;
89
+ }
90
+
91
+ function rowToProject(row: ProjectRow): Project {
92
+ return {
93
+ id: row.id,
94
+ projectId: row.projectId,
95
+ organizationId: row.organizationId,
96
+ name: row.name,
97
+ description: row.description ?? undefined,
98
+ status: row.status as Project['status'],
99
+ tier: row.tier as Project['tier'],
100
+ createdAt: new Date(row.createdAt),
101
+ updatedAt: new Date(row.updatedAt),
102
+ };
103
+ }
104
+
105
+ function rowToSubscription(row: SubscriptionRow): Subscription {
106
+ return {
107
+ id: row.id,
108
+ projectId: row.projectId,
109
+ organizationId: row.organizationId,
110
+ plan: row.plan as Subscription['plan'],
111
+ status: row.status as Subscription['status'],
112
+ billingCycle: row.billingCycle as Subscription['billingCycle'],
113
+ currentPeriodStart: new Date(row.currentPeriodStart),
114
+ currentPeriodEnd: new Date(row.currentPeriodEnd),
115
+ cancelAtPeriodEnd: Boolean(row.cancelAtPeriodEnd),
116
+ };
117
+ }
118
+
119
+ // ============ Handler Factory ============
120
+
121
+ export function createSaasHandlers(db: DatabasePort) {
122
+ /**
123
+ * List projects
124
+ */
125
+ async function listProjects(
126
+ input: ListProjectsInput
127
+ ): Promise<ListProjectsOutput> {
128
+ const {
129
+ projectId,
130
+ organizationId,
131
+ status,
132
+ search,
133
+ limit = 20,
134
+ offset = 0,
135
+ } = input;
136
+
137
+ let whereClause = 'WHERE projectId = ?';
138
+ const params: (string | number)[] = [projectId];
139
+
140
+ if (organizationId) {
141
+ whereClause += ' AND organizationId = ?';
142
+ params.push(organizationId);
143
+ }
144
+
145
+ if (status && status !== 'all') {
146
+ whereClause += ' AND status = ?';
147
+ params.push(status);
148
+ }
149
+
150
+ if (search) {
151
+ whereClause += ' AND (name LIKE ? OR description LIKE ?)';
152
+ params.push(`%${search}%`, `%${search}%`);
153
+ }
154
+
155
+ const countResult = (
156
+ await db.query(
157
+ `SELECT COUNT(*) as count FROM saas_project ${whereClause}`,
158
+ params
159
+ )
160
+ ).rows as unknown as { count: number }[];
161
+ const total = countResult[0]?.count ?? 0;
162
+
163
+ const rows = (
164
+ await db.query(
165
+ `SELECT * FROM saas_project ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
166
+ [...params, limit, offset]
167
+ )
168
+ ).rows as unknown as ProjectRow[];
169
+
170
+ return {
171
+ items: rows.map(rowToProject),
172
+ total,
173
+ hasMore: offset + rows.length < total,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Get a single project
179
+ */
180
+ async function getProject(id: string): Promise<Project | null> {
181
+ const rows = (
182
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])
183
+ ).rows as unknown as ProjectRow[];
184
+ return rows[0] ? rowToProject(rows[0]) : null;
185
+ }
186
+
187
+ /**
188
+ * Create a project
189
+ */
190
+ async function createProject(
191
+ input: CreateProjectInput,
192
+ context: { projectId: string; organizationId: string }
193
+ ): Promise<Project> {
194
+ const id = generateId('proj');
195
+ const now = new Date().toISOString();
196
+
197
+ await db.execute(
198
+ `INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
199
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
200
+ [
201
+ id,
202
+ context.projectId,
203
+ context.organizationId,
204
+ input.name,
205
+ input.description ?? null,
206
+ 'DRAFT',
207
+ input.tier ?? 'FREE',
208
+ now,
209
+ now,
210
+ ]
211
+ );
212
+
213
+ const rows = (
214
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])
215
+ ).rows as unknown as ProjectRow[];
216
+
217
+ return rowToProject(rows[0]!);
218
+ }
219
+
220
+ /**
221
+ * Update a project
222
+ */
223
+ async function updateProject(input: UpdateProjectInput): Promise<Project> {
224
+ const now = new Date().toISOString();
225
+ const updates: string[] = ['updatedAt = ?'];
226
+ const params: (string | null)[] = [now];
227
+
228
+ if (input.name !== undefined) {
229
+ updates.push('name = ?');
230
+ params.push(input.name);
231
+ }
232
+
233
+ if (input.description !== undefined) {
234
+ updates.push('description = ?');
235
+ params.push(input.description);
236
+ }
237
+
238
+ if (input.status !== undefined) {
239
+ updates.push('status = ?');
240
+ params.push(input.status);
241
+ }
242
+
243
+ params.push(input.id);
244
+
245
+ await db.execute(
246
+ `UPDATE saas_project SET ${updates.join(', ')} WHERE id = ?`,
247
+ params
248
+ );
249
+
250
+ const rows = (
251
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [input.id])
252
+ ).rows as unknown as ProjectRow[];
253
+
254
+ if (!rows[0]) {
255
+ throw new Error('NOT_FOUND');
256
+ }
257
+
258
+ return rowToProject(rows[0]);
259
+ }
260
+
261
+ /**
262
+ * Delete a project
263
+ */
264
+ async function deleteProject(id: string): Promise<void> {
265
+ await db.execute(`DELETE FROM saas_project WHERE id = ?`, [id]);
266
+ }
267
+
268
+ /**
269
+ * Get subscription for an organization
270
+ */
271
+ async function getSubscription(input: {
272
+ projectId: string;
273
+ organizationId?: string;
274
+ }): Promise<Subscription | null> {
275
+ let query = `SELECT * FROM saas_subscription WHERE projectId = ?`;
276
+ const params: string[] = [input.projectId];
277
+
278
+ if (input.organizationId) {
279
+ query += ' AND organizationId = ?';
280
+ params.push(input.organizationId);
281
+ }
282
+
283
+ query += ' LIMIT 1';
284
+
285
+ const rows = (await db.query(query, params))
286
+ .rows as unknown as SubscriptionRow[];
287
+ return rows[0] ? rowToSubscription(rows[0]) : null;
288
+ }
289
+
290
+ return {
291
+ listProjects,
292
+ getProject,
293
+ createProject,
294
+ updateProject,
295
+ deleteProject,
296
+ getSubscription,
297
+ };
298
+ }
299
+
300
+ export type SaasHandlers = ReturnType<typeof createSaasHandlers>;
package/src/index.ts ADDED
@@ -0,0 +1,76 @@
1
+ // SaaS Boilerplate Example
2
+ // Demonstrates ContractSpec principles for a complete SaaS application
3
+
4
+ // Export all domain modules
5
+ export * from './billing';
6
+ export * from './project';
7
+ export * from './settings';
8
+ export * from './dashboard';
9
+
10
+ // Export feature and example metadata
11
+ export * from './saas-boilerplate.feature';
12
+ export * from './ui';
13
+ export {
14
+ createSaasHandlers,
15
+ type SaasHandlers,
16
+ } from './handlers/saas.handlers';
17
+ export { default as example } from './example';
18
+
19
+ // Import docs for registration
20
+ import './docs';
21
+
22
+ // Schema composition configuration
23
+ import { identityRbacSchemaContribution } from '@contractspec/lib.identity-rbac';
24
+ import { jobsSchemaContribution } from '@contractspec/lib.jobs';
25
+ import { auditTrailSchemaContribution } from '@contractspec/module.audit-trail';
26
+ import { notificationsSchemaContribution } from '@contractspec/module.notifications';
27
+ import type { ModuleSchemaContribution } from '@contractspec/lib.schema';
28
+ import {
29
+ ProjectEntity,
30
+ ProjectMemberEntity,
31
+ ProjectStatusEnum,
32
+ } from './project/project.entity';
33
+ import {
34
+ SettingsEntity,
35
+ FeatureFlagEntity,
36
+ SettingsScopeEnum,
37
+ } from './settings';
38
+ import {
39
+ SubscriptionEntity,
40
+ BillingUsageEntity,
41
+ UsageLimitEntity,
42
+ SubscriptionStatusEnum,
43
+ } from './billing/billing.entity';
44
+
45
+ /**
46
+ * SaaS boilerplate schema contribution.
47
+ */
48
+ export const saasBoilerplateSchemaContribution: ModuleSchemaContribution = {
49
+ moduleId: '@contractspec/example.saas-boilerplate',
50
+ entities: [
51
+ ProjectEntity,
52
+ ProjectMemberEntity,
53
+ SettingsEntity,
54
+ FeatureFlagEntity,
55
+ SubscriptionEntity,
56
+ BillingUsageEntity,
57
+ UsageLimitEntity,
58
+ ],
59
+ enums: [ProjectStatusEnum, SettingsScopeEnum, SubscriptionStatusEnum],
60
+ };
61
+
62
+ /**
63
+ * Complete schema composition for SaaS Boilerplate.
64
+ * Use with `database schema:compose` to generate Prisma schema.
65
+ */
66
+ export const schemaComposition = {
67
+ modules: [
68
+ identityRbacSchemaContribution,
69
+ jobsSchemaContribution,
70
+ auditTrailSchemaContribution,
71
+ notificationsSchemaContribution,
72
+ saasBoilerplateSchemaContribution,
73
+ ],
74
+ provider: 'postgresql' as const,
75
+ outputPath: './prisma/schema/generated.prisma',
76
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * SaaS Boilerplate Presentations - re-exports from domain modules for backward compatibility.
3
+ */
4
+
5
+ // Billing presentations
6
+ export {
7
+ SubscriptionPresentation,
8
+ UsageDashboardPresentation,
9
+ } from '../billing/billing.presentation';
10
+
11
+ // Project presentations
12
+ export {
13
+ ProjectListPresentation,
14
+ ProjectDetailPresentation,
15
+ } from '../project/project.presentation';
16
+
17
+ // Dashboard presentations
18
+ export {
19
+ SaasDashboardPresentation,
20
+ SettingsPanelPresentation,
21
+ } from '../dashboard/dashboard.presentation';
22
+
23
+ // All presentations collection
24
+ export const SaasBoilerplatePresentations = {
25
+ // Billing
26
+ SubscriptionPresentation: undefined,
27
+ UsageDashboardPresentation: undefined,
28
+
29
+ // Project
30
+ ProjectListPresentation: undefined,
31
+ ProjectDetailPresentation: undefined,
32
+
33
+ // Dashboard
34
+ SaasDashboardPresentation: undefined,
35
+ SettingsPanelPresentation: undefined,
36
+ };