@contractspec/example.saas-boilerplate 3.7.6 → 3.8.2

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 (143) hide show
  1. package/.turbo/turbo-build.log +39 -27
  2. package/AGENTS.md +50 -27
  3. package/CHANGELOG.md +36 -0
  4. package/README.md +65 -144
  5. package/dist/billing/billing.event.js +1 -1
  6. package/dist/billing/index.d.ts +6 -6
  7. package/dist/billing/index.js +1 -1
  8. package/dist/browser/billing/billing.event.js +1 -1
  9. package/dist/browser/billing/index.js +1 -1
  10. package/dist/browser/index.js +1147 -869
  11. package/dist/browser/project/index.js +209 -209
  12. package/dist/browser/project/project.event.js +1 -1
  13. package/dist/browser/saas-boilerplate.feature.js +208 -0
  14. package/dist/browser/ui/SaasDashboard.js +356 -105
  15. package/dist/browser/ui/SaasDashboard.visualizations.js +249 -0
  16. package/dist/browser/ui/SaasProjectList.js +7 -7
  17. package/dist/browser/ui/SaasSettingsPanel.js +12 -12
  18. package/dist/browser/ui/hooks/index.js +2 -2
  19. package/dist/browser/ui/hooks/useProjectList.js +1 -1
  20. package/dist/browser/ui/hooks/useProjectMutations.js +1 -1
  21. package/dist/browser/ui/index.js +790 -521
  22. package/dist/browser/ui/modals/CreateProjectModal.js +10 -10
  23. package/dist/browser/ui/modals/ProjectActionsModal.js +13 -13
  24. package/dist/browser/ui/modals/index.js +23 -23
  25. package/dist/browser/ui/renderers/index.js +341 -115
  26. package/dist/browser/ui/renderers/project-list.markdown.js +229 -3
  27. package/dist/browser/ui/renderers/project-list.renderer.js +7 -7
  28. package/dist/browser/visualizations/catalog.js +155 -0
  29. package/dist/browser/visualizations/index.js +217 -0
  30. package/dist/browser/visualizations/selectors.js +210 -0
  31. package/dist/handlers/index.d.ts +2 -2
  32. package/dist/index.d.ts +5 -4
  33. package/dist/index.js +1147 -869
  34. package/dist/node/billing/billing.event.js +1 -1
  35. package/dist/node/billing/index.js +1 -1
  36. package/dist/node/index.js +1147 -869
  37. package/dist/node/project/index.js +209 -209
  38. package/dist/node/project/project.event.js +1 -1
  39. package/dist/node/saas-boilerplate.feature.js +208 -0
  40. package/dist/node/ui/SaasDashboard.js +356 -105
  41. package/dist/node/ui/SaasDashboard.visualizations.js +249 -0
  42. package/dist/node/ui/SaasProjectList.js +7 -7
  43. package/dist/node/ui/SaasSettingsPanel.js +12 -12
  44. package/dist/node/ui/hooks/index.js +2 -2
  45. package/dist/node/ui/hooks/useProjectList.js +1 -1
  46. package/dist/node/ui/hooks/useProjectMutations.js +1 -1
  47. package/dist/node/ui/index.js +790 -521
  48. package/dist/node/ui/modals/CreateProjectModal.js +10 -10
  49. package/dist/node/ui/modals/ProjectActionsModal.js +13 -13
  50. package/dist/node/ui/modals/index.js +23 -23
  51. package/dist/node/ui/renderers/index.js +341 -115
  52. package/dist/node/ui/renderers/project-list.markdown.js +229 -3
  53. package/dist/node/ui/renderers/project-list.renderer.js +7 -7
  54. package/dist/node/visualizations/catalog.js +155 -0
  55. package/dist/node/visualizations/index.js +217 -0
  56. package/dist/node/visualizations/selectors.js +210 -0
  57. package/dist/presentations/index.d.ts +1 -1
  58. package/dist/project/index.d.ts +7 -7
  59. package/dist/project/index.js +209 -209
  60. package/dist/project/project.event.js +1 -1
  61. package/dist/saas-boilerplate.feature.js +208 -0
  62. package/dist/settings/index.d.ts +1 -1
  63. package/dist/ui/SaasDashboard.js +356 -105
  64. package/dist/ui/SaasDashboard.visualizations.d.ts +5 -0
  65. package/dist/ui/SaasDashboard.visualizations.js +250 -0
  66. package/dist/ui/SaasProjectList.js +7 -7
  67. package/dist/ui/SaasSettingsPanel.js +12 -12
  68. package/dist/ui/hooks/index.d.ts +2 -2
  69. package/dist/ui/hooks/index.js +2 -2
  70. package/dist/ui/hooks/useProjectList.d.ts +5 -0
  71. package/dist/ui/hooks/useProjectList.js +1 -1
  72. package/dist/ui/hooks/useProjectMutations.d.ts +8 -0
  73. package/dist/ui/hooks/useProjectMutations.js +1 -1
  74. package/dist/ui/index.d.ts +4 -4
  75. package/dist/ui/index.js +790 -521
  76. package/dist/ui/modals/CreateProjectModal.js +10 -10
  77. package/dist/ui/modals/ProjectActionsModal.js +13 -13
  78. package/dist/ui/modals/index.js +23 -23
  79. package/dist/ui/renderers/index.d.ts +1 -1
  80. package/dist/ui/renderers/index.js +341 -115
  81. package/dist/ui/renderers/project-list.markdown.js +229 -3
  82. package/dist/ui/renderers/project-list.renderer.d.ts +1 -1
  83. package/dist/ui/renderers/project-list.renderer.js +7 -7
  84. package/dist/visualizations/catalog.d.ts +11 -0
  85. package/dist/visualizations/catalog.js +156 -0
  86. package/dist/visualizations/index.d.ts +2 -0
  87. package/dist/visualizations/index.js +218 -0
  88. package/dist/visualizations/selectors.d.ts +8 -0
  89. package/dist/visualizations/selectors.js +211 -0
  90. package/dist/visualizations/selectors.test.d.ts +1 -0
  91. package/package.json +70 -14
  92. package/src/billing/billing.entity.ts +132 -132
  93. package/src/billing/billing.enum.ts +9 -9
  94. package/src/billing/billing.event.ts +71 -71
  95. package/src/billing/billing.handler.ts +87 -87
  96. package/src/billing/billing.operations.ts +158 -158
  97. package/src/billing/billing.presentation.ts +45 -45
  98. package/src/billing/billing.schema.ts +76 -76
  99. package/src/billing/index.ts +43 -48
  100. package/src/dashboard/dashboard.presentation.ts +45 -45
  101. package/src/dashboard/index.ts +2 -2
  102. package/src/docs/saas-boilerplate.docblock.ts +43 -43
  103. package/src/example.ts +32 -32
  104. package/src/handlers/index.ts +9 -9
  105. package/src/handlers/saas.handlers.ts +250 -249
  106. package/src/index.ts +41 -41
  107. package/src/presentations/index.ts +18 -20
  108. package/src/project/index.ts +45 -50
  109. package/src/project/project.entity.ts +68 -68
  110. package/src/project/project.enum.ts +8 -8
  111. package/src/project/project.event.ts +79 -79
  112. package/src/project/project.handler.ts +103 -103
  113. package/src/project/project.operations.ts +236 -236
  114. package/src/project/project.presentation.ts +46 -46
  115. package/src/project/project.schema.ts +90 -90
  116. package/src/saas-boilerplate.feature.ts +103 -100
  117. package/src/seeders/index.ts +20 -20
  118. package/src/settings/index.ts +2 -3
  119. package/src/settings/settings.entity.ts +65 -65
  120. package/src/settings/settings.enum.ts +4 -4
  121. package/src/shared/mock-data.ts +92 -92
  122. package/src/shared/overlay-types.ts +23 -23
  123. package/src/tests/operations.test-spec.ts +96 -96
  124. package/src/ui/SaasDashboard.tsx +278 -270
  125. package/src/ui/SaasDashboard.visualizations.tsx +41 -0
  126. package/src/ui/SaasProjectList.tsx +90 -90
  127. package/src/ui/SaasSettingsPanel.tsx +84 -84
  128. package/src/ui/hooks/index.ts +3 -3
  129. package/src/ui/hooks/useProjectList.ts +69 -68
  130. package/src/ui/hooks/useProjectMutations.ts +144 -143
  131. package/src/ui/index.ts +8 -12
  132. package/src/ui/modals/CreateProjectModal.tsx +154 -154
  133. package/src/ui/modals/ProjectActionsModal.tsx +321 -321
  134. package/src/ui/overlays/demo-overlays.ts +49 -49
  135. package/src/ui/renderers/index.ts +5 -4
  136. package/src/ui/renderers/project-list.markdown.ts +229 -205
  137. package/src/ui/renderers/project-list.renderer.tsx +14 -13
  138. package/src/visualizations/catalog.ts +153 -0
  139. package/src/visualizations/index.ts +2 -0
  140. package/src/visualizations/selectors.test.ts +25 -0
  141. package/src/visualizations/selectors.ts +85 -0
  142. package/tsconfig.json +7 -8
  143. package/tsdown.config.js +7 -3
