launchbase 1.1.2 → 1.1.3

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.
@@ -13,29 +13,179 @@ enum Role {
13
13
  MEMBER
14
14
  }
15
15
 
16
+ // ============================================
17
+ // Core Multi-Tenant Models
18
+ // ============================================
19
+
16
20
  model User {
17
- id String @id @default(cuid())
18
- email String @unique
19
- passwordHash String?
20
- name String?
21
- image String?
22
- emailVerified DateTime?
21
+ id String @id @default(cuid())
22
+ email String @unique
23
+ name String?
24
+ image String?
25
+ passwordHash String?
26
+ emailVerified DateTime?
23
27
  twoFactorSecret String?
24
- twoFactorEnabled Boolean @default(false)
25
- oauthProvider String? // google, github, etc.
26
- oauthId String? // OAuth provider user ID
27
- createdAt DateTime @default(now())
28
- memberships Membership[]
29
- refreshTokens RefreshToken[]
30
- verificationTokens VerificationToken[]
28
+ twoFactorEnabled Boolean @default(false)
29
+ oauthProvider String? // google, github, etc.
30
+ oauthId String? // OAuth provider user ID
31
+ createdAt DateTime @default(now())
32
+ updatedAt DateTime @updatedAt
33
+
34
+ // Relations
35
+ ownedOrganizations Organization[]
36
+ memberships OrganizationMember[]
37
+ refreshTokens RefreshToken[]
38
+ verificationTokens VerificationToken[]
31
39
  passwordResetTokens PasswordResetToken[]
32
- oauthAccounts OAuthAccount[]
33
- files File[]
40
+ oauthAccounts OAuthAccount[]
41
+ files File[]
34
42
 
35
43
  @@index([email])
36
44
  @@index([oauthProvider, oauthId])
37
45
  }
38
46
 
