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,154 @@
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ NotFoundException,
5
+ } from '@nestjs/common';
6
+ import type {
7
+ FullQueryResults,
8
+ NeonQueryFunction,
9
+ } from '@neondatabase/serverless';
10
+ import { and, desc, eq } from 'drizzle-orm';
11
+ import { DrizzleService } from '../db/drizzle.service';
12
+ import { projects, organizations, queryHistory } from '../db/schema';
13
+ import type { QueryResult } from '@apiDatabase/types';
14
+
15
+ @Injectable()
16
+ export class SqlEditorService {
17
+ constructor(private drizzle: DrizzleService) {}
18
+
19
+ private assertSafeIdentifier(name: string, label: string): void {
20
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
21
+ throw new BadRequestException(`Invalid ${label}: ${name}`);
22
+ }
23
+ }
24
+
25
+ private parseSingleStatement(querySql: string): string {
26
+ const statements = querySql
27
+ .split(';')
28
+ .map((part) => part.trim())
29
+ .filter((part) => part.length > 0);
30
+
31
+ if (statements.length === 0) {
32
+ throw new BadRequestException('SQL query cannot be empty');
33
+ }
34
+
35
+ if (statements.length > 1) {
36
+ throw new BadRequestException(
37
+ 'Only one SQL statement per run. Remove extra statements after a semicolon (e.g. a trailing SELECT).',
38
+ );
39
+ }
40
+
41
+ return statements[0];
42
+ }
43
+
44
+ private getNeonClient(): NeonQueryFunction<false, true> {
45
+ const client = (
46
+ this.drizzle.db as unknown as {
47
+ $client: NeonQueryFunction<false, true>;
48
+ }
49
+ ).$client;
50
+
51
+ if (!client?.transaction) {
52
+ throw new Error('Neon HTTP client is not available');
53
+ }
54
+
55
+ return client;
56
+ }
57
+
58
+ private normalizePgResult(result: FullQueryResults<false>): {
59
+ rows: Record<string, unknown>[];
60
+ rowCount: number;
61
+ command: string;
62
+ } {
63
+ const rows = (result.rows ?? []) as Record<string, unknown>[];
64
+
65
+ return {
66
+ rows,
67
+ rowCount: result.rowCount ?? rows.length,
68
+ command: result.command ?? 'SELECT',
69
+ };
70
+ }
71
+
72
+ private async getProject(orgSlug: string, projectSlug: string) {
73
+ const [row] = await this.drizzle.db
74
+ .select({
75
+ id: projects.id,
76
+ dbSchema: projects.dbSchema,
77
+ })
78
+ .from(projects)
79
+ .innerJoin(organizations, eq(projects.orgId, organizations.id))
80
+ .where(
81
+ and(eq(organizations.slug, orgSlug), eq(projects.slug, projectSlug)),
82
+ )
83
+ .limit(1);
84
+
85
+ if (!row) throw new NotFoundException('Project not found');
86
+ return row;
87
+ }
88
+
89
+ async executeQuery(
90
+ orgSlug: string,
91
+ projectSlug: string,
92
+ querySql: string,
93
+ ): Promise<QueryResult> {
94
+ const project = await this.getProject(orgSlug, projectSlug);
95
+ const statement = this.parseSingleStatement(querySql);
96
+
97
+ const normalised = statement.trim().toUpperCase();
98
+ const blocked = ['DROP DATABASE', 'DROP SCHEMA', 'TRUNCATE'];
99
+ if (blocked.some((b) => normalised.startsWith(b))) {
100
+ throw new BadRequestException('This statement is not allowed');
101
+ }
102
+
103
+ this.assertSafeIdentifier(project.dbSchema, 'project schema');
104
+
105
+ const start = Date.now();
106
+
107
+ try {
108
+ const neonSql = this.getNeonClient();
109
+
110
+ const results = await neonSql.transaction(
111
+ (txn) => [
112
+ txn`SET LOCAL search_path TO ${txn.unsafe(`"${project.dbSchema}", public`)}`,
113
+ txn`${txn.unsafe(statement)}`,
114
+ ],
115
+ { fullResults: true, readOnly: false },
116
+ );
117
+
118
+ const executionTimeMs = Date.now() - start;
119
+ const { rows, rowCount, command } = this.normalizePgResult(
120
+ results[results.length - 1],
121
+ );
122
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
123
+
124
+ await this.drizzle.db.insert(queryHistory).values({
125
+ projectId: project.id,
126
+ sql: querySql,
127
+ executionTimeMs,
128
+ rowCount,
129
+ });
130
+
131
+ return {
132
+ rows,
133
+ columns,
134
+ rowCount,
135
+ executionTimeMs,
136
+ command,
137
+ };
138
+ } catch (err: unknown) {
139
+ const message = err instanceof Error ? err.message : 'Query failed';
140
+ throw new BadRequestException(message);
141
+ }
142
+ }
143
+
144
+ async getHistory(orgSlug: string, projectSlug: string) {
145
+ const project = await this.getProject(orgSlug, projectSlug);
146
+
147
+ return this.drizzle.db
148
+ .select()
149
+ .from(queryHistory)
150
+ .where(eq(queryHistory.projectId, project.id))
151
+ .orderBy(desc(queryHistory.createdAt))
152
+ .limit(50);
153
+ }
154
+ }
@@ -0,0 +1,140 @@
1
+ import {
2
+ All,
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ Next,
8
+ Param,
9
+ Post,
10
+ Req,
11
+ Res,
12
+ UseGuards,
13
+ } from '@nestjs/common';
14
+ import type { NextFunction, Request, Response } from 'express';
15
+ import { IsNumber, IsString } from 'class-validator';
16
+ import { PROJECT_KEY_ROLES } from '@apiDatabase/constants';
17
+ import { createRouteHandler } from 'uploadthing/express';
18
+ import { StorageService } from './storage.service';
19
+ import { storageRouter } from './uploadthing';
20
+ import {
21
+ ProjectKeyGuard,
22
+ type ProjectKeyPayload,
23
+ } from '../project-api/project-key.guard';
24
+ import { ForbiddenException } from '@nestjs/common';
25
+
26
+ class SaveObjectDto {
27
+ @IsString()
28
+ name!: string;
29
+
30
+ @IsNumber()
31
+ size!: number;
32
+
33
+ @IsString()
34
+ type!: string;
35
+
36
+ @IsString()
37
+ utKey!: string;
38
+
39
+ @IsString()
40
+ url!: string;
41
+ }
42
+
43
+ const utHandler = createRouteHandler({ router: storageRouter });
44
+
45
+ @Controller('projects/:projectSlug/storage')
46
+ @UseGuards(ProjectKeyGuard)
47
+ export class ProjectStorageController {
48
+ constructor(private storageService: StorageService) {}
49
+
50
+ private getProjectKey(req: Request): ProjectKeyPayload {
51
+ return req['projectKey'] as ProjectKeyPayload;
52
+ }
53
+
54
+ private assertWriteAccess(req: Request): void {
55
+ const { role } = this.getProjectKey(req);
56
+ if (role !== PROJECT_KEY_ROLES.SERVICE_ROLE) {
57
+ throw new ForbiddenException(
58
+ 'Write operations require the service role key',
59
+ );
60
+ }
61
+ }
62
+
63
+ @Get('buckets/:bucketName/objects')
64
+ async getObjects(
65
+ @Req() req: Request,
66
+ @Param('projectSlug') projectSlug: string,
67
+ @Param('bucketName') bucketName: string,
68
+ ) {
69
+ const { projectId } = this.getProjectKey(req);
70
+ await this.storageService.assertProjectSlug(projectId, projectSlug);
71
+ const bucket = await this.storageService.getBucketByName(
72
+ projectId,
73
+ bucketName,
74
+ );
75
+ return this.storageService.getObjects(bucket.id);
76
+ }
77
+
78
+ @All('buckets/:bucketName/upload')
79
+ async handleUpload(
80
+ @Req() req: Request,
81
+ @Res() res: Response,
82
+ @Next() next: NextFunction,
83
+ @Param('projectSlug') projectSlug: string,
84
+ @Param('bucketName') bucketName: string,
85
+ ): Promise<void> {
86
+ this.assertWriteAccess(req);
87
+ const { projectId } = this.getProjectKey(req);
88
+ await this.storageService.assertProjectSlug(projectId, projectSlug);
89
+ await this.storageService.getBucketByName(projectId, bucketName);
90
+
91
+ const originalUrl = req.url;
92
+ const queryIndex = originalUrl.indexOf('?');
93
+ const query = queryIndex >= 0 ? originalUrl.slice(queryIndex) : '';
94
+ req.url = `/${query}`;
95
+ utHandler(req, res, (err?: unknown) => {
96
+ req.url = originalUrl;
97
+ if (err) next(err);
98
+ });
99
+ }
100
+
101
+ @Post('buckets/:bucketName/objects')
102
+ async saveObject(
103
+ @Req() req: Request,
104
+ @Param('projectSlug') projectSlug: string,
105
+ @Param('bucketName') bucketName: string,
106
+ @Body() file: SaveObjectDto,
107
+ ) {
108
+ this.assertWriteAccess(req);
109
+ const { projectId } = this.getProjectKey(req);
110
+ await this.storageService.assertProjectSlug(projectId, projectSlug);
111
+ const bucket = await this.storageService.getBucketByName(
112
+ projectId,
113
+ bucketName,
114
+ );
115
+ return this.storageService.saveObject(bucket.id, file);
116
+ }
117
+
118
+ @Delete('objects/:objectId')
119
+ async deleteObject(
120
+ @Req() req: Request,
121
+ @Param('projectSlug') projectSlug: string,
122
+ @Param('objectId') objectId: string,
123
+ ) {
124
+ this.assertWriteAccess(req);
125
+ const { projectId } = this.getProjectKey(req);
126
+ await this.storageService.assertProjectSlug(projectId, projectSlug);
127
+ return this.storageService.deleteObject(objectId);
128
+ }
129
+
130
+ @Get('objects/:objectId/signed-url')
131
+ async getSignedUrl(
132
+ @Req() req: Request,
133
+ @Param('projectSlug') projectSlug: string,
134
+ @Param('objectId') objectId: string,
135
+ ) {
136
+ const { projectId } = this.getProjectKey(req);
137
+ await this.storageService.assertProjectSlug(projectId, projectSlug);
138
+ return this.storageService.getSignedUrl(objectId);
139
+ }
140
+ }
@@ -0,0 +1,118 @@
1
+ import {
2
+ All,
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ Get,
7
+ Next,
8
+ Param,
9
+ Post,
10
+ Req,
11
+ Res,
12
+ UseGuards,
13
+ } from '@nestjs/common';
14
+ import type { NextFunction, Request, Response } from 'express';
15
+ import { createRouteHandler } from 'uploadthing/express';
16
+ import { IsNumber, IsEnum, IsString } from 'class-validator';
17
+ import { StorageService } from './storage.service';
18
+ import { storageRouter } from './uploadthing';
19
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
20
+ import { OrgRoleGuard } from '../auth/guards/org-role.guards';
21
+ import type { BucketAccess } from '@apiDatabase/types';
22
+
23
+ class CreateBucketDto {
24
+ @IsString()
25
+ name!: string;
26
+
27
+ @IsEnum(['public', 'private'])
28
+ access!: BucketAccess;
29
+ }
30
+
31
+ class SaveObjectDto {
32
+ @IsString()
33
+ name!: string;
34
+
35
+ @IsNumber()
36
+ size!: number;
37
+
38
+ @IsString()
39
+ type!: string;
40
+
41
+ @IsString()
42
+ utKey!: string;
43
+
44
+ @IsString()
45
+ url!: string;
46
+ }
47
+
48
+ const utHandler = createRouteHandler({ router: storageRouter });
49
+
50
+ @Controller('orgs/:slug/projects/:projectSlug/storage')
51
+ @UseGuards(JwtAuthGuard, OrgRoleGuard)
52
+ export class StorageController {
53
+ constructor(private storageService: StorageService) {}
54
+
55
+ @Get('buckets')
56
+ getBuckets(
57
+ @Param('slug') slug: string,
58
+ @Param('projectSlug') projectSlug: string,
59
+ ) {
60
+ return this.storageService.getBuckets(slug, projectSlug);
61
+ }
62
+
63
+ @Post('buckets')
64
+ createBucket(
65
+ @Param('slug') slug: string,
66
+ @Param('projectSlug') projectSlug: string,
67
+ @Body() dto: CreateBucketDto,
68
+ ) {
69
+ return this.storageService.createBucket(
70
+ slug,
71
+ projectSlug,
72
+ dto.name,
73
+ dto.access,
74
+ );
75
+ }
76
+
77
+ @Delete('buckets/:bucketId')
78
+ deleteBucket(@Param('bucketId') bucketId: string) {
79
+ return this.storageService.deleteBucket(bucketId);
80
+ }
81
+
82
+ @Get('buckets/:bucketId/objects')
83
+ getObjects(@Param('bucketId') bucketId: string) {
84
+ return this.storageService.getObjects(bucketId);
85
+ }
86
+
87
+ // UploadThing upload endpoint — proxies the UT protocol (GET + POST)
88
+ @All('buckets/:bucketId/upload')
89
+ handleUpload(
90
+ @Req() req: Request,
91
+ @Res() res: Response,
92
+ @Next() next: NextFunction,
93
+ ): void {
94
+ const originalUrl = req.url;
95
+ const queryIndex = originalUrl.indexOf('?');
96
+ const query = queryIndex >= 0 ? originalUrl.slice(queryIndex) : '';
97
+ req.url = `/${query}`;
98
+ utHandler(req, res, (err?: unknown) => {
99
+ req.url = originalUrl;
100
+ if (err) next(err);
101
+ });
102
+ }
103
+
104
+ @Post('buckets/:bucketId/objects')
105
+ saveObject(@Param('bucketId') bucketId: string, @Body() file: SaveObjectDto) {
106
+ return this.storageService.saveObject(bucketId, file);
107
+ }
108
+
109
+ @Delete('objects/:objectId')
110
+ deleteObject(@Param('objectId') objectId: string) {
111
+ return this.storageService.deleteObject(objectId);
112
+ }
113
+
114
+ @Get('objects/:objectId/signed-url')
115
+ getSignedUrl(@Param('objectId') objectId: string) {
116
+ return this.storageService.getSignedUrl(objectId);
117
+ }
118
+ }
@@ -0,0 +1,15 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { StorageService } from './storage.service';
3
+ import { StorageController } from './storage.controller';
4
+ import { AuthModule } from '../auth/auth.module';
5
+ import { ProjectStorageController } from './project-storage.controller';
6
+ import { JwtModule } from '@nestjs/jwt';
7
+ import { ProjectKeyGuard } from 'src/project-api/project-key.guard';
8
+
9
+ @Module({
10
+ imports: [AuthModule, JwtModule.register({})],
11
+
12
+ providers: [StorageService, ProjectKeyGuard],
13
+ controllers: [StorageController, ProjectStorageController],
14
+ })
15
+ export class StorageModule {}
@@ -0,0 +1,191 @@
1
+ import {
2
+ BadRequestException,
3
+ ForbiddenException,
4
+ Injectable,
5
+ NotFoundException,
6
+ } from '@nestjs/common';
7
+ import { and, eq } from 'drizzle-orm';
8
+ import { UTApi } from 'uploadthing/server';
9
+ import { DrizzleService } from '../db/drizzle.service';
10
+ import {
11
+ projects,
12
+ organizations,
13
+ storageBuckets,
14
+ storageObjects,
15
+ } from '../db/schema';
16
+ import type { BucketAccess } from '@apiDatabase/types';
17
+
18
+ @Injectable()
19
+ export class StorageService {
20
+ private utapi = new UTApi();
21
+
22
+ constructor(private drizzle: DrizzleService) {}
23
+
24
+ private async getProject(orgSlug: string, projectSlug: string) {
25
+ const [row] = await this.drizzle.db
26
+ .select({ id: projects.id })
27
+ .from(projects)
28
+ .innerJoin(organizations, eq(projects.orgId, organizations.id))
29
+ .where(
30
+ and(eq(organizations.slug, orgSlug), eq(projects.slug, projectSlug)),
31
+ )
32
+ .limit(1);
33
+
34
+ if (!row) throw new NotFoundException('Project not found');
35
+ return row;
36
+ }
37
+
38
+ async getBuckets(orgSlug: string, projectSlug: string) {
39
+ const project = await this.getProject(orgSlug, projectSlug);
40
+ return this.drizzle.db
41
+ .select()
42
+ .from(storageBuckets)
43
+ .where(eq(storageBuckets.projectId, project.id));
44
+ }
45
+
46
+ async createBucket(
47
+ orgSlug: string,
48
+ projectSlug: string,
49
+ name: string,
50
+ access: BucketAccess,
51
+ ) {
52
+ const project = await this.getProject(orgSlug, projectSlug);
53
+
54
+ const trimmed = name.trim();
55
+ if (!trimmed) {
56
+ throw new BadRequestException('Bucket name is required');
57
+ }
58
+
59
+ const [bucket] = await this.drizzle.db
60
+ .insert(storageBuckets)
61
+ .values({ projectId: project.id, name: trimmed, access })
62
+ .returning();
63
+
64
+ return bucket;
65
+ }
66
+
67
+ async deleteBucket(bucketId: string) {
68
+ const objects = await this.drizzle.db
69
+ .select({ utKey: storageObjects.utKey })
70
+ .from(storageObjects)
71
+ .where(eq(storageObjects.bucketId, bucketId));
72
+
73
+ if (objects.length > 0) {
74
+ await this.utapi.deleteFiles(objects.map((o) => o.utKey));
75
+ }
76
+
77
+ await this.drizzle.db
78
+ .delete(storageBuckets)
79
+ .where(eq(storageBuckets.id, bucketId));
80
+
81
+ return { message: 'Bucket deleted' };
82
+ }
83
+
84
+ async getObjects(bucketId: string) {
85
+ return this.drizzle.db
86
+ .select()
87
+ .from(storageObjects)
88
+ .where(eq(storageObjects.bucketId, bucketId));
89
+ }
90
+
91
+ async saveObject(
92
+ bucketId: string,
93
+ file: {
94
+ name: string;
95
+ size: number;
96
+ type: string;
97
+ utKey: string;
98
+ url: string;
99
+ },
100
+ ) {
101
+ const [bucket] = await this.drizzle.db
102
+ .select()
103
+ .from(storageBuckets)
104
+ .where(eq(storageBuckets.id, bucketId))
105
+ .limit(1);
106
+
107
+ if (!bucket) throw new NotFoundException('Bucket not found');
108
+
109
+ const url = bucket.access === 'public' ? file.url : '';
110
+
111
+ const [object] = await this.drizzle.db
112
+ .insert(storageObjects)
113
+ .values({
114
+ bucketId,
115
+ name: file.name,
116
+ size: file.size,
117
+ mimeType: file.type,
118
+ utKey: file.utKey,
119
+ url,
120
+ })
121
+ .returning();
122
+
123
+ return object;
124
+ }
125
+
126
+ async deleteObject(objectId: string) {
127
+ const [object] = await this.drizzle.db
128
+ .select()
129
+ .from(storageObjects)
130
+ .where(eq(storageObjects.id, objectId))
131
+ .limit(1);
132
+
133
+ if (!object) throw new NotFoundException('File not found');
134
+
135
+ await this.utapi.deleteFiles(object.utKey);
136
+
137
+ await this.drizzle.db
138
+ .delete(storageObjects)
139
+ .where(eq(storageObjects.id, objectId));
140
+
141
+ return { message: 'File deleted' };
142
+ }
143
+
144
+ async getSignedUrl(objectId: string) {
145
+ const [object] = await this.drizzle.db
146
+ .select()
147
+ .from(storageObjects)
148
+ .where(eq(storageObjects.id, objectId))
149
+ .limit(1);
150
+
151
+ if (!object) throw new NotFoundException('File not found');
152
+
153
+ const { ufsUrl } = await this.utapi.generateSignedURL(object.utKey, {
154
+ expiresIn: 3600,
155
+ });
156
+
157
+ return { url: ufsUrl };
158
+ }
159
+ async assertProjectSlug(
160
+ projectId: string,
161
+ projectSlug: string,
162
+ ): Promise<void> {
163
+ const [project] = await this.drizzle.db
164
+ .select({ slug: projects.slug })
165
+ .from(projects)
166
+ .where(eq(projects.id, projectId))
167
+ .limit(1);
168
+
169
+ if (!project) throw new NotFoundException('Project not found');
170
+
171
+ if (project.slug !== projectSlug) {
172
+ throw new ForbiddenException('API key does not match this project URL');
173
+ }
174
+ }
175
+
176
+ async getBucketByName(projectId: string, bucketName: string) {
177
+ const [bucket] = await this.drizzle.db
178
+ .select()
179
+ .from(storageBuckets)
180
+ .where(
181
+ and(
182
+ eq(storageBuckets.projectId, projectId),
183
+ eq(storageBuckets.name, bucketName),
184
+ ),
185
+ )
186
+ .limit(1);
187
+
188
+ if (!bucket) throw new NotFoundException('Bucket not found');
189
+ return bucket;
190
+ }
191
+ }
@@ -0,0 +1,23 @@
1
+ import { createUploadthing, type FileRouter } from 'uploadthing/express';
2
+ import { FileRoute } from 'uploadthing/types';
3
+
4
+ const f = createUploadthing();
5
+
6
+ export const storageRouter: {
7
+ bucketUploader: FileRoute<{
8
+ input: undefined;
9
+ output: null;
10
+ errorShape: any;
11
+ }>;
12
+ } = {
13
+ bucketUploader: f({
14
+ blob: {
15
+ maxFileSize: '512MB',
16
+ maxFileCount: 10,
17
+ },
18
+ }).onUploadComplete((data) => {
19
+ console.log('UploadThing upload complete:', data.file.name);
20
+ }),
21
+ } satisfies FileRouter;
22
+
23
+ export type OurStorageRouter = typeof storageRouter;
@@ -0,0 +1,15 @@
1
+ import { IsIn, IsOptional, IsString } from 'class-validator';
2
+ import { COLUMN_TYPES } from '@apiDatabase/constants';
3
+ import type { ColumnType } from '@apiDatabase/types';
4
+
5
+ export class AddColumnDto {
6
+ @IsString()
7
+ name!: string;
8
+
9
+ @IsIn(COLUMN_TYPES)
10
+ type!: ColumnType;
11
+
12
+ @IsOptional()
13
+ @IsString()
14
+ defaultValue?: string;
15
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ IsArray,
3
+ IsBoolean,
4
+ IsIn,
5
+ IsOptional,
6
+ IsString,
7
+ MinLength,
8
+ ValidateNested,
9
+ } from 'class-validator';
10
+ import { Type } from 'class-transformer';
11
+ import { COLUMN_TYPES } from '@apiDatabase/constants';
12
+ import type {
13
+ CreateTableInput,
14
+ CreateColumnInput,
15
+ ColumnType,
16
+ } from '@apiDatabase/types';
17
+
18
+ export class CreateColumnDto implements CreateColumnInput {
19
+ @IsString()
20
+ @MinLength(1)
21
+ name!: string;
22
+
23
+ @IsIn(COLUMN_TYPES)
24
+ type!: ColumnType;
25
+
26
+ @IsBoolean()
27
+ isNullable!: boolean;
28
+
29
+ @IsBoolean()
30
+ isPrimaryKey!: boolean;
31
+
32
+ @IsString()
33
+ @IsOptional()
34
+ defaultValue?: string;
35
+
36
+ @IsString()
37
+ @IsOptional()
38
+ foreignKeyTable?: string;
39
+
40
+ @IsString()
41
+ @IsOptional()
42
+ foreignKeyColumn?: string;
43
+ }
44
+
45
+ export class CreateTableDto implements CreateTableInput {
46
+ @IsString()
47
+ @MinLength(1)
48
+ name!: string;
49
+
50
+ @IsArray()
51
+ @ValidateNested({ each: true })
52
+ @Type(() => CreateColumnDto)
53
+ columns!: CreateColumnDto[];
54
+ }