ta_data_mas 0.0.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 (219) hide show
  1. package/README.md +42 -0
  2. package/apps/api/.prettierrc +4 -0
  3. package/apps/api/README.md +98 -0
  4. package/apps/api/drizzle/0000_right_drax.sql +87 -0
  5. package/apps/api/drizzle/meta/0000_snapshot.json +588 -0
  6. package/apps/api/drizzle/meta/_journal.json +13 -0
  7. package/apps/api/drizzle.config.ts +12 -0
  8. package/apps/api/eslint.config.mjs +35 -0
  9. package/apps/api/nest-cli.json +8 -0
  10. package/apps/api/package.json +99 -0
  11. package/apps/api/src/app.controller.spec.ts +25 -0
  12. package/apps/api/src/app.controller.ts +9 -0
  13. package/apps/api/src/app.module.ts +37 -0
  14. package/apps/api/src/app.service.ts +8 -0
  15. package/apps/api/src/auth/auth.controller.ts +113 -0
  16. package/apps/api/src/auth/auth.module.ts +15 -0
  17. package/apps/api/src/auth/auth.service.ts +309 -0
  18. package/apps/api/src/auth/decorators/current-user-decorator.ts +10 -0
  19. package/apps/api/src/auth/decorators/require-org-role-decorator.ts +7 -0
  20. package/apps/api/src/auth/dto/login.dto.ts +10 -0
  21. package/apps/api/src/auth/dto/register.dto.ts +14 -0
  22. package/apps/api/src/auth/guards/jwt-auth-guard.ts +40 -0
  23. package/apps/api/src/auth/guards/org-role.guards.ts +60 -0
  24. package/apps/api/src/db/db.module.ts +9 -0
  25. package/apps/api/src/db/drizzle.service.ts +16 -0
  26. package/apps/api/src/db/schema/index.ts +6 -0
  27. package/apps/api/src/db/schema/organizations.ts +30 -0
  28. package/apps/api/src/db/schema/projects.ts +26 -0
  29. package/apps/api/src/db/schema/query-history.ts +16 -0
  30. package/apps/api/src/db/schema/storage-buckets.ts +17 -0
  31. package/apps/api/src/db/schema/storage-objects.ts +18 -0
  32. package/apps/api/src/db/schema/users.ts +14 -0
  33. package/apps/api/src/main.ts +51 -0
  34. package/apps/api/src/members/dto/invite-member-dto.ts +6 -0
  35. package/apps/api/src/members/dto/update-role.dto.ts +8 -0
  36. package/apps/api/src/members/invite.service.ts +147 -0
  37. package/apps/api/src/members/members.controller.ts +56 -0
  38. package/apps/api/src/members/members.module.ts +13 -0
  39. package/apps/api/src/members/members.service.ts +85 -0
  40. package/apps/api/src/orgs/dto/create-org.dto.ts +8 -0
  41. package/apps/api/src/orgs/orgs.controller.ts +27 -0
  42. package/apps/api/src/orgs/orgs.module.ts +12 -0
  43. package/apps/api/src/orgs/orgs.service.ts +86 -0
  44. package/apps/api/src/project-api/project-api.controller.ts +95 -0
  45. package/apps/api/src/project-api/project-api.module.ts +12 -0
  46. package/apps/api/src/project-api/project-api.service.ts +196 -0
  47. package/apps/api/src/project-api/project-key.guard.ts +45 -0
  48. package/apps/api/src/project-api/query-parser.ts +129 -0
  49. package/apps/api/src/project-auth/dto/magic-link.dto.ts +7 -0
  50. package/apps/api/src/project-auth/dto/signin.dto.ts +10 -0
  51. package/apps/api/src/project-auth/dto/signup.dto.ts +11 -0
  52. package/apps/api/src/project-auth/project-auth-dashboard.controller.ts +35 -0
  53. package/apps/api/src/project-auth/project-auth.controller.ts +111 -0
  54. package/apps/api/src/project-auth/project-auth.module.ts +13 -0
  55. package/apps/api/src/project-auth/project-auth.service.ts +440 -0
  56. package/apps/api/src/projects/dto/create-project.dto.ts +9 -0
  57. package/apps/api/src/projects/projects.controller.ts +34 -0
  58. package/apps/api/src/projects/projects.module.ts +12 -0
  59. package/apps/api/src/projects/projects.service.ts +136 -0
  60. package/apps/api/src/realtime/realtime-socket.type.ts +14 -0
  61. package/apps/api/src/realtime/realtime.controller.ts +63 -0
  62. package/apps/api/src/realtime/realtime.gateway.ts +150 -0
  63. package/apps/api/src/realtime/realtime.module.ts +14 -0
  64. package/apps/api/src/realtime/realtime.service.ts +101 -0
  65. package/apps/api/src/realtime/trigger.service.ts +95 -0
  66. package/apps/api/src/sql-editor/dto/execute-query.dto.ts +7 -0
  67. package/apps/api/src/sql-editor/sql-editor.controller.ts +28 -0
  68. package/apps/api/src/sql-editor/sql-editor.module.ts +12 -0
  69. package/apps/api/src/sql-editor/sql-editor.service.ts +154 -0
  70. package/apps/api/src/storage/project-storage.controller.ts +140 -0
  71. package/apps/api/src/storage/storage.controller.ts +118 -0
  72. package/apps/api/src/storage/storage.module.ts +15 -0
  73. package/apps/api/src/storage/storage.service.ts +191 -0
  74. package/apps/api/src/storage/uploadthing.ts +23 -0
  75. package/apps/api/src/table-editor/dto/alter-table.dto.ts +15 -0
  76. package/apps/api/src/table-editor/dto/create-table.dto.ts +54 -0
  77. package/apps/api/src/table-editor/table-editor.controller.ts +117 -0
  78. package/apps/api/src/table-editor/table-editor.module.ts +12 -0
  79. package/apps/api/src/table-editor/table-editor.service.ts +366 -0
  80. package/apps/api/test/app.e2e-spec.ts +29 -0
  81. package/apps/api/test/jest-e2e.json +9 -0
  82. package/apps/api/tsconfig.build.json +4 -0
  83. package/apps/api/tsconfig.json +25 -0
  84. package/apps/web/.env.example +12 -0
  85. package/apps/web/AGENTS.md +9 -0
  86. package/apps/web/CLAUDE.md +1 -0
  87. package/apps/web/README.md +36 -0
  88. package/apps/web/components.json +25 -0
  89. package/apps/web/eslint.config.mjs +18 -0
  90. package/apps/web/next.config.ts +11 -0
  91. package/apps/web/package.json +48 -0
  92. package/apps/web/postcss.config.mjs +7 -0
  93. package/apps/web/public/file.svg +1 -0
  94. package/apps/web/public/globe.svg +1 -0
  95. package/apps/web/public/logo.svg +4 -0
  96. package/apps/web/public/next.svg +1 -0
  97. package/apps/web/public/vercel.svg +1 -0
  98. package/apps/web/public/window.svg +1 -0
  99. package/apps/web/src/app/(dashboard)/layout.tsx +33 -0
  100. package/apps/web/src/app/(dashboard)/organizations/(list)/layout.tsx +7 -0
  101. package/apps/web/src/app/(dashboard)/organizations/(list)/new/page.tsx +26 -0
  102. package/apps/web/src/app/(dashboard)/organizations/(list)/page.tsx +44 -0
  103. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/api/page.tsx +13 -0
  104. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/layout.tsx +19 -0
  105. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/page.tsx +10 -0
  106. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/providers/page.tsx +19 -0
  107. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/url-configuration/page.tsx +24 -0
  108. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/auth/users/page.tsx +13 -0
  109. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/database/page.tsx +29 -0
  110. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/realtime/page.tsx +28 -0
  111. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/sql/page.tsx +21 -0
  112. package/apps/web/src/app/(dashboard)/organizations/[slug]/[projectSlug]/storage/page.tsx +28 -0
  113. package/apps/web/src/app/(dashboard)/organizations/[slug]/layout.tsx +20 -0
  114. package/apps/web/src/app/(dashboard)/organizations/[slug]/projects/page.tsx +62 -0
  115. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/members/page.tsx +28 -0
  116. package/apps/web/src/app/(dashboard)/organizations/[slug]/settings/page.tsx +10 -0
  117. package/apps/web/src/app/(dashboard)/settings/page.tsx +12 -0
  118. package/apps/web/src/app/dashboard/page.tsx +14 -0
  119. package/apps/web/src/app/favicon.ico +0 -0
  120. package/apps/web/src/app/globals.css +115 -0
  121. package/apps/web/src/app/layout.tsx +36 -0
  122. package/apps/web/src/app/login/page.tsx +199 -0
  123. package/apps/web/src/app/page.tsx +100 -0
  124. package/apps/web/src/app/register/page.tsx +218 -0
  125. package/apps/web/src/components/app-sidebar.tsx +301 -0
  126. package/apps/web/src/components/navbar.tsx +63 -0
  127. package/apps/web/src/components/topNav.tsx +68 -0
  128. package/apps/web/src/components/ui/avatar.tsx +112 -0
  129. package/apps/web/src/components/ui/badge.tsx +48 -0
  130. package/apps/web/src/components/ui/button.tsx +66 -0
  131. package/apps/web/src/components/ui/card.tsx +102 -0
  132. package/apps/web/src/components/ui/checkbox.tsx +32 -0
  133. package/apps/web/src/components/ui/dialog.tsx +168 -0
  134. package/apps/web/src/components/ui/dropdown-menu.tsx +269 -0
  135. package/apps/web/src/components/ui/field.tsx +238 -0
  136. package/apps/web/src/components/ui/input.tsx +19 -0
  137. package/apps/web/src/components/ui/label.tsx +24 -0
  138. package/apps/web/src/components/ui/resizable.tsx +50 -0
  139. package/apps/web/src/components/ui/scroll-area.tsx +55 -0
  140. package/apps/web/src/components/ui/select.tsx +192 -0
  141. package/apps/web/src/components/ui/separator.tsx +28 -0
  142. package/apps/web/src/components/ui/sheet.tsx +147 -0
  143. package/apps/web/src/components/ui/sidebar.tsx +703 -0
  144. package/apps/web/src/components/ui/skeleton.tsx +13 -0
  145. package/apps/web/src/components/ui/switch.tsx +33 -0
  146. package/apps/web/src/components/ui/table.tsx +116 -0
  147. package/apps/web/src/components/ui/tooltip.tsx +57 -0
  148. package/apps/web/src/features/api-docs/api-docs-client.tsx +227 -0
  149. package/apps/web/src/features/api-docs/api-docs-helpers.server.ts +86 -0
  150. package/apps/web/src/features/auth/action.ts +67 -0
  151. package/apps/web/src/features/auth/client.schema.ts +13 -0
  152. package/apps/web/src/features/auth/constants.ts +6 -0
  153. package/apps/web/src/features/auth/cookies.ts +86 -0
  154. package/apps/web/src/features/auth/server.schema.ts +11 -0
  155. package/apps/web/src/features/auth/sign-out.ts +17 -0
  156. package/apps/web/src/features/members/action.ts +66 -0
  157. package/apps/web/src/features/members/client.schema.ts +11 -0
  158. package/apps/web/src/features/members/constants.ts +8 -0
  159. package/apps/web/src/features/members/members-client.tsx +198 -0
  160. package/apps/web/src/features/members/members-helpers.server.ts +21 -0
  161. package/apps/web/src/features/members/server.schema.ts +19 -0
  162. package/apps/web/src/features/organization/action.ts +45 -0
  163. package/apps/web/src/features/organization/client.schema.ts +5 -0
  164. package/apps/web/src/features/organization/constants.tsx +3 -0
  165. package/apps/web/src/features/organization/create-org-form.tsx +66 -0
  166. package/apps/web/src/features/organization/organization-helpers.server.ts +35 -0
  167. package/apps/web/src/features/organization/server.schema.ts +10 -0
  168. package/apps/web/src/features/project-auth/project-auth-action.ts +78 -0
  169. package/apps/web/src/features/project-auth/project-auth-client.schema.ts +18 -0
  170. package/apps/web/src/features/project-auth/project-auth-constants.ts +8 -0
  171. package/apps/web/src/features/project-auth/project-auth-helpers.server.ts +39 -0
  172. package/apps/web/src/features/project-auth/project-auth-providers.tsx +222 -0
  173. package/apps/web/src/features/project-auth/project-auth-server.schema.ts +22 -0
  174. package/apps/web/src/features/project-auth/project-auth-shell.tsx +106 -0
  175. package/apps/web/src/features/project-auth/project-auth-url-config.tsx +151 -0
  176. package/apps/web/src/features/project-auth/project-auth-users.tsx +138 -0
  177. package/apps/web/src/features/projects/action.ts +46 -0
  178. package/apps/web/src/features/projects/client.schema.ts +9 -0
  179. package/apps/web/src/features/projects/constants.ts +6 -0
  180. package/apps/web/src/features/projects/create-project-modal.tsx +117 -0
  181. package/apps/web/src/features/projects/project-helpers.server.ts +38 -0
  182. package/apps/web/src/features/projects/server.schema.ts +10 -0
  183. package/apps/web/src/features/realtime/realtime-client.tsx +294 -0
  184. package/apps/web/src/features/realtime/realtime-helpers.server.ts +40 -0
  185. package/apps/web/src/features/sql-editor/sql-editor-client.tsx +324 -0
  186. package/apps/web/src/features/sql-editor/sql-editor-helpers.server.ts +22 -0
  187. package/apps/web/src/features/storage/storage-client.tsx +500 -0
  188. package/apps/web/src/features/storage/storage-helpers.server.ts +36 -0
  189. package/apps/web/src/features/table-editor/action.ts +97 -0
  190. package/apps/web/src/features/table-editor/add-column-dialog.tsx +184 -0
  191. package/apps/web/src/features/table-editor/client.schema.ts +36 -0
  192. package/apps/web/src/features/table-editor/create-table-drawer.tsx +542 -0
  193. package/apps/web/src/features/table-editor/server.schema.ts +41 -0
  194. package/apps/web/src/features/table-editor/table-editor-client.tsx +737 -0
  195. package/apps/web/src/features/table-editor/table-editor-helpers.server.ts +39 -0
  196. package/apps/web/src/hooks/use-mobile.ts +19 -0
  197. package/apps/web/src/lib/axios.ts +8 -0
  198. package/apps/web/src/lib/utils.ts +6 -0
  199. package/apps/web/src/proxy.ts +72 -0
  200. package/apps/web/src/server-utils/utils.ts +10 -0
  201. package/apps/web/tsconfig.json +34 -0
  202. package/package.json +22 -0
  203. package/packages/apiDatabase-js/package.json +39 -0
  204. package/packages/apiDatabase-js/src/auth/index.ts +121 -0
  205. package/packages/apiDatabase-js/src/client.ts +25 -0
  206. package/packages/apiDatabase-js/src/db/index.ts +15 -0
  207. package/packages/apiDatabase-js/src/db/query-builder.ts +194 -0
  208. package/packages/apiDatabase-js/src/index.ts +16 -0
  209. package/packages/apiDatabase-js/src/realtime/index.ts +84 -0
  210. package/packages/apiDatabase-js/src/storage/index.ts +130 -0
  211. package/packages/apiDatabase-js/tsconfig.json +13 -0
  212. package/packages/apiDatabase-js/tsdown.config.ts +20 -0
  213. package/packages/constants/package.json +6 -0
  214. package/packages/constants/src/index.ts +76 -0
  215. package/packages/tsconfig.json +13 -0
  216. package/packages/tsdown.config.ts +20 -0
  217. package/packages/types/package.json +6 -0
  218. package/packages/types/src/index.ts +254 -0
  219. package/pnpm-workspace.yaml +10 -0