47
+ model Organization {
48
+ id String @id @default(cuid())
49
+ name String
50
+ slug String @unique
51
+ ownerId String
52
+ plan String @default("free")
53
+ stripeCustomerId String?
54
+ stripeSubscriptionId String?
55
+ createdAt DateTime @default(now())
56
+ updatedAt DateTime @updatedAt
57
+
58
+ // Relations
59
+ owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
60
+ members OrganizationMember[]
61
+ projects Project[]
62
+ invitations Invitation[]
63
+
64
+ @@index([slug])
65
+ @@index([ownerId])
66
+ }
67
+
68
+ model OrganizationMember {
69
+ id String @id @default(cuid())
70
+ organizationId String
71
+ userId String
72
+ role Role
73
+ createdAt DateTime @default(now())
74
+
75
+ // Relations
76
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
77
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
78
+
79
+ @@unique([organizationId, userId])
80
+ @@index([organizationId])
81
+ @@index([userId])
82
+ }
83
+
84
+ model Project {
85
+ id String @id @default(cuid())
86
+ organizationId String
87
+ name String
88
+ slug String
89
+ createdAt DateTime @default(now())
90
+ updatedAt DateTime @updatedAt
91
+
92
+ // Relations
93
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
94
+ edgeFunctions EdgeFunction[]
95
+ vectorCollections VectorCollection[]
96
+ deployments Deployment[]
97
+ dbCollections DBCollection[]
98
+
99
+ @@unique([organizationId, slug])
100
+ @@index([organizationId])
101
+ @@index([slug])
102
+ }
103
+
104
+ // ============================================
105
+ // Project-Scoped Resources
106
+ // ============================================
107
+
108
+ model EdgeFunction {
109
+ id String @id @default(cuid())
110
+ projectId String
111
+ name String
112
+ slug String
113
+ runtime String @default("nodejs18")
114
+ sourceCode String
115
+ environment Json?
116
+ triggers Json?
117
+ status String @default("draft")
118
+ deployedAt DateTime?
119
+ deploymentUrl String?
120
+ createdAt DateTime @default(now())
121
+ updatedAt DateTime @updatedAt
122
+ logs EdgeFunctionLog[]
123
+
124
+ // Relations
125
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
126
+
127
+ @@unique([projectId, slug])
128
+ @@index([projectId])
129
+ @@index([status])
130
+ }
131
+
132
+ model EdgeFunctionLog {
133
+ id String @id @default(cuid())
134
+ functionId String
135
+ status String
136
+ duration Int
137
+ input Json?
138
+ output Json?
139
+ error String?
140
+ triggeredBy String?
141
+ createdAt DateTime @default(now())
142
+
143
+ // Relations
144
+ function EdgeFunction @relation(fields: [functionId], references: [id], onDelete: Cascade)
145
+
146
+ @@index([functionId])
147
+ @@index([createdAt])
148
+ }
149
+
150
+ model VectorCollection {
151
+ id String @id @default(cuid())
152
+ projectId String
153
+ name String
154
+ provider String @default("local")
155
+ dimension Int @default(1536)
156
+ embeddingModel String @default("text-embedding-3-small")
157
+ createdAt DateTime @default(now())
158
+ updatedAt DateTime @updatedAt
159
+
160
+ // Relations
161
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
162
+
163
+ @@unique([projectId, name])
164
+ @@index([projectId])
165
+ }
166
+
167
+ model Deployment {
168
+ id String @id @default(cuid())
169
+ projectId String
170
+ status String @default("pending")
171
+ version String?
172
+ deployedAt DateTime?
173
+ platform String?
174
+ url String?
175
+ logs Json?
176
+ createdAt DateTime @default(now())
177
+
178
+ // Relations
179
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
180
+
181
+ @@index([projectId])
182
+ @@index([status])
183
+ }
184
+
185
+ // ============================================
186
+ // Auth & Session Models
187
+ // ============================================
188
+
39
189
  model VerificationToken {
40
190
  id String @id @default(cuid())
41
191
  userId String
@@ -60,76 +210,59 @@ model PasswordResetToken {
60
210
  @@index([token])
61
211
  }
62
212
 
63
- model Invitation {
213
+ model RefreshToken {
64
214
  id String @id @default(cuid())
65
- email String
66
- orgId String
67
- role Role
68
- token String @unique
215
+ userId String
216
+ tokenHash String
217
+ revokedAt DateTime?
69
218
  expiresAt DateTime
70
219
  createdAt DateTime @default(now())
71
- acceptedAt DateTime?
72
-
73
- org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
74
-
75
- @@unique([email, orgId])
76
- @@index([token])
77
- }
78
-
79
- model Organization {
80
- id String @id @default(cuid())
81
- name String
82
- plan String @default("free")
83
- stripeCustomerId String?
84
- stripeSubscriptionId String?
85
- createdAt DateTime @default(now())
86
- memberships Membership[]
87
- projects Project[]
88
- invitations Invitation[]
89
- }
90
-
91
- model Membership {
92
- id String @id @default(cuid())
93
- userId String
94
- orgId String
95
- role Role
96
- createdAt DateTime @default(now())
97
220
 
98
221
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
99
- org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
100
222
 
101
- @@unique([userId, orgId])
102
- @@index([orgId])
223
+ @@index([userId])
103
224
  }
104
225
 
105
- model Project {
106
- id String @id @default(cuid())
107
- orgId String
108
- name String
109
- createdAt DateTime @default(now())
226
+ model Invitation {
227
+ id String @id @default(cuid())
228
+ email String
229
+ orgId String
230
+ role Role
231
+ token String @unique
232
+ expiresAt DateTime
233
+ createdAt DateTime @default(now())
234
+ acceptedAt DateTime?
110
235
 
111
236
  org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
112
237
 
113
- @@index([orgId])
238
+ @@unique([email, orgId])
239
+ @@index([token])
114
240
  }
115
241
 
116
- model RefreshToken {
117
- id String @id @default(cuid())
118
- userId String
119
- tokenHash String
120
- revokedAt DateTime?
121
- expiresAt DateTime
122
- createdAt DateTime @default(now())
242
+ model OAuthAccount {
243
+ id String @id @default(cuid())
244
+ userId String
245
+ provider String
246
+ providerId String
247
+ accessToken String?
248
+ refreshToken String?
249
+ createdAt DateTime @default(now())
123
250
 
124
251
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
125
252
 
253
+ @@unique([provider, providerId])
126
254
  @@index([userId])
127
255
  }
128
256
 
257
+ // ============================================
258
+ // Other Models
259
+ // ============================================
260
+
129
261
  model AuditLog {
130
262
  id String @id @default(cuid())
131
263
  orgId String?
132
264
  userId String?
265
+ projectId String?
133
266
  action String
134
267
  entity String?
135
268
  entityId String?
@@ -140,101 +273,31 @@ model AuditLog {
140
273
 
141
274
  @@index([orgId])
142
275
  @@index([userId])
276
+ @@index([projectId])
143
277
  @@index([action])
144
278
  @@index([createdAt])
145
279
  }
146
280
 
147
- model OAuthAccount {
281
+ model File {
148
282
  id String @id @default(cuid())
149
283
  userId String
150
- provider String
151
- providerId String
152
- accessToken String?
153
- refreshToken String?
154
- createdAt DateTime @default(now())
155
-
156
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
157
-
158
- @@unique([provider, providerId])
159
- @@index([userId])
160
- }
161
-
162
- model File {
163
- id String @id @default(cuid())
164
- userId String
165
- orgId String?
166
- filename String
284
+ orgId String?
285
+ projectId String?
286
+ filename String
167
287
  originalName String
168
- mimeType String
169
- size Int
170
- url String
171
- provider String @default("local")
172
- createdAt DateTime @default(now())
288
+ mimeType String
289
+ size Int
290
+ url String
291
+ provider String @default("local")
292
+ createdAt DateTime @default(now())
173
293
 
174
294
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
175
295
 
176
296
  @@index([userId])
177
297
  @@index([orgId])
298
+ @@index([projectId])
178
299
  }
179
300
 
180
- // Edge Functions
181
- model EdgeFunction {
182
- id String @id @default(cuid())
183
- name String
184
- description String?
185
- code String
186
- runtime String @default("nodejs18")
187
- timeout Int @default(30000)
188
- memory Int @default(128)
189
- environment Json?
190
- triggers Json?
191
- status String @default("draft")
192
- deployedAt DateTime?
193
- deploymentUrl String?
194
- deploymentId String?
195
- organizationId String
196
- createdBy String
197
- createdAt DateTime @default(now())
198
- updatedAt DateTime @updatedAt
199
- logs EdgeFunctionLog[]
200
-
201
- @@unique([organizationId, name])
202
- @@index([organizationId])
203
- @@index([status])
204
- }
205
-
206
- model EdgeFunctionLog {
207
- id String @id @default(cuid())
208
- functionId String
209
- status String
210
- duration Int
211
- input Json?
212
- output Json?
213
- error String?
214
- triggeredBy String?
215
- createdAt DateTime @default(now())
216
-
217
- function EdgeFunction @relation(fields: [functionId], references: [id], onDelete: Cascade)
218
-
219
- @@index([functionId])
220
- @@index([createdAt])
221
- }
222
-
223
- // Vector Embeddings
224
- model VectorCollection {
225
- id String @id @default(cuid())
226
- name String
227
- description String?
228
- dimension Int @default(1536)
229
- embeddingModel String @default("text-embedding-3-small")
230
- organizationId String
231
- createdAt DateTime @default(now())
232
-
233
- @@unique([organizationId, name])
234
- @@index([organizationId])
235
- }
236
-
237
- // Frontend Scanner Results
238
301
  model ScannedProject {
239
302
  id String @id @default(cuid())
240
303
  userId String
@@ -253,18 +316,19 @@ model ScannedProject {
253
316
  @@index([status])
254
317
  }
255
318
 
256
- // NoSQL Database Collections
257
319
  model DBCollection {
258
- id String @id @default(cuid())
320
+ id String @id @default(cuid())
259
321
  projectId String
260
322
  name String
261
323
  description String?
262
324
  schema Json?
263
325
  indexes Json?
264
- createdAt DateTime @default(now())
265
- updatedAt DateTime @updatedAt
326
+ createdAt DateTime @default(now())
327
+ updatedAt DateTime @updatedAt
266
328
  documents DBDocument[]
267
329
 
330
+ project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
331
+
268
332
  @@unique([projectId, name])
269
333
  @@index([projectId])
270
334
  }
@@ -59,8 +59,7 @@ const { orgs } = await client.orgs.list();
59
59
 
60
60
  // Create organization
61
61
  const org = await client.orgs.create({
62
- name: 'My Company',
63
- slug: 'my-company'
62
+ name: 'My Company'
64
63
  });
65
64
 
66
65
  // Get organization
@@ -18,7 +18,6 @@ export interface User {
18
18
  export interface Organization {
19
19
  id: string;
20
20
  name: string;
21
- slug: string;
22
21
  plan: string;
23
22
  stripeCustomerId?: string;
24
23
  createdAt: Date;
@@ -27,7 +26,6 @@ export interface Organization {
27
26
  export interface Project {
28
27
  id: string;
29
28
  name: string;
30
- description?: string;
31
29
  orgId: string;
32
30
  createdAt: Date;
33
31
  }
@@ -229,7 +227,7 @@ export class LaunchBaseClient {
229
227
  return res.data;
230
228
  }
231
229
 
232
- async createProject(data: { name: string; description?: string }): Promise<Project> {
230
+ async createProject(data: { name: string }): Promise<Project> {
233
231
  const res = await this.client.post('/api/projects', data);
234
232
  return res.data;
235
233
  }
@@ -34,7 +34,7 @@ export class AuditInterceptor implements NestInterceptor {
34
34
  return next.handle().pipe(
35
35
  tap({
36
36
  next: (result) => {
37
- const entityId = result?.id ?? result?.project?.id ?? result?.org?.id ?? result?.membership?.id;
37
+ const entityId = result?.id ?? result?.project?.id ?? result?.org?.id ?? result?.organizationMember?.id;
38
38
 
39
39
  this.audit.log({
40
40
  orgId: tenant?.orgId,
@@ -65,6 +65,9 @@ export class AuthService {
65
65
 
66
66
  const passwordHash = await this.hashPassword(dto.password);
67
67
 
68
+ // Generate slug from org name
69
+ const slug = dto.orgName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
70
+
68
71
  const user = await this.prisma.user.create({
69
72
  data: {
70
73
  email: dto.email,
@@ -75,14 +78,16 @@ export class AuthService {
75
78
  // Create first org for the user
76
79
  const org = await this.prisma.organization.create({
77
80
  data: {
78
- name: dto.orgName
81
+ name: dto.orgName,
82
+ slug,
83
+ ownerId: user.id
79
84
  }
80
85
  });
81
86
 
82
- await this.prisma.membership.create({
87
+ await this.prisma.organizationMember.create({
83
88
  data: {
84
89
  userId: user.id,
85
- orgId: org.id,
90
+ organizationId: org.id,
86
91
  role: Role.OWNER
87
92
  }
88
93
  });
@@ -91,7 +96,7 @@ export class AuthService {
91
96
 
92
97
  return {
93
98
  user: { id: user.id, email: user.email },
94
- org: { id: org.id, name: org.name },
99
+ org: { id: org.id, name: org.name, slug: org.slug },
95
100
  ...tokens
96
101
  };
97
102
  }
@@ -99,7 +104,7 @@ export class AuthService {
99
104
  async login(dto: LoginDto) {
100
105
  const user = await this.prisma.user.findUnique({
101
106
  where: { email: dto.email },
102
- include: { memberships: { include: { org: true } } }
107
+ include: { organizationMemberships: { include: { organization: true } } }
103
108
  });
104
109
  if (!user || !user.passwordHash) throw new UnauthorizedException('Invalid credentials');
105
110
 
@@ -109,11 +114,11 @@ export class AuthService {
109
114
  const tokens = await this.issueTokens({ id: user.id, email: user.email });
110
115
 
111
116
  // Get first org for the user
112
- const firstMembership = user.memberships[0];
117
+ const firstMembership = user.organizationMemberships[0];
113
118
 
114
119
  return {
115
120
  user: { id: user.id, email: user.email },
116
- org: firstMembership ? { id: firstMembership.org.id, name: firstMembership.org.name } : null,
121
+ org: firstMembership ? { id: firstMembership.organization.id, name: firstMembership.organization.name, slug: firstMembership.organization.slug } : null,
117
122
  ...tokens
118
123
  };
119
124
  }
@@ -196,7 +196,7 @@ export class BillingService {
196
196
  }
197
197
 
198
198
  if (type === 'members') {
199
- const count = await this.prisma.membership.count({ where: { orgId } });
199
+ const count = await this.prisma.organizationMember.count({ where: { organizationId: orgId } });
200
200
  return count < limits.members;
201
201
  }
202
202
 
@@ -22,17 +22,27 @@ export class AllExceptionsFilter implements ExceptionFilter {
22
22
  ? exception.getStatus()
23
23
  : HttpStatus.INTERNAL_SERVER_ERROR;
24
24
 
25
- const message =
26
- exception instanceof HttpException
27
- ? exception.getResponse()
28
- : 'Internal server error';
25
+ let errorMessage: string;
26
+ if (exception instanceof HttpException) {
27
+ const response = exception.getResponse();
28
+ if (typeof response === 'string') {
29
+ errorMessage = response;
30
+ } else if (typeof response === 'object' && response !== null && 'message' in response) {
31
+ const msg = (response as any).message;
32
+ errorMessage = Array.isArray(msg) ? msg[0] : String(msg);
33
+ } else {
34
+ errorMessage = 'An error occurred';
35
+ }
36
+ } else {
37
+ errorMessage = 'Internal server error';
38
+ }
29
39
 
30
40
  const errorResponse = {
31
41
  statusCode: status,
32
42
  timestamp: new Date().toISOString(),
33
43
  path: request.url,
34
44
  method: request.method,
35
- message: typeof message === 'object' ? message : { message },
45
+ message: errorMessage,
36
46
  };
37
47
 
38
48
  // Log the error
@@ -0,0 +1,52 @@
1
+ import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
2
+ import { PrismaService } from '../prisma/prisma.service';
3
+
4
+ @Injectable()
5
+ export class ProjectGuard implements CanActivate {
6
+ constructor(private readonly prisma: PrismaService) {}
7
+
8
+ async canActivate(context: ExecutionContext): Promise<boolean> {
9
+ const req = context.switchToHttp().getRequest();
10
+ const user = req.user as { userId?: string } | undefined;
11
+ const tenant = req.tenant as { orgId?: string; role?: string } | undefined;
12
+
13
+ // Get project identifier from params or headers
14
+ const projectSlug = req.params?.projectSlug || req.headers['x-project-slug'] as string | undefined;
15
+ const projectId = req.params?.projectId || req.headers['x-project-id'] as string | undefined;
16
+
17
+ if (!user?.userId) throw new ForbiddenException('Missing auth context');
18
+ if (!tenant?.orgId) throw new ForbiddenException('Missing tenant context - ensure TenantGuard runs first');
19
+
20
+ // Find project by slug or ID
21
+ let project;
22
+ if (projectId) {
23
+ project = await this.prisma.project.findUnique({
24
+ where: { id: projectId }
25
+ });
26
+ } else if (projectSlug) {
27
+ project = await this.prisma.project.findFirst({
28
+ where: {
29
+ slug: projectSlug,
30
+ organizationId: tenant.orgId
31
+ }
32
+ });
33
+ }
34
+
35
+ if (!project) throw new ForbiddenException('Project not found');
36
+
37
+ // Verify project belongs to the organization
38
+ if (project.organizationId !== tenant.orgId) {
39
+ throw new ForbiddenException('Project does not belong to this organization');
40
+ }
41
+
42
+ // Attach project to request for use in controllers
43
+ req.project = {
44
+ id: project.id,
45
+ slug: project.slug,
46
+ name: project.name,
47
+ organizationId: project.organizationId
48
+ };
49
+
50
+ return true;
51
+ }
52
+ }
@@ -13,11 +13,11 @@ export class TenantGuard implements CanActivate {
13
13
  if (!user?.userId) throw new ForbiddenException('Missing auth context');
14
14
  if (!orgId) throw new ForbiddenException('Missing x-org-id header');
15
15
 
16
- const membership = await this.prisma.membership.findUnique({
16
+ const membership = await this.prisma.organizationMember.findUnique({
17
17
  where: {
18
- userId_orgId: {
18
+ organizationId_userId: {
19
19
  userId: user.userId,
20
- orgId
20
+ organizationId: orgId
21
21
  }
22
22
  }
23
23
  });