@@ -6,295 +6,296 @@
6
6
  import type { DatabasePort } from '@contractspec/lib.runtime-sandbox';
7
7
  /* eslint-disable @typescript-eslint/no-non-null-assertion */
8
8
  import { web } from '@contractspec/lib.runtime-sandbox';
9
+
9
10
  const { generateId } = web;
10
11
 
11
12
  // ============ Types ============
12
13
 
13
14
  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;
15
+ id: string;
16
+ projectId: string;
17
+ organizationId: string;
18
+ name: string;
19
+ description?: string;
20
+ status: 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
21
+ tier: 'FREE' | 'PRO' | 'ENTERPRISE';
22
+ createdAt: Date;
23
+ updatedAt: Date;
23
24
  }
24
25
 
25
26
  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;
27
+ id: string;
28
+ projectId: string;
29
+ organizationId: string;
30
+ plan: 'FREE' | 'PRO' | 'ENTERPRISE';
31
+ status: 'ACTIVE' | 'PAST_DUE' | 'CANCELED';
32
+ billingCycle: 'MONTHLY' | 'YEARLY';
33
+ currentPeriodStart: Date;
34
+ currentPeriodEnd: Date;
35
+ cancelAtPeriodEnd: boolean;
35
36
  }
36
37
 
37
38
  export interface ListProjectsInput {
38
- projectId: string;
39
- organizationId?: string;
40
- status?: Project['status'] | 'all';
41
- search?: string;
42
- limit?: number;
43
- offset?: number;
39
+ projectId: string;
40
+ organizationId?: string;
41
+ status?: Project['status'] | 'all';
42
+ search?: string;
43
+ limit?: number;
44
+ offset?: number;
44
45
  }
