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,9 @@
1
+ import { z } from "zod";
2
+ import type { CreateProjectInput } from "@apiDatabase/types";
3
+
4
+ export const createProjectSchema = z.object({
5
+ name: z
6
+ .string()
7
+ .min(2, "Name must be at least 2 characters")
8
+ .max(50, "Name must be at most 50 characters"),
9
+ }) satisfies z.ZodType<CreateProjectInput>;
@@ -0,0 +1,6 @@
1
+ export const PROJECTS_INTENT = {
2
+ CREATE: "CREATE",
3
+ } as const;
4
+
5
+ export type ProjectsIntent =
6
+ (typeof PROJECTS_INTENT)[keyof typeof PROJECTS_INTENT];
@@ -0,0 +1,117 @@
1
+ "use client";
2
+
3
+ import { useActionState, useState } from "react";
4
+ import { Plus } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogContent,
9
+ DialogDescription,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogTrigger,
13
+ } from "@/components/ui/dialog";
14
+ import {
15
+ Field,
16
+ FieldError,
17
+ FieldGroup,
18
+ FieldLabel,
19
+ } from "@/components/ui/field";
20
+ import { Input } from "@/components/ui/input";
21
+ import z from "zod";
22
+ import { createProjectSchema } from "./client.schema";
23
+ import { projectsAction } from "./action";
24
+ import { Controller, useForm } from "react-hook-form";
25
+ import { zodResolver } from "@hookform/resolvers/zod";
26
+ import { PROJECTS_INTENT } from "./constants";
27
+
28
+ interface CreateProjectModalProps {
29
+ slug: string;
30
+ }
31
+
32
+ type CreateProjectValues = z.infer<typeof createProjectSchema>;
33
+
34
+ export function CreateProjectModal({ slug }: CreateProjectModalProps) {
35
+ const [open, setOpen] = useState(false);
36
+ const boundAction = projectsAction.bind(null, { slug });
37
+ const [state, formAction, isPending] = useActionState(boundAction, {});
38
+
39
+ const form = useForm<CreateProjectValues>({
40
+ resolver: zodResolver(createProjectSchema),
41
+ defaultValues: { name: "" },
42
+ });
43
+
44
+ // close modal on success
45
+ if (state.success && open) {
46
+ setOpen(false);
47
+ form.reset();
48
+ }
49
+
50
+ return (
51
+ <Dialog open={open} onOpenChange={setOpen}>
52
+ <DialogTrigger asChild>
53
+ <Button size="sm">
54
+ <Plus size={14} className="mr-1.5" />
55
+ New project
56
+ </Button>
57
+ </DialogTrigger>
58
+ <DialogContent>
59
+ <DialogHeader>
60
+ <DialogTitle>Create a new project</DialogTitle>
61
+ <DialogDescription>
62
+ Each project gets its own database schema, URL, and API keys.
63
+ </DialogDescription>
64
+ </DialogHeader>
65
+
66
+ <form
67
+ action={formAction}
68
+ noValidate
69
+ onSubmitCapture={async (e) => {
70
+ const ok = await form.trigger(undefined, { shouldFocus: true });
71
+ if (!ok) e.preventDefault();
72
+ }}
73
+ className="space-y-4"
74
+ >
75
+ <FieldGroup>
76
+ <Controller
77
+ name="name"
78
+ control={form.control}
79
+ render={({ field, fieldState }) => (
80
+ <Field data-invalid={fieldState.invalid}>
81
+ <FieldLabel htmlFor="project-name">Project name</FieldLabel>
82
+ <Input
83
+ {...field}
84
+ id="project-name"
85
+ placeholder="my-project"
86
+ aria-invalid={fieldState.invalid}
87
+ />
88
+ {fieldState.invalid && (
89
+ <FieldError errors={[fieldState.error]} />
90
+ )}
91
+ </Field>
92
+ )}
93
+ />
94
+ </FieldGroup>
95
+
96
+ <div className="flex justify-end gap-2">
97
+ <Button
98
+ type="button"
99
+ variant="outline"
100
+ onClick={() => setOpen(false)}
101
+ >
102
+ Cancel
103
+ </Button>
104
+ <Button
105
+ type="submit"
106
+ name="intent"
107
+ value={PROJECTS_INTENT.CREATE}
108
+ disabled={isPending}
109
+ >
110
+ {isPending ? "Creating..." : "Create project"}
111
+ </Button>
112
+ </div>
113
+ </form>
114
+ </DialogContent>
115
+ </Dialog>
116
+ );
117
+ }
@@ -0,0 +1,38 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { Project } from "@apiDatabase/types";
6
+
7
+ export async function retrieveProjectsFromApi(
8
+ orgSlug: string,
9
+ ): Promise<Project[]> {
10
+ const token = await retrieveTokenFromCookie();
11
+
12
+ try {
13
+ const { data } = await apiClient.get<Project[]>(
14
+ `/orgs/${orgSlug}/projects`,
15
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
16
+ );
17
+ return data;
18
+ } catch {
19
+ redirect("/organizations");
20
+ }
21
+ }
22
+
23
+ export async function retrieveProjectBySlugFromApi(
24
+ orgSlug: string,
25
+ projectSlug: string,
26
+ ): Promise<Project> {
27
+ const token = await retrieveTokenFromCookie();
28
+
29
+ try {
30
+ const { data } = await apiClient.get<Project>(
31
+ `/orgs/${orgSlug}/projects/${projectSlug}`,
32
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
33
+ );
34
+ return data;
35
+ } catch {
36
+ redirect(`/organizations/${orgSlug}/projects`);
37
+ }
38
+ }
@@ -0,0 +1,10 @@
1
+ import { z } from "zod";
2
+ import { PROJECTS_INTENT } from "./constants";
3
+ import { createProjectSchema } from "./client.schema";
4
+
5
+ export const projectsServerSchema = z.discriminatedUnion("intent", [
6
+ z.object({
7
+ intent: z.literal(PROJECTS_INTENT.CREATE),
8
+ ...createProjectSchema.shape,
9
+ }),
10
+ ]);
@@ -0,0 +1,294 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+ import { io, Socket } from "socket.io-client";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Switch } from "@/components/ui/switch";
8
+ import { ScrollArea } from "@/components/ui/scroll-area";
9
+ import { Radio, Trash2, Table2 } from "lucide-react";
10
+ import { cn } from "@/lib/utils";
11
+ import { REALTIME_EVENTS } from "@apiDatabase/constants";
12
+ import type { RealtimeEvent } from "@apiDatabase/types";
13
+
14
+ interface RealtimeClientProps {
15
+ orgSlug: string;
16
+ projectSlug: string;
17
+ projectId: string;
18
+ anonKey: string;
19
+ tables: string[];
20
+ }
21
+
22
+ const EVENT_COLORS: Record<string, string> = {
23
+ INSERT: "bg-green-100 text-green-700",
24
+ UPDATE: "bg-yellow-100 text-yellow-700",
25
+ DELETE: "bg-red-100 text-red-700",
26
+ };
27
+
28
+ function getRealtimeSocketBaseUrl(): string {
29
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
30
+ // WebSocket namespace is NOT under /api
31
+ return apiUrl.replace(/\/api\/?$/, "");
32
+ }
33
+
34
+ export function RealtimeClient({
35
+ orgSlug,
36
+ projectSlug,
37
+ projectId,
38
+ anonKey,
39
+ tables,
40
+ }: RealtimeClientProps) {
41
+ const [connected, setConnected] = useState(false);
42
+ const [subscribedTables, setSubscribedTables] = useState<Set<string>>(
43
+ new Set(),
44
+ );
45
+ const [events, setEvents] = useState<(RealtimeEvent & { id: string })[]>([]);
46
+ const socketRef = useRef<Socket | null>(null);
47
+ const subscribedRef = useRef<Set<string>>(new Set());
48
+
49
+ useEffect(() => {
50
+ subscribedRef.current = subscribedTables;
51
+ }, [subscribedTables]);
52
+
53
+ useEffect(() => {
54
+ const socket = io(`${getRealtimeSocketBaseUrl()}/realtime`, {
55
+ auth: { token: anonKey },
56
+ transports: ["websocket", "polling"],
57
+ });
58
+
59
+ socket.on("connect", () => {
60
+ setConnected(true);
61
+ // re-subscribe after reconnect
62
+ for (const tableName of subscribedRef.current) {
63
+ socket.emit(REALTIME_EVENTS.SUBSCRIBE, tableName);
64
+ }
65
+ });
66
+
67
+ socket.on("disconnect", () => setConnected(false));
68
+
69
+ socket.on(REALTIME_EVENTS.ERROR, (msg: string) => {
70
+ console.error("Realtime error:", msg);
71
+ setConnected(false);
72
+ });
73
+
74
+ socket.on(REALTIME_EVENTS.EVENT, (event: RealtimeEvent) => {
75
+ setEvents((prev) => [
76
+ { ...event, id: crypto.randomUUID() },
77
+ ...prev.slice(0, 99),
78
+ ]);
79
+ });
80
+
81
+ socketRef.current = socket;
82
+
83
+ return () => {
84
+ socket.disconnect();
85
+ };
86
+ }, [anonKey]);
87
+
88
+ const toggleTable = useCallback(
89
+ async (tableName: string, enabled: boolean) => {
90
+ const socket = socketRef.current;
91
+ if (!socket) return;
92
+
93
+ const apiUrl =
94
+ process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api";
95
+
96
+ if (enabled) {
97
+ const res = await fetch(
98
+ `${apiUrl}/orgs/${orgSlug}/projects/${projectSlug}/realtime/${tableName}/enable`,
99
+ { method: "POST", credentials: "include" },
100
+ );
101
+
102
+ if (!res.ok) return;
103
+
104
+ socket.emit(REALTIME_EVENTS.SUBSCRIBE, tableName);
105
+ setSubscribedTables((prev) => new Set([...prev, tableName]));
106
+ } else {
107
+ socket.emit(REALTIME_EVENTS.UNSUBSCRIBE, tableName);
108
+ setSubscribedTables((prev) => {
109
+ const next = new Set(prev);
110
+ next.delete(tableName);
111
+ return next;
112
+ });
113
+
114
+ await fetch(
115
+ `${apiUrl}/orgs/${orgSlug}/projects/${projectSlug}/realtime/${tableName}/disable`,
116
+ { method: "DELETE", credentials: "include" },
117
+ );
118
+ }
119
+ },
120
+ [orgSlug, projectSlug],
121
+ );
122
+
123
+ return (
124
+ <div className="space-y-6">
125
+ <div>
126
+ <h1 className="text-2xl font-medium">Realtime</h1>
127
+ <p className="mt-1 text-sm text-muted-foreground">
128
+ Subscribe to table changes and see them broadcast live
129
+ </p>
130
+ </div>
131
+
132
+ <div className="flex items-center gap-2">
133
+ <div
134
+ className={cn(
135
+ "h-2 w-2 rounded-full",
136
+ connected ? "bg-green-500" : "bg-red-500",
137
+ )}
138
+ />
139
+ <span className="text-sm text-muted-foreground">
140
+ {connected ? "Connected" : "Disconnected"}
141
+ </span>
142
+ <Radio
143
+ size={14}
144
+ className={cn(
145
+ "ml-1",
146
+ connected ? "text-green-500" : "text-muted-foreground",
147
+ )}
148
+ />
149
+ <span className="ml-2 font-mono text-xs text-muted-foreground">
150
+ project:{projectId}
151
+ </span>
152
+ </div>
153
+
154
+ <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
155
+ <div className="space-y-3 lg:col-span-1">
156
+ <h2 className="text-sm font-medium">Tables</h2>
157
+
158
+ {tables.length === 0 ? (
159
+ <p className="text-sm text-muted-foreground">
160
+ No tables yet — create one in the Table Editor
161
+ </p>
162
+ ) : (
163
+ <div className="space-y-2">
164
+ {tables.map((table) => (
165
+ <div
166
+ key={table}
167
+ className="flex items-center justify-between rounded-xl border border-border p-3"
168
+ >
169
+ <div className="flex items-center gap-2">
170
+ <Table2 size={14} className="text-muted-foreground" />
171
+ <span className="font-mono text-sm">{table}</span>
172
+ </div>
173
+ <div className="flex items-center gap-2">
174
+ <span
175
+ className={cn(
176
+ "text-xs font-medium",
177
+ subscribedTables.has(table)
178
+ ? "text-green-600"
179
+ : "text-muted-foreground",
180
+ )}
181
+ >
182
+ {subscribedTables.has(table) ? "Listening" : "Off"}
183
+ </span>
184
+ <Switch
185
+ checked={subscribedTables.has(table)}
186
+ onCheckedChange={(checked) =>
187
+ void toggleTable(table, checked)
188
+ }
189
+ disabled={!connected}
190
+ className="h-6 w-11 shrink-0 border border-border shadow-sm data-[state=checked]:bg-primary data-[state=unchecked]:bg-neutral-300 data-[state=unchecked]:dark:bg-neutral-600 [&_[data-slot=switch-thumb]]:size-5 [&_[data-slot=switch-thumb]]:bg-white [&_[data-slot=switch-thumb]]:shadow"
191
+ />
192
+ </div>
193
+ </div>
194
+ ))}
195
+ </div>
196
+ )}
197
+ </div>
198
+
199
+ <div className="space-y-3 lg:col-span-2">
200
+ <div className="flex items-center justify-between">
201
+ <h2 className="text-sm font-medium">
202
+ Event Feed
203
+ {events.length > 0 && (
204
+ <span className="ml-2 font-normal text-muted-foreground">
205
+ ({events.length})
206
+ </span>
207
+ )}
208
+ </h2>
209
+ {events.length > 0 && (
210
+ <Button
211
+ variant="ghost"
212
+ size="sm"
213
+ className="gap-1.5 text-muted-foreground"
214
+ onClick={() => setEvents([])}
215
+ >
216
+ <Trash2 size={12} />
217
+ Clear
218
+ </Button>
219
+ )}
220
+ </div>
221
+
222
+ {subscribedTables.size === 0 ? (
223
+ <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border py-16 text-center">
224
+ <Radio size={24} className="mb-3 text-muted-foreground" />
225
+ <p className="text-sm text-muted-foreground">
226
+ Toggle a table to start listening
227
+ </p>
228
+ <p className="mt-1 text-xs text-muted-foreground">
229
+ Events will appear here in real time
230
+ </p>
231
+ </div>
232
+ ) : events.length === 0 ? (
233
+ <div className="flex flex-col items-center justify-center rounded-xl border border-dashed border-border py-16 text-center">
234
+ <div className="mb-3 flex gap-1">
235
+ <div
236
+ className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground"
237
+ style={{ animationDelay: "0ms" }}
238
+ />
239
+ <div
240
+ className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground"
241
+ style={{ animationDelay: "150ms" }}
242
+ />
243
+ <div
244
+ className="h-1.5 w-1.5 animate-bounce rounded-full bg-muted-foreground"
245
+ style={{ animationDelay: "300ms" }}
246
+ />
247
+ </div>
248
+ <p className="text-sm text-muted-foreground">
249
+ Listening for changes...
250
+ </p>
251
+ <p className="mt-1 text-xs text-muted-foreground">
252
+ Insert, update, or delete a row to see an event
253
+ </p>
254
+ </div>
255
+ ) : (
256
+ <ScrollArea className="h-[500px] rounded-xl border border-border">
257
+ <div className="divide-y divide-border">
258
+ {events.map((event) => (
259
+ <div key={event.id} className="space-y-2 px-4 py-3">
260
+ <div className="flex items-center gap-2">
261
+ <Badge
262
+ variant="secondary"
263
+ className={cn(
264
+ "font-mono text-xs font-bold",
265
+ EVENT_COLORS[event.type],
266
+ )}
267
+ >
268
+ {event.type}
269
+ </Badge>
270
+ <span className="font-mono text-sm text-muted-foreground">
271
+ {event.table}
272
+ </span>
273
+ <span className="ml-auto text-xs text-muted-foreground">
274
+ {new Date(event.timestamp).toLocaleTimeString()}
275
+ </span>
276
+ </div>
277
+ <pre className="overflow-x-auto rounded-lg bg-muted p-2 font-mono text-xs">
278
+ {JSON.stringify(event.record, null, 2)}
279
+ </pre>
280
+ {event.oldRecord && (
281
+ <pre className="overflow-x-auto rounded-lg bg-muted/50 p-2 font-mono text-xs text-muted-foreground">
282
+ {JSON.stringify(event.oldRecord, null, 2)}
283
+ </pre>
284
+ )}
285
+ </div>
286
+ ))}
287
+ </div>
288
+ </ScrollArea>
289
+ )}
290
+ </div>
291
+ </div>
292
+ </div>
293
+ );
294
+ }
@@ -0,0 +1,40 @@
1
+ import { redirect } from "next/navigation";
2
+ import apiClient from "@/lib/axios";
3
+ import { retrieveTokenFromCookie } from "@/server-utils/utils";
4
+ import { COOKIE_KEYS } from "@apiDatabase/constants";
5
+ import type { Project, ProjectBySlugResponse } from "@apiDatabase/types";
6
+
7
+ export async function retrieveProjectForRealtime(
8
+ orgSlug: string,
9
+ projectSlug: string,
10
+ ): Promise<Pick<Project, "id" | "anonKey">> {
11
+ const token = await retrieveTokenFromCookie();
12
+
13
+ try {
14
+ const { data } = await apiClient.get<ProjectBySlugResponse>(
15
+ `/orgs/${orgSlug}/projects/${projectSlug}`,
16
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
17
+ );
18
+
19
+ return { id: data.projects.id, anonKey: data.projects.anonKey };
20
+ } catch {
21
+ redirect(`/organizations/${orgSlug}/projects`);
22
+ }
23
+ }
24
+
25
+ export async function retrieveTablesForRealtime(
26
+ orgSlug: string,
27
+ projectSlug: string,
28
+ ): Promise<string[]> {
29
+ const token = await retrieveTokenFromCookie();
30
+
31
+ try {
32
+ const { data } = await apiClient.get<string[]>(
33
+ `/orgs/${orgSlug}/projects/${projectSlug}/tables`,
34
+ { headers: { Cookie: `${COOKIE_KEYS.ACCESS_TOKEN}=${token}` } },
35
+ );
36
+ return data;
37
+ } catch {
38
+ return [];
39
+ }
40
+ }