@@ -12,7 +12,7 @@ import type {
1212 WorkspacePermissions ,
1313 WorkspaceRole ,
1414} from "@sugabots/contracts" ;
15- import { and , asc , eq , gt , sql } from "drizzle-orm" ;
15+ import { and , asc , eq , gt , or , sql } from "drizzle-orm" ;
1616import { Context , Data , DateTime , Duration , Effect , Layer } from "effect" ;
1717import { Accounts } from "../../accounts/accounts.ts" ;
1818import {
@@ -40,6 +40,7 @@ import { user, workspace, workspaceInvite, workspaceMember } from "../../databas
4040import { Email } from "../../email/email.ts" ;
4141import { Ids , isUuid } from "../../ids/ids.ts" ;
4242import { Installation } from "../../installation/installation.ts" ;
43+ import { offeredModels } from "../../providers/model-providers/model-provider-reads.ts" ;
4344import { ModelProviderRepository } from "../../providers/model-providers/model-provider-repository.ts" ;
4445import { SearchProviderRepository } from "../../providers/search-providers/search-provider-repository.ts" ;
4546import { type UserFacing , UserMessage } from "../../user-message.ts" ;
@@ -123,10 +124,18 @@ export interface Interface {
123124 readonly cancelInvitation : (
124125 input : ForInvitation ,
125126 ) => Effect . Effect < void , AuthorizationDenied , CurrentActor . Service > ;
126- /** For the person the invitation was sent to. */
127+ /**
128+ * For the person the invitation was sent to: one still to accept, or one
129+ * they have accepted and whose workspace they are still in.
130+ */
127131 readonly invitation : (
128132 input : ForInvitation ,
129133 ) => Effect . Effect < InvitationPreview , ResourceHidden | NotTheInvitee , CurrentActor . Service > ;
134+ /**
135+ * Lets the invitee in, with their Personal pod. Accepting again, as a
136+ * second tab or a reload does, answers with the workspace and changes
137+ * nothing.
138+ */
130139 readonly accept : (
131140 input : ForInvitation ,
132141 ) => Effect . Effect <
@@ -160,6 +169,25 @@ export const make = Effect.gen(function* () {
160169 const modelProviders = yield * ModelProviderRepository . Service ;
161170 const ids = yield * Ids . Service ;
162171
172+ /**
173+ * The Personal pod of somebody joining, its assistant on the workspace's
174+ * default model, or on another the workspace offers while it does not offer
175+ * that one.
176+ */
177+ const provisionJoining = ( workspaceId : string , userId : string ) =>
178+ Effect . gen ( function * ( ) {
179+ const { models, defaultModel } = yield * offeredModels ( workspaceId ) ;
180+ // The default goes unoffered while a failed test has its provider off.
181+ const model = models . some ( ( offered ) => offered . modelId === defaultModel )
182+ ? defaultModel
183+ : models [ 0 ] ?. modelId ;
184+ if ( ! model ) return yield * personalPods . provision ( { workspaceId, userId } ) ;
185+ return yield * personalPods . provisionWithModel ( { workspaceId, userId, model } ) . pipe (
186+ // Switched off since it was read: start on none, as while the workspace offers none.
187+ Effect . catchTag ( "ModelNotEnabled" , ( ) => personalPods . provision ( { workspaceId, userId } ) ) ,
188+ ) ;
189+ } ) ;
190+
163191 const sendInvitation = (
164192 invitationId : string ,
165193 to : string ,
@@ -556,7 +584,12 @@ export const make = Effect.gen(function* () {
556584 Effect . gen ( function * ( ) {
557585 const { userId } = yield * CurrentActor . Service ;
558586 const invitation = yield * invitationFor ( userId , input . invitationId ) ;
559- return { workspaceName : invitation . workspaceName , inviterName : invitation . inviterName } ;
587+ if ( invitation . status === "accepted" ) return invitation ;
588+ return {
589+ status : "pending" as const ,
590+ workspaceName : invitation . workspaceName ,
591+ inviterName : invitation . inviterName ,
592+ } ;
560593 } ) ,
561594 ) ,
562595
@@ -567,6 +600,7 @@ export const make = Effect.gen(function* () {
567600 Effect . gen ( function * ( ) {
568601 const { userId } = yield * CurrentActor . Service ;
569602 const invitation = yield * invitationFor ( userId , input . invitationId ) ;
603+ if ( invitation . status === "accepted" ) return { workspaceId : invitation . workspaceId } ;
570604 if ( accounts . requireEmailVerification && ! invitation . inviteeVerified ) {
571605 return yield * new EmailUnverified ( ) ;
572606 }
@@ -586,7 +620,7 @@ export const make = Effect.gen(function* () {
586620 . where ( eq ( workspaceInvite . id , input . invitationId ) ) ;
587621 } ) ,
588622 ) ;
589- yield * personalPods . provision ( { workspaceId : invitation . workspaceId , userId } ) ;
623+ yield * provisionJoining ( invitation . workspaceId , userId ) ;
590624 return { workspaceId : invitation . workspaceId } ;
591625 } ) ,
592626 ) ,
@@ -738,16 +772,20 @@ function deleteMember(memberId: string) {
738772}
739773
740774/**
741- * A pending, unexpired invitation, locked for an enclosing transaction, when
742- * `userId` is the person it was sent to. Anything else is hidden, so a link
743- * cannot be probed.
775+ * The invitation `invitationId`, locked for an enclosing transaction, when
776+ * `userId` is the person it was sent to: pending and unexpired, or accepted
777+ * while they are still in its workspace, so its link leads back in. Anything
778+ * else is hidden, so a link cannot be probed, and leaving a workspace ends
779+ * what its link can do.
744780 */
745781function invitationFor ( userId : string , invitationId : string ) {
746782 return Effect . gen ( function * ( ) {
747783 const now = yield * DateTime . nowAsDate ;
748784 const [ invitation ] = yield * query ( ( db ) =>
749785 db
750786 . select ( {
787+ status : workspaceInvite . status ,
788+ stillIn : workspaceMember . id ,
751789 workspaceId : workspaceInvite . workspaceId ,
752790 email : workspaceInvite . email ,
753791 role : workspaceInvite . role ,
@@ -757,24 +795,45 @@ function invitationFor(userId: string, invitationId: string) {
757795 . from ( workspaceInvite )
758796 . innerJoin ( workspace , eq ( workspace . id , workspaceInvite . workspaceId ) )
759797 . innerJoin ( user , eq ( user . id , workspaceInvite . inviterId ) )
798+ . leftJoin (
799+ workspaceMember ,
800+ and (
801+ eq ( workspaceMember . workspaceId , workspaceInvite . workspaceId ) ,
802+ eq ( workspaceMember . userId , userId ) ,
803+ ) ,
804+ )
760805 . where (
761806 and (
762807 eq ( workspaceInvite . id , invitationId ) ,
763- eq ( workspaceInvite . status , "pending" ) ,
764- gt ( workspaceInvite . expiresAt , now ) ,
808+ or (
809+ and ( eq ( workspaceInvite . status , "pending" ) , gt ( workspaceInvite . expiresAt , now ) ) ,
810+ eq ( workspaceInvite . status , "accepted" ) ,
811+ ) ,
765812 ) ,
766813 )
767814 . for ( "update" , { of : workspaceInvite } ) ,
768815 ) ;
769- if ( ! invitation ) return yield * new ResourceHidden ( { resource : "invitation" } ) ;
816+ const hidden = new ResourceHidden ( { resource : "invitation" } ) ;
817+ if ( ! invitation ) return yield * hidden ;
818+ if ( invitation . status === "accepted" && invitation . stillIn === null ) return yield * hidden ;
770819 const [ invitee ] = yield * query ( ( db ) =>
771820 db
772821 . select ( { email : user . email , emailVerified : user . emailVerified } )
773822 . from ( user )
774823 . where ( eq ( user . id , userId ) ) ,
775824 ) ;
776825 if ( invitee ?. email . toLowerCase ( ) !== invitation . email ) return yield * new NotTheInvitee ( ) ;
777- return { ...invitation , inviteeVerified : invitee . emailVerified } ;
826+ if ( invitation . status === "accepted" ) {
827+ return { status : "accepted" as const , workspaceId : invitation . workspaceId } ;
828+ }
829+ return {
830+ status : "pending" as const ,
831+ workspaceId : invitation . workspaceId ,
832+ role : invitation . role ,
833+ workspaceName : invitation . workspaceName ,
834+ inviterName : invitation . inviterName ,
835+ inviteeVerified : invitee . emailVerified ,
836+ } ;
778837 } ) ;
779838}
780839
0 commit comments