22
33import { db } from "@cap/database" ;
44import { getCurrentUser } from "@cap/database/auth/session" ;
5- import { nanoId , nanoIdLength } from "@cap/database/helpers" ;
6- import { spaceMembers , spaces , users } from "@cap/database/schema" ;
7- import { S3Buckets } from "@cap/web-backend" ;
8- import { Space } from "@cap/web-domain" ;
9- import { and , eq , inArray } from "drizzle-orm" ;
10- import { Effect , Option } from "effect" ;
5+ import { nanoId } from "@cap/database/helpers" ;
6+ import { spaceMembers , spaces } from "@cap/database/schema" ;
7+ import {
8+ type ImageUpload ,
9+ Space ,
10+ SpaceMemberId ,
11+ type SpaceMemberRole ,
12+ User ,
13+ } from "@cap/web-domain" ;
14+ import { and , eq } from "drizzle-orm" ;
1115import { revalidatePath } from "next/cache" ;
12- import { v4 as uuidv4 } from "uuid" ;
13- import { runPromise } from "@/lib/server" ;
16+ import { uploadSpaceIcon } from "./upload-space-icon" ;
1417
1518interface CreateSpaceResponse {
1619 success : boolean ;
@@ -63,115 +66,59 @@ export async function createSpace(
6366
6467 // Generate the space ID early so we can use it in the file path
6568 const spaceId = Space . SpaceId . make ( nanoId ( ) ) ;
66-
67- const iconFile = formData . get ( "icon" ) as File | null ;
68- let iconUrl = null ;
69-
70- if ( iconFile ) {
71- // Validate file type
72- if ( ! iconFile . type . startsWith ( "image/" ) ) {
73- return {
74- success : false ,
75- error : "File must be an image" ,
76- } ;
69+ let iconUrl : ImageUpload . ImageUrlOrKey | null = null ;
70+
71+ await db ( ) . transaction ( async ( tx ) => {
72+ // Create the space first
73+ await tx . insert ( spaces ) . values ( {
74+ id : spaceId ,
75+ name,
76+ organizationId : user . activeOrganizationId ,
77+ createdById : user . id ,
78+ iconUrl : null ,
79+ } ) ;
80+
81+ // --- Member Management Logic ---
82+ // Collect member user IDs from formData
83+ const memberUserIds : string [ ] = [ ] ;
84+ for ( const entry of formData . getAll ( "members[]" ) ) {
85+ if ( typeof entry === "string" && entry . length > 0 ) {
86+ memberUserIds . push ( entry ) ;
87+ }
7788 }
7889
79- // Validate file size (limit to 2MB)
80- if ( iconFile . size > 2 * 1024 * 1024 ) {
81- return {
82- success : false ,
83- error : "File size must be less than 2MB" ,
84- } ;
85- }
86-
87- try {
88- // Create a unique file key
89- const fileExtension = iconFile . name . split ( "." ) . pop ( ) ;
90- const fileKey = `organizations/${
91- user . activeOrganizationId
92- } /spaces/${ spaceId } /icon-${ Date . now ( ) } .${ fileExtension } `;
93-
94- await Effect . gen ( function * ( ) {
95- const [ bucket ] = yield * S3Buckets . getBucketAccess ( Option . none ( ) ) ;
96-
97- yield * bucket . putObject (
98- fileKey ,
99- yield * Effect . promise ( ( ) => iconFile . bytes ( ) ) ,
100- { contentType : iconFile . type } ,
101- ) ;
102- iconUrl = fileKey ;
103- } ) . pipe ( runPromise ) ;
104- } catch ( error ) {
105- console . error ( "Error uploading space icon:" , error ) ;
106- return {
107- success : false ,
108- error : "Failed to upload space icon" ,
109- } ;
110- }
111- }
112-
113- await db ( ) . insert ( spaces ) . values ( {
114- id : spaceId ,
115- name,
116- organizationId : user . activeOrganizationId ,
117- createdById : user . id ,
118- iconUrl,
119- createdAt : new Date ( ) ,
120- updatedAt : new Date ( ) ,
121- } ) ;
122-
123- // --- Member Management Logic ---
124- // Collect member emails from formData
125- const members : string [ ] = [ ] ;
126- for ( const entry of formData . getAll ( "members[]" ) ) {
127- if ( typeof entry === "string" && entry . length > 0 ) {
128- members . push ( entry ) ;
90+ // Always add the creator as Admin (if not already in the list)
91+ if ( ! memberUserIds . includes ( user . id ) ) {
92+ memberUserIds . push ( user . id ) ;
12993 }
130- }
131-
132- // Always add the creator as Owner (if not already in the list)
133- const memberEmailsSet = new Set ( members . map ( ( e ) => e . toLowerCase ( ) ) ) ;
134- const creatorEmail = user . email . toLowerCase ( ) ;
135- if ( ! memberEmailsSet . has ( creatorEmail ) ) {
136- members . push ( user . email ) ;
137- }
13894
139- // Look up user IDs for each email
140- if ( members . length > 0 ) {
141- // Fetch all users with these emails
142- const usersFound = await db ( )
143- . select ( { id : users . id , email : users . email } )
144- . from ( users )
145- . where ( inArray ( users . email , members ) ) ;
146-
147- // Map email to userId
148- const emailToUserId = Object . fromEntries (
149- usersFound . map ( ( u ) => [ u . email . toLowerCase ( ) , u . id ] ) ,
150- ) ;
151-
152- // Prepare spaceMembers insertions
153- const spaceMembersToInsert = members
154- . map ( ( email ) => {
155- const userId = emailToUserId [ email . toLowerCase ( ) ] ;
156- if ( ! userId ) return null ;
157- // Creator is always Owner, others are Member
158- const role =
159- email . toLowerCase ( ) === creatorEmail
160- ? ( "Admin" as const )
161- : ( "member" as const ) ;
95+ // Create space members
96+ if ( memberUserIds . length > 0 ) {
97+ const spaceMembersToInsert = memberUserIds . map ( ( userId ) => {
98+ // Creator is always Admin, others are member
99+ const role : SpaceMemberRole = userId === user . id ? "Admin" : "member" ;
162100 return {
163- id : uuidv4 ( ) . substring ( 0 , nanoIdLength ) ,
101+ id : SpaceMemberId . make ( nanoId ( ) ) ,
164102 spaceId,
165- userId,
103+ userId : User . UserId . make ( userId ) ,
166104 role,
167- createdAt : new Date ( ) ,
168- updatedAt : new Date ( ) ,
169105 } ;
170- } )
171- . filter ( ( v ) : v is NonNullable < typeof v > => Boolean ( v ) ) ;
106+ } ) ;
172107
173- if ( spaceMembersToInsert . length > 0 ) {
174- await db ( ) . insert ( spaceMembers ) . values ( spaceMembersToInsert ) ;
108+ await tx . insert ( spaceMembers ) . values ( spaceMembersToInsert ) ;
109+ }
110+ } ) ;
111+
112+ const iconFile = formData . get ( "icon" ) as File | null ;
113+
114+ if ( iconFile ) {
115+ try {
116+ const iconFormData = new FormData ( ) ;
117+ iconFormData . append ( "icon" , iconFile ) ;
118+ const result = await uploadSpaceIcon ( iconFormData , spaceId ) ;
119+ iconUrl = result . iconUrl ;
120+ } catch ( error ) {
121+ console . error ( "Error uploading space icon:" , error ) ;
175122 }
176123 }
177124
@@ -180,8 +127,8 @@ export async function createSpace(
180127 return {
181128 success : true ,
182129 spaceId,
183- name,
184130 iconUrl,
131+ name,
185132 } ;
186133 } catch ( error ) {
187134 console . error ( "Error creating space:" , error ) ;
0 commit comments