diff --git a/docs/content/docs/plugins/organization.mdx b/docs/content/docs/plugins/organization.mdx index def8f64689..f71cdfef9b 100644 --- a/docs/content/docs/plugins/organization.mdx +++ b/docs/content/docs/plugins/organization.mdx @@ -407,6 +407,10 @@ type getFullOrganization = { * The organization slug to get. */ organizationSlug?: string = "org-slug" + /** + * The limit of members to get. By default, it uses the membershipLimit option which defaults to 100. + */ + membersLimit?: number = 100 } ``` diff --git a/packages/better-auth/src/plugins/organization/adapter.ts b/packages/better-auth/src/plugins/organization/adapter.ts index 61282a18a4..5a740c2271 100644 --- a/packages/better-auth/src/plugins/organization/adapter.ts +++ b/packages/better-auth/src/plugins/organization/adapter.ts @@ -353,10 +353,12 @@ export const getOrgAdapter = ( organizationId, isSlug, includeTeams, + membersLimit, }: { organizationId: string; isSlug?: boolean; includeTeams?: boolean; + membersLimit?: number; }) => { const org = await adapter.findOne>({ model: "organization", @@ -373,7 +375,7 @@ export const getOrgAdapter = ( adapter.findMany>({ model: "member", where: [{ field: "organizationId", value: org.id }], - limit: options?.membershipLimit || 100, + limit: membersLimit ?? options?.membershipLimit ?? 100, }), includeTeams ? adapter.findMany>({ diff --git a/packages/better-auth/src/plugins/organization/organization.test.ts b/packages/better-auth/src/plugins/organization/organization.test.ts index c8b510f4e6..78c95c4f17 100644 --- a/packages/better-auth/src/plugins/organization/organization.test.ts +++ b/packages/better-auth/src/plugins/organization/organization.test.ts @@ -767,6 +767,20 @@ describe("organization", async (it) => { expect(getFullOrganization.data?.members.length).toBe(6); }); + it("should allow listing members with membersLimit", async () => { + const { headers } = await signInWithTestUser(); + const members = await client.organization.getFullOrganization({ + query: { + membersLimit: 1, + }, + fetchOptions: { + headers, + }, + }); + console.log(members); + expect(members.data?.members.length).toBe(1); + }); + it("should allow listing invitations for an org", async () => { const invitations = await client.organization.listInvitations({ query: { diff --git a/packages/better-auth/src/plugins/organization/routes/crud-org.test.ts b/packages/better-auth/src/plugins/organization/routes/crud-org.test.ts new file mode 100644 index 0000000000..f69803b9dd --- /dev/null +++ b/packages/better-auth/src/plugins/organization/routes/crud-org.test.ts @@ -0,0 +1,278 @@ +import { describe, expect, it } from "vitest"; +import { getTestInstance } from "../../../test-utils/test-instance"; +import { organization } from "../organization"; +import { createAuthClient } from "../../../client"; +import { organizationClient } from "../client"; +import { ORGANIZATION_ERROR_CODES } from "../error-codes"; + +describe("get-full-organization", async () => { + const { auth, signInWithTestUser, cookieSetter } = await getTestInstance({ + user: { + modelName: "users", + }, + plugins: [ + organization({ + membershipLimit: 6, + async sendInvitationEmail(data, request) {}, + schema: { + organization: { + modelName: "team", + }, + member: { + modelName: "teamMembers", + fields: { + userId: "user_id", + }, + }, + }, + invitationLimit: 3, + }), + ], + logger: { + level: "error", + }, + }); + + const { headers } = await signInWithTestUser(); + const client = createAuthClient({ + plugins: [organizationClient()], + baseURL: "http://localhost:3000/api/auth", + fetchOptions: { + customFetchImpl: async (url, init) => { + return auth.handler(new Request(url, init)); + }, + }, + }); + const org = await client.organization.create({ + name: "test", + slug: "test", + metadata: { + test: "test", + }, + fetchOptions: { + headers, + }, + }); + const secondOrg = await client.organization.create({ + name: "test-second", + slug: "test-second", + metadata: { + test: "second-org", + }, + fetchOptions: { + headers, + }, + }); + + it("should get organization by organizationId", async () => { + const { headers } = await signInWithTestUser(); + + //set the second org as active + await client.organization.setActive({ + organizationId: secondOrg.data?.id as string, + fetchOptions: { + headers, + }, + }); + const orgById = await client.organization.getFullOrganization({ + query: { + // get the first org + organizationId: org.data?.id as string, + }, + fetchOptions: { + headers, + }, + }); + expect(orgById.data?.name).toBe("test"); + }); + + it("should get organization by organizationSlug", async () => { + const { headers } = await signInWithTestUser(); + const orgBySlug = await client.organization.getFullOrganization({ + query: { + organizationSlug: "test", + }, + fetchOptions: { + headers, + }, + }); + expect(orgBySlug.data?.name).toBe("test"); + }); + + it("should return null when no active organization and no query params", async () => { + await client.organization.setActive({ + organizationId: null, + fetchOptions: { + headers, + }, + }); + const result = await client.organization.getFullOrganization({ + fetchOptions: { + headers: headers, + }, + }); + expect(result.data).toBeNull(); + expect(result.error).toBeNull(); + }); + + it("should throw FORBIDDEN when user is not a member of the organization", async () => { + const newHeaders = new Headers(); + await client.signUp.email( + { + email: "test3@test.com", + password: "password", + name: "test3", + }, + { + onSuccess: cookieSetter(newHeaders), + }, + ); + const result = await client.organization.getFullOrganization({ + query: { + organizationId: org.data?.id as string, + }, + fetchOptions: { + headers: newHeaders, + }, + }); + expect(result.error?.status).toBe(403); + expect(result.error?.message).toContain( + ORGANIZATION_ERROR_CODES.USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, + ); + }); + + it("should throw BAD_REQUEST when organization doesn't exist", async () => { + const result = await client.organization.getFullOrganization({ + query: { + organizationId: "non-existent-org-id", + }, + fetchOptions: { + headers, + }, + }); + expect(result.error?.status).toBe(400); + expect(result.error?.message).toContain( + ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, + ); + }); + + it("should include invitations in the response", async () => { + await client.organization.setActive({ + organizationId: org.data?.id as string, + fetchOptions: { + headers, + }, + }); + + // Create an invitation + await client.organization.inviteMember({ + email: "invited@test.com", + role: "member", + fetchOptions: { + headers, + }, + }); + + const fullOrg = await client.organization.getFullOrganization({ + fetchOptions: { + headers, + }, + }); + + expect(fullOrg.data?.invitations).toBeDefined(); + expect(Array.isArray(fullOrg.data?.invitations)).toBe(true); + const invitation = fullOrg.data?.invitations.find( + (inv: any) => inv.email === "invited@test.com", + ); + expect(invitation).toBeDefined(); + expect(invitation?.role).toBe("member"); + }); + + it("should prioritize organizationSlug over organizationId when both are provided", async () => { + const result = await client.organization.getFullOrganization({ + query: { + organizationId: org.data?.id as string, + organizationSlug: secondOrg.data?.slug as string, + }, + fetchOptions: { + headers, + }, + }); + expect(result.data).toBeTruthy(); + expect(result.data?.name).toBe(secondOrg.data?.name); + }); + + it("should allow listing members with membersLimit", async () => { + const { headers } = await signInWithTestUser(); + await client.organization.setActive({ + organizationId: org.data?.id as string, + fetchOptions: { + headers, + }, + }); + const newUser = await auth.api.signUpEmail({ + body: { + email: "test2@test.com", + password: "password", + name: "test2", + }, + }); + await auth.api.addMember({ + body: { + userId: newUser.user.id, + role: "member", + organizationId: org.data?.id as string, + }, + }); + const FullOrganization = await client.organization.getFullOrganization({ + fetchOptions: { + headers, + }, + }); + expect(FullOrganization.data?.members.length).toBe(2); + + const limitedMembers = await client.organization.getFullOrganization({ + query: { + membersLimit: 1, + }, + fetchOptions: { + headers, + }, + }); + expect(limitedMembers.data?.members.length).toBe(1); + }); + + it("should use default membershipLimit when no membersLimit is specified", async () => { + await client.organization.setActive({ + organizationId: org.data?.id as string, + fetchOptions: { + headers, + }, + }); + for (let i = 3; i <= 5; i++) { + const newUser = await auth.api.signUpEmail({ + body: { + email: `test-${i}@test.com`, + password: "password", + name: `test${i}`, + }, + }); + await auth.api.addMember({ + body: { + userId: newUser.user.id, + role: "member", + organizationId: org.data?.id as string, + }, + }); + } + + const fullOrg = await client.organization.getFullOrganization({ + fetchOptions: { + headers, + }, + }); + + expect(fullOrg.data?.members.length).toBeGreaterThan(3); + expect(fullOrg.data?.members.length).toBeLessThanOrEqual(6); + }); +}); diff --git a/packages/better-auth/src/plugins/organization/routes/crud-org.ts b/packages/better-auth/src/plugins/organization/routes/crud-org.ts index 711e7d6e5c..0b6881696b 100644 --- a/packages/better-auth/src/plugins/organization/routes/crud-org.ts +++ b/packages/better-auth/src/plugins/organization/routes/crud-org.ts @@ -553,6 +553,14 @@ export const getFullOrganization = ( description: "The organization slug to get", }) .optional(), + membersLimit: z + .number() + .or(z.string().transform((val) => parseInt(val))) + .meta({ + description: + "The limit of members to get. By default, it uses the membershipLimit option which defaults to 100.", + }) + .optional(), }), ), requireHeaders: true, @@ -583,7 +591,9 @@ export const getFullOrganization = ( ctx.query?.organizationSlug || ctx.query?.organizationId || session.session.activeOrganizationId; + // return null if no organization is found to avoid erroring since this is a usual scenario if (!organizationId) { + ctx.context.logger.info("No active organization found, returning null"); return ctx.json(null, { status: 200, }); @@ -593,7 +603,13 @@ export const getFullOrganization = ( organizationId, isSlug: !!ctx.query?.organizationSlug, includeTeams: ctx.context.orgOptions.teams?.enabled, + membersLimit: ctx.query?.membersLimit, }); + if (!organization) { + throw new APIError("BAD_REQUEST", { + message: ORGANIZATION_ERROR_CODES.ORGANIZATION_NOT_FOUND, + }); + } const isMember = organization?.members.find( (member) => member.userId === session.user.id, );