@contractspec/example.saas-boilerplate 1.46.0 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$bundle.log +183 -108
- package/.turbo/turbo-build.log +182 -107
- package/CHANGELOG.md +58 -0
- package/README.md +0 -1
- package/dist/billing/billing.event.d.ts +4 -4
- package/dist/billing/billing.event.js +1 -1
- package/dist/billing/billing.operations.d.ts +5 -5
- package/dist/billing/billing.presentation.d.ts +3 -4
- package/dist/billing/billing.presentation.d.ts.map +1 -1
- package/dist/billing/billing.presentation.js +5 -5
- package/dist/billing/billing.presentation.js.map +1 -1
- package/dist/dashboard/dashboard.presentation.d.ts +3 -4
- package/dist/dashboard/dashboard.presentation.d.ts.map +1 -1
- package/dist/dashboard/dashboard.presentation.js +5 -5
- package/dist/dashboard/dashboard.presentation.js.map +1 -1
- package/dist/example.d.ts +2 -2
- package/dist/example.d.ts.map +1 -1
- package/dist/example.js +4 -2
- package/dist/example.js.map +1 -1
- package/dist/handlers/index.d.ts +2 -1
- package/dist/handlers/index.js +2 -1
- package/dist/handlers/saas.handlers.d.ts +68 -0
- package/dist/handlers/saas.handlers.d.ts.map +1 -0
- package/dist/handlers/saas.handlers.js +148 -0
- package/dist/handlers/saas.handlers.js.map +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/project/project.event.d.ts +5 -5
- package/dist/project/project.event.d.ts.map +1 -1
- package/dist/project/project.event.js +1 -1
- package/dist/project/project.operations.d.ts +6 -6
- package/dist/project/project.presentation.d.ts +3 -4
- package/dist/project/project.presentation.d.ts.map +1 -1
- package/dist/project/project.presentation.js +5 -5
- package/dist/project/project.presentation.js.map +1 -1
- package/dist/saas-boilerplate.feature.d.ts +2 -2
- package/dist/saas-boilerplate.feature.d.ts.map +1 -1
- package/dist/saas-boilerplate.feature.js +9 -2
- package/dist/saas-boilerplate.feature.js.map +1 -1
- package/dist/seeders/index.d.ts +10 -0
- package/dist/seeders/index.d.ts.map +1 -0
- package/dist/seeders/index.js +19 -0
- package/dist/seeders/index.js.map +1 -0
- package/dist/shared/overlay-types.d.ts +34 -0
- package/dist/shared/overlay-types.d.ts.map +1 -0
- package/dist/shared/overlay-types.js +0 -0
- package/dist/tests/operations.test-spec.d.ts +10 -0
- package/dist/tests/operations.test-spec.d.ts.map +1 -0
- package/dist/tests/operations.test-spec.js +123 -0
- package/dist/tests/operations.test-spec.js.map +1 -0
- package/dist/ui/SaasDashboard.d.ts +7 -0
- package/dist/ui/SaasDashboard.d.ts.map +1 -0
- package/dist/ui/SaasDashboard.js +298 -0
- package/dist/ui/SaasDashboard.js.map +1 -0
- package/dist/ui/SaasProjectList.d.ts +14 -0
- package/dist/ui/SaasProjectList.d.ts.map +1 -0
- package/dist/ui/SaasProjectList.js +76 -0
- package/dist/ui/SaasProjectList.js.map +1 -0
- package/dist/ui/SaasSettingsPanel.d.ts +7 -0
- package/dist/ui/SaasSettingsPanel.d.ts.map +1 -0
- package/dist/ui/SaasSettingsPanel.js +138 -0
- package/dist/ui/SaasSettingsPanel.js.map +1 -0
- package/dist/ui/hooks/index.d.ts +3 -0
- package/dist/ui/hooks/index.js +6 -0
- package/dist/ui/hooks/useProjectList.d.ts +34 -0
- package/dist/ui/hooks/useProjectList.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectList.js +75 -0
- package/dist/ui/hooks/useProjectList.js.map +1 -0
- package/dist/ui/hooks/useProjectMutations.d.ts +28 -0
- package/dist/ui/hooks/useProjectMutations.d.ts.map +1 -0
- package/dist/ui/hooks/useProjectMutations.js +146 -0
- package/dist/ui/hooks/useProjectMutations.js.map +1 -0
- package/dist/ui/index.d.ts +14 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/modals/CreateProjectModal.d.ts +23 -0
- package/dist/ui/modals/CreateProjectModal.d.ts.map +1 -0
- package/dist/ui/modals/CreateProjectModal.js +139 -0
- package/dist/ui/modals/CreateProjectModal.js.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts +38 -0
- package/dist/ui/modals/ProjectActionsModal.d.ts.map +1 -0
- package/dist/ui/modals/ProjectActionsModal.js +292 -0
- package/dist/ui/modals/ProjectActionsModal.js.map +1 -0
- package/dist/ui/modals/index.d.ts +3 -0
- package/dist/ui/modals/index.js +4 -0
- package/dist/ui/overlays/demo-overlays.d.ts +19 -0
- package/dist/ui/overlays/demo-overlays.d.ts.map +1 -0
- package/dist/ui/overlays/demo-overlays.js +70 -0
- package/dist/ui/overlays/demo-overlays.js.map +1 -0
- package/dist/ui/overlays/index.d.ts +2 -0
- package/dist/ui/overlays/index.js +3 -0
- package/dist/ui/renderers/index.d.ts +3 -0
- package/dist/ui/renderers/index.js +4 -0
- package/dist/ui/renderers/project-list.markdown.d.ts +31 -0
- package/dist/ui/renderers/project-list.markdown.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.markdown.js +148 -0
- package/dist/ui/renderers/project-list.markdown.js.map +1 -0
- package/dist/ui/renderers/project-list.renderer.d.ts +9 -0
- package/dist/ui/renderers/project-list.renderer.d.ts.map +1 -0
- package/dist/ui/renderers/project-list.renderer.js +17 -0
- package/dist/ui/renderers/project-list.renderer.js.map +1 -0
- package/package.json +38 -14
- package/src/billing/billing.presentation.ts +5 -6
- package/src/dashboard/dashboard.presentation.ts +5 -6
- package/src/example.ts +3 -3
- package/src/handlers/index.ts +3 -0
- package/src/handlers/saas.handlers.ts +300 -0
- package/src/index.ts +5 -0
- package/src/project/project.presentation.ts +5 -6
- package/src/saas-boilerplate.feature.ts +3 -3
- package/src/seeders/index.ts +28 -0
- package/src/shared/overlay-types.ts +39 -0
- package/src/tests/operations.test-spec.ts +109 -0
- package/src/ui/SaasDashboard.tsx +325 -0
- package/src/ui/SaasProjectList.tsx +113 -0
- package/src/ui/SaasSettingsPanel.tsx +96 -0
- package/src/ui/hooks/index.ts +10 -0
- package/src/ui/hooks/useProjectList.ts +95 -0
- package/src/ui/hooks/useProjectMutations.ts +166 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/modals/CreateProjectModal.tsx +176 -0
- package/src/ui/modals/ProjectActionsModal.tsx +346 -0
- package/src/ui/modals/index.ts +2 -0
- package/src/ui/overlays/demo-overlays.ts +74 -0
- package/src/ui/overlays/index.ts +1 -0
- package/src/ui/renderers/index.ts +7 -0
- package/src/ui/renderers/project-list.markdown.ts +239 -0
- package/src/ui/renderers/project-list.renderer.tsx +22 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineTestSpec } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
export const ProjectListTest = defineTestSpec({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'saas.project.list.test',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
stability: 'experimental',
|
|
8
|
+
owners: ['@example.saas-boilerplate'],
|
|
9
|
+
description: 'Test for listing projects',
|
|
10
|
+
tags: ['test'],
|
|
11
|
+
},
|
|
12
|
+
target: {
|
|
13
|
+
type: 'operation',
|
|
14
|
+
operation: { key: 'saas.project.list', version: '1.0.0' },
|
|
15
|
+
},
|
|
16
|
+
scenarios: [
|
|
17
|
+
{
|
|
18
|
+
key: 'success',
|
|
19
|
+
when: { operation: { key: 'saas.project.list' } },
|
|
20
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'error',
|
|
24
|
+
when: { operation: { key: 'saas.project.list' } },
|
|
25
|
+
then: [{ type: 'expectError' }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export const ProjectGetTest = defineTestSpec({
|
|
31
|
+
meta: {
|
|
32
|
+
key: 'saas.project.get.test',
|
|
33
|
+
version: '1.0.0',
|
|
34
|
+
stability: 'experimental',
|
|
35
|
+
owners: ['@example.saas-boilerplate'],
|
|
36
|
+
description: 'Test for getting project',
|
|
37
|
+
tags: ['test'],
|
|
38
|
+
},
|
|
39
|
+
target: {
|
|
40
|
+
type: 'operation',
|
|
41
|
+
operation: { key: 'saas.project.get', version: '1.0.0' },
|
|
42
|
+
},
|
|
43
|
+
scenarios: [
|
|
44
|
+
{
|
|
45
|
+
key: 'success',
|
|
46
|
+
when: { operation: { key: 'saas.project.get' } },
|
|
47
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'error',
|
|
51
|
+
when: { operation: { key: 'saas.project.get' } },
|
|
52
|
+
then: [{ type: 'expectError' }],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const BillingSubscriptionGetTest = defineTestSpec({
|
|
58
|
+
meta: {
|
|
59
|
+
key: 'saas.billing.subscription.get.test',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
stability: 'experimental',
|
|
62
|
+
owners: ['@example.saas-boilerplate'],
|
|
63
|
+
description: 'Test for getting subscription',
|
|
64
|
+
tags: ['test'],
|
|
65
|
+
},
|
|
66
|
+
target: {
|
|
67
|
+
type: 'operation',
|
|
68
|
+
operation: { key: 'saas.billing.subscription.get', version: '1.0.0' },
|
|
69
|
+
},
|
|
70
|
+
scenarios: [
|
|
71
|
+
{
|
|
72
|
+
key: 'success',
|
|
73
|
+
when: { operation: { key: 'saas.billing.subscription.get' } },
|
|
74
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'error',
|
|
78
|
+
when: { operation: { key: 'saas.billing.subscription.get' } },
|
|
79
|
+
then: [{ type: 'expectError' }],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const BillingUsageSummaryTest = defineTestSpec({
|
|
85
|
+
meta: {
|
|
86
|
+
key: 'saas.billing.usage.summary.test',
|
|
87
|
+
version: '1.0.0',
|
|
88
|
+
stability: 'experimental',
|
|
89
|
+
owners: ['@example.saas-boilerplate'],
|
|
90
|
+
description: 'Test for getting usage summary',
|
|
91
|
+
tags: ['test'],
|
|
92
|
+
},
|
|
93
|
+
target: {
|
|
94
|
+
type: 'operation',
|
|
95
|
+
operation: { key: 'saas.billing.usage.summary', version: '1.0.0' },
|
|
96
|
+
},
|
|
97
|
+
scenarios: [
|
|
98
|
+
{
|
|
99
|
+
key: 'success',
|
|
100
|
+
when: { operation: { key: 'saas.billing.usage.summary' } },
|
|
101
|
+
then: [{ type: 'expectOutput', match: {} }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: 'error',
|
|
105
|
+
when: { operation: { key: 'saas.billing.usage.summary' } },
|
|
106
|
+
then: [{ type: 'expectError' }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|