@contractspec/example.saas-boilerplate 1.46.1 → 1.48.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.
Files changed (135) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +183 -108
  2. package/.turbo/turbo-build.log +182 -107
  3. package/CHANGELOG.md +64 -0
  4. package/README.md +0 -1
  5. package/dist/billing/billing.event.d.ts +4 -4
  6. package/dist/billing/billing.event.js +1 -1
  7. package/dist/billing/billing.operations.d.ts +5 -5
  8. package/dist/billing/billing.presentation.d.ts +3 -4
  9. package/dist/billing/billing.presentation.d.ts.map +1 -1
  10. package/dist/billing/billing.presentation.js +5 -5
  11. package/dist/billing/billing.presentation.js.map +1 -1
  12. package/dist/dashboard/dashboard.presentation.d.ts +3 -4
  13. package/dist/dashboard/dashboard.presentation.d.ts.map +1 -1
  14. package/dist/dashboard/dashboard.presentation.js +5 -5
  15. package/dist/dashboard/dashboard.presentation.js.map +1 -1
  16. package/dist/example.d.ts +2 -2
  17. package/dist/example.d.ts.map +1 -1
  18. package/dist/example.js +4 -2
  19. package/dist/example.js.map +1 -1
  20. package/dist/handlers/index.d.ts +2 -1
  21. package/dist/handlers/index.js +2 -1
  22. package/dist/handlers/saas.handlers.d.ts +68 -0
  23. package/dist/handlers/saas.handlers.d.ts.map +1 -0
  24. package/dist/handlers/saas.handlers.js +148 -0
  25. package/dist/handlers/saas.handlers.js.map +1 -0
  26. package/dist/index.d.ts +13 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +13 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/project/project.enum.d.ts +3 -3
  31. package/dist/project/project.event.d.ts +22 -22
  32. package/dist/project/project.event.d.ts.map +1 -1
  33. package/dist/project/project.event.js +1 -1
  34. package/dist/project/project.operations.d.ts +103 -103
  35. package/dist/project/project.presentation.d.ts +3 -4
  36. package/dist/project/project.presentation.d.ts.map +1 -1
  37. package/dist/project/project.presentation.js +5 -5
  38. package/dist/project/project.presentation.js.map +1 -1
  39. package/dist/project/project.schema.d.ts +54 -54
  40. package/dist/saas-boilerplate.feature.d.ts +2 -2
  41. package/dist/saas-boilerplate.feature.d.ts.map +1 -1
  42. package/dist/saas-boilerplate.feature.js +9 -2
  43. package/dist/saas-boilerplate.feature.js.map +1 -1
  44. package/dist/seeders/index.d.ts +10 -0
  45. package/dist/seeders/index.d.ts.map +1 -0
  46. package/dist/seeders/index.js +19 -0
  47. package/dist/seeders/index.js.map +1 -0
  48. package/dist/settings/settings.entity.d.ts +24 -24
  49. package/dist/settings/settings.enum.d.ts +2 -2
  50. package/dist/shared/overlay-types.d.ts +34 -0
  51. package/dist/shared/overlay-types.d.ts.map +1 -0
  52. package/dist/shared/overlay-types.js +0 -0
  53. package/dist/tests/operations.test-spec.d.ts +10 -0
  54. package/dist/tests/operations.test-spec.d.ts.map +1 -0
  55. package/dist/tests/operations.test-spec.js +123 -0
  56. package/dist/tests/operations.test-spec.js.map +1 -0
  57. package/dist/ui/SaasDashboard.d.ts +7 -0
  58. package/dist/ui/SaasDashboard.d.ts.map +1 -0
  59. package/dist/ui/SaasDashboard.js +298 -0
  60. package/dist/ui/SaasDashboard.js.map +1 -0
  61. package/dist/ui/SaasProjectList.d.ts +14 -0
  62. package/dist/ui/SaasProjectList.d.ts.map +1 -0
  63. package/dist/ui/SaasProjectList.js +76 -0
  64. package/dist/ui/SaasProjectList.js.map +1 -0
  65. package/dist/ui/SaasSettingsPanel.d.ts +7 -0
  66. package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
  67. package/dist/ui/SaasSettingsPanel.js +138 -0
  68. package/dist/ui/SaasSettingsPanel.js.map +1 -0
  69. package/dist/ui/hooks/index.d.ts +3 -0
  70. package/dist/ui/hooks/index.js +6 -0
  71. package/dist/ui/hooks/useProjectList.d.ts +34 -0
  72. package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
  73. package/dist/ui/hooks/useProjectList.js +75 -0
  74. package/dist/ui/hooks/useProjectList.js.map +1 -0
  75. package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
  76. package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
  77. package/dist/ui/hooks/useProjectMutations.js +146 -0
  78. package/dist/ui/hooks/useProjectMutations.js.map +1 -0
  79. package/dist/ui/index.d.ts +14 -0
  80. package/dist/ui/index.js +15 -0
  81. package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
  82. package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
  83. package/dist/ui/modals/CreateProjectModal.js +139 -0
  84. package/dist/ui/modals/CreateProjectModal.js.map +1 -0
  85. package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
  86. package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
  87. package/dist/ui/modals/ProjectActionsModal.js +292 -0
  88. package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
  89. package/dist/ui/modals/index.d.ts +3 -0
  90. package/dist/ui/modals/index.js +4 -0
  91. package/dist/ui/overlays/demo-overlays.d.ts +19 -0
  92. package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
  93. package/dist/ui/overlays/demo-overlays.js +70 -0
  94. package/dist/ui/overlays/demo-overlays.js.map +1 -0
  95. package/dist/ui/overlays/index.d.ts +2 -0
  96. package/dist/ui/overlays/index.js +3 -0
  97. package/dist/ui/renderers/index.d.ts +3 -0
  98. package/dist/ui/renderers/index.js +4 -0
  99. package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
  100. package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
  101. package/dist/ui/renderers/project-list.markdown.js +148 -0
  102. package/dist/ui/renderers/project-list.markdown.js.map +1 -0
  103. package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
  104. package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
  105. package/dist/ui/renderers/project-list.renderer.js +17 -0
  106. package/dist/ui/renderers/project-list.renderer.js.map +1 -0
  107. package/package.json +38 -14
  108. package/src/billing/billing.presentation.ts +5 -6
  109. package/src/dashboard/dashboard.presentation.ts +5 -6
  110. package/src/example.ts +3 -3
  111. package/src/handlers/index.ts +3 -0
  112. package/src/handlers/saas.handlers.ts +300 -0
  113. package/src/index.ts +5 -0
  114. package/src/project/project.presentation.ts +5 -6
  115. package/src/saas-boilerplate.feature.ts +3 -3
  116. package/src/seeders/index.ts +28 -0
  117. package/src/shared/overlay-types.ts +39 -0
  118. package/src/tests/operations.test-spec.ts +109 -0
  119. package/src/ui/SaasDashboard.tsx +325 -0
  120. package/src/ui/SaasProjectList.tsx +113 -0
  121. package/src/ui/SaasSettingsPanel.tsx +96 -0
  122. package/src/ui/hooks/index.ts +10 -0
  123. package/src/ui/hooks/useProjectList.ts +95 -0
  124. package/src/ui/hooks/useProjectMutations.ts +166 -0
  125. package/src/ui/index.ts +18 -0
  126. package/src/ui/modals/CreateProjectModal.tsx +176 -0
  127. package/src/ui/modals/ProjectActionsModal.tsx +346 -0
  128. package/src/ui/modals/index.ts +2 -0
  129. package/src/ui/overlays/demo-overlays.ts +74 -0
  130. package/src/ui/overlays/index.ts +1 -0
  131. package/src/ui/renderers/index.ts +7 -0
  132. package/src/ui/renderers/project-list.markdown.ts +239 -0
  133. package/src/ui/renderers/project-list.renderer.tsx +22 -0
  134. package/tsconfig.json +1 -1
  135. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,9 @@
