Skip to content

Commit 0148b94

Browse files
authored
feat: add permissions command for managing plan and member permissions (#14)
Add `permissions` command with list, create, update, delete, and link/unlink subcommands for managing permissions on plans and members. Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent d963262 commit 0148b94

5 files changed

Lines changed: 495 additions & 0 deletions

File tree

ARCHITECTURE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ memberstack-cli/
1919
│ │ ├── auth.ts # OAuth login, logout, status
2020
│ │ ├── custom-fields.ts # Custom field listing
2121
│ │ ├── members.ts # Member CRUD, search, pagination
22+
│ │ ├── permissions.ts # Permission CRUD, link/unlink to plans and members
2223
│ │ ├── plans.ts # Plan CRUD, ordering, redirects, permissions
2324
│ │ ├── prices.ts # Price management (create, update, activate, deactivate, delete)
2425
│ │ ├── records.ts # Record CRUD, query, find, import/export, bulk ops
@@ -43,6 +44,7 @@ memberstack-cli/
4344
│ │ ├── apps.test.ts
4445
│ │ ├── custom-fields.test.ts
4546
│ │ ├── members.test.ts
47+
│ │ ├── permissions.test.ts
4648
│ │ ├── plans.test.ts
4749
│ │ ├── prices.test.ts
4850
│ │ ├── records.test.ts

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ memberstack skills add memberstack-cli
5959
| `whoami` | Show current authenticated app and user |
6060
| `apps` | View, create, update, delete, and restore apps |
6161
| `members` | List, create, update, delete, import/export, bulk ops |
62+
| `permissions` | Create, update, delete, and link/unlink to plans and members |
6263
| `plans` | List, create, update, delete, and reorder plans |
6364
| `prices` | Create, update, activate, deactivate, and delete prices |
6465
| `tables` | List, create, update, delete, and describe schema |

src/commands/permissions.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { Command } from "commander";
2+
import yoctoSpinner from "yocto-spinner";
3+
import { graphqlRequest } from "../lib/graphql-client.js";
4+
import {
5+
printError,
6+
printRecord,
7+
printSuccess,
8+
printTable,
9+
} from "../lib/utils.js";
10+
11+
interface Permission {
12+
description: string | null;
13+
id: string;
14+
name: string;
15+
}
16+
17+
const PERMISSION_FIELDS = `
18+
id
19+
name
20+
description
21+
`;
22+
23+
const collect = (value: string, previous: string[]): string[] => [
24+
...previous,
25+
value,
26+
];
27+
28+
export const permissionsCommand = new Command("permissions")
29+
.usage("<command> [options]")
30+
.description("Manage permissions");
31+
32+
permissionsCommand
33+
.command("list")
34+
.description("List all permissions")
35+
.action(async () => {
36+
const spinner = yoctoSpinner({ text: "Fetching permissions..." }).start();
37+
try {
38+
const result = await graphqlRequest<{
39+
getPermissions: Permission[];
40+
}>({
41+
query: `query { getPermissions { ${PERMISSION_FIELDS} } }`,
42+
});
43+
spinner.stop();
44+
printTable(result.getPermissions);
45+
} catch (error) {
46+
spinner.stop();
47+
printError(
48+
error instanceof Error ? error.message : "An unknown error occurred"
49+
);
50+
process.exitCode = 1;
51+
}
52+
});
53+
54+
permissionsCommand
55+
.command("create")
56+
.description("Create a permission")
57+
.requiredOption("--name <name>", "Permission name")
58+
.option("--description <desc>", "Permission description")
59+
.action(async (opts: { name: string; description?: string }) => {
60+
const spinner = yoctoSpinner({ text: "Creating permission..." }).start();
61+
try {
62+
const input: Record<string, string> = { name: opts.name };
63+
if (opts.description) {
64+
input.description = opts.description;
65+
}
66+
const result = await graphqlRequest<{
67+
createPermission: Permission;
68+
}>({
69+
query: `mutation($input: CreatePermissionInput!) {
70+
createPermission(input: $input) {
71+
${PERMISSION_FIELDS}
72+
}
73+
}`,
74+
variables: { input },
75+
});
76+
spinner.stop();
77+
printSuccess("Permission created successfully.");
78+
printRecord(result.createPermission);
79+
} catch (error) {
80+
spinner.stop();
81+
printError(
82+
error instanceof Error ? error.message : "An unknown error occurred"
83+
);
84+
process.exitCode = 1;
85+
}
86+
});
87+
88+
permissionsCommand
89+
.command("update")
90+
.description("Update a permission")
91+
.argument("<permissionId>", "Permission ID to update")
92+
.option("--name <name>", "Permission name")
93+
.option("--description <desc>", "Permission description")
94+
.action(
95+
async (
96+
permissionId: string,
97+
opts: { name?: string; description?: string }
98+
) => {
99+
const input: Record<string, string> = { permissionId };
100+
if (opts.name) {
101+
input.name = opts.name;
102+
}
103+
if (opts.description) {
104+
input.description = opts.description;
105+
}
106+
107+
if (Object.keys(input).length <= 1) {
108+
printError(
109+
"No update options provided. Use --help to see available options."
110+
);
111+
process.exitCode = 1;
112+
return;
113+
}
114+
115+
const spinner = yoctoSpinner({ text: "Updating permission..." }).start();
116+
try {
117+
const result = await graphqlRequest<{
118+
updatePermission: Permission;
119+
}>({
120+
query: `mutation($input: UpdatePermissionInput!) {
121+
updatePermission(input: $input) {
122+
${PERMISSION_FIELDS}
123+
}
124+
}`,
125+
variables: { input },
126+
});
127+
spinner.stop();
128+
printSuccess("Permission updated successfully.");
129+
printRecord(result.updatePermission);
130+
} catch (error) {
131+
spinner.stop();
132+
printError(
133+
error instanceof Error ? error.message : "An unknown error occurred"
134+
);
135+
process.exitCode = 1;
136+
}
137+
}
138+
);
139+
140+
permissionsCommand
141+
.command("delete")
142+
.description("Delete a permission")
143+
.argument("<permissionId>", "Permission ID to delete")
144+
.action(async (permissionId: string) => {
145+
const spinner = yoctoSpinner({ text: "Deleting permission..." }).start();
146+
try {
147+
const result = await graphqlRequest<{
148+
deletePermission: Permission;
149+
}>({
150+
query: `mutation($input: DeletePermissionInput!) {
151+
deletePermission(input: $input) {
152+
${PERMISSION_FIELDS}
153+
}
154+
}`,
155+
variables: { input: { permissionId } },
156+
});
157+
spinner.stop();
158+
printSuccess(`Permission "${result.deletePermission.name}" deleted.`);
159+
} catch (error) {
160+
spinner.stop();
161+
printError(
162+
error instanceof Error ? error.message : "An unknown error occurred"
163+
);
164+
process.exitCode = 1;
165+
}
166+
});
167+
168+
permissionsCommand
169+
.command("link-plan")
170+
.description("Link permissions to a plan")
171+
.requiredOption("--plan-id <id>", "Plan ID")
172+
.requiredOption(
173+
"--permission-id <id>",
174+
"Permission ID (repeatable)",
175+
collect,
176+
[]
177+
)
178+
.action(async (opts: { planId: string; permissionId: string[] }) => {
179+
const spinner = yoctoSpinner({
180+
text: "Linking permissions to plan...",
181+
}).start();
182+
try {
183+
await graphqlRequest({
184+
query: `mutation($input: LinkPermissionsToPlanInput!) {
185+
linkPermissionsToPlan(input: $input) { id name }
186+
}`,
187+
variables: {
188+
input: { planId: opts.planId, permissionIds: opts.permissionId },
189+
},
190+
});
191+
spinner.stop();
192+
printSuccess(
193+
`Linked ${opts.permissionId.length} permission(s) to plan ${opts.planId}.`
194+
);
195+
} catch (error) {
196+
spinner.stop();
197+
printError(
198+
error instanceof Error ? error.message : "An unknown error occurred"
199+
);
200+
process.exitCode = 1;
201+
}
202+
});
203+
204+
permissionsCommand
205+
.command("unlink-plan")
206+
.description("Unlink a permission from a plan")
207+
.requiredOption("--plan-id <id>", "Plan ID")
208+
.requiredOption("--permission-id <id>", "Permission ID")
209+
.action(async (opts: { planId: string; permissionId: string }) => {
210+
const spinner = yoctoSpinner({
211+
text: "Unlinking permission from plan...",
212+
}).start();
213+
try {
214+
await graphqlRequest({
215+
query: `mutation($input: DetachPermissionFromPlanInput!) {
216+
detachPermissionFromPlan(input: $input) { id name }
217+
}`,
218+
variables: {
219+
input: { planId: opts.planId, permissionId: opts.permissionId },
220+
},
221+
});
222+
spinner.stop();
223+
printSuccess(
224+
`Unlinked permission ${opts.permissionId} from plan ${opts.planId}.`
225+
);
226+
} catch (error) {
227+
spinner.stop();
228+
printError(
229+
error instanceof Error ? error.message : "An unknown error occurred"
230+
);
231+
process.exitCode = 1;
232+
}
233+
});
234+
235+
permissionsCommand
236+
.command("link-member")
237+
.description("Link permissions to a member")
238+
.requiredOption("--member-id <id>", "Member ID")
239+
.requiredOption(
240+
"--permission-id <id>",
241+
"Permission ID (repeatable)",
242+
collect,
243+
[]
244+
)
245+
.action(async (opts: { memberId: string; permissionId: string[] }) => {
246+
const spinner = yoctoSpinner({
247+
text: "Linking permissions to member...",
248+
}).start();
249+
try {
250+
await graphqlRequest({
251+
query: `mutation($input: LinkPermissionsToMemberInput!) {
252+
linkPermissionsToMember(input: $input) { id }
253+
}`,
254+
variables: {
255+
input: { memberId: opts.memberId, permissionIds: opts.permissionId },
256+
},
257+
});
258+
spinner.stop();
259+
printSuccess(
260+
`Linked ${opts.permissionId.length} permission(s) to member ${opts.memberId}.`
261+
);
262+
} catch (error) {
263+
spinner.stop();
264+
printError(
265+
error instanceof Error ? error.message : "An unknown error occurred"
266+
);
267+
process.exitCode = 1;
268+
}
269+
});
270+
271+
permissionsCommand
272+
.command("unlink-member")
273+
.description("Unlink a permission from a member")
274+
.requiredOption("--member-id <id>", "Member ID")
275+
.requiredOption("--permission-id <id>", "Permission ID")
276+
.action(async (opts: { memberId: string; permissionId: string }) => {
277+
const spinner = yoctoSpinner({
278+
text: "Unlinking permission from member...",
279+
}).start();
280+
try {
281+
await graphqlRequest({
282+
query: `mutation($input: DetachPermissionFromMemberInput!) {
283+
detachPermissionFromMember(input: $input) { id }
284+
}`,
285+
variables: {
286+
input: { memberId: opts.memberId, permissionId: opts.permissionId },
287+
},
288+
});
289+
spinner.stop();
290+
printSuccess(
291+
`Unlinked permission ${opts.permissionId} from member ${opts.memberId}.`
292+
);
293+
} catch (error) {
294+
spinner.stop();
295+
printError(
296+
error instanceof Error ? error.message : "An unknown error occurred"
297+
);
298+
process.exitCode = 1;
299+
}
300+
});

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { appsCommand } from "./commands/apps.js";
77
import { authCommand } from "./commands/auth.js";
88
import { customFieldsCommand } from "./commands/custom-fields.js";
99
import { membersCommand } from "./commands/members.js";
10+
import { permissionsCommand } from "./commands/permissions.js";
1011
import { plansCommand } from "./commands/plans.js";
1112
import { pricesCommand } from "./commands/prices.js";
1213
import { recordsCommand } from "./commands/records.js";
@@ -60,6 +61,7 @@ program.addCommand(appsCommand);
6061
program.addCommand(authCommand);
6162
program.addCommand(whoamiCommand);
6263
program.addCommand(membersCommand);
64+
program.addCommand(permissionsCommand);
6365
program.addCommand(plansCommand);
6466
program.addCommand(pricesCommand);
6567
program.addCommand(tablesCommand);

0 commit comments

Comments
 (0)