45
46
 
46
47
  export interface ListProjectsOutput {
47
- items: Project[];
48
- total: number;
49
- hasMore: boolean;
48
+ items: Project[];
49
+ total: number;
50
+ hasMore: boolean;
50
51
  }
51
52
 
52
53
  export interface CreateProjectInput {
53
- name: string;
54
- description?: string;
55
- tier?: Project['tier'];
54
+ name: string;
55
+ description?: string;
56
+ tier?: Project['tier'];
56
57
  }
57
58
 
58
59
  export interface UpdateProjectInput {
59
- id: string;
60
- name?: string;
61
- description?: string;
62
- status?: Project['status'];
60
+ id: string;
61
+ name?: string;
62
+ description?: string;
63
+ status?: Project['status'];
63
64
  }
64
65
 
65
66
  // ============ Row Types ============
66
67
 
67
68
  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;
69
+ id: string;
70
+ projectId: string;
71
+ organizationId: string;
72
+ name: string;
73
+ description: string | null;
74
+ status: string;
75
+ tier: string;
76
+ createdAt: string;
77
+ updatedAt: string;
77
78
  }
78
79
 
79
80
  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;
81
+ id: string;
82
+ projectId: string;
83
+ organizationId: string;
84
+ plan: string;
85
+ status: string;
86
+ billingCycle: string;
87
+ currentPeriodStart: string;
88
+ currentPeriodEnd: string;
89
+ cancelAtPeriodEnd: number;
89
90
  }
90
91
 
91
92
  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
- };
93
+ return {
94
+ id: row.id,
95
+ projectId: row.projectId,
96
+ organizationId: row.organizationId,
97
+ name: row.name,
98
+ description: row.description ?? undefined,
99
+ status: row.status as Project['status'],
100
+ tier: row.tier as Project['tier'],
101
+ createdAt: new Date(row.createdAt),
102
+ updatedAt: new Date(row.updatedAt),
103
+ };
103
104
  }
104
105
 
105
106
  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
