33// ---------------------------------------------------------------------------
44
55import { env } from "cloudflare:workers" ;
6- import { Context , Data , Effect , Layer } from "effect" ;
6+ import { Context , Data , Effect , Layer , Option , Schema } from "effect" ;
77import { GeneratePortalLinkIntent , WorkOS } from "@workos-inc/node/worker" ;
88import { WorkOSError , tryPromiseService , withServiceLogging } from "./errors" ;
99
@@ -24,6 +24,88 @@ type RawWorkOS = WorkOS & {
2424 ) => Promise < { readonly data : unknown } > ;
2525} ;
2626
27+ type WorkOSListMetadata = {
28+ readonly before ?: string | null ;
29+ readonly after ?: string | null ;
30+ } ;
31+
32+ type WorkOSAutoPaginatable < Resource > = {
33+ readonly object : "list" ;
34+ readonly data : Resource [ ] ;
35+ readonly listMetadata : WorkOSListMetadata ;
36+ readonly autoPagination : ( ) => Promise < Resource [ ] > ;
37+ } ;
38+
39+ export type WorkOSCollectedList < Resource > = {
40+ readonly object : "list" ;
41+ readonly data : Resource [ ] ;
42+ readonly listMetadata : {
43+ readonly before : string | null ;
44+ readonly after : string | null ;
45+ } ;
46+ } ;
47+
48+ const RawWorkOSListMetadata = Schema . Struct ( {
49+ before : Schema . optional ( Schema . NullOr ( Schema . String ) ) ,
50+ after : Schema . optional ( Schema . NullOr ( Schema . String ) ) ,
51+ } ) ;
52+
53+ const RawWorkOSListResponse = Schema . Struct ( {
54+ data : Schema . Array ( Schema . Unknown ) ,
55+ listMetadata : Schema . optional ( RawWorkOSListMetadata ) ,
56+ list_metadata : Schema . optional ( RawWorkOSListMetadata ) ,
57+ } ) ;
58+
59+ const decodeRawWorkOSListResponse = Schema . decodeUnknownOption ( RawWorkOSListResponse ) ;
60+
61+ const completedListMetadata = {
62+ before : null ,
63+ after : null ,
64+ } as const ;
65+
66+ const nextCursorFromRawList = ( response : typeof RawWorkOSListResponse . Type ) : string | null =>
67+ response . listMetadata ?. after ?? response . list_metadata ?. after ?? null ;
68+
69+ export const collectWorkOSList = async < Resource > (
70+ response : WorkOSAutoPaginatable < Resource > ,
71+ ) : Promise < WorkOSCollectedList < Resource > > => {
72+ const data = response . listMetadata . after ? await response . autoPagination ( ) : response . data ;
73+ return {
74+ object : "list" ,
75+ data,
76+ listMetadata : completedListMetadata ,
77+ } ;
78+ } ;
79+
80+ export const collectRawWorkOSList = async (
81+ loadPage : ( after ?: string ) => Promise < unknown > ,
82+ ) : Promise < WorkOSCollectedList < unknown > > => {
83+ const first = Option . getOrNull ( decodeRawWorkOSListResponse ( await loadPage ( ) ) ) ;
84+ if ( ! first ) {
85+ return {
86+ object : "list" ,
87+ data : [ ] ,
88+ listMetadata : completedListMetadata ,
89+ } ;
90+ }
91+
92+ const data = [ ...first . data ] ;
93+ let after = nextCursorFromRawList ( first ) ;
94+
95+ while ( after ) {
96+ const next = Option . getOrNull ( decodeRawWorkOSListResponse ( await loadPage ( after ) ) ) ;
97+ if ( ! next ) break ;
98+ data . push ( ...next . data ) ;
99+ after = nextCursorFromRawList ( next ) ;
100+ }
101+
102+ return {
103+ object : "list" ,
104+ data,
105+ listMetadata : completedListMetadata ,
106+ } ;
107+ } ;
108+
27109class WorkOSAuthConfigurationError extends Data . TaggedError ( "WorkOSAuthConfigurationError" ) < {
28110 readonly message : string ;
29111} > { }
@@ -132,11 +214,13 @@ const make = Effect.gen(function* () {
132214
133215 /** List organization memberships for a user. */
134216 listUserMemberships : ( userId : string ) =>
135- use ( ( wos ) =>
136- wos . userManagement . listOrganizationMemberships ( {
137- userId,
138- statuses : [ "active" , "pending" ] ,
139- } ) ,
217+ use ( async ( wos ) =>
218+ collectWorkOSList (
219+ await wos . userManagement . listOrganizationMemberships ( {
220+ userId,
221+ statuses : [ "active" , "pending" ] ,
222+ } ) ,
223+ ) ,
140224 ) ,
141225
142226 /**
@@ -183,10 +267,16 @@ const make = Effect.gen(function* () {
183267 listUserApiKeys : ( userId : string , organizationId : string ) =>
184268 use ( async ( wos ) => {
185269 const raw = wos as RawWorkOS ;
186- const response = await raw . get ( `/user_management/users/${ userId } /api_keys` , {
187- query : { organization_id : organizationId } ,
270+ return collectRawWorkOSList ( async ( after ) => {
271+ const response = await raw . get ( `/user_management/users/${ userId } /api_keys` , {
272+ query : {
273+ organization_id : organizationId ,
274+ limit : 100 ,
275+ ...( after ? { after } : { } ) ,
276+ } ,
277+ } ) ;
278+ return response . data ;
188279 } ) ;
189- return response . data ;
190280 } ) ,
191281
192282 createUserApiKey : ( params : { userId : string ; organizationId : string ; name : string } ) =>
@@ -203,12 +293,25 @@ const make = Effect.gen(function* () {
203293
204294 /** List organization memberships with user details. */
205295 listOrgMembers : ( organizationId : string ) =>
206- use ( ( wos ) =>
207- wos . userManagement . listOrganizationMemberships ( {
296+ use ( async ( wos ) =>
297+ collectWorkOSList (
298+ await wos . userManagement . listOrganizationMemberships ( {
299+ organizationId,
300+ statuses : [ "active" , "pending" ] ,
301+ } ) ,
302+ ) ,
303+ ) ,
304+
305+ /** Get a user's membership in an organization. */
306+ getUserOrgMembership : ( organizationId : string , userId : string ) =>
307+ use ( async ( wos ) => {
308+ const response = await wos . userManagement . listOrganizationMemberships ( {
208309 organizationId,
310+ userId,
209311 statuses : [ "active" , "pending" ] ,
210- } ) ,
211- ) ,
312+ } ) ;
313+ return response . data [ 0 ] ?? null ;
314+ } ) ,
212315
213316 /** Get a user by ID. */
214317 getUser : ( userId : string ) => use ( ( wos ) => wos . userManagement . getUser ( userId ) ) ,
@@ -229,7 +332,13 @@ const make = Effect.gen(function* () {
229332 * API level, so we filter after.
230333 */
231334 listPendingInvitations : ( organizationId : string ) =>
232- use ( ( wos ) => wos . userManagement . listInvitations ( { organizationId } ) ) . pipe (
335+ use ( async ( wos ) =>
336+ collectWorkOSList (
337+ await wos . userManagement . listInvitations ( {
338+ organizationId,
339+ } ) ,
340+ ) ,
341+ ) . pipe (
233342 Effect . map ( ( response ) => ( {
234343 ...response ,
235344 data : response . data . filter ( ( i ) => i . state === "pending" ) ,
@@ -238,7 +347,13 @@ const make = Effect.gen(function* () {
238347
239348 /** List invitations for an email address (across all orgs). */
240349 listInvitationsByEmail : ( email : string ) =>
241- use ( ( wos ) => wos . userManagement . listInvitations ( { email } ) ) ,
350+ use ( async ( wos ) =>
351+ collectWorkOSList (
352+ await wos . userManagement . listInvitations ( {
353+ email,
354+ } ) ,
355+ ) ,
356+ ) ,
242357
243358 /** Accept an invitation; returns the (now accepted) invitation. */
244359 acceptInvitation : ( invitationId : string ) =>
0 commit comments