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,40 @@
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 { COOKIE_KEYS } from '@apiDatabase/constants';
11
+ import type { JwtPayload } from '@apiDatabase/types';
12
+
13
+ @Injectable()
14
+ export class JwtAuthGuard implements CanActivate {
15
+ constructor(
16
+ private jwtService: JwtService,
17
+ private configService: ConfigService,
18
+ ) {}
19
+
20
+ canActivate(context: ExecutionContext): boolean {
21
+ const request = context.switchToHttp().getRequest<Request>();
22
+
23
+ const token = request.cookies?.[COOKIE_KEYS.ACCESS_TOKEN] as
24
+ string | undefined;
25
+
26
+ if (!token) {
27
+ throw new UnauthorizedException('Not authenticated');
28
+ }
29
+
30
+ try {
31
+ const payload = this.jwtService.verify<JwtPayload>(token, {
32
+ secret: this.configService.get<string>('JWT_ACCESS_SECRET'),
33
+ });
34
+ request['user'] = payload;
35
+ return true;
36
+ } catch {
37
+ throw new UnauthorizedException('Invalid or expired token');
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ CanActivate,
3
+ ExecutionContext,
4
+ ForbiddenException,
5
+ Injectable,
6
+ NotFoundException,
7
+ } from '@nestjs/common';
8
+ import { Reflector } from '@nestjs/core';
9
+ import { and, eq, isNull } from 'drizzle-orm';
10
+ import { DrizzleService } from '../../db/drizzle.service';
11
+ import { organizations, orgMembers } from '../../db/schema';
12
+ import { ORG_ROLE_KEY } from '../decorators/require-org-role-decorator';
13
+ import type { OrgRole, JwtPayload } from '@apiDatabase/types';
14
+ import { Request } from 'express';
15
+
16
+ interface AuthenticatedRequest extends Request {
17
+ user: JwtPayload;
18
+ memberRole?: string;
19
+ }
20
+
21
+ @Injectable()
22
+ export class OrgRoleGuard implements CanActivate {
23
+ constructor(
24
+ private drizzle: DrizzleService,
25
+ private reflector: Reflector,
26
+ ) {}
27
+
28
+ async canActivate(context: ExecutionContext): Promise<boolean> {
29
+ const requiredRole = this.reflector.get<OrgRole>(
30
+ ORG_ROLE_KEY,
31
+ context.getHandler(),
32
+ );
33
+
34
+ const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
35
+ const user = request.user;
36
+ const orgSlug = request.params['slug'] as string;
37
+
38
+ const [member] = await this.drizzle.db
39
+ .select({ role: orgMembers.role })
40
+ .from(orgMembers)
41
+ .innerJoin(organizations, eq(orgMembers.orgId, organizations.id))
42
+ .where(
43
+ and(
44
+ eq(organizations.slug, orgSlug),
45
+ eq(orgMembers.userId, user.sub),
46
+ isNull(orgMembers.removedAt),
47
+ ),
48
+ )
49
+ .limit(1);
50
+
51
+ if (!member) throw new NotFoundException('Organization not found');
52
+
53
+ if (requiredRole && member.role !== requiredRole) {
54
+ throw new ForbiddenException('Insufficient permissions');
55
+ }
56
+
57
+ request.memberRole = member.role;
58
+ return true;
59
+ }
60
+ }
@@ -0,0 +1,9 @@
1
+ import { Module, Global } from '@nestjs/common';
2
+ import { DrizzleService } from './drizzle.service';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [DrizzleService],
7
+ exports: [DrizzleService],
8
+ })
9
+ export class DbModule {}
@@ -0,0 +1,16 @@
1
+ import { Injectable, OnModuleInit } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { drizzle } from 'drizzle-orm/neon-http';
4
+ import * as schema from './schema';
5
+
6
+ @Injectable()
7
+ export class DrizzleService implements OnModuleInit {
8
+ public db!: ReturnType<typeof drizzle<typeof schema>>;
9
+
10
+ constructor(private config: ConfigService) {}
11
+
12
+ onModuleInit() {
13
+ const databaseUrl = this.config.get<string>('DATABASE_URL')!;
14
+ this.db = drizzle(databaseUrl, { schema });
15
+ }
16
+ }
@@ -0,0 +1,6 @@
1
+ export * from './users';
2
+ export * from './organizations';
3
+ export * from './projects';
4
+ export * from './query-history';
5
+ export * from './storage-buckets';
6
+ export * from './storage-objects';
@@ -0,0 +1,30 @@
1
+ import { pgEnum, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+ import { users } from './users';
3
+
4
+ export const orgRoleEnum = pgEnum('org_role', ['admin', 'developer']);
5
+
6
+ export const organizations = pgTable('organizations', {
7
+ id: uuid().defaultRandom().primaryKey(),
8
+ name: text('name').notNull(),
9
+ slug: text('slug').notNull().unique(),
10
+ createdAt: timestamp('created_at').notNull().defaultNow(),
11
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
12
+ });
13
+
14
+ export const orgMembers = pgTable('org_members', {
15
+ id: uuid().defaultRandom().primaryKey(),
16
+ orgId: uuid('org_id')
17
+ .notNull()
18
+ .references(() => organizations.id, { onDelete: 'cascade' }),
19
+ userId: uuid('user_id')
20
+ .notNull()
21
+ .references(() => users.id, { onDelete: 'cascade' }),
22
+ role: orgRoleEnum('role').notNull().default('developer'),
23
+ createdAt: timestamp('created_at').notNull().defaultNow(),
24
+ removedAt: timestamp('removed_at'),
25
+ });
26
+
27
+ export type Organization = typeof organizations.$inferSelect;
28
+ export type NewOrganization = typeof organizations.$inferInsert;
29
+ export type OrgMember = typeof orgMembers.$inferSelect;
30
+ export type NewOrgMember = typeof orgMembers.$inferInsert;
@@ -0,0 +1,26 @@
1
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+ import { organizations } from './organizations';
3
+
4
+ export const projects = pgTable('projects', {
5
+ id: uuid().defaultRandom().primaryKey(),
6
+ orgId: uuid('org_id')
7
+ .notNull()
8
+ .references(() => organizations.id, { onDelete: 'cascade' }),
9
+ name: text('name').notNull(),
10
+ slug: text('slug').notNull().unique(),
11
+ dbSchema: text('db_schema').notNull().unique(),
12
+ projectUrl: text('project_url').notNull().unique(),
13
+ anonKey: text('anon_key').notNull(),
14
+ serviceRoleKey: text('service_role_key').notNull(),
15
+ googleClientId: text('google_client_id'),
16
+ googleClientSecret: text('google_client_secret'),
17
+ githubClientId: text('github_client_id'),
18
+ githubClientSecret: text('github_client_secret'),
19
+ authJwtSecret: text('auth_jwt_secret').notNull(),
20
+ siteUrl: text('site_url'),
21
+ createdAt: timestamp('created_at').notNull().defaultNow(),
22
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
23
+ });
24
+
25
+ export type Project = typeof projects.$inferSelect;
26
+ export type NewProject = typeof projects.$inferInsert;
@@ -0,0 +1,16 @@
1
+ import { pgTable, text, timestamp, uuid, integer } from 'drizzle-orm/pg-core';
2
+ import { projects } from './projects';
3
+
4
+ export const queryHistory = pgTable('query_history', {
5
+ id: uuid().defaultRandom().primaryKey(),
6
+ projectId: uuid('project_id')
7
+ .notNull()
8
+ .references(() => projects.id, { onDelete: 'cascade' }),
9
+ sql: text('sql').notNull(),
10
+ executionTimeMs: integer('execution_time_ms').notNull(),
11
+ rowCount: integer('row_count').notNull(),
12
+ createdAt: timestamp('created_at').notNull().defaultNow(),
13
+ });
14
+
15
+ export type QueryHistoryItem = typeof queryHistory.$inferSelect;
16
+ export type NewQueryHistoryItem = typeof queryHistory.$inferInsert;
@@ -0,0 +1,17 @@
1
+ import { pgEnum, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+ import { projects } from './projects';
3
+
4
+ export const bucketAccessEnum = pgEnum('bucket_access', ['public', 'private']);
5
+
6
+ export const storageBuckets = pgTable('storage_buckets', {
7
+ id: uuid().defaultRandom().primaryKey(),
8
+ projectId: uuid('project_id')
9
+ .notNull()
10
+ .references(() => projects.id, { onDelete: 'cascade' }),
11
+ name: text('name').notNull(),
12
+ access: bucketAccessEnum('access').notNull().default('public'),
13
+ createdAt: timestamp('created_at').notNull().defaultNow(),
14
+ });
15
+
16
+ export type StorageBucket = typeof storageBuckets.$inferSelect;
17
+ export type NewStorageBucket = typeof storageBuckets.$inferInsert;
@@ -0,0 +1,18 @@
1
+ import { integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+ import { storageBuckets } from './storage-buckets';
3
+
4
+ export const storageObjects = pgTable('storage_objects', {
5
+ id: uuid().defaultRandom().primaryKey(),
6
+ bucketId: uuid('bucket_id')
7
+ .notNull()
8
+ .references(() => storageBuckets.id, { onDelete: 'cascade' }),
9
+ name: text('name').notNull(),
10
+ size: integer('size').notNull(),
11
+ mimeType: text('mime_type').notNull(),
12
+ utKey: text('ut_key').notNull(),
13
+ url: text('url').notNull().default(''),
14
+ createdAt: timestamp('created_at').notNull().defaultNow(),
15
+ });
16
+
17
+ export type StorageObject = typeof storageObjects.$inferSelect;
18
+ export type NewStorageObject = typeof storageObjects.$inferInsert;
@@ -0,0 +1,14 @@
1
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
2
+
3
+ export const users = pgTable('users', {
4
+ id: uuid().defaultRandom().primaryKey(),
5
+ email: text('email').notNull().unique(),
6
+ name: text('name'),
7
+ avatarUrl: text('avatar_url'),
8
+ passwordHash: text('password_hash'),
9
+ createdAt: timestamp('created_at').notNull().defaultNow(),
10
+ updatedAt: timestamp('updated_at').notNull().defaultNow(),
11
+ });
12
+
13
+ export type User = typeof users.$inferSelect;
14
+ export type NewUser = typeof users.$inferInsert;
@@ -0,0 +1,51 @@
1
+ import { NestFactory } from '@nestjs/core';
2
+ import { AppModule } from './app.module';
3
+ import { ValidationPipe } from '@nestjs/common';
4
+ import cookieParser from 'cookie-parser';
5
+ import { IoAdapter } from '@nestjs/platform-socket.io';
6
+
7
+ async function bootstrap() {
8
+ const app = await NestFactory.create(AppModule);
9
+
10
+ app.useWebSocketAdapter(new IoAdapter(app));
11
+
12
+ app.use(cookieParser());
13
+
14
+ app.setGlobalPrefix('api'); // /api/....
15
+
16
+ app.useGlobalPipes(
17
+ new ValidationPipe({
18
+ whitelist: true,
19
+ forbidNonWhitelisted: true,
20
+ transform: true,
21
+ }),
22
+ );
23
+
24
+ app.enableCors({
25
+ origin: process.env.WEB_URL ?? 'http://localhost:3001',
26
+ credentials: true,
27
+ });
28
+
29
+ // app.enableCors({
30
+ // origin: (origin, callback) => {
31
+ // // allow server-to-server / curl (no Origin header)
32
+ // if (!origin) return callback(null, true);
33
+
34
+ // const webUrl = process.env.WEB_URL ?? 'http://localhost:3001';
35
+ // if (origin === webUrl) return callback(null, true);
36
+
37
+ // // SDK demo: npx serve . from packages/apiDatabase-js (any localhost port)
38
+ // if (/^http:\/\/localhost(:\d+)?$/.test(origin)) {
39
+ // return callback(null, true);
40
+ // }
41
+
42
+ // callback(new Error(`CORS blocked: ${origin}`));
43
+ // },
44
+ // credentials: true,
45
+ // });
46
+
47
+ const port = process.env.PORT ?? 3001;
48
+ await app.listen(port);
49
+ console.log(`API running on http://localhost:${port}/api`);
50
+ }
51
+ void bootstrap();
@@ -0,0 +1,6 @@
1
+ import { IsEmail } from 'class-validator';
2
+
3
+ export class InviteMemberDto {
4
+ @IsEmail()
5
+ email!: string;
6
+ }
@@ -0,0 +1,8 @@
1
+ import { IsEnum } from 'class-validator';
2
+ import { ORG_ROLES } from '@apiDatabase/constants';
3
+ import type { OrgRole } from '@apiDatabase/types';
4
+
5
+ export class UpdateRoleDto {
6
+ @IsEnum(ORG_ROLES)
7
+ role!: OrgRole;
8
+ }
@@ -0,0 +1,147 @@
1
+ import { Injectable, BadRequestException } from '@nestjs/common';
2
+ import { JwtService } from '@nestjs/jwt';
3
+ import { ConfigService } from '@nestjs/config';
4
+ import { Resend } from 'resend';
5
+ import { and, eq, isNull } from 'drizzle-orm';
6
+ import { DrizzleService } from '../db/drizzle.service';
7
+ import { organizations, orgMembers, users } from '../db/schema';
8
+ import { INVITE_EXPIRES_IN } from '@apiDatabase/constants';
9
+
10
+ interface InvitePayload {
11
+ email: string;
12
+ orgId: string;
13
+ orgName: string;
14
+ }
15
+
16
+ @Injectable()
17
+ export class InviteService {
18
+ private resend: Resend;
19
+
20
+ constructor(
21
+ private drizzle: DrizzleService,
22
+ private jwtService: JwtService,
23
+ private configService: ConfigService,
24
+ ) {
25
+ this.resend = new Resend(this.configService.get('RESEND_API_KEY'));
26
+ }
27
+
28
+ async sendInvite(orgSlug: string, email: string) {
29
+ // get the org
30
+ const [org] = await this.drizzle.db
31
+ .select()
32
+ .from(organizations)
33
+ .where(eq(organizations.slug, orgSlug))
34
+ .limit(1);
35
+
36
+ if (!org) throw new BadRequestException('Organization not found');
37
+
38
+ // check if user is already an active member
39
+ const existingUser = await this.drizzle.db
40
+ .select({ id: users.id })
41
+ .from(users)
42
+ .where(eq(users.email, email))
43
+ .limit(1);
44
+
45
+ if (existingUser.length > 0) {
46
+ const existingMember = await this.drizzle.db
47
+ .select()
48
+ .from(orgMembers)
49
+ .where(
50
+ and(
51
+ eq(orgMembers.orgId, org.id),
52
+ eq(orgMembers.userId, existingUser[0].id),
53
+ isNull(orgMembers.removedAt),
54
+ ),
55
+ )
56
+ .limit(1);
57
+
58
+ if (existingMember.length > 0) {
59
+ throw new BadRequestException(
60
+ 'User is already a member of this organization',
61
+ );
62
+ }
63
+ }
64
+
65
+ // sign invite token — expires in 24h
66
+ const token = this.jwtService.sign(
67
+ { email, orgId: org.id, orgName: org.name } satisfies InvitePayload,
68
+ {
69
+ secret: this.configService.get('INVITE_SECRET'),
70
+ expiresIn: INVITE_EXPIRES_IN,
71
+ },
72
+ );
73
+
74
+ const inviteUrl = `${this.configService.get<string>('API_URL')}/auth/invite/accept?token=${token}`;
75
+
76
+ // send the email via Resend
77
+ await this.resend.emails.send({
78
+ from: 'onboarding@resend.dev',
79
+ to: email,
80
+ subject: `You've been invited to join ${org.name} on Api_Database`,
81
+ html: `
82
+ <div style="font-family: sans-serif; max-width: 480px; margin: 0 auto;">
83
+ <h2>You're invited to join ${org.name}</h2>
84
+ <p>Someone has invited you to collaborate on Api_Database.</p>
85
+ <a
86
+ href="${inviteUrl}"
87
+ style="display: inline-block; background: #000; color: #fff; padding: 12px 24px; border-radius: 8px; text-decoration: none; font-weight: 600; margin: 16px 0;">
88
+ Accept invite
89
+ </a>
90
+ <p style="color: #999; font-size: 13px;">This link expires in 24 hours.</p>
91
+ </div>
92
+ `,
93
+ });
94
+
95
+ return { message: 'Invite sent' };
96
+ }
97
+
98
+ async acceptInvite(token: string) {
99
+ let payload: InvitePayload;
100
+
101
+ try {
102
+ payload = this.jwtService.verify<InvitePayload>(token, {
103
+ secret: this.configService.get('INVITE_SECRET'),
104
+ });
105
+ } catch {
106
+ throw new BadRequestException('Invalid or expired invite link');
107
+ }
108
+
109
+ let [user] = await this.drizzle.db
110
+ .select()
111
+ .from(users)
112
+ .where(eq(users.email, payload.email))
113
+ .limit(1);
114
+
115
+ if (!user) {
116
+ const [newUser] = await this.drizzle.db
117
+ .insert(users)
118
+ .values({ email: payload.email })
119
+ .returning();
120
+ user = newUser;
121
+ }
122
+
123
+ const existing = await this.drizzle.db
124
+ .select()
125
+ .from(orgMembers)
126
+ .where(
127
+ and(
128
+ eq(orgMembers.orgId, payload.orgId),
129
+ eq(orgMembers.userId, user.id),
130
+ isNull(orgMembers.removedAt),
131
+ ),
132
+ )
133
+ .limit(1);
134
+
135
+ if (existing.length > 0) {
136
+ return { message: 'Already a member' };
137
+ }
138
+
139
+ await this.drizzle.db.insert(orgMembers).values({
140
+ orgId: payload.orgId,
141
+ userId: user.id,
142
+ role: 'developer',
143
+ });
144
+
145
+ return { message: 'Invite accepted', email: user.email };
146
+ }
147
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Delete,
5
+ Get,
6
+ Param,
7
+ Patch,
8
+ Post,
9
+ UseGuards,
10
+ } from '@nestjs/common';
11
+ import { MembersService } from './members.service';
12
+ import { InviteService } from './invite.service';
13
+ import { UpdateRoleDto } from './dto/update-role.dto';
14
+ import { InviteMemberDto } from './dto/invite-member-dto';
15
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
16
+ import { OrgRoleGuard } from '../auth/guards/org-role.guards';
17
+ import { RequireOrgRole } from '../auth/decorators/require-org-role-decorator';
18
+
19
+ @Controller('orgs/:slug/members')
20
+ @UseGuards(JwtAuthGuard, OrgRoleGuard)
21
+ export class MembersController {
22
+ constructor(
23
+ private membersService: MembersService,
24
+ private inviteService: InviteService,
25
+ ) {}
26
+
27
+ @Get()
28
+ getMembers(@Param('slug') slug: string) {
29
+ return this.membersService.getMembers(slug);
30
+ }
31
+
32
+ @Patch(':memberId/role')
33
+ @RequireOrgRole('admin')
34
+ updateRole(
35
+ @Param('slug') slug: string,
36
+ @Param('memberId') memberId: string,
37
+ @Body() dto: UpdateRoleDto,
38
+ ) {
39
+ return this.membersService.updateRole(slug, memberId, dto);
40
+ }
41
+
42
+ @Delete(':memberId')
43
+ @RequireOrgRole('admin')
44
+ removeMember(
45
+ @Param('slug') slug: string,
46
+ @Param('memberId') memberId: string,
47
+ ) {
48
+ return this.membersService.removeMember(slug, memberId);
49
+ }
50
+
51
+ @Post('invite')
52
+ @RequireOrgRole('admin')
53
+ sendInvite(@Param('slug') slug: string, @Body() dto: InviteMemberDto) {
54
+ return this.inviteService.sendInvite(slug, dto.email);
55
+ }
56
+ }
@@ -0,0 +1,13 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { JwtModule } from '@nestjs/jwt';
3
+ import { MembersService } from './members.service';
4
+ import { InviteService } from './invite.service';
5
+ import { MembersController } from './members.controller';
6
+
7
+ @Module({
8
+ imports: [JwtModule.register({})],
9
+ providers: [MembersService, InviteService],
10
+ controllers: [MembersController],
11
+ exports: [InviteService],
12
+ })
13
+ export class MembersModule {}
@@ -0,0 +1,85 @@
1
+ import {
2
+ BadRequestException,
3
+ Injectable,
4
+ NotFoundException,
5
+ } from '@nestjs/common';
6
+ import { and, eq, isNull } from 'drizzle-orm';
7
+ import { DrizzleService } from '../db/drizzle.service';
8
+ import { orgMembers, organizations, users } from '../db/schema';
9
+ import { UpdateRoleDto } from './dto/update-role.dto';
10
+
11
+ @Injectable()
12
+ export class MembersService {
13
+ constructor(private drizzle: DrizzleService) {}
14
+
15
+ async getMembers(orgSlug: string) {
16
+ return this.drizzle.db
17
+ .select({
18
+ id: orgMembers.id,
19
+ role: orgMembers.role,
20
+ createdAt: orgMembers.createdAt,
21
+ user: {
22
+ id: users.id,
23
+ name: users.name,
24
+ email: users.email,
25
+ avatarUrl: users.avatarUrl,
26
+ },
27
+ })
28
+ .from(orgMembers)
29
+ .innerJoin(organizations, eq(orgMembers.orgId, organizations.id))
30
+ .innerJoin(users, eq(orgMembers.userId, users.id))
31
+ .where(
32
+ and(eq(organizations.slug, orgSlug), isNull(orgMembers.removedAt)),
33
+ );
34
+ }
35
+
36
+ async updateRole(orgSlug: string, memberId: string, dto: UpdateRoleDto) {
37
+ if (dto.role === 'developer') {
38
+ await this.ensureNotLastAdmin(orgSlug, memberId);
39
+ }
40
+
41
+ const [updated] = await this.drizzle.db
42
+ .update(orgMembers)
43
+ .set({ role: dto.role })
44
+ .where(eq(orgMembers.id, memberId))
45
+ .returning();
46
+
47
+ if (!updated) throw new NotFoundException('Member not found');
48
+ return updated;
49
+ }
50
+
51
+ async removeMember(orgSlug: string, memberId: string) {
52
+ await this.ensureNotLastAdmin(orgSlug, memberId);
53
+
54
+ const [updated] = await this.drizzle.db
55
+ .update(orgMembers)
56
+ .set({ removedAt: new Date() })
57
+ .where(eq(orgMembers.id, memberId))
58
+ .returning();
59
+
60
+ if (!updated) throw new NotFoundException('Member not found');
61
+ return { message: 'Member removed' };
62
+ }
63
+
64
+ private async ensureNotLastAdmin(orgSlug: string, memberId: string) {
65
+ const admins = await this.drizzle.db
66
+ .select({ id: orgMembers.id })
67
+ .from(orgMembers)
68
+ .innerJoin(organizations, eq(orgMembers.orgId, organizations.id))
69
+ .where(
70
+ and(
71
+ eq(organizations.slug, orgSlug),
72
+ eq(orgMembers.role, 'admin'),
73
+ isNull(orgMembers.removedAt),
74
+ ),
75
+ );
76
+
77
+ const targetIsAdmin = admins.some((a) => a.id === memberId);
78
+
79
+ if (targetIsAdmin && admins.length === 1) {
80
+ throw new BadRequestException(
81
+ 'Cannot remove or demote the last admin of an organization',
82
+ );
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,8 @@
1
+ import { IsString, MinLength } from 'class-validator';
2
+ import type { CreateOrgInput } from '@apiDatabase/types';
3
+
4
+ export class CreateOrgDto implements CreateOrgInput {
5
+ @IsString()
6
+ @MinLength(2)
7
+ name!: string;
8
+ }
@@ -0,0 +1,27 @@
1
+ import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
2
+ import { OrgsService } from './orgs.service';
3
+ import { CreateOrgDto } from './dto/create-org.dto';
4
+ import { JwtAuthGuard } from '../auth/guards/jwt-auth-guard';
5
+ import { CurrentUser } from '../auth/decorators/current-user-decorator';
6
+ import type { JwtPayload } from '@apiDatabase/types';
7
+
8
+ @Controller('orgs')
9
+ @UseGuards(JwtAuthGuard)
10
+ export class OrgsController {
11
+ constructor(private orgsService: OrgsService) {}
12
+
13
+ @Get()
14
+ getMyOrgs(@CurrentUser() user: JwtPayload) {
15
+ return this.orgsService.getMyOrgs(user.sub);
16
+ }
17
+
18
+ @Get(':slug')
19
+ getOrgBySlug(@Param('slug') slug: string, @CurrentUser() user: JwtPayload) {
20
+ return this.orgsService.getOrgBySlug(slug, user.sub);
21
+ }
22
+
23
+ @Post()
24
+ createOrg(@Body() dto: CreateOrgDto, @CurrentUser() user: JwtPayload) {
25
+ return this.orgsService.createOrg(dto, user.sub);
26
+ }
27
+ }