ultra-dex 3.1.0 → 3.2.0

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 (44) hide show
  1. package/README.md +79 -74
  2. package/assets/code-patterns/clerk-middleware.ts +138 -0
  3. package/assets/code-patterns/prisma-schema.prisma +224 -0
  4. package/assets/code-patterns/rls-policies.sql +246 -0
  5. package/assets/code-patterns/server-actions.ts +191 -0
  6. package/assets/code-patterns/trpc-router.ts +258 -0
  7. package/assets/cursor-rules/13-ai-integration.mdc +155 -0
  8. package/assets/cursor-rules/14-server-components.mdc +81 -0
  9. package/assets/cursor-rules/15-server-actions.mdc +102 -0
  10. package/assets/cursor-rules/16-edge-middleware.mdc +105 -0
  11. package/assets/cursor-rules/17-streaming-ssr.mdc +138 -0
  12. package/bin/ultra-dex.js +38 -1
  13. package/lib/commands/agents.js +16 -13
  14. package/lib/commands/banner.js +43 -21
  15. package/lib/commands/build.js +26 -17
  16. package/lib/commands/doctor.js +98 -79
  17. package/lib/commands/generate.js +19 -16
  18. package/lib/commands/init.js +52 -56
  19. package/lib/commands/scaffold.js +151 -0
  20. package/lib/commands/serve.js +15 -13
  21. package/lib/commands/state.js +43 -70
  22. package/lib/commands/swarm.js +31 -9
  23. package/lib/config/theme.js +47 -0
  24. package/lib/templates/code/clerk-middleware.ts +138 -0
  25. package/lib/templates/code/prisma-schema.prisma +224 -0
  26. package/lib/templates/code/rls-policies.sql +246 -0
  27. package/lib/templates/code/server-actions.ts +191 -0
  28. package/lib/templates/code/trpc-router.ts +258 -0
  29. package/lib/themes/doomsday.js +229 -0
  30. package/lib/ui/index.js +5 -0
  31. package/lib/ui/interface.js +241 -0
  32. package/lib/ui/spinners.js +116 -0
  33. package/lib/ui/theme.js +183 -0
  34. package/lib/utils/agents.js +32 -0
  35. package/lib/utils/help.js +64 -0
  36. package/lib/utils/messages.js +35 -0
  37. package/lib/utils/progress.js +24 -0
  38. package/lib/utils/prompts.js +47 -0
  39. package/lib/utils/spinners.js +46 -0
  40. package/lib/utils/status.js +31 -0
  41. package/lib/utils/tables.js +41 -0
  42. package/lib/utils/theme-state.js +9 -0
  43. package/lib/utils/version-display.js +32 -0
  44. package/package.json +10 -1