1
+ import { PresentationRenderer } from "@contractspec/lib.contracts";
2
+ import * as React from "react";
3
+
4
+ //#region src/ui/renderers/project-list.renderer.d.ts
5
+
6
+ declare const projectListReactRenderer: PresentationRenderer<React.ReactElement>;
7
+ //#endregion
8
+ export { projectListReactRenderer };
9
+ //# sourceMappingURL=project-list.renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-list.renderer.d.ts","names":[],"sources":["../../../src/ui/renderers/project-list.renderer.tsx"],"sourcesContent":[],"mappings":";;;;;cAOa,0BAA0B,qBAAqB,KAAA,CAAM"}
@@ -0,0 +1,17 @@
1
+ import { SaasProjectList } from "../SaasProjectList.js";
2
+ import "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+
5
+ //#region src/ui/renderers/project-list.renderer.tsx
6
+ const projectListReactRenderer = {
7
+ target: "react",
8
+ render: async (desc, _ctx) => {
9
+ if (desc.source.type !== "component") throw new Error("Invalid source type");
10
+ if (desc.source.componentKey !== "SaasProjectListView") throw new Error(`Unknown component: ${desc.source.componentKey}`);
11
+ return /* @__PURE__ */ jsx(SaasProjectList, {});
12
+ }
13
+ };
14
+
15
+ //#endregion
16
+ export { projectListReactRenderer };
17
+ //# sourceMappingURL=project-list.renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-list.renderer.js","names":[],"sources":["../../../src/ui/renderers/project-list.renderer.tsx"],"sourcesContent":["/**\n * React renderer for SaaS Project List presentation\n */\nimport * as React from 'react';\nimport type { PresentationRenderer } from '@contractspec/lib.contracts';\nimport { SaasProjectList } from '../SaasProjectList';\n\nexport const projectListReactRenderer: PresentationRenderer<React.ReactElement> =\n {\n target: 'react',\n render: async (desc, _ctx) => {\n if (desc.source.type !== 'component') {\n throw new Error('Invalid source type');\n }\n\n if (desc.source.componentKey !== 'SaasProjectListView') {\n throw new Error(`Unknown component: ${desc.source.componentKey}`);\n }\n\n return <SaasProjectList />;\n },\n };\n"],"mappings":";;;;;AAOA,MAAa,2BACX;CACE,QAAQ;CACR,QAAQ,OAAO,MAAM,SAAS;AAC5B,MAAI,KAAK,OAAO,SAAS,YACvB,OAAM,IAAI,MAAM,sBAAsB;AAGxC,MAAI,KAAK,OAAO,iBAAiB,sBAC/B,OAAM,IAAI,MAAM,sBAAsB,KAAK,OAAO,eAAe;AAGnE,SAAO,oBAAC,oBAAkB;;CAE7B"}
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "@contractspec/example.saas-boilerplate",
3
- "version": "1.46.1",
3
+ "version": "1.48.0",
4
4
  "description": "SaaS Boilerplate - Users, Orgs, Projects, Billing, Settings",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