@@ -0,0 +1,12 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { OrgsService } from './orgs.service';
3
+ import { OrgsController } from './orgs.controller';
4
+ import { AuthModule } from '../auth/auth.module';
5
+
6
+ @Module({
7
+ imports: [AuthModule],
8
+ providers: [OrgsService],
9
+ controllers: [OrgsController],
10
+ exports: [OrgsService],
11
+ })
12
+ export class OrgsModule {}
@@ -0,0 +1,86 @@
1
+ import { Injectable, NotFoundException } from '@nestjs/common';
2
+ import { and, eq, isNull, sql } from 'drizzle-orm';
3
+ import { randomBytes } from 'crypto';
4
+ import slugify from 'slugify';
5
+ import { DrizzleService } from '../db/drizzle.service';
6
+ import { organizations, orgMembers, projects } from '../db/schema';
7
+ import { CreateOrgDto } from './dto/create-org.dto';
8
+
9
+ @Injectable()
10
+ export class OrgsService {
11
+ constructor(private drizzle: DrizzleService) {}
12
+
13
+ private generateOrgSlug(name: string): string {
14
+ const base = slugify(`${name}-org`, { lower: true, strict: true });
15
+ const suffix = randomBytes(3).toString('hex');
16
+ return `${base}-${suffix}`;
17
+ }
18
+
19
+ async getMyOrgs(userId: string) {
20
+ return this.drizzle.db
21
+ .select({
22
+ id: organizations.id,
23
+ name: organizations.name,
24
+ slug: organizations.slug,
25
+ createdAt: organizations.createdAt,
26
+ updatedAt: organizations.updatedAt,
27
+ role: orgMembers.role,
28
+ projectCount: sql<number>`cast(count(distinct ${projects.id}) as int)`,
29
+ memberCount: sql<number>`(
30
+ select cast(count(*) as int)
31
+ from org_members om
32
+ where om.org_id = ${organizations.id}
33
+ and om.removed_at is null
34
+ )`,
35
+ })
36
+ .from(orgMembers)
37
+ .innerJoin(organizations, eq(orgMembers.orgId, organizations.id))
38
+ .leftJoin(projects, eq(projects.orgId, organizations.id))
39
+ .where(and(eq(orgMembers.userId, userId), isNull(orgMembers.removedAt)))
40
+ .groupBy(organizations.id, orgMembers.role);
41
+ }
42
+
43
+ async getOrgBySlug(slug: string, userId: string) {
44
+ const [row] = await this.drizzle.db
45
+ .select({
46
+ id: organizations.id,
47
+ name: organizations.name,
48
+ slug: organizations.slug,
49
+ createdAt: organizations.createdAt,
50
+ updatedAt: organizations.updatedAt,
51
+ role: orgMembers.role,
52
+ })
53
+ .from(organizations)
54
+ .innerJoin(
55
+ orgMembers,
56
+ and(
57
+ eq(orgMembers.orgId, organizations.id),
58
+ eq(orgMembers.userId, userId),
59
+ isNull(orgMembers.removedAt),
60
+ ),
61
+ )
62
+ .where(eq(organizations.slug, slug))
63
+ .limit(1);
64
+
65
+ if (!row) throw new NotFoundException('Organization not found');
66
+ return row;
67
+ }
68
+
69
+ async createOrg(dto: CreateOrgDto, userId: string) {
70
+ const [org] = await this.drizzle.db
71
+ .insert(organizations)
72
+ .values({
73
+ name: dto.name,
74
+ slug: this.generateOrgSlug(dto.name),
75
+ })
76
+ .returning();
77
+
78
+ await this.drizzle.db.insert(orgMembers).values({
79
+ orgId: org.id,
80
+ userId,
81
+ role: 'admin',
82
+ });
83
+
84
+ return org;
85
+ }
86
+ }
@@ -0,0 +1,95 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Delete,
5
+ ForbiddenException,
6
+ Get,
7
+ Param,
8
+ Patch,
9
+ Post,
10
+ Query,
11
+ Req,
12
+ UseGuards,
13
+ } from '@nestjs/common';
14
+ import type { Request as ExpressRequest } from 'express';
15
+ import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
16
+ import { ProjectApiService } from './project-api.service';
17
+ import { ProjectKeyGuard, ProjectKeyPayload } from './project-key.guard';
18
+
19
+ @Controller('projects/:projectSlug/rest')
20
+ @UseGuards(ProjectKeyGuard)
21
+ export class ProjectApiController {
22
+ constructor(private projectApiService: ProjectApiService) {}
23
+
24
+ private getProjectKey(req: ExpressRequest): ProjectKeyPayload {
25
+ return req['projectKey'] as ProjectKeyPayload;
26
+ }
27
+
28
+ private assertWriteAccess(req: ExpressRequest): void {
29
+ const { role } = this.getProjectKey(req);
30
+ if (role !== PROJECT_KEY_ROLES.SERVICE_ROLE) {
31
+ throw new ForbiddenException(
32
+ 'Write operations require the service role key',
33
+ );
34
+ }
35
+ }
36
+
37
+ @Get(':table')
38
+ getRows(
39
+ @Req() req: ExpressRequest,
40
+ @Param('projectSlug') projectSlug: string,
41
+ @Param('table') table: string,
42
+ @Query() query: Record<string, string>,
43
+ ) {
44
+ const { projectId } = this.getProjectKey(req);
45
+ return this.projectApiService.getRows(projectId, projectSlug, table, query);
46
+ }
47
+
48
+ @Post(':table')
49
+ insertRow(
50
+ @Req() req: ExpressRequest,
51
+ @Param('projectSlug') projectSlug: string,
52
+ @Param('table') table: string,
53
+ @Body() body: Record<string, unknown>,
54
+ ) {
55
+ this.assertWriteAccess(req);
56
+ const { projectId } = this.getProjectKey(req);
57
+ return this.projectApiService.insertRow(
58
+ projectId,
59
+ projectSlug,
60
+ table,
61
+ body,
62
+ );
63
+ }
64
+
65
+ @Patch(':table/:id')
66
+ updateRow(
67
+ @Req() req: ExpressRequest,
68
+ @Param('projectSlug') projectSlug: string,
69
+ @Param('table') table: string,
70
+ @Param('id') id: string,
71
+ @Body() body: Record<string, unknown>,
72
+ ) {
73
+ this.assertWriteAccess(req);
74
+ const { projectId } = this.getProjectKey(req);
75
+ return this.projectApiService.updateRow(
76
+ projectId,
77
+ projectSlug,
78
+ table,
79
+ id,
80
+ body,
81
+ );
82
+ }
83
+
84
+ @Delete(':table/:id')
85
+ deleteRow(
86
+ @Req() req: ExpressRequest,
87
+ @Param('projectSlug') projectSlug: string,
88
+ @Param('table') table: string,
89
+ @Param('id') id: string,
90
+ ) {
91
+ this.assertWriteAccess(req);
92
+ const { projectId } = this.getProjectKey(req);
93
+ return this.projectApiService.deleteRow(projectId, projectSlug, table, id);
94
+ }
95
+ }
@@ -0,0 +1,12 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { ProjectApiService } from './project-api.service';
4
+ import { ProjectApiController } from './project-api.controller';
5
+ import { ProjectKeyGuard } from './project-key.guard';
6
+
7
+ @Module({
8
+ imports: [JwtModule.register({})],
9
+ providers: [ProjectApiService, ProjectKeyGuard],
10
+ controllers: [ProjectApiController],
11
+ })
12
+ export class ProjectApiModule {}
@@ -0,0 +1,196 @@
1
+ import {
2
+ BadRequestException,
3
+ ForbiddenException,
4
+ Injectable,
5
+ NotFoundException,
6
+ } from '@nestjs/common';
7
+ import { eq } from 'drizzle-orm';
8
+ import { DrizzleService } from '../db/drizzle.service';
9
+ import { projects } from '../db/schema';
10
+ import { parseQueryParams, buildWhereClause } from './query-parser';
11
+
12
+ @Injectable()
13
+ export class ProjectApiService {
14
+ constructor(private drizzle: DrizzleService) {}
15
+
16
+ private assertSafeIdentifier(name: string, label: string): void {
17
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
18
+ throw new BadRequestException(`Invalid ${label}: ${name}`);
19
+ }
20
+ }
21
+
22
+ private formatLiteral(value: unknown): string {
23
+ if (value === null || value === undefined) return 'NULL';
24
+ if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
25
+ if (typeof value === 'number' && Number.isFinite(value)) {
26
+ return String(value);
27
+ }
28
+ if (typeof value === 'string') {
29
+ return `'${value.replace(/'/g, "''")}'`;
30
+ }
31
+ if (typeof value === 'object') {
32
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'::jsonb`;
33
+ }
34
+ throw new BadRequestException('Unsupported column value');
35
+ }
36
+
37
+ async resolveProjectSchema(
38
+ projectId: string,
39
+ projectSlug: string,
40
+ ): Promise<string> {
41
+ const [project] = await this.drizzle.db
42
+ .select({ dbSchema: projects.dbSchema, slug: projects.slug })
43
+ .from(projects)
44
+ .where(eq(projects.id, projectId))
45
+ .limit(1);
46
+
47
+ if (!project) throw new NotFoundException('Project not found');
48
+
49
+ if (project.slug !== projectSlug) {
50
+ throw new ForbiddenException('API key does not match this project URL');
51
+ }
52
+
53
+ return project.dbSchema;
54
+ }
55
+
56
+ private async getPrimaryKeyColumn(
57
+ schema: string,
58
+ tableName: string,
59
+ ): Promise<string> {
60
+ const result = await this.drizzle.db.execute<{ column_name: string }>(
61
+ `SELECT kcu.column_name
62
+ FROM information_schema.table_constraints tc
63
+ JOIN information_schema.key_column_usage kcu
64
+ ON tc.constraint_name = kcu.constraint_name
65
+ AND tc.table_schema = kcu.table_schema
66
+ WHERE tc.constraint_type = 'PRIMARY KEY'
67
+ AND tc.table_schema = '${schema}'
68
+ AND tc.table_name = '${tableName}'
69
+ ORDER BY kcu.ordinal_position ASC
70
+ LIMIT 1`,
71
+ );
72
+
73
+ const pk = result.rows[0]?.column_name;
74
+ if (!pk) {
75
+ throw new BadRequestException(`Table "${tableName}" has no primary key`);
76
+ }
77
+
78
+ return pk;
79
+ }
80
+
81
+ async getRows(
82
+ projectId: string,
83
+ projectSlug: string,
84
+ tableName: string,
85
+ rawParams: Record<string, string>,
86
+ ) {
87
+ this.assertSafeIdentifier(tableName, 'table name');
88
+ const schema = await this.resolveProjectSchema(projectId, projectSlug);
89
+ const { select, filters, orderBy, limit, offset } =
90
+ parseQueryParams(rawParams);
91
+
92
+ const columns =
93
+ select.length > 0 ? select.map((c) => `"${c}"`).join(', ') : '*';
94
+
95
+ const where = buildWhereClause(filters);
96
+ const order = orderBy
97
+ ? `ORDER BY "${orderBy.column}" ${orderBy.direction}`
98
+ : '';
99
+
100
+ const sql = `
101
+ SELECT ${columns}
102
+ FROM "${schema}"."${tableName}"
103
+ ${where}
104
+ ${order}
105
+ LIMIT ${limit}
106
+ OFFSET ${offset}
107
+ `;
108
+
109
+ const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
110
+ return result.rows;
111
+ }
112
+
113
+ async insertRow(
114
+ projectId: string,
115
+ projectSlug: string,
116
+ tableName: string,
117
+ body: Record<string, unknown>,
118
+ ) {
119
+ this.assertSafeIdentifier(tableName, 'table name');
120
+ const schema = await this.resolveProjectSchema(projectId, projectSlug);
121
+
122
+ const entries = Object.entries(body);
123
+ if (entries.length === 0) {
124
+ throw new BadRequestException('Request body cannot be empty');
125
+ }
126
+
127
+ entries.forEach(([col]) => this.assertSafeIdentifier(col, 'column name'));
128
+
129
+ const columns = entries.map(([c]) => `"${c}"`).join(', ');
130
+ const values = entries.map(([, v]) => this.formatLiteral(v)).join(', ');
131
+
132
+ const sql = `
133
+ INSERT INTO "${schema}"."${tableName}" (${columns})
134
+ VALUES (${values})
135
+ RETURNING *
136
+ `;
137
+
138
+ const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
139
+ return result.rows[0];
140
+ }
141
+
142
+ async updateRow(
143
+ projectId: string,
144
+ projectSlug: string,
145
+ tableName: string,
146
+ rowId: string,
147
+ body: Record<string, unknown>,
148
+ ) {
149
+ this.assertSafeIdentifier(tableName, 'table name');
150
+ const schema = await this.resolveProjectSchema(projectId, projectSlug);
151
+ const pkColumn = await this.getPrimaryKeyColumn(schema, tableName);
152
+
153
+ const entries = Object.entries(body);
154
+ if (entries.length === 0) {
155
+ throw new BadRequestException('Request body cannot be empty');
156
+ }
157
+
158
+ entries.forEach(([col]) => this.assertSafeIdentifier(col, 'column name'));
159
+
160
+ const setClauses = entries
161
+ .map(([col, val]) => `"${col}" = ${this.formatLiteral(val)}`)
162
+ .join(', ');
163
+
164
+ const sql = `
165
+ UPDATE "${schema}"."${tableName}"
166
+ SET ${setClauses}
167
+ WHERE "${pkColumn}" = ${this.formatLiteral(rowId)}
168
+ RETURNING *
169
+ `;
170
+
171
+ const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
172
+ if (!result.rows[0]) throw new NotFoundException('Row not found');
173
+ return result.rows[0];
174
+ }
175
+
176
+ async deleteRow(
177
+ projectId: string,
178
+ projectSlug: string,
179
+ tableName: string,
180
+ rowId: string,
181
+ ) {
182
+ this.assertSafeIdentifier(tableName, 'table name');
183
+ const schema = await this.resolveProjectSchema(projectId, projectSlug);
184
+ const pkColumn = await this.getPrimaryKeyColumn(schema, tableName);
185
+
186
+ const sql = `
187
+ DELETE FROM "${schema}"."${tableName}"
188
+ WHERE "${pkColumn}" = ${this.formatLiteral(rowId)}
189
+ RETURNING *
190
+ `;
191
+
192
+ const result = await this.drizzle.db.execute<Record<string, unknown>>(sql);
193
+ if (!result.rows[0]) throw new NotFoundException('Row not found');
194
+ return result.rows[0];
195
+ }
196
+ }
@@ -0,0 +1,45 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ Injectable,
5
+ UnauthorizedException,
6
+ } from '@nestjs/common';
7
+ import { JwtService } from '@nestjs/jwt';
8
+ import { ConfigService } from '@nestjs/config';
9
+ import { Request } from 'express';
10
+ import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
11
+
12
+ export interface ProjectKeyPayload {
13
+ projectId: string;
14
+ role: typeof PROJECT_KEY_ROLES.ANON | typeof PROJECT_KEY_ROLES.SERVICE_ROLE;
15
+ }
16
+
17
+ @Injectable()
18
+ export class ProjectKeyGuard implements CanActivate {
19
+ constructor(
20
+ private jwtService: JwtService,
21
+ private configService: ConfigService,
22
+ ) {}
23
+
24
+ canActivate(context: ExecutionContext): boolean {
25
+ const request = context.switchToHttp().getRequest<Request>();
26
+ const authHeader = request.headers.authorization;
27
+
28
+ if (!authHeader?.startsWith('Bearer ')) {
29
+ throw new UnauthorizedException('Missing API key');
30
+ }
31
+
32
+ const token = authHeader.slice(7);
33
+
34
+ try {
35
+ const payload = this.jwtService.verify<ProjectKeyPayload>(token, {
36
+ secret: this.configService.get<string>('PROJECT_JWT_SECRET'),
37
+ });
38
+
39
+ request['projectKey'] = payload;
40
+ return true;
41
+ } catch {
42
+ throw new UnauthorizedException('Invalid API key');
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,129 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import {
3
+ RESERVED_QUERY_PARAMS,
4
+ FILTER_OPERATORS,
5
+ type FilterOperator,
6
+ } from '@apiDatabase/constants';
7
+
8
+ export interface ParsedQuery {
9
+ select: string[];
10
+ filters: FilterClause[];
11
+ orderBy: OrderClause | null;
12
+ limit: number;
13
+ offset: number;
14
+ }
15
+
16
+ interface FilterClause {
17
+ column: string;
18
+ operator: string;
19
+ value: string;
20
+ }
21
+
22
+ interface OrderClause {
23
+ column: string;
24
+ direction: 'ASC' | 'DESC';
25
+ }
26
+
27
+ function assertSafeIdentifier(name: string, label: string): void {
28
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
29
+ throw new BadRequestException(`Invalid ${label}: ${name}`);
30
+ }
31
+ }
32
+
33
+ function formatSqlValue(operator: string, rawValue: string): string {
34
+ const value = rawValue.replace(/;/g, '');
35
+
36
+ if (operator === 'IS') {
37
+ return value.toUpperCase() === 'NULL' ? 'NULL' : 'NOT NULL';
38
+ }
39
+
40
+ if (operator === 'LIKE' || operator === 'ILIKE') {
41
+ return `'${value.replace(/'/g, "''")}'`;
42
+ }
43
+
44
+ if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
45
+ return value.toLowerCase();
46
+ }
47
+
48
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
49
+ return value;
50
+ }
51
+
52
+ return `'${value.replace(/'/g, "''")}'`;
53
+ }
54
+
55
+ export function parseQueryParams(params: Record<string, string>): ParsedQuery {
56
+ const filters: FilterClause[] = [];
57
+ let select: string[] = [];
58
+ let orderBy: OrderClause | null = null;
59
+ let limit = 100;
60
+ let offset = 0;
61
+
62
+ for (const [key, value] of Object.entries(params)) {
63
+ if (RESERVED_QUERY_PARAMS.has(key)) {
64
+ switch (key) {
65
+ case 'select':
66
+ select = value
67
+ .split(',')
68
+ .map((c) => c.trim())
69
+ .filter(Boolean);
70
+ select.forEach((col) => assertSafeIdentifier(col, 'select column'));
71
+ break;
72
+ case 'order': {
73
+ const [col, dir] = value.split('.');
74
+ assertSafeIdentifier(col, 'order column');
75
+ orderBy = {
76
+ column: col,
77
+ direction: dir?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC',
78
+ };
79
+ break;
80
+ }
81
+ case 'limit':
82
+ limit = Math.min(parseInt(value, 10) || 100, 1000);
83
+ break;
84
+ case 'offset':
85
+ offset = Math.max(parseInt(value, 10) || 0, 0);
86
+ break;
87
+ }
88
+ continue;
89
+ }
90
+
91
+ assertSafeIdentifier(key, 'filter column');
92
+
93
+ const dotIndex = value.indexOf('.');
94
+ if (dotIndex === -1) continue;
95
+
96
+ const operator = value.slice(0, dotIndex) as FilterOperator;
97
+ const filterValue = value.slice(dotIndex + 1);
98
+
99
+ if (!(operator in FILTER_OPERATORS)) continue;
100
+
101
+ filters.push({
102
+ column: key,
103
+ operator: FILTER_OPERATORS[operator],
104
+ value: filterValue,
105
+ });
106
+ }
107
+
108
+ return { select, filters, orderBy, limit, offset };
109
+ }
110
+
111
+ export function buildWhereClause(filters: FilterClause[]): string {
112
+ if (filters.length === 0) return '';
113
+
114
+ const clauses = filters.map(({ column, operator, value }) => {
115
+ const sqlValue = formatSqlValue(operator, value);
116
+
117
+ if (operator === 'IS') {
118
+ return `"${column}" IS ${sqlValue}`;
119
+ }
120
+
121
+ if (operator === 'LIKE' || operator === 'ILIKE') {
122
+ return `"${column}" ${operator} ${sqlValue}`;
123
+ }
124
+
125
+ return `"${column}" ${operator} ${sqlValue}`;
126
+ });
127
+
128
+ return `WHERE ${clauses.join(' AND ')}`;
129
+ }
@@ -0,0 +1,7 @@
1
+ import { IsEmail } from 'class-validator';
2
+ import type { MagicLinkInput } from '@apiDatabase/types';
3
+
4
+ export class MagicLinkDto implements MagicLinkInput {
5
+ @IsEmail()
6
+ email!: string;
7
+ }
@@ -0,0 +1,10 @@
1
+ import { IsEmail, IsString } from 'class-validator';
2
+ import type { SignInInput } from '@apiDatabase/types';
3
+
4
+ export class SignInDto implements SignInInput {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ password!: string;
10
+ }
@@ -0,0 +1,11 @@
1
+ import { IsEmail, IsString, MinLength } from 'class-validator';
2
+ import type { SignUpInput } from '@apiDatabase/types';
3
+
4
+ export class SignUpDto implements SignUpInput {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ @MinLength(8)
10
+ password!: string;
11
+ }
@@ -0,0 +1,35 @@
1
+ import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
2
+ import { ProjectAuthService } from './project-auth.service';
3
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
4
+ import { OrgRoleGuard } from '../auth/guards/org-role.guards';
5
+
6
+ @Controller('orgs/:slug/projects/:projectSlug/auth')
7
+ @UseGuards(JwtAuthGuard, OrgRoleGuard)
8
+ export class ProjectAuthDashboardController {
9
+ constructor(private projectAuthService: ProjectAuthService) {}
10
+
11
+ @Get('users')
12
+ getUsers(@Param('projectSlug') projectSlug: string) {
13
+ return this.projectAuthService.getUsers(projectSlug);
14
+ }
15
+
16
+ @Get('settings')
17
+ getSettings(@Param('projectSlug') projectSlug: string) {
18
+ return this.projectAuthService.getOAuthSettings(projectSlug);
19
+ }
20
+
21
+ @Post('settings')
22
+ updateSettings(
23
+ @Param('projectSlug') projectSlug: string,
24
+ @Body()
25
+ settings: {
26
+ siteUrl?: string;
27
+ googleClientId?: string;
28
+ googleClientSecret?: string;
29
+ githubClientId?: string;
30
+ githubClientSecret?: string;
31
+ },
32
+ ) {
33
+ return this.projectAuthService.updateOAuthSettings(projectSlug, settings);
34
+ }
35
+ }