- };
107
+ return {
108
+ id: row.id,
109
+ projectId: row.projectId,
110
+ organizationId: row.organizationId,
111
+ plan: row.plan as Subscription['plan'],
112
+ status: row.status as Subscription['status'],
113
+ billingCycle: row.billingCycle as Subscription['billingCycle'],
114
+ currentPeriodStart: new Date(row.currentPeriodStart),
115
+ currentPeriodEnd: new Date(row.currentPeriodEnd),
116
+ cancelAtPeriodEnd: Boolean(row.cancelAtPeriodEnd),
117
+ };
117
118
  }
118
119
 
119
120
  // ============ Handler Factory ============
120
121
 
121
122
  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)
123
+ /**
124
+ * List projects
125
+ */
126
+ async function listProjects(
127
+ input: ListProjectsInput
128
+ ): Promise<ListProjectsOutput> {
129
+ const {
130
+ projectId,
131
+ organizationId,
132
+ status,
133
+ search,
134
+ limit = 20,
135
+ offset = 0,
136
+ } = input;
137
+
138
+ let whereClause = 'WHERE projectId = ?';
139
+ const params: (string | number)[] = [projectId];
140
+
141
+ if (organizationId) {
142
+ whereClause += ' AND organizationId = ?';
143
+ params.push(organizationId);
144
+ }
145
+
146
+ if (status && status !== 'all') {
147
+ whereClause += ' AND status = ?';
148
+ params.push(status);
149
+ }
150
+
151
+ if (search) {
152
+ whereClause += ' AND (name LIKE ? OR description LIKE ?)';
153
+ params.push(`%${search}%`, `%${search}%`);
154
+ }
155
+
156
+ const countResult = (
157
+ await db.query(
158
+ `SELECT COUNT(*) as count FROM saas_project ${whereClause}`,
159
+ params
160
+ )
161
+ ).rows as unknown as { count: number }[];
162
+ const total = countResult[0]?.count ?? 0;
163
+
164
+ const rows = (
165
+ await db.query(
166
+ `SELECT * FROM saas_project ${whereClause} ORDER BY createdAt DESC LIMIT ? OFFSET ?`,
167
+ [...params, limit, offset]
168
+ )
169
+ ).rows as unknown as ProjectRow[];
170
+
171
+ return {
172
+ items: rows.map(rowToProject),
173
+ total,
174
+ hasMore: offset + rows.length < total,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Get a single project
180
+ */
181
+ async function getProject(id: string): Promise<Project | null> {
182
+ const rows = (
183
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])
184
+ ).rows as unknown as ProjectRow[];
185
+ return rows[0] ? rowToProject(rows[0]) : null;
186
+ }
187
+
188
+ /**
189
+ * Create a project
190
+ */
191
+ async function createProject(
192
+ input: CreateProjectInput,
193
+ context: { projectId: string; organizationId: string }
194
+ ): Promise<Project> {
195
+ const id = generateId('proj');
196
+ const now = new Date().toISOString();
197
+
198
+ await db.execute(
199
+ `INSERT INTO saas_project (id, projectId, organizationId, name, description, status, tier, createdAt, updatedAt)
199
200
  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
- };
201
+ [
202
+ id,
203
+ context.projectId,
204
+ context.organizationId,
205
+ input.name,
206
+ input.description ?? null,
207
+ 'DRAFT',
208
+ input.tier ?? 'FREE',
209
+ now,
210
+ now,
211
+ ]
212
+ );
213
+
214
+ const rows = (
215
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [id])
216
+ ).rows as unknown as ProjectRow[];
217
+
218
+ return rowToProject(rows[0]!);
219
+ }
220
+
221
+ /**
222
+ * Update a project
223
+ */
224
+ async function updateProject(input: UpdateProjectInput): Promise<Project> {
225
+ const now = new Date().toISOString();
226
+ const updates: string[] = ['updatedAt = ?'];
227
+ const params: (string | null)[] = [now];
228
+
229
+ if (input.name !== undefined) {
230
+ updates.push('name = ?');
231
+ params.push(input.name);
232
+ }
233
+
234
+ if (input.description !== undefined) {
235
+ updates.push('description = ?');
236
+ params.push(input.description);
237
+ }
238
+
239
+ if (input.status !== undefined) {
240
+ updates.push('status = ?');
241
+ params.push(input.status);
242
+ }
243
+
244
+ params.push(input.id);
245
+
246
+ await db.execute(
247
+ `UPDATE saas_project SET ${updates.join(', ')} WHERE id = ?`,
248
+ params
249
+ );
250
+
251
+ const rows = (
252
+ await db.query(`SELECT * FROM saas_project WHERE id = ?`, [input.id])
253
+ ).rows as unknown as ProjectRow[];
254
+
255
+ if (!rows[0]) {
256
+ throw new Error('NOT_FOUND');
257
+ }
258
+
259
+ return rowToProject(rows[0]);
260
+ }
261
+
262
+ /**
263
+ * Delete a project
264
+ */
265
+ async function deleteProject(id: string): Promise<void> {
266
+ await db.execute(`DELETE FROM saas_project WHERE id = ?`, [id]);
267
+ }
268
+
269
+ /**
270
+ * Get subscription for an organization
271
+ */
272
+ async function getSubscription(input: {
273
+ projectId: string;
274
+ organizationId?: string;
275
+ }): Promise<Subscription | null> {
276
+ let query = `SELECT * FROM saas_subscription WHERE projectId = ?`;
277
+ const params: string[] = [input.projectId];
278
+
279
+ if (input.organizationId) {
280
+ query += ' AND organizationId = ?';
281
+ params.push(input.organizationId);
282
+ }
283
+
284
+ query += ' LIMIT 1';
285
+
286
+ const rows = (await db.query(query, params))
287
+ .rows as unknown as SubscriptionRow[];
288
+ return rows[0] ? rowToSubscription(rows[0]) : null;
289
+ }
290
+
291
+ return {
292
+ listProjects,
293
+ getProject,
294
+ createProject,
295
+ updateProject,
296
+ deleteProject,
297
+ getSubscription,
298
+ };
298
299
  }
299
300
 
300
301
  export type SaasHandlers = ReturnType<typeof createSaasHandlers>;
package/src/index.ts CHANGED
@@ -3,18 +3,18 @@
3
3
 
4
4
  // Export all domain modules
5
5
  export * from './billing';
6
- export * from './project';
7
- export * from './settings';
8
6
  export * from './dashboard';
9
-
7
+ export { default as example } from './example';
8
+ export {
9
+ createSaasHandlers,
10
+ type SaasHandlers,
11
+ } from './handlers/saas.handlers';
12
+ export * from './project';
10
13
  // Export feature and example metadata
11
14
  export * from './saas-boilerplate.feature';
15
+ export * from './settings';
12
16
  export * from './ui';
13
- export {
14
- createSaasHandlers,
15
- type SaasHandlers,
16
- } from './handlers/saas.handlers';
17
- export { default as example } from './example';
17
+ export * from './visualizations';
18
18
 
19
19
  // Import docs for registration
20
20
  import './docs';
@@ -22,41 +22,41 @@ import './docs';
22
22
  // Schema composition configuration
23
23
  import { identityRbacSchemaContribution } from '@contractspec/lib.identity-rbac';
24
24
  import { jobsSchemaContribution } from '@contractspec/lib.jobs';
25
+ import type { ModuleSchemaContribution } from '@contractspec/lib.schema';
25
26
  import { auditTrailSchemaContribution } from '@contractspec/module.audit-trail';
26
27
  import { notificationsSchemaContribution } from '@contractspec/module.notifications';
27
- import type { ModuleSchemaContribution } from '@contractspec/lib.schema';
28
28
  import {
29
- ProjectEntity,
30
- ProjectMemberEntity,
31
- ProjectStatusEnum,
29
+ BillingUsageEntity,
30
+ SubscriptionEntity,
31
+ SubscriptionStatusEnum,
32
+ UsageLimitEntity,
33
+ } from './billing/billing.entity';
34
+ import {
35
+ ProjectEntity,
36
+ ProjectMemberEntity,
37
+ ProjectStatusEnum,
32
38
  } from './project/project.entity';
33
39
  import {
34
- SettingsEntity,
35
- FeatureFlagEntity,
36
- SettingsScopeEnum,
40
+ FeatureFlagEntity,
41
+ SettingsEntity,
42
+ SettingsScopeEnum,
37
43
  } from './settings';
38
- import {
39
- SubscriptionEntity,
40
- BillingUsageEntity,
41
- UsageLimitEntity,
42
- SubscriptionStatusEnum,
43
- } from './billing/billing.entity';
44
44
 
45
45
  /**
46
46
  * SaaS boilerplate schema contribution.
47
47
  */
48
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],
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
60
  };
61
61
 
62
62
  /**
@@ -64,13 +64,13 @@ export const saasBoilerplateSchemaContribution: ModuleSchemaContribution = {
64
64
  * Use with `database schema:compose` to generate Prisma schema.
65
65
  */
66
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',
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
76
  };