7
6
  "types": "./dist/index.d.ts",
8
7
  "exports": {
9
8
  ".": "./dist/index.js",
@@ -21,6 +20,7 @@
21
20
  "./docs/saas-boilerplate.docblock": "./dist/docs/saas-boilerplate.docblock.js",
22
21
  "./example": "./dist/example.js",
23
22
  "./handlers": "./dist/handlers/index.js",
23
+ "./handlers/saas.handlers": "./dist/handlers/saas.handlers.js",
24
24
  "./presentations": "./dist/presentations/index.js",
25
25
  "./project": "./dist/project/index.js",
26
26
  "./project/project.entity": "./dist/project/project.entity.js",
@@ -31,10 +31,28 @@
31
31
  "./project/project.presentation": "./dist/project/project.presentation.js",
32
32
  "./project/project.schema": "./dist/project/project.schema.js",
33
33
  "./saas-boilerplate.feature": "./dist/saas-boilerplate.feature.js",
34
+ "./seeders": "./dist/seeders/index.js",
34
35
  "./settings": "./dist/settings/index.js",
35
36
  "./settings/settings.entity": "./dist/settings/settings.entity.js",
36
37
  "./settings/settings.enum": "./dist/settings/settings.enum.js",
37
38
  "./shared/mock-data": "./dist/shared/mock-data.js",
39
+ "./shared/overlay-types": "./dist/shared/overlay-types.js",
40
+ "./tests/operations.test-spec": "./dist/tests/operations.test-spec.js",
41
+ "./ui": "./dist/ui/index.js",
42
+ "./ui/hooks": "./dist/ui/hooks/index.js",
43
+ "./ui/hooks/useProjectList": "./dist/ui/hooks/useProjectList.js",
44
+ "./ui/hooks/useProjectMutations": "./dist/ui/hooks/useProjectMutations.js",
45
+ "./ui/modals": "./dist/ui/modals/index.js",
46
+ "./ui/modals/CreateProjectModal": "./dist/ui/modals/CreateProjectModal.js",
47
+ "./ui/modals/ProjectActionsModal": "./dist/ui/modals/ProjectActionsModal.js",
48
+ "./ui/overlays": "./dist/ui/overlays/index.js",
49
+ "./ui/overlays/demo-overlays": "./dist/ui/overlays/demo-overlays.js",
50
+ "./ui/renderers": "./dist/ui/renderers/index.js",
51
+ "./ui/renderers/project-list.markdown": "./dist/ui/renderers/project-list.markdown.js",
52
+ "./ui/renderers/project-list.renderer": "./dist/ui/renderers/project-list.renderer.js",
53
+ "./ui/SaasDashboard": "./dist/ui/SaasDashboard.js",
54
+ "./ui/SaasProjectList": "./dist/ui/SaasProjectList.js",
55
+ "./ui/SaasSettingsPanel": "./dist/ui/SaasSettingsPanel.js",
38
56
  "./*": "./*"
39
57
  },