@@ -0,0 +1,246 @@
1
+ -- Ultra-Dex Production Pattern: Row-Level Security Policies
2
+ -- Run after Prisma migrations to add RLS
3
+
4
+ -- =============================================================================
5
+ -- ENABLE RLS ON TABLES
6
+ -- =============================================================================
7
+
8
+ ALTER TABLE users ENABLE ROW LEVEL SECURITY;
9
+ ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
10
+ ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
11
+ ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
12
+ ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
13
+ ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
14
+ ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
15
+ ALTER TABLE activity_logs ENABLE ROW LEVEL SECURITY;
16
+
17
+ -- =============================================================================
18
+ -- HELPER FUNCTIONS
19
+ -- =============================================================================
20
+
21
+ -- Get current user's clerk ID from session
22
+ CREATE OR REPLACE FUNCTION auth.clerk_id()
23
+ RETURNS TEXT AS $$
24
+ SELECT current_setting('app.clerk_id', true)::TEXT;
25
+ $$ LANGUAGE SQL STABLE;
26
+
27
+ -- Get current user's internal ID
28
+ CREATE OR REPLACE FUNCTION auth.user_id()
29
+ RETURNS TEXT AS $$
30
+ SELECT id FROM users WHERE clerk_id = auth.clerk_id();
31
+ $$ LANGUAGE SQL STABLE;
32
+
33
+ -- Check if user is member of organization
34
+ CREATE OR REPLACE FUNCTION auth.is_org_member(org_id TEXT)
35
+ RETURNS BOOLEAN AS $$
36
+ SELECT EXISTS (
37
+ SELECT 1 FROM organization_members om
38
+ JOIN users u ON u.id = om.user_id
39
+ WHERE om.organization_id = org_id
40
+ AND u.clerk_id = auth.clerk_id()
41
+ );
42
+ $$ LANGUAGE SQL STABLE;
43
+
44
+ -- Check if user is admin of organization
45
+ CREATE OR REPLACE FUNCTION auth.is_org_admin(org_id TEXT)
46
+ RETURNS BOOLEAN AS $$
47
+ SELECT EXISTS (
48
+ SELECT 1 FROM organization_members om
49
+ JOIN users u ON u.id = om.user_id
50
+ WHERE om.organization_id = org_id
51
+ AND u.clerk_id = auth.clerk_id()
52
+ AND om.role IN ('OWNER', 'ADMIN')
53
+ );
54
+ $$ LANGUAGE SQL STABLE;
55
+
56
+ -- =============================================================================
57
+ -- USERS POLICIES
58
+ -- =============================================================================
59
+
60
+ -- Users can read their own data
61
+ CREATE POLICY "users_select_own" ON users
62
+ FOR SELECT
63
+ USING (clerk_id = auth.clerk_id());
64
+
65
+ -- Users can update their own data
66
+ CREATE POLICY "users_update_own" ON users
67
+ FOR UPDATE
68
+ USING (clerk_id = auth.clerk_id());
69
+
70
+ -- Admins can read all users
71
+ CREATE POLICY "users_select_admin" ON users
72
+ FOR SELECT
73
+ USING (
74
+ EXISTS (
75
+ SELECT 1 FROM users u
76
+ WHERE u.clerk_id = auth.clerk_id()
77
+ AND u.role IN ('ADMIN', 'SUPER_ADMIN')
78
+ )
79
+ );
80
+
81
+ -- =============================================================================
82
+ -- ORGANIZATIONS POLICIES
83
+ -- =============================================================================
84
+
85
+ -- Members can view their organizations
86
+ CREATE POLICY "organizations_select_members" ON organizations
87
+ FOR SELECT
88
+ USING (auth.is_org_member(id));
89
+
90
+ -- Only admins can update organizations
91
+ CREATE POLICY "organizations_update_admin" ON organizations
92
+ FOR UPDATE
93
+ USING (auth.is_org_admin(id));
94
+
95
+ -- Only owners can delete organizations
96
+ CREATE POLICY "organizations_delete_owner" ON organizations
97
+ FOR DELETE
98
+ USING (
99
+ EXISTS (
100
+ SELECT 1 FROM organization_members om
101
+ JOIN users u ON u.id = om.user_id
102
+ WHERE om.organization_id = organizations.id
103
+ AND u.clerk_id = auth.clerk_id()
104
+ AND om.role = 'OWNER'
105
+ )
106
+ );
107
+
108
+ -- =============================================================================
109
+ -- ORGANIZATION MEMBERS POLICIES
110
+ -- =============================================================================
111
+
112
+ -- Members can view org membership
113
+ CREATE POLICY "org_members_select" ON organization_members
114
+ FOR SELECT
115
+ USING (auth.is_org_member(organization_id));
116
+
117
+ -- Only admins can add/remove members
118
+ CREATE POLICY "org_members_insert" ON organization_members
119
+ FOR INSERT
120
+ WITH CHECK (auth.is_org_admin(organization_id));
121
+
122
+ CREATE POLICY "org_members_delete" ON organization_members
123
+ FOR DELETE
124
+ USING (auth.is_org_admin(organization_id));
125
+
126
+ -- =============================================================================
127
+ -- PROJECTS POLICIES
128
+ -- =============================================================================
129
+
130
+ -- Members can view projects
131
+ CREATE POLICY "projects_select" ON projects
132
+ FOR SELECT
133
+ USING (auth.is_org_member(organization_id));
134
+
135
+ -- Members (non-viewers) can create projects
136
+ CREATE POLICY "projects_insert" ON projects
137
+ FOR INSERT
138
+ WITH CHECK (
139
+ EXISTS (
140
+ SELECT 1 FROM organization_members om
141
+ JOIN users u ON u.id = om.user_id
142
+ WHERE om.organization_id = projects.organization_id
143
+ AND u.clerk_id = auth.clerk_id()
144
+ AND om.role IN ('OWNER', 'ADMIN', 'MEMBER')
145
+ )
146
+ );
147
+
148
+ -- Members can update projects they own, admins can update any
149
+ CREATE POLICY "projects_update" ON projects
150
+ FOR UPDATE
151
+ USING (
152
+ (owner_id = auth.user_id()) OR auth.is_org_admin(organization_id)
153
+ );
154
+
155
+ -- Only admins can delete projects
156
+ CREATE POLICY "projects_delete" ON projects
157
+ FOR DELETE
158
+ USING (auth.is_org_admin(organization_id));
159
+
160
+ -- =============================================================================
161
+ -- TASKS POLICIES
162
+ -- =============================================================================
163
+
164
+ -- Inherit from project access
165
+ CREATE POLICY "tasks_select" ON tasks
166
+ FOR SELECT
167
+ USING (
168
+ EXISTS (
169
+ SELECT 1 FROM projects p
170
+ WHERE p.id = tasks.project_id
171
+ AND auth.is_org_member(p.organization_id)
172
+ )
173
+ );
174
+
175
+ CREATE POLICY "tasks_insert" ON tasks
176
+ FOR INSERT
177
+ WITH CHECK (
178
+ EXISTS (
179
+ SELECT 1 FROM projects p
180
+ JOIN organization_members om ON om.organization_id = p.organization_id
181
+ JOIN users u ON u.id = om.user_id
182
+ WHERE p.id = tasks.project_id
183
+ AND u.clerk_id = auth.clerk_id()
184
+ AND om.role IN ('OWNER', 'ADMIN', 'MEMBER')
185
+ )
186
+ );
187
+
188
+ CREATE POLICY "tasks_update" ON tasks
189
+ FOR UPDATE
190
+ USING (
191
+ EXISTS (
192
+ SELECT 1 FROM projects p
193
+ WHERE p.id = tasks.project_id
194
+ AND auth.is_org_member(p.organization_id)
195
+ )
196
+ );
197
+
198
+ CREATE POLICY "tasks_delete" ON tasks
199
+ FOR DELETE
200
+ USING (
201
+ EXISTS (
202
+ SELECT 1 FROM projects p
203
+ WHERE p.id = tasks.project_id
204
+ AND auth.is_org_admin(p.organization_id)
205
+ )
206
+ );
207
+
208
+ -- =============================================================================
209
+ -- INVOICES POLICIES
210
+ -- =============================================================================
211
+
212
+ -- Only org admins can view invoices
213
+ CREATE POLICY "invoices_select" ON invoices
214
+ FOR SELECT
215
+ USING (auth.is_org_admin(organization_id));
216
+
217
+ -- =============================================================================
218
+ -- ACTIVITY LOGS POLICIES
219
+ -- =============================================================================
220
+
221
+ -- Users can view their own activity
222
+ CREATE POLICY "activity_logs_select_own" ON activity_logs
223
+ FOR SELECT
224
+ USING (user_id = auth.user_id());
225
+
226
+ -- Admins can view all activity in their orgs
227
+ -- (would need to add org_id to activity_logs for this)
228
+
229
+ -- =============================================================================
230
+ -- USAGE: Set session context before queries
231
+ -- =============================================================================
232
+
233
+ /*
234
+ -- In your API routes or middleware, set the clerk_id:
235
+
236
+ -- For Prisma with raw queries:
237
+ await prisma.$executeRaw`SELECT set_config('app.clerk_id', ${clerkId}, true)`;
238
+
239
+ -- Then all subsequent queries will respect RLS policies
240
+
241
+ -- For Supabase:
242
+ const { data } = await supabase
243
+ .from('projects')
244
+ .select('*')
245
+ // RLS automatically applied based on auth.uid()
246
+ */
@@ -0,0 +1,191 @@
1
+ // Ultra-Dex Production Pattern: Next.js 15 Server Actions
2
+ // Copy this file to your app/actions/ directory
3
+
4
+ 'use server';
5
+
6
+ import { z } from 'zod';
7
+ import { revalidatePath } from 'next/cache';
8
+ import { redirect } from 'next/navigation';
9
+ import { auth } from '@clerk/nextjs/server';
10
+ import { prisma } from '@/lib/prisma';
11
+
12
+ // =============================================================================
13
+ // SCHEMA DEFINITIONS
14
+ // =============================================================================
15
+
16
+ const CreateUserSchema = z.object({
17
+ email: z.string().email('Invalid email address'),
18
+ name: z.string().min(2, 'Name must be at least 2 characters'),
19
+ role: z.enum(['USER', 'ADMIN']).default('USER'),
20
+ });
21
+
22
+ const UpdateUserSchema = z.object({
23
+ id: z.string().uuid(),
24
+ name: z.string().min(2).optional(),
25
+ email: z.string().email().optional(),
26
+ });
27
+
28
+ // =============================================================================
29
+ // ACTION RESPONSE TYPE
30
+ // =============================================================================
31
+
32
+ type ActionResponse<T = void> =
33
+ | { success: true; data: T }
34
+ | { success: false; error: string; fieldErrors?: Record<string, string[]> };
35
+
36
+ // =============================================================================
37
+ // USER ACTIONS
38
+ // =============================================================================
39
+
40
+ export async function createUser(
41
+ prevState: ActionResponse<{ id: string }> | null,
42
+ formData: FormData
43
+ ): Promise<ActionResponse<{ id: string }>> {
44
+ const { userId } = await auth();
45
+
46
+ if (!userId) {
47
+ return { success: false, error: 'Unauthorized' };
48
+ }
49
+
50
+ const rawData = {
51
+ email: formData.get('email'),
52
+ name: formData.get('name'),
53
+ role: formData.get('role') || 'USER',
54
+ };
55
+
56
+ const validated = CreateUserSchema.safeParse(rawData);
57
+
58
+ if (!validated.success) {
59
+ return {
60
+ success: false,
61
+ error: 'Validation failed',
62
+ fieldErrors: validated.error.flatten().fieldErrors,
63
+ };
64
+ }
65
+
66
+ try {
67
+ const user = await prisma.user.create({
68
+ data: validated.data,
69
+ });
70
+
71
+ console.log(JSON.stringify({
72
+ level: 'info',
73
+ event: 'user_created',
74
+ userId: user.id,
75
+ createdBy: userId,
76
+ }));
77
+
78
+ revalidatePath('/users');
79
+ return { success: true, data: { id: user.id } };
80
+ } catch (error) {
81
+ console.error('Failed to create user:', error);
82
+ return { success: false, error: 'Failed to create user' };
83
+ }
84
+ }
85
+
86
+ export async function updateUser(
87
+ prevState: ActionResponse | null,
88
+ formData: FormData
89
+ ): Promise<ActionResponse> {
90
+ const { userId } = await auth();
91
+
92
+ if (!userId) {
93
+ return { success: false, error: 'Unauthorized' };
94
+ }
95
+
96
+ const rawData = {
97
+ id: formData.get('id'),
98
+ name: formData.get('name'),
99
+ email: formData.get('email'),
100
+ };
101
+
102
+ const validated = UpdateUserSchema.safeParse(rawData);
103
+
104
+ if (!validated.success) {
105
+ return {
106
+ success: false,
107
+ error: 'Validation failed',
108
+ fieldErrors: validated.error.flatten().fieldErrors,
109
+ };
110
+ }
111
+
112
+ try {
113
+ await prisma.user.update({
114
+ where: { id: validated.data.id },
115
+ data: {
116
+ ...(validated.data.name && { name: validated.data.name }),
117
+ ...(validated.data.email && { email: validated.data.email }),
118
+ },
119
+ });
120
+
121
+ revalidatePath('/users');
122
+ revalidatePath(`/users/${validated.data.id}`);
123
+ return { success: true, data: undefined };
124
+ } catch (error) {
125
+ console.error('Failed to update user:', error);
126
+ return { success: false, error: 'Failed to update user' };
127
+ }
128
+ }
129
+
130
+ export async function deleteUser(id: string): Promise<ActionResponse> {
131
+ const { userId } = await auth();
132
+
133
+ if (!userId) {
134
+ return { success: false, error: 'Unauthorized' };
135
+ }
136
+
137
+ try {
138
+ await prisma.user.delete({
139
+ where: { id },
140
+ });
141
+
142
+ console.log(JSON.stringify({
143
+ level: 'info',
144
+ event: 'user_deleted',
145
+ deletedUserId: id,
146
+ deletedBy: userId,
147
+ }));
148
+
149
+ revalidatePath('/users');
150
+ return { success: true, data: undefined };
151
+ } catch (error) {
152
+ console.error('Failed to delete user:', error);
153
+ return { success: false, error: 'Failed to delete user' };
154
+ }
155
+ }
156
+
157
+ // =============================================================================
158
+ // USAGE IN COMPONENTS
159
+ // =============================================================================
160
+
161
+ /*
162
+ // In a Client Component:
163
+
164
+ 'use client';
165
+
166
+ import { useActionState } from 'react';
167
+ import { createUser } from '@/app/actions/user';
168
+
169
+ export function CreateUserForm() {
170
+ const [state, formAction, isPending] = useActionState(createUser, null);
171
+
172
+ return (
173
+ <form action={formAction}>
174
+ <input name="name" placeholder="Name" required />
175
+ <input name="email" type="email" placeholder="Email" required />
176
+
177
+ {state?.error && (
178
+ <p className="text-red-500">{state.error}</p>
179
+ )}
180
+
181
+ {state?.fieldErrors?.email && (
182
+ <p className="text-red-500">{state.fieldErrors.email[0]}</p>
183
+ )}
184
+
185
+ <button type="submit" disabled={isPending}>
186
+ {isPending ? 'Creating...' : 'Create User'}
187
+ </button>
188
+ </form>
189
+ );
190
+ }
191
+ */
@@ -0,0 +1,258 @@
1
+ // Ultra-Dex Production Pattern: tRPC Router
2
+ // Copy to server/routers/ directory
3
+
4
+ import { z } from 'zod';
5
+ import { TRPCError } from '@trpc/server';
6
+ import { router, publicProcedure, protectedProcedure, adminProcedure } from '../trpc';
7
+ import { prisma } from '@/lib/prisma';
8
+
9
+ // =============================================================================
10
+ // USER ROUTER
11
+ // =============================================================================
12
+
13
+ export const userRouter = router({
14
+ // Get current user profile
15
+ me: protectedProcedure.query(async ({ ctx }) => {
16
+ const user = await prisma.user.findUnique({
17
+ where: { clerkId: ctx.userId },
18
+ include: {
19
+ organizationMemberships: {
20
+ include: { organization: true },
21
+ },
22
+ },
23
+ });
24
+
25
+ if (!user) {
26
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
27
+ }
28
+
29
+ return user;
30
+ }),
31
+
32
+ // Update current user profile
33
+ update: protectedProcedure
34
+ .input(
35
+ z.object({
36
+ name: z.string().min(2).optional(),
37
+ imageUrl: z.string().url().optional(),
38
+ })
39
+ )
40
+ .mutation(async ({ ctx, input }) => {
41
+ return prisma.user.update({
42
+ where: { clerkId: ctx.userId },
43
+ data: input,
44
+ });
45
+ }),
46
+
47
+ // List all users (admin only)
48
+ list: adminProcedure
49
+ .input(
50
+ z.object({
51
+ limit: z.number().min(1).max(100).default(50),
52
+ cursor: z.string().optional(),
53
+ })
54
+ )
55
+ .query(async ({ input }) => {
56
+ const users = await prisma.user.findMany({
57
+ take: input.limit + 1,
58
+ cursor: input.cursor ? { id: input.cursor } : undefined,
59
+ orderBy: { createdAt: 'desc' },
60
+ });
61
+
62
+ let nextCursor: string | undefined;
63
+ if (users.length > input.limit) {
64
+ const nextItem = users.pop();
65
+ nextCursor = nextItem!.id;
66
+ }
67
+
68
+ return { users, nextCursor };
69
+ }),
70
+ });
71
+
72
+ // =============================================================================
73
+ // PROJECT ROUTER
74
+ // =============================================================================
75
+
76
+ export const projectRouter = router({
77
+ // List projects for current organization
78
+ list: protectedProcedure
79
+ .input(
80
+ z.object({
81
+ organizationId: z.string(),
82
+ status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
83
+ })
84
+ )
85
+ .query(async ({ ctx, input }) => {
86
+ // Verify user has access to organization
87
+ const membership = await prisma.organizationMember.findFirst({
88
+ where: {
89
+ organizationId: input.organizationId,
90
+ user: { clerkId: ctx.userId },
91
+ },
92
+ });
93
+
94
+ if (!membership) {
95
+ throw new TRPCError({ code: 'FORBIDDEN', message: 'Access denied' });
96
+ }
97
+
98
+ return prisma.project.findMany({
99
+ where: {
100
+ organizationId: input.organizationId,
101
+ ...(input.status && { status: input.status }),
102
+ },
103
+ include: {
104
+ owner: { select: { name: true, imageUrl: true } },
105
+ _count: { select: { tasks: true } },
106
+ },
107
+ orderBy: { updatedAt: 'desc' },
108
+ });
109
+ }),
110
+
111
+ // Get single project
112
+ get: protectedProcedure
113
+ .input(z.object({ id: z.string() }))
114
+ .query(async ({ ctx, input }) => {
115
+ const project = await prisma.project.findUnique({
116
+ where: { id: input.id },
117
+ include: {
118
+ owner: true,
119
+ tasks: { orderBy: { createdAt: 'desc' } },
120
+ organization: true,
121
+ },
122
+ });
123
+
124
+ if (!project) {
125
+ throw new TRPCError({ code: 'NOT_FOUND' });
126
+ }
127
+
128
+ // Verify access
129
+ const membership = await prisma.organizationMember.findFirst({
130
+ where: {
131
+ organizationId: project.organizationId,
132
+ user: { clerkId: ctx.userId },
133
+ },
134
+ });
135
+
136
+ if (!membership) {
137
+ throw new TRPCError({ code: 'FORBIDDEN' });
138
+ }
139
+
140
+ return project;
141
+ }),
142
+
143
+ // Create project
144
+ create: protectedProcedure
145
+ .input(
146
+ z.object({
147
+ name: z.string().min(1).max(100),
148
+ description: z.string().max(500).optional(),
149
+ organizationId: z.string(),
150
+ })
151
+ )
152
+ .mutation(async ({ ctx, input }) => {
153
+ // Verify user has write access to organization
154
+ const membership = await prisma.organizationMember.findFirst({
155
+ where: {
156
+ organizationId: input.organizationId,
157
+ user: { clerkId: ctx.userId },
158
+ role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
159
+ },
160
+ });
161
+
162
+ if (!membership) {
163
+ throw new TRPCError({ code: 'FORBIDDEN' });
164
+ }
165
+
166
+ const user = await prisma.user.findUnique({
167
+ where: { clerkId: ctx.userId },
168
+ });
169
+
170
+ return prisma.project.create({
171
+ data: {
172
+ name: input.name,
173
+ description: input.description,
174
+ organizationId: input.organizationId,
175
+ ownerId: user!.id,
176
+ },
177
+ });
178
+ }),
179
+
180
+ // Update project
181
+ update: protectedProcedure
182
+ .input(
183
+ z.object({
184
+ id: z.string(),
185
+ name: z.string().min(1).max(100).optional(),
186
+ description: z.string().max(500).optional(),
187
+ status: z.enum(['ACTIVE', 'ARCHIVED', 'COMPLETED']).optional(),
188
+ })
189
+ )
190
+ .mutation(async ({ ctx, input }) => {
191
+ const { id, ...data } = input;
192
+
193
+ const project = await prisma.project.findUnique({
194
+ where: { id },
195
+ });
196
+
197
+ if (!project) {
198
+ throw new TRPCError({ code: 'NOT_FOUND' });
199
+ }
200
+
201
+ // Verify write access
202
+ const membership = await prisma.organizationMember.findFirst({
203
+ where: {
204
+ organizationId: project.organizationId,
205
+ user: { clerkId: ctx.userId },
206
+ role: { in: ['OWNER', 'ADMIN', 'MEMBER'] },
207
+ },
208
+ });
209
+
210
+ if (!membership) {
211
+ throw new TRPCError({ code: 'FORBIDDEN' });
212
+ }
213
+
214
+ return prisma.project.update({
215
+ where: { id },
216
+ data,
217
+ });
218
+ }),
219
+
220
+ // Delete project
221
+ delete: protectedProcedure
222
+ .input(z.object({ id: z.string() }))
223
+ .mutation(async ({ ctx, input }) => {
224
+ const project = await prisma.project.findUnique({
225
+ where: { id: input.id },
226
+ });
227
+
228
+ if (!project) {
229
+ throw new TRPCError({ code: 'NOT_FOUND' });
230
+ }
231
+
232
+ // Only owner/admin can delete
233
+ const membership = await prisma.organizationMember.findFirst({
234
+ where: {
235
+ organizationId: project.organizationId,
236
+ user: { clerkId: ctx.userId },
237
+ role: { in: ['OWNER', 'ADMIN'] },
238
+ },
239
+ });
240
+
241
+ if (!membership) {
242
+ throw new TRPCError({ code: 'FORBIDDEN' });
243
+ }
244
+
245
+ return prisma.project.delete({ where: { id: input.id } });
246
+ }),
247
+ });
248
+
249
+ // =============================================================================
250
+ // ROOT ROUTER
251
+ // =============================================================================
252
+
253
+ export const appRouter = router({
254
+ user: userRouter,
255
+ project: projectRouter,
256
+ });
257
+
258
+ export type AppRouter = typeof appRouter;