mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
feat(organization): add membersLimit param to allow to fetch more or less members than the membership limit (#3580)
* docs(api-key): move schema section to the bottom * fix: shouldn't refresh a token if access token expires is undefined or null (#3577) * fix: ensure session is added to context when reading from cookie cache (#3578) When session data is present in the cookie, `get-session` was early-returning without adding the session to the context, causing hooks to receive a null session. This change ensures the session is consistently added to the context. * feat(organization): add membersLimit param to allow to fetch more or less members than the membership limit * add test * Update packages/better-auth/src/plugins/organization/adapter.ts Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Gagan G <80091397+gaganref@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Gagan G
cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
parent
7f508f0d22
commit
518ab70495
@@ -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
|
||||
}
|
||||
```
|
||||
</APIMethod>
|
||||
|
||||
@@ -353,10 +353,12 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
|
||||
organizationId,
|
||||
isSlug,
|
||||
includeTeams,
|
||||
membersLimit,
|
||||
}: {
|
||||
organizationId: string;
|
||||
isSlug?: boolean;
|
||||
includeTeams?: boolean;
|
||||
membersLimit?: number;
|
||||
}) => {
|
||||
const org = await adapter.findOne<InferOrganization<O>>({
|
||||
model: "organization",
|
||||
@@ -373,7 +375,7 @@ export const getOrgAdapter = <O extends OrganizationOptions>(
|
||||
adapter.findMany<InferMember<O>>({
|
||||
model: "member",
|
||||
where: [{ field: "organizationId", value: org.id }],
|
||||
limit: options?.membershipLimit || 100,
|
||||
limit: membersLimit ?? options?.membershipLimit ?? 100,
|
||||
}),
|
||||
includeTeams
|
||||
? adapter.findMany<InferTeam<O>>({
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -553,6 +553,14 @@ export const getFullOrganization = <O extends OrganizationOptions>(
|
||||
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 = <O extends OrganizationOptions>(
|
||||
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 = <O extends OrganizationOptions>(
|
||||
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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user