@slates-integrations/tableau 0.2.0-rc.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +85 -0
  2. package/docs/SPEC.md +61 -0
  3. package/logo.png +0 -0
  4. package/package.json +19 -0
  5. package/slate.json +18 -0
  6. package/src/auth.ts +237 -0
  7. package/src/config.ts +17 -0
  8. package/src/index.ts +55 -0
  9. package/src/lib/client.ts +959 -0
  10. package/src/lib/errors.ts +94 -0
  11. package/src/lib/helpers.ts +15 -0
  12. package/src/lib/normalizers.ts +9 -0
  13. package/src/spec.ts +12 -0
  14. package/src/tools/export-view.ts +112 -0
  15. package/src/tools/get-site-info.ts +47 -0
  16. package/src/tools/get-view-data.ts +31 -0
  17. package/src/tools/index.ts +18 -0
  18. package/src/tools/list-datasources.ts +78 -0
  19. package/src/tools/list-views.ts +70 -0
  20. package/src/tools/list-workbooks.ts +88 -0
  21. package/src/tools/manage-alerts.ts +139 -0
  22. package/src/tools/manage-collections.ts +254 -0
  23. package/src/tools/manage-custom-views.ts +159 -0
  24. package/src/tools/manage-datasource.ts +129 -0
  25. package/src/tools/manage-favorites.ts +80 -0
  26. package/src/tools/manage-flows.ts +170 -0
  27. package/src/tools/manage-groups.ts +178 -0
  28. package/src/tools/manage-jobs.ts +120 -0
  29. package/src/tools/manage-permissions.ts +118 -0
  30. package/src/tools/manage-projects.ts +162 -0
  31. package/src/tools/manage-users.ts +184 -0
  32. package/src/tools/manage-workbook.ts +160 -0
  33. package/src/triggers/datasource-events.ts +119 -0
  34. package/src/triggers/index.ts +6 -0
  35. package/src/triggers/label-events.ts +98 -0
  36. package/src/triggers/site-events.ts +97 -0
  37. package/src/triggers/user-events.ts +98 -0
  38. package/src/triggers/view-events.ts +83 -0
  39. package/src/triggers/workbook-events.ts +108 -0
  40. package/tsconfig.json +23 -0