40
58
  "scripts": {
@@ -48,23 +66,29 @@
48
66
  "lint": "bun lint:fix",
49
67
  "lint:fix": "eslint src --fix",
50
68
  "lint:check": "eslint src",
51
- "test": "bun run"
69
+ "test": "bun test"
52
70
  },
53
71
  "dependencies": {
54
- "@contractspec/lib.identity-rbac": "1.46.1",
55
- "@contractspec/lib.jobs": "1.46.1",
56
- "@contractspec/module.audit-trail": "1.46.1",
57
- "@contractspec/module.notifications": "1.46.1",
58
- "@contractspec/lib.contracts": "1.46.1",
59
- "@contractspec/lib.schema": "1.46.1"
72
+ "@contractspec/lib.identity-rbac": "1.48.0",
73
+ "@contractspec/lib.jobs": "1.48.0",
74
+ "@contractspec/module.audit-trail": "1.48.0",
75
+ "@contractspec/module.notifications": "1.48.0",
76
+ "@contractspec/lib.contracts": "1.48.0",
77
+ "@contractspec/lib.schema": "1.48.0",
78
+ "@contractspec/lib.example-shared-ui": "1.2.0",
79
+ "@contractspec/lib.design-system": "1.48.0",
80
+ "@contractspec/lib.runtime-sandbox": "0.3.0",
81
+ "react": "19.2.3",
82
+ "react-dom": "19.2.3"
60
83
  },
61
84
  "devDependencies": {
62
- "@contractspec/tool.tsdown": "1.46.1",
63
- "@contractspec/tool.typescript": "1.46.1",
64
- "tsdown": "^0.18.3",
65
- "typescript": "^5.9.3"
85
+ "@contractspec/tool.tsdown": "1.48.0",
86
+ "@contractspec/tool.typescript": "1.48.0",
87
+ "tsdown": "^0.19.0",
88
+ "typescript": "^5.9.3",
89
+ "@types/react": "^19.2.8",
90
+ "@types/react-dom": "^19.2.2"
66
91
  },
67
- "module": "./dist/index.js",
68
92
  "publishConfig": {
69
93
  "exports": {
70
94
  ".": "./dist/index.js",
@@ -1,10 +1,9 @@
1
- import type { PresentationSpec } from '@contractspec/lib.contracts';
2
- import { StabilityEnum } from '@contractspec/lib.contracts';
1
+ import { definePresentation, StabilityEnum } from '@contractspec/lib.contracts';
3
2
 
4
3
  /**
5
4
  * Presentation for subscription overview.
6
5
  */
7
- export const SubscriptionPresentation: PresentationSpec = {
6
+ export const SubscriptionPresentation = definePresentation({
8
7
  meta: {
9
8
  key: 'saas.billing.subscription',
10
9
  version: '1.0.0',
@@ -27,12 +26,12 @@ export const SubscriptionPresentation: PresentationSpec = {
27
26
  policy: {
28
27
  flags: ['saas.billing.enabled'],
29
28
  },
30
- };
29
+ });
31
30
 
32
31
  /**
33
32
  * Presentation for usage dashboard.
34
33
  */
35
- export const UsageDashboardPresentation: PresentationSpec = {
34
+ export const UsageDashboardPresentation = definePresentation({
36
35
  meta: {
37
36
  key: 'saas.billing.usage',
38
37
  version: '1.0.0',
@@ -54,4 +53,4 @@ export const UsageDashboardPresentation: PresentationSpec = {
54
53
  policy: {
55
54
  flags: ['saas.billing.enabled'],
56
55
  },
57
- };
56
+ });
@@ -1,10 +1,9 @@
1
- import type { PresentationSpec } from '@contractspec/lib.contracts';
2
- import { StabilityEnum } from '@contractspec/lib.contracts';
1
+ import { definePresentation, StabilityEnum } from '@contractspec/lib.contracts';
3
2
 
4
3
  /**
5
4
  * Main dashboard presentation for the SaaS application.
6
5
  */
7
- export const SaasDashboardPresentation: PresentationSpec = {
6
+ export const SaasDashboardPresentation = definePresentation({
8
7
  meta: {
9
8
  key: 'saas.dashboard',
10
9
  version: '1.0.0',
@@ -27,12 +26,12 @@ export const SaasDashboardPresentation: PresentationSpec = {
27
26
  policy: {
28
27
  flags: ['saas.enabled'],
29
28
  },
30
- };
29
+ });
31
30
 
32
31
  /**
33
32
  * Settings panel presentation.
34
33
  */
35
- export const SettingsPanelPresentation: PresentationSpec = {
34
+ export const SettingsPanelPresentation = definePresentation({
36
35
  meta: {
37
36
  key: 'saas.settings',
38
37
  version: '1.0.0',
@@ -54,4 +53,4 @@ export const SettingsPanelPresentation: PresentationSpec = {
54
53
  policy: {
55
54
  flags: ['saas.enabled'],
56
55
  },
57
- };
56
+ });
package/src/example.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { ExampleSpec } from '@contractspec/lib.contracts';
1
+ import { defineExample } from '@contractspec/lib.contracts';
2
2
 
3
- const example: ExampleSpec = {
3
+ const example = defineExample({
4
4
  meta: {
5
5
  key: 'saas-boilerplate',
6
6
  version: '1.0.0',
@@ -33,6 +33,6 @@ const example: ExampleSpec = {
33
33
  studio: { enabled: true, installable: true },
34
34
  mcp: { enabled: true },
35
35
  },
36
- };
36
+ });
37
37
 
38
38
  export default example;
@@ -18,3 +18,6 @@ export {
18
18
  mockUpdateProjectHandler,
19
19
  mockDeleteProjectHandler,
20
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 CHANGED
@@ -9,6 +9,11 @@ export * from './dashboard';
9
9
 
10
10
  // Export feature and example metadata
11
11
  export * from './saas-boilerplate.feature';
12
+ export * from './ui';
13
+ export {
14
+ createSaasHandlers,
15
+ type SaasHandlers,
16
+ } from './handlers/saas.handlers';
12
17
  export { default as example } from './example';
13
18
 
14
19
  // Import docs for registration
@@ -1,11 +1,10 @@
1
- import type { PresentationSpec } from '@contractspec/lib.contracts';
2
- import { StabilityEnum } from '@contractspec/lib.contracts';
1
+ import { definePresentation, StabilityEnum } from '@contractspec/lib.contracts';
3
2
  import { ProjectModel } from './project.schema';
4
3
 
5
4
  /**
6
5
  * Presentation for displaying a list of projects.
7
6
  */
8
- export const ProjectListPresentation: PresentationSpec = {
7
+ export const ProjectListPresentation = definePresentation({
9
8
  meta: {
10
9
  key: 'saas.project.list',
11
10
  version: '1.0.0',
@@ -29,12 +28,12 @@ export const ProjectListPresentation: PresentationSpec = {
29
28
  policy: {
30
29
  flags: ['saas.projects.enabled'],
31
30
  },
32
- };
31
+ });
33
32
 
34
33
  /**
35
34
  * Presentation for project detail view.
36
35
  */
37
- export const ProjectDetailPresentation: PresentationSpec = {
36
+ export const ProjectDetailPresentation = definePresentation({
38
37
  meta: {
39
38
  key: 'saas.project.detail',
40
39
  version: '1.0.0',
@@ -56,4 +55,4 @@ export const ProjectDetailPresentation: PresentationSpec = {
56
55
  policy: {
57
56
  flags: ['saas.projects.enabled'],
58
57
  },
59
- };
58
+ });
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * Defines the feature module for the SaaS application foundation.
5
5
  */
6
- import type { FeatureModuleSpec } from '@contractspec/lib.contracts';
6
+ import { defineFeature } from '@contractspec/lib.contracts';
7
7
 
8
8
  /**
9
9
  * SaaS Boilerplate feature module that bundles project management,
10
10
  * billing, and settings operations into an installable feature.
11
11
  */
12
- export const SaasBoilerplateFeature: FeatureModuleSpec = {
12
+ export const SaasBoilerplateFeature = defineFeature({
13
13
  meta: {
14
14
  key: 'saas-boilerplate',
15
15
  title: 'SaaS Boilerplate',
@@ -110,4 +110,4 @@ export const SaasBoilerplateFeature: FeatureModuleSpec = {
110
110
  { key: 'notifications', version: '1.0.0' },
111
111
  ],
112
112
  },
113
- };
113
+ });
@@ -0,0 +1,28 @@
1
+ import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
2
+
3
+ export async function seedSaasBoilerplate(params: {
4
+ projectId: string;
5
+ db: DatabasePort;
6
+ }) {
7
+ const { projectId, db } = params;
8
+
9
+ const existing = await db.query(
10
+ `SELECT COUNT(*) as count FROM saas_project WHERE "projectId" = $1`,
11
+ [projectId]
12
+ );
13
+ if ((existing.rows[0]?.count as number) > 0) return;
14
+
15
+ await db.execute(
16
+ `INSERT INTO saas_project (id, "projectId", "organizationId", name, description, status, tier)
17
+ VALUES ($1, $2, $3, $4, $5, $6, $7)`,
18
+ [
19
+ 'saas_proj_1',
20
+ projectId,
21
+ 'org_demo',
22
+ 'Demo Project',
23
+ 'A demo SaaS project',
24
+ 'ACTIVE',
25
+ 'PRO',
26
+ ]
27
+ );
28
+ }
@@ -0,0 +1,39 @@
1
+ export interface OverlayDefinition {
2
+ overlayId: string;
3
+ version: string;
4
+ description: string;
5
+ appliesTo: Record<string, string>;
6
+ modifications: OverlayModification[];
7
+ }
8
+
9
+ export type OverlayModification =
10
+ | HideFieldModification
11
+ | RenameLabelModification
12
+ | AddBadgeModification
13
+ | SetLimitModification;
14
+
15
+ export interface HideFieldModification {
16
+ type: 'hideField';
17
+ field: string;
18
+ reason?: string;
19
+ }
20
+
21
+ export interface RenameLabelModification {
22
+ type: 'renameLabel';
23
+ field: string;
24
+ newLabel: string;
25
+ }
26
+
27
+ export interface AddBadgeModification {
28
+ type: 'addBadge';
29
+ position: 'header' | 'footer';
30
+ label: string;
31
+ variant: 'warning' | 'info' | 'error' | 'success' | 'default';
32
+ }
33
+
34
+ export interface SetLimitModification {
35
+ type: 'setLimit';
36
+ field: string;
37
+ max: number;
38
+ message: string;
39
+ }