mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-18 17:50:21 +00:00
fix: page GET /teams like every other team listing
`listTeamsForUser` returned every row and the controller never read `req.query`, so `limit`, `cursor` and `includeTotal` were accepted and ignored on the one team route that did not paginate. The docs promise a capped page there, and every sibling listing already honours it. Keyset-paginated on `id` per doc/pagination.md, with the envelope the other routes return. The app-actor directory filter still runs over the page, so an app sees the same teams it saw before, just a page at a time.
This commit is contained in:
1 parent
71399c5939
commit
f3b05ca822
3 files changed
+88
-12
No files matched your search
@@ -111,16 +111,25 @@ export class TeamController extends PuterController {
|
||||
async listTeams(req: Request, res: Response): Promise<void> {
|
||||
const userId = this.#requireUserId(req);
|
||||
await this.#requireTeamsAvailable(req, userId);
|
||||
const teams = await this.stores.team.listTeamsForUser(userId);
|
||||
const page = await this.stores.team.listTeamsForUser(userId, {
|
||||
limit: req.query.limit,
|
||||
cursor:
|
||||
typeof req.query.cursor === 'string'
|
||||
? req.query.cursor
|
||||
: undefined,
|
||||
includeTotal: req.query.includeTotal === 'true',
|
||||
});
|
||||
// An app only sees a team whose owner opened the directory to apps;
|
||||
// a team that hasn't is indistinguishable from no team at all.
|
||||
const visible = isAccountContext(req.actor)
|
||||
? teams
|
||||
: teams.filter((t) => this.services.team.isDirectoryOpen(t));
|
||||
? page.items
|
||||
: page.items.filter((t) => this.services.team.isDirectoryOpen(t));
|
||||
res.json({
|
||||
items: visible.map((t) =>
|
||||
toClientTeam(t, t.owner_user_id === userId),
|
||||
),
|
||||
...(page.cursor ? { cursor: page.cursor } : {}),
|
||||
...(page.total === undefined ? {} : { total: page.total }),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -467,8 +467,8 @@ describe('TeamStore', () => {
|
||||
await store.addMember(a.uid, member.id, { orgOwned: true });
|
||||
await store.addMember(b.uid, member.id, { orgOwned: true });
|
||||
|
||||
const teams = await store.listTeamsForUser(member.id);
|
||||
expect(teams.map((t) => t.uid).sort()).toEqual([a.uid, b.uid].sort());
|
||||
const { items } = await store.listTeamsForUser(member.id);
|
||||
expect(items.map((t) => t.uid).sort()).toEqual([a.uid, b.uid].sort());
|
||||
});
|
||||
|
||||
it('drops a soft-deleted team from the user\'s list', async () => {
|
||||
@@ -481,10 +481,41 @@ describe('TeamStore', () => {
|
||||
await store.addMember(team.uid, member.id, { orgOwned: true });
|
||||
await store.softDelete(team.uid);
|
||||
|
||||
expect(await store.listTeamsForUser(member.id)).toEqual([]);
|
||||
expect((await store.listTeamsForUser(member.id)).items).toEqual([]);
|
||||
await expect(store.isMember(team.uid, member.id)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('pages the user team list and counts on request', async () => {
|
||||
const member = await makeUser();
|
||||
const made = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const team = await store.create({
|
||||
ownerUserId: owner.id,
|
||||
name: `Paged ${i}`,
|
||||
handle: freeHandle(),
|
||||
});
|
||||
await store.addMember(team.uid, member.id, { orgOwned: true });
|
||||
made.push(team.uid);
|
||||
}
|
||||
|
||||
const first = await store.listTeamsForUser(member.id, {
|
||||
limit: 2,
|
||||
includeTotal: true,
|
||||
});
|
||||
expect(first.items).toHaveLength(2);
|
||||
expect(first.total).toBe(3);
|
||||
expect(first.cursor).toBeTruthy();
|
||||
|
||||
const second = await store.listTeamsForUser(member.id, {
|
||||
cursor: first.cursor,
|
||||
});
|
||||
expect(second.items).toHaveLength(1);
|
||||
expect(second.cursor).toBeUndefined();
|
||||
expect(
|
||||
[...first.items, ...second.items].map((t) => t.uid).sort(),
|
||||
).toEqual([...made].sort());
|
||||
});
|
||||
|
||||
it('refuses to add a member to a group that is not a team', async () => {
|
||||
const [seeded] = (await server.clients.db.read(
|
||||
'SELECT `uid` FROM `group` WHERE `kind` IS NULL LIMIT 1',
|
||||
|
||||
@@ -610,14 +610,50 @@ export class TeamStore extends PuterStore {
|
||||
}
|
||||
|
||||
/** Teams this user belongs to, oldest first. */
|
||||
async listTeamsForUser(userId: number): Promise<TeamRow[]> {
|
||||
const rows = await this.clients.db.read(
|
||||
/** A user's teams, keyset-paginated on `id` per doc/pagination.md. */
|
||||
async listTeamsForUser(
|
||||
userId: number,
|
||||
opts: { limit?: unknown; cursor?: string; includeTotal?: boolean } = {},
|
||||
): Promise<PageResult<TeamRow>> {
|
||||
const limit =
|
||||
normalizeLimit(opts.limit, { cap: MEMBER_PAGE_CAP }) ??
|
||||
MEMBER_PAGE_SIZE;
|
||||
const page = decodeCursor(opts.cursor, 'team cursor');
|
||||
const after = typeof page?.id === 'number' ? page.id : null;
|
||||
|
||||
const rows = (await this.clients.db.read(
|
||||
'SELECT g.* FROM `group` g ' +
|
||||
'JOIN `jct_user_group` ug ON ug.`group_id` = g.`id` ' +
|
||||
`WHERE ug.\`user_id\` = ? AND g.${this.#live()} ORDER BY g.\`id\``,
|
||||
[userId, TEAM_KIND],
|
||||
);
|
||||
return rows as unknown as TeamRow[];
|
||||
`WHERE ug.\`user_id\` = ? AND g.${this.#live()}` +
|
||||
(after === null ? '' : ' AND g.`id` > ?') +
|
||||
' ORDER BY g.`id` LIMIT ?',
|
||||
after === null
|
||||
? [userId, TEAM_KIND, limit + 1]
|
||||
: [userId, TEAM_KIND, after, limit + 1],
|
||||
)) as unknown as TeamRow[];
|
||||
|
||||
const items = rows.slice(0, limit);
|
||||
const cursor =
|
||||
rows.length > limit
|
||||
? encodeCursor({ id: items[items.length - 1].id })
|
||||
: undefined;
|
||||
|
||||
let total: number | undefined;
|
||||
if (opts.includeTotal) {
|
||||
const totals = (await this.clients.db.read(
|
||||
'SELECT COUNT(*) AS n FROM `group` g ' +
|
||||
'JOIN `jct_user_group` ug ON ug.`group_id` = g.`id` ' +
|
||||
`WHERE ug.\`user_id\` = ? AND g.${this.#live()}`,
|
||||
[userId, TEAM_KIND],
|
||||
)) as unknown as Array<{ n: number | string }>;
|
||||
total = Number(totals[0]?.n ?? 0);
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
...(cursor ? { cursor } : {}),
|
||||
...(total === undefined ? {} : { total }),
|
||||
};
|
||||
}
|
||||
|
||||
/** `orgOwned` decides who pays: 1 team-created, 0 the team owner. */
|
||||
|
||||
Reference in new issue
Block a user