@@ -0,0 +1,80 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { tableauServiceError } from '../lib/errors';
6
+
7
+ export let manageFavorites = SlateTool.create(spec, {
8
+ name: 'Manage Favorites',
9
+ key: 'manage_favorites',
10
+ description: `List, add, or remove favorites for a user. Supports workbooks, views, data sources, projects, and flows.`
11
+ })
12
+ .input(
13
+ z.object({
14
+ action: z.enum(['list', 'add', 'remove']).describe('Operation to perform'),
15
+ userId: z.string().describe('User LUID'),
16
+ resourceType: z
17
+ .enum(['workbook', 'view', 'datasource', 'project', 'flow'])
18
+ .optional()
19
+ .describe('Type of resource (for add/remove)'),
20
+ resourceId: z.string().optional().describe('LUID of the resource (for add/remove)'),
21
+ label: z.string().optional().describe('Label for the favorite (for add)')
22
+ })
23
+ )
24
+ .output(
25
+ z.object({
26
+ favorites: z.any().optional().describe('User favorites list'),
27
+ added: z.boolean().optional(),
28
+ removed: z.boolean().optional()
29
+ })
30
+ )
31
+ .handleInvocation(async ctx => {
32
+ let client = createClient(ctx.config, ctx.auth);
33
+ let { action, userId } = ctx.input;
34
+
35
+ if (action === 'list') {
36
+ let result = await client.getFavorites(userId);
37
+ return {
38
+ output: { favorites: result.favorites },
39
+ message: `Retrieved favorites for user \`${userId}\`.`
40
+ };
41
+ }
42
+
43
+ if (action === 'add') {
44
+ if (!ctx.input.resourceType) {
45
+ throw tableauServiceError('resourceType is required for add action.');
46
+ }
47
+ if (!ctx.input.resourceId) {
48
+ throw tableauServiceError('resourceId is required for add action.');
49
+ }
50
+
51
+ await client.addFavorite(
52
+ userId,
53
+ ctx.input.resourceType,
54
+ ctx.input.resourceId,
55
+ ctx.input.label || ctx.input.resourceType
56
+ );
57
+ return {
58
+ output: { added: true },
59
+ message: `Added ${ctx.input.resourceType} \`${ctx.input.resourceId}\` to favorites.`
60
+ };
61
+ }
62
+
63
+ if (action === 'remove') {
64
+ if (!ctx.input.resourceType) {
65
+ throw tableauServiceError('resourceType is required for remove action.');
66
+ }
67
+ if (!ctx.input.resourceId) {
68
+ throw tableauServiceError('resourceId is required for remove action.');
69
+ }
70
+
71
+ await client.deleteFavorite(userId, ctx.input.resourceType + 's', ctx.input.resourceId);
72
+ return {
73
+ output: { removed: true },
74
+ message: `Removed ${ctx.input.resourceType} \`${ctx.input.resourceId}\` from favorites.`
75
+ };
76
+ }
77
+
78
+ throw tableauServiceError(`Unknown action: ${action}`);
79
+ })
80
+ .build();
@@ -0,0 +1,170 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { tableauServiceError } from '../lib/errors';
6
+
7
+ export let manageFlows = SlateTool.create(spec, {
8
+ name: 'Manage Flows',
9
+ key: 'manage_flows',
10
+ description: `List, get, update, delete, or run Tableau Prep flows. Use the **action** field to select the operation.`
11
+ })
12
+ .input(
13
+ z.object({
14
+ action: z
15
+ .enum(['list', 'get', 'update', 'delete', 'run'])
16
+ .describe('Operation to perform'),
17
+ flowId: z
18
+ .string()
19
+ .optional()
20
+ .describe('Flow LUID (required for get, update, delete, run)'),
21
+ name: z.string().optional().describe('New name (for update)'),
22
+ description: z.string().optional().describe('New description (for update)'),
23
+ projectId: z.string().optional().describe('New project LUID (for update)'),
24
+ ownerUserId: z.string().optional().describe('New owner LUID (for update)'),
25
+ pageSize: z.number().optional().describe('Page size for list'),
26
+ pageNumber: z.number().optional().describe('Page number for list'),
27
+ filter: z.string().optional().describe('Filter expression for list'),
28
+ sort: z.string().optional().describe('Sort expression for list')
29
+ })
30
+ )
31
+ .output(
32
+ z.object({
33
+ flows: z
34
+ .array(
35
+ z.object({
36
+ flowId: z.string(),
37
+ name: z.string().optional(),
38
+ description: z.string().optional(),
39
+ projectId: z.string().optional(),
40
+ projectName: z.string().optional(),
41
+ ownerId: z.string().optional(),
42
+ createdAt: z.string().optional(),
43
+ updatedAt: z.string().optional()
44
+ })
45
+ )
46
+ .optional(),
47
+ flow: z
48
+ .object({
49
+ flowId: z.string(),
50
+ name: z.string().optional(),
51
+ description: z.string().optional(),
52
+ projectId: z.string().optional(),
53
+ projectName: z.string().optional(),
54
+ ownerId: z.string().optional(),
55
+ createdAt: z.string().optional(),
56
+ updatedAt: z.string().optional()
57
+ })
58
+ .optional(),
59
+ totalCount: z.number().optional(),
60
+ jobId: z.string().optional(),
61
+ deleted: z.boolean().optional()
62
+ })
63
+ )
64
+ .handleInvocation(async ctx => {
65
+ let client = createClient(ctx.config, ctx.auth);
66
+ let { action } = ctx.input;
67
+
68
+ if (action === 'list') {
69
+ let result = await client.queryFlows({
70
+ pageSize: ctx.input.pageSize,
71
+ pageNumber: ctx.input.pageNumber,
72
+ filter: ctx.input.filter,
73
+ sort: ctx.input.sort
74
+ });
75
+ let pagination = result.pagination || {};
76
+ let flows = (result.flows?.flow || []).map((f: any) => ({
77
+ flowId: f.id,
78
+ name: f.name,
79
+ description: f.description,
80
+ projectId: f.project?.id,
81
+ projectName: f.project?.name,
82
+ ownerId: f.owner?.id,
83
+ createdAt: f.createdAt,
84
+ updatedAt: f.updatedAt
85
+ }));
86
+ return {
87
+ output: { flows, totalCount: Number(pagination.totalAvailable || 0) },
88
+ message: `Found **${flows.length}** flows (${pagination.totalAvailable || 0} total).`
89
+ };
90
+ }
91
+
92
+ if (action === 'get') {
93
+ if (!ctx.input.flowId) throw tableauServiceError('flowId is required for get action.');
94
+
95
+ let f = await client.getFlow(ctx.input.flowId);
96
+ return {
97
+ output: {
98
+ flow: {
99
+ flowId: f.id,
100
+ name: f.name,
101
+ description: f.description,
102
+ projectId: f.project?.id,
103
+ projectName: f.project?.name,
104
+ ownerId: f.owner?.id,
105
+ createdAt: f.createdAt,
106
+ updatedAt: f.updatedAt
107
+ }
108
+ },
109
+ message: `Retrieved flow **${f.name}**.`
110
+ };
111
+ }
112
+
113
+ if (action === 'update') {
114
+ if (!ctx.input.flowId)
115
+ throw tableauServiceError('flowId is required for update action.');
116
+ if (
117
+ ctx.input.name === undefined &&
118
+ ctx.input.description === undefined &&
119
+ ctx.input.projectId === undefined &&
120
+ ctx.input.ownerUserId === undefined
121
+ ) {
122
+ throw tableauServiceError('Provide at least one field to update a flow.');
123
+ }
124
+
125
+ let f = await client.updateFlow(ctx.input.flowId, {
126
+ name: ctx.input.name,
127
+ description: ctx.input.description,
128
+ projectId: ctx.input.projectId,
129
+ ownerUserId: ctx.input.ownerUserId
130
+ });
131
+ return {
132
+ output: {
133
+ flow: {
134
+ flowId: f.id,
135
+ name: f.name,
136
+ description: f.description,
137
+ projectId: f.project?.id,
138
+ projectName: f.project?.name,
139
+ ownerId: f.owner?.id,
140
+ updatedAt: f.updatedAt
141
+ }
142
+ },
143
+ message: `Updated flow **${f.name}**.`
144
+ };
145
+ }
146
+
147
+ if (action === 'delete') {
148
+ if (!ctx.input.flowId)
149
+ throw tableauServiceError('flowId is required for delete action.');
150
+
151
+ await client.deleteFlow(ctx.input.flowId);
152
+ return {
153
+ output: { deleted: true },
154
+ message: `Deleted flow \`${ctx.input.flowId}\`.`
155
+ };
156
+ }
157
+
158
+ if (action === 'run') {
159
+ if (!ctx.input.flowId) throw tableauServiceError('flowId is required for run action.');
160
+
161
+ let job = await client.runFlow(ctx.input.flowId);
162
+ return {
163
+ output: { jobId: job?.id },
164
+ message: `Started flow run for \`${ctx.input.flowId}\`. Job ID: \`${job?.id}\`.`
165
+ };
166
+ }
167
+
168
+ throw tableauServiceError(`Unknown action: ${action}`);
169
+ })
170
+ .build();
@@ -0,0 +1,178 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { tableauServiceError } from '../lib/errors';
6
+
7
+ export let manageGroups = SlateTool.create(spec, {
8
+ name: 'Manage Groups',
9
+ key: 'manage_groups',
10
+ description: `List, create, update, delete groups, and add or remove users from groups. Use the **action** field to select the operation.`,
11
+ tags: { destructive: true }
12
+ })
13
+ .input(
14
+ z.object({
15
+ action: z
16
+ .enum(['list', 'create', 'update', 'delete', 'addUser', 'removeUser', 'listUsers'])
17
+ .describe('Operation to perform'),
18
+ groupId: z
19
+ .string()
20
+ .optional()
21
+ .describe('Group LUID (required for update, delete, addUser, removeUser, listUsers)'),
22
+ userId: z.string().optional().describe('User LUID (for addUser, removeUser)'),
23
+ name: z.string().optional().describe('Group name (for create, update)'),
24
+ minimumSiteRole: z
25
+ .string()
26
+ .optional()
27
+ .describe('Minimum site role for group members (for create, update)'),
28
+ pageSize: z.number().optional().describe('Number of items per page'),
29
+ pageNumber: z.number().optional().describe('Page number (1-based)'),
30
+ filter: z.string().optional().describe('Filter expression for list'),
31
+ sort: z.string().optional().describe('Sort expression for list')
32
+ })
33
+ )
34
+ .output(
35
+ z.object({
36
+ groups: z
37
+ .array(
38
+ z.object({
39
+ groupId: z.string(),
40
+ name: z.string().optional(),
41
+ minimumSiteRole: z.string().optional()
42
+ })
43
+ )
44
+ .optional(),
45
+ group: z
46
+ .object({
47
+ groupId: z.string(),
48
+ name: z.string().optional(),
49
+ minimumSiteRole: z.string().optional()
50
+ })
51
+ .optional(),
52
+ users: z
53
+ .array(
54
+ z.object({
55
+ userId: z.string(),
56
+ name: z.string().optional(),
57
+ siteRole: z.string().optional()
58
+ })
59
+ )
60
+ .optional(),
61
+ totalCount: z.number().optional(),
62
+ deleted: z.boolean().optional(),
63
+ userAdded: z.boolean().optional(),
64
+ userRemoved: z.boolean().optional()
65
+ })
66
+ )
67
+ .handleInvocation(async ctx => {
68
+ let client = createClient(ctx.config, ctx.auth);
69
+ let { action } = ctx.input;
70
+
71
+ if (action === 'list') {
72
+ let result = await client.queryGroups({
73
+ pageSize: ctx.input.pageSize,
74
+ pageNumber: ctx.input.pageNumber,
75
+ filter: ctx.input.filter,
76
+ sort: ctx.input.sort
77
+ });
78
+ let pagination = result.pagination || {};
79
+ let groups = (result.groups?.group || []).map((g: any) => ({
80
+ groupId: g.id,
81
+ name: g.name,
82
+ minimumSiteRole: g.minimumSiteRole
83
+ }));
84
+ return {
85
+ output: { groups, totalCount: Number(pagination.totalAvailable || 0) },
86
+ message: `Found **${groups.length}** groups (${pagination.totalAvailable || 0} total).`
87
+ };
88
+ }
89
+
90
+ if (action === 'create') {
91
+ if (!ctx.input.name) throw tableauServiceError('name is required for create action.');
92
+
93
+ let g = await client.createGroup(ctx.input.name, ctx.input.minimumSiteRole);
94
+ return {
95
+ output: { group: { groupId: g.id, name: g.name, minimumSiteRole: g.minimumSiteRole } },
96
+ message: `Created group **${g.name}**.`
97
+ };
98
+ }
99
+
100
+ if (action === 'update') {
101
+ if (!ctx.input.groupId)
102
+ throw tableauServiceError('groupId is required for update action.');
103
+ if (ctx.input.name === undefined && ctx.input.minimumSiteRole === undefined) {
104
+ throw tableauServiceError('Provide name or minimumSiteRole to update a group.');
105
+ }
106
+
107
+ let g = await client.updateGroup(ctx.input.groupId, {
108
+ name: ctx.input.name,
109
+ minimumSiteRole: ctx.input.minimumSiteRole
110
+ });
111
+ return {
112
+ output: { group: { groupId: g.id, name: g.name, minimumSiteRole: g.minimumSiteRole } },
113
+ message: `Updated group **${g.name}**.`
114
+ };
115
+ }
116
+
117
+ if (action === 'delete') {
118
+ if (!ctx.input.groupId)
119
+ throw tableauServiceError('groupId is required for delete action.');
120
+
121
+ await client.deleteGroup(ctx.input.groupId);
122
+ return {
123
+ output: { deleted: true },
124
+ message: `Deleted group \`${ctx.input.groupId}\`.`
125
+ };
126
+ }
127
+
128
+ if (action === 'addUser') {
129
+ if (!ctx.input.groupId)
130
+ throw tableauServiceError('groupId is required for addUser action.');
131
+ if (!ctx.input.userId)
132
+ throw tableauServiceError('userId is required for addUser action.');
133
+
134
+ await client.addUserToGroup(ctx.input.groupId, ctx.input.userId);
135
+ return {
136
+ output: { userAdded: true },
137
+ message: `Added user \`${ctx.input.userId}\` to group \`${ctx.input.groupId}\`.`
138
+ };
139
+ }
140
+
141
+ if (action === 'removeUser') {
142
+ if (!ctx.input.groupId) {
143
+ throw tableauServiceError('groupId is required for removeUser action.');
144
+ }
145
+ if (!ctx.input.userId)
146
+ throw tableauServiceError('userId is required for removeUser action.');
147
+
148
+ await client.removeUserFromGroup(ctx.input.groupId, ctx.input.userId);
149
+ return {
150
+ output: { userRemoved: true },
151
+ message: `Removed user \`${ctx.input.userId}\` from group \`${ctx.input.groupId}\`.`
152
+ };
153
+ }
154
+
155
+ if (action === 'listUsers') {
156
+ if (!ctx.input.groupId) {
157
+ throw tableauServiceError('groupId is required for listUsers action.');
158
+ }
159
+
160
+ let result = await client.getUsersInGroup(ctx.input.groupId, {
161
+ pageSize: ctx.input.pageSize,
162
+ pageNumber: ctx.input.pageNumber
163
+ });
164
+ let pagination = result.pagination || {};
165
+ let users = (result.users?.user || []).map((u: any) => ({
166
+ userId: u.id,
167
+ name: u.name,
168
+ siteRole: u.siteRole
169
+ }));
170
+ return {
171
+ output: { users, totalCount: Number(pagination.totalAvailable || 0) },
172
+ message: `Found **${users.length}** users in group (${pagination.totalAvailable || 0} total).`
173
+ };
174
+ }
175
+
176
+ throw tableauServiceError(`Unknown action: ${action}`);
177
+ })
178
+ .build();
@@ -0,0 +1,120 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { tableauServiceError } from '../lib/errors';
6
+
7
+ export let manageJobs = SlateTool.create(spec, {
8
+ name: 'Manage Jobs',
9
+ key: 'manage_jobs',
10
+ description: `List, get details, or cancel background jobs (extract refreshes, flow runs, subscriptions). Use the **action** field to select the operation.`
11
+ })
12
+ .input(
13
+ z.object({
14
+ action: z.enum(['list', 'get', 'cancel']).describe('Operation to perform'),
15
+ jobId: z.string().optional().describe('Job LUID (required for get, cancel)'),
16
+ pageSize: z.number().optional().describe('Page size for list'),
17
+ pageNumber: z.number().optional().describe('Page number for list'),
18
+ filter: z
19
+ .string()
20
+ .optional()
21
+ .describe('Filter expression for list (e.g., "jobType:eq:refresh_extracts")')
22
+ })
23
+ )
24
+ .output(
25
+ z.object({
26
+ jobs: z
27
+ .array(
28
+ z.object({
29
+ jobId: z.string(),
30
+ jobType: z.string().optional(),
31
+ status: z.string().optional(),
32
+ createdAt: z.string().optional(),
33
+ startedAt: z.string().optional(),
34
+ endedAt: z.string().optional(),
35
+ progress: z.number().optional(),
36
+ title: z.string().optional()
37
+ })
38
+ )
39
+ .optional(),
40
+ job: z
41
+ .object({
42
+ jobId: z.string(),
43
+ jobType: z.string().optional(),
44
+ status: z.string().optional(),
45
+ createdAt: z.string().optional(),
46
+ startedAt: z.string().optional(),
47
+ endedAt: z.string().optional(),
48
+ progress: z.number().optional(),
49
+ title: z.string().optional(),
50
+ finishCode: z.number().optional()
51
+ })
52
+ .optional(),
53
+ totalCount: z.number().optional(),
54
+ cancelled: z.boolean().optional()
55
+ })
56
+ )
57
+ .handleInvocation(async ctx => {
58
+ let client = createClient(ctx.config, ctx.auth);
59
+ let { action } = ctx.input;
60
+
61
+ if (action === 'list') {
62
+ let result = await client.queryJobs({
63
+ pageSize: ctx.input.pageSize,
64
+ pageNumber: ctx.input.pageNumber,
65
+ filter: ctx.input.filter
66
+ });
67
+
68
+ let pagination = result.pagination || {};
69
+ let jobs = (result.backgroundJobs?.backgroundJob || []).map((j: any) => ({
70
+ jobId: j.id,
71
+ jobType: j.jobType,
72
+ status: j.status,
73
+ createdAt: j.createdAt,
74
+ startedAt: j.startedAt,
75
+ endedAt: j.endedAt,
76
+ progress: j.progress != null ? Number(j.progress) : undefined,
77
+ title: j.title
78
+ }));
79
+
80
+ return {
81
+ output: { jobs, totalCount: Number(pagination.totalAvailable || 0) },
82
+ message: `Found **${jobs.length}** jobs (${pagination.totalAvailable || 0} total).`
83
+ };
84
+ }
85
+
86
+ if (action === 'get') {
87
+ if (!ctx.input.jobId) throw tableauServiceError('jobId is required for get action.');
88
+
89
+ let j = await client.getJob(ctx.input.jobId);
90
+ return {
91
+ output: {
92
+ job: {
93
+ jobId: j.id,
94
+ jobType: j.jobType,
95
+ status: j.status,
96
+ createdAt: j.createdAt,
97
+ startedAt: j.startedAt,
98
+ endedAt: j.endedAt,
99
+ progress: j.progress != null ? Number(j.progress) : undefined,
100
+ title: j.title,
101
+ finishCode: j.finishCode != null ? Number(j.finishCode) : undefined
102
+ }
103
+ },
104
+ message: `Job \`${j.id}\` is **${j.status}** (type: ${j.jobType}).`
105
+ };
106
+ }
107
+
108
+ if (action === 'cancel') {
109
+ if (!ctx.input.jobId) throw tableauServiceError('jobId is required for cancel action.');
110
+
111
+ await client.cancelJob(ctx.input.jobId);
112
+ return {
113
+ output: { cancelled: true },
114
+ message: `Cancelled job \`${ctx.input.jobId}\`.`
115
+ };
116
+ }
117
+
118
+ throw tableauServiceError(`Unknown action: ${action}`);
119
+ })
120
+ .build();
@@ -0,0 +1,118 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { tableauServiceError } from '../lib/errors';
6
+
7
+ export let managePermissions = SlateTool.create(spec, {
8
+ name: 'Manage Permissions',
9
+ key: 'manage_permissions',
10
+ description: `Query, add, or delete permissions on Tableau resources (workbooks, datasources, projects, views, flows). Permissions are granted to users or groups with specific capability modes.`,
11
+ instructions: [
12
+ 'For resourceType use the plural API path: "workbooks", "datasources", "projects", "views", "flows", or "collections".',
13
+ 'For delete, granteeType must be "users" or "groups" (plural).'
14
+ ]
15
+ })
16
+ .input(
17
+ z.object({
18
+ action: z.enum(['query', 'add', 'delete']).describe('Operation to perform'),
19
+ resourceType: z
20
+ .enum(['workbooks', 'datasources', 'projects', 'views', 'flows', 'collections'])
21
+ .describe(
22
+ 'Resource type path: "workbooks", "datasources", "projects", "views", "flows", or "collections"'
23
+ ),
24
+ resourceId: z.string().describe('LUID of the resource'),
25
+ permissions: z
26
+ .array(
27
+ z.object({
28
+ granteeType: z
29
+ .enum(['user', 'group'])
30
+ .describe('Whether the grantee is a user or group'),
31
+ granteeId: z.string().describe('LUID of the user or group'),
32
+ capabilities: z
33
+ .array(
34
+ z.object({
35
+ name: z
36
+ .string()
37
+ .describe('Capability name (e.g., "Read", "Write", "ExportData")'),
38
+ mode: z.enum(['Allow', 'Deny']).describe('Permission mode')
39
+ })
40
+ )
41
+ .describe('Capabilities to grant')
42
+ })
43
+ )
44
+ .optional()
45
+ .describe('Permissions to add (for add action)'),
46
+ granteeType: z
47
+ .enum(['users', 'groups'])
48
+ .optional()
49
+ .describe('Grantee type plural (for delete)'),
50
+ granteeId: z.string().optional().describe('LUID of user or group (for delete)'),
51
+ capabilityName: z.string().optional().describe('Capability name to delete (for delete)'),
52
+ capabilityMode: z
53
+ .enum(['Allow', 'Deny'])
54
+ .optional()
55
+ .describe('Capability mode to delete (for delete)')
56
+ })
57
+ )
58
+ .output(
59
+ z.object({
60
+ permissions: z.any().optional(),
61
+ deleted: z.boolean().optional()
62
+ })
63
+ )
64
+ .handleInvocation(async ctx => {
65
+ let client = createClient(ctx.config, ctx.auth);
66
+ let { action, resourceType, resourceId } = ctx.input;
67
+
68
+ if (action === 'query') {
69
+ let perms = await client.queryPermissions(resourceType, resourceId);
70
+ return {
71
+ output: { permissions: perms },
72
+ message: `Retrieved permissions for ${resourceType} \`${resourceId}\`.`
73
+ };
74
+ }
75
+
76
+ if (action === 'add') {
77
+ if (!ctx.input.permissions?.length) {
78
+ throw tableauServiceError('permissions is required for add action.');
79
+ }
80
+
81
+ let perms = await client.addPermissions(resourceType, resourceId, ctx.input.permissions);
82
+ return {
83
+ output: { permissions: perms },
84
+ message: `Added permissions to ${resourceType} \`${resourceId}\`.`
85
+ };
86
+ }
87
+
88
+ if (action === 'delete') {
89
+ if (!ctx.input.granteeType) {
90
+ throw tableauServiceError('granteeType is required for delete action.');
91
+ }
92
+ if (!ctx.input.granteeId) {
93
+ throw tableauServiceError('granteeId is required for delete action.');
94
+ }
95
+ if (!ctx.input.capabilityName) {
96
+ throw tableauServiceError('capabilityName is required for delete action.');
97
+ }
98
+ if (!ctx.input.capabilityMode) {
99
+ throw tableauServiceError('capabilityMode is required for delete action.');
100
+ }
101
+
102
+ await client.deletePermission(
103
+ resourceType,
104
+ resourceId,
105
+ ctx.input.granteeType,
106
+ ctx.input.granteeId,
107
+ ctx.input.capabilityName,
108
+ ctx.input.capabilityMode
109
+ );
110
+ return {
111
+ output: { deleted: true },
112
+ message: `Deleted ${ctx.input.capabilityName} (${ctx.input.capabilityMode}) permission from ${resourceType} \`${resourceId}\`.`
113
+ };
114
+ }
115
+
116
+ throw tableauServiceError(`Unknown action: ${action}`);
117
+ })
118
+ .build();