From f6e1d900a233581c4c3646343a9161e402161078 Mon Sep 17 00:00:00 2001 From: ps-kwang <135043922+ps-kwang@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:33:35 -0400 Subject: [PATCH] feat(users): add api endpoint to list users in a team (#65) --- api/openapi.ts | 185 ++++++++++++++++++++++++++++++++++++++ api/teams.ts | 4 + commands/user/list/mod.ts | 78 ++++++++++++++++ commands/user/mod.ts | 27 ++++++ 4 files changed, 294 insertions(+) create mode 100644 commands/user/list/mod.ts create mode 100644 commands/user/mod.ts diff --git a/api/openapi.ts b/api/openapi.ts index 77380d4..a6a8a5a 100644 --- a/api/openapi.ts +++ b/api/openapi.ts @@ -760,6 +760,25 @@ export interface paths { */ patch: operations["teamSecrets-update"]; }; + "/teams/{teamId}/users": { + /** + * List team members + * @description List team members + */ + get: operations["teamMemberships-listByTeamId"]; + }; + "/teams/{teamId}/users/{userId}": { + /** + * Update a team membership + * @description Update a team membership + */ + put: operations["teamMemberships-update"]; + /** + * Remove a user from a team + * @description Remove a user from a team + */ + delete: operations["teamMemberships-removeUser"]; + }; "/templates": { /** * List templates @@ -15042,6 +15061,172 @@ export interface operations { default: components["responses"]["error"]; }; }; + /** + * List team members + * @description List team members + */ + "teamMemberships-listByTeamId": { + parameters: { + readonly query: { + /** @description Fetch the next page of results after this cursor. */ + after?: string; + /** @description The number of items to fetch after this page. */ + limit?: number; + /** @description Order results by one of these fields. */ + orderBy?: "dtConfirmed"; + /** @description The order to sort the results by. */ + order?: "asc" | "desc"; + /** @description Filter team members by their role on the team. */ + role?: "member" | "admin" | "owner"; + }; + readonly path: { + /** @description The team's ID */ + teamId: string; + }; + }; + responses: { + /** @description Successful response */ + 200: { + content: { + readonly "application/json": { + /** @description Whether there are more pages of results available. */ + readonly hasMore: boolean; + /** @description The items on this page. */ + readonly items: readonly ({ + /** + * Format: date-time + * @description The date the user confirmed their membership + * @default null + */ + readonly dtConfirmed?: Date; + /** @description Whether the user is an admin of the team */ + readonly isAdmin: boolean; + /** @description Whether the user is the owner of the team */ + readonly isOwner: boolean; + readonly user: { + /** @description The email address of the user */ + readonly email: string; + /** + * @description The first name of the user + * @default null + */ + readonly firstName?: string | null; + /** @description The ID of the user */ + readonly id: string; + /** + * Format: date-time + * @description The date the user was last active. + * @default null + */ + readonly lastActive?: Date; + /** + * @description The last name of the user + * @default null + */ + readonly lastName?: string | null; + /** + * @description The URL of the team's profile image. + * @default null + */ + readonly publicProfileImageUrl?: string | null; + }; + })[]; + /** @description The cursor required to fetch the next page of results. i.e. `?after=nextPage`. This is `null` when there is no next page. */ + readonly nextPage?: string; + }; + }; + }; + default: components["responses"]["error"]; + }; + }; + /** + * Update a team membership + * @description Update a team membership + */ + "teamMemberships-update": { + parameters: { + readonly path: { + /** @description The team's ID */ + teamId: string; + /** @description The user's ID */ + userId: string; + }; + }; + readonly requestBody: { + readonly content: { + readonly "application/json": { + /** @description Whether the user will gain admin access or not */ + readonly isAdmin: boolean; + }; + }; + }; + responses: { + /** @description Successful response */ + 200: { + content: { + readonly "application/json": { + /** + * Format: date-time + * @description The date the user confirmed their membership + * @default null + */ + readonly dtConfirmed?: Date; + /** + * Format: date-time + * @description The date the user was removed from the team + * @default null + */ + readonly dtDeleted?: Date; + /** @description Whether the user is an admin of the team */ + readonly isAdmin: boolean; + /** @description Whether the user is the owner of the team */ + readonly isOwner: boolean; + }; + }; + }; + default: components["responses"]["error"]; + }; + }; + /** + * Remove a user from a team + * @description Remove a user from a team + */ + "teamMemberships-removeUser": { + parameters: { + readonly path: { + /** @description The team's ID */ + teamId: string; + /** @description The user's ID */ + userId: string; + }; + }; + responses: { + /** @description Successful response */ + 200: { + content: { + readonly "application/json": { + /** + * Format: date-time + * @description The date the user confirmed their membership + * @default null + */ + readonly dtConfirmed?: Date; + /** + * Format: date-time + * @description The date the user was removed from the team + * @default null + */ + readonly dtDeleted?: Date; + /** @description Whether the user is an admin of the team */ + readonly isAdmin: boolean; + /** @description Whether the user is the owner of the team */ + readonly isOwner: boolean; + }; + }; + }; + default: components["responses"]["error"]; + }; + }; /** * List templates * @description Fetches a list of templates. diff --git a/api/teams.ts b/api/teams.ts index 0f80213..107abc6 100644 --- a/api/teams.ts +++ b/api/teams.ts @@ -7,3 +7,7 @@ export const teamSecrets = { update: client("/teams/{id}/secrets/{name}").patch, delete: client("/teams/{id}/secrets/{name}").delete, }; + +export const teamUsers = { + list: client("/teams/{teamId}/users").get, +}; diff --git a/commands/user/list/mod.ts b/commands/user/list/mod.ts new file mode 100644 index 0000000..108444c --- /dev/null +++ b/commands/user/list/mod.ts @@ -0,0 +1,78 @@ +import { command, flag, flags } from "../../../zcli.ts"; +import { asserts } from "../../../lib/asserts.ts"; +import { dataTable } from "../../../lib/data-table.ts"; +import { loading } from "../../../lib/loading.ts"; +import * as psFlags from "../../../flags.ts"; +import { pickJson } from "../../../lib/pick-json.ts"; +import { config } from "../../../config.ts"; +import { teamUsers } from "../../../api/teams.ts"; +import { defaultFields } from "../mod.ts"; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = []; + +export const list = command("list", { + short: "List users.", + long: ({ root }) => ` + List users in your team. + + Pick a subset of fields to display: + \`\`\` + ${root.name} user list -F email -F dtCreated + \`\`\` + `, + commands: subCommands, + flags: psFlags.paginator.merge(flags({ + "team-id": flag({ + aliases: ["t"], + short: "The ID of the team to list users in", + long: ` + The ID of the team to list users in. If not specified, the current team + will be used. + `, + }).ostring(), + })), + // We use command metadata in the `persistentPreRun` function to check if a + // command requires an API key. If it does, we'll check to see if one is + // set. If not, we'll throw an error. + meta: { + requireApiKey: true, + }, +}).run( + async function* ({ flags }) { + let teamId = flags["team-id"]; + if (!teamId) { + const team = await config.get("team"); + asserts(team, "You must be in a team to list users."); + teamId = team; + } + + const result = await loading( + teamUsers.list({ + limit: flags.limit, + after: flags.after, + order: flags.asc ? "asc" : undefined, + teamId, + }), + { enabled: !flags.json }, + ); + + asserts(result.ok, result); + + if (!flags.json) { + for await ( + const line of dataTable( + result.data.items, + flags.fields ?? defaultFields, + ) + ) { + yield line; + } + } else { + yield pickJson(result.data, flags.fields); + } + }, +); diff --git a/commands/user/mod.ts b/commands/user/mod.ts new file mode 100644 index 0000000..57bb292 --- /dev/null +++ b/commands/user/mod.ts @@ -0,0 +1,27 @@ +import { command } from "../../zcli.ts"; +import { list } from "./list/mod.ts"; + +export const defaultFields = [ + "id", + "teamId", +]; + +/** + * This variable is automatically generated by `zcli add`. Do not remove this + * or change its name unless you're no longer using `zcli add`. + */ +const subCommands: ReturnType[] = [ + list, +]; + +export const user = command("user", { + short: "Information about users", + long: ` + Show information about users. + `, + commands: subCommands, +}).run(function* ({ ctx }) { + for (const line of user.help(ctx)) { + yield line; + } +});