88 ProjectId ,
99 ProjectNameConflict ,
1010 ProjectNotFound ,
11+ type ProjectReader ,
1112 ProjectRepository ,
1213 ProjectSourceAuthority ,
1314} from "@dx/domain" ;
@@ -174,12 +175,17 @@ export const ProjectRepositoryD1 = Layer.effect(
174175 WHERE member.userId = ${ userId }
175176 AND organization.lifecycleState = 'active')` ;
176177 // A private Project of the user, or any workspace Project of the user's
177- // active workspace. Another member's projectless scope stays private.
178- const accessibleBy = ( userId : string ) => sql `
179- ((workspace_id IS NULL AND owner_user_id = ${ userId } )
180- OR (workspace_id IS NOT NULL
178+ // active workspace. Another member's projectless scope stays private. A
179+ // reader held to one workspace (an External API App) reaches only that
180+ // workspace's Projects, never the user's private ones.
181+ const accessibleBy = ( { userId, workspaceId } : ProjectReader ) => {
182+ const inWorkspace = sql `(workspace_id IS NOT NULL
181183 AND workspace_id = ${ activeWorkspaceOf ( userId ) }
182- AND (name != ${ PROJECTLESS_PROJECT_NAME } OR owner_user_id = ${ userId } )))` ;
184+ AND (name != ${ PROJECTLESS_PROJECT_NAME } OR owner_user_id = ${ userId } ))` ;
185+ return workspaceId === undefined
186+ ? sql `((workspace_id IS NULL AND owner_user_id = ${ userId } ) OR ${ inWorkspace } )`
187+ : sql `(workspace_id = ${ workspaceId } AND ${ inWorkspace } )` ;
188+ } ;
183189 // Changing a workspace Project takes its creator or a workspace admin,
184190 // while either is still an active member.
185191 const manageableBy = ( userId : string ) => sql `
@@ -410,13 +416,13 @@ export const ProjectRepositoryD1 = Layer.effect(
410416 ) ,
411417 ) ,
412418
413- findOwnedById : ( projectId , ownerUserId ) =>
419+ findOwnedById : ( projectId , reader ) =>
414420 observe (
415421 "project.findOwnedById" ,
416422 { projectId } ,
417423 sql < ProjectRow > `
418424 SELECT * FROM project_read_model
419- WHERE id = ${ projectId } AND ${ accessibleBy ( ownerUserId ) }
425+ WHERE id = ${ projectId } AND ${ accessibleBy ( reader ) }
420426 ` . pipe (
421427 Effect . catchTag ( "SqlError" , ( cause ) =>
422428 Effect . fail ( unavailable ( "project.findOwnedById" ) ( cause ) ) ,
@@ -430,7 +436,7 @@ export const ProjectRepositoryD1 = Layer.effect(
430436 ) ,
431437 ) ,
432438
433- listOwned : ( ownerUserId , request ) =>
439+ listOwned : ( reader , request ) =>
434440 observe (
435441 "project.listOwned" ,
436442 { } ,
@@ -448,15 +454,15 @@ export const ProjectRepositoryD1 = Layer.effect(
448454 cursor
449455 ? sql < ProjectRow > `
450456 SELECT * FROM project_read_model
451- WHERE ${ accessibleBy ( ownerUserId ) }
457+ WHERE ${ accessibleBy ( reader ) }
452458 AND name != ${ PROJECTLESS_PROJECT_NAME }
453459 AND (created_at, id) < (${ cursorTime } , ${ cursor . id } )
454460 ORDER BY created_at DESC, id DESC
455461 LIMIT ${ limit + 1 }
456462 `
457463 : sql < ProjectRow > `
458464 SELECT * FROM project_read_model
459- WHERE ${ accessibleBy ( ownerUserId ) }
465+ WHERE ${ accessibleBy ( reader ) }
460466 AND name != ${ PROJECTLESS_PROJECT_NAME }
461467 ORDER BY created_at DESC, id DESC
462468 LIMIT ${ limit + 1 }
0 commit comments