@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,94 @@
1
+ import { ServiceError, badRequestError } from '@lowerdeck/error';
2
+
3
+ type ErrorResponse = {
4
+ status?: number;
5
+ statusText?: string;
6
+ data?: unknown;
7
+ };
8
+
9
+ let isRecord = (value: unknown): value is Record<string, unknown> =>
10
+ typeof value === 'object' && value !== null;
11
+
12
+ let pushString = (messages: string[], value: unknown) => {
13
+ if (typeof value === 'string' && value.trim()) {
14
+ let trimmed = value.trim();
15
+ if (!messages.includes(trimmed)) messages.push(trimmed);
16
+ }
17
+ };
18
+
19
+ let collectTableauMessages = (value: unknown, messages: string[]) => {
20
+ if (!isRecord(value)) return;
21
+
22
+ pushString(messages, value.summary);
23
+ pushString(messages, value.detail);
24
+ pushString(messages, value.message);
25
+ pushString(messages, value.errorMessage);
26
+ pushString(messages, value.title);
27
+
28
+ if (typeof value.code === 'string' && value.code.trim()) {
29
+ pushString(messages, `code ${value.code}`);
30
+ }
31
+
32
+ let nestedError = value.error;
33
+ if (isRecord(nestedError)) collectTableauMessages(nestedError, messages);
34
+
35
+ let errors = value.errors;
36
+ if (Array.isArray(errors)) {
37
+ for (let item of errors) collectTableauMessages(item, messages);
38
+ }
39
+ };
40
+
41
+ let extractTableauMessage = (error: unknown) => {
42
+ let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
43
+ let errorData = isRecord(error)
44
+ ? (error.data as Record<string, unknown> | undefined)
45
+ : undefined;
46
+ let baggage = isRecord(errorData?.baggage)
47
+ ? (errorData.baggage as Record<string, unknown>)
48
+ : undefined;
49
+ let data = response?.data ?? baggage?.response;
50
+ let messages: string[] = [];
51
+
52
+ if (isRecord(data)) {
53
+ collectTableauMessages(data, messages);
54
+ } else if (typeof data === 'string') {
55
+ pushString(messages, data.slice(0, 500));
56
+ }
57
+
58
+ if (messages.length > 0) {
59
+ return messages.join(' - ');
60
+ }
61
+
62
+ if (error instanceof Error && error.message) {
63
+ return error.message;
64
+ }
65
+
66
+ return 'Unknown error';
67
+ };
68
+
69
+ export let tableauServiceError = (message: string) =>
70
+ new ServiceError(badRequestError({ message }));
71
+
72
+ export let tableauApiError = (error: unknown, operation = 'request') => {
73
+ if (error instanceof ServiceError) return error;
74
+
75
+ let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined;
76
+ let status = response?.status;
77
+ let statusLabel =
78
+ status !== undefined
79
+ ? `HTTP ${status}${response?.statusText ? ` ${response.statusText}` : ''}: `
80
+ : '';
81
+
82
+ let serviceError = tableauServiceError(
83
+ `Tableau API ${operation} failed: ${statusLabel}${extractTableauMessage(error)}`
84
+ );
85
+
86
+ serviceError.data.reason = 'tableau_api_error';
87
+ serviceError.data.upstreamStatus = status;
88
+
89
+ if (error instanceof Error) {
90
+ serviceError.setParent(error);
91
+ }
92
+
93
+ return serviceError;
94
+ };
@@ -0,0 +1,15 @@
1
+ import { TableauClient } from './client';
2
+
3
+ export let createClient = (
4
+ config: { serverUrl: string; apiVersion: string; siteContentUrl: string },
5
+ auth: { token: string; siteId: string; userId?: string; expiresAt?: string }
6
+ ) => {
7
+ return new TableauClient({
8
+ serverUrl: config.serverUrl,
9
+ apiVersion: config.apiVersion,
10
+ siteId: auth.siteId,
11
+ token: auth.token,
12
+ userId: auth.userId,
13
+ expiresAt: auth.expiresAt
14
+ });
15
+ };
@@ -0,0 +1,9 @@
1
+ export let normalizeBoolean = (value: unknown) => {
2
+ if (typeof value === 'boolean') return value;
3
+ if (typeof value === 'string') {
4
+ let normalized = value.toLowerCase();
5
+ if (normalized === 'true') return true;
6
+ if (normalized === 'false') return false;
7
+ }
8
+ return undefined;
9
+ };
package/src/spec.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { SlateSpecification } from 'slates';
2
+ import { auth } from './auth';
3
+ import { config } from './config';
4
+
5
+ export let spec = SlateSpecification.create({
6
+ key: 'tableau',
7
+ name: 'Tableau',
8
+ description: undefined,
9
+ metadata: {},
10
+ config,
11
+ auth
12
+ });
@@ -0,0 +1,112 @@
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
+ let filterValueSchema = z.union([z.string(), z.number(), z.boolean()]);
8
+
9
+ let pdfPageTypeSchema = z.enum([
10
+ 'A3',
11
+ 'A4',
12
+ 'A5',
13
+ 'B5',
14
+ 'Executive',
15
+ 'Folio',
16
+ 'Ledger',
17
+ 'Legal',
18
+ 'Letter',
19
+ 'Note',
20
+ 'Quarto',
21
+ 'Tabloid'
22
+ ]);
23
+
24
+ export let exportView = SlateTool.create(spec, {
25
+ name: 'Export View',
26
+ key: 'export_view',
27
+ description: `Export a Tableau view as CSV data, a PNG image, or a PDF file. Supports Tableau view filter query parameters and cache max-age controls.`,
28
+ tags: { readOnly: true }
29
+ })
30
+ .input(
31
+ z.object({
32
+ viewId: z.string().describe('LUID of the view to export'),
33
+ format: z.enum(['csv', 'image', 'pdf']).describe('Export format to retrieve'),
34
+ maxAgeMinutes: z
35
+ .number()
36
+ .int()
37
+ .min(1)
38
+ .optional()
39
+ .describe('Minimum cache age before Tableau refreshes the exported view'),
40
+ filters: z
41
+ .record(z.string(), filterValueSchema)
42
+ .optional()
43
+ .describe('View filters keyed by field name; sent as vf_<field>=value'),
44
+ imageResolution: z
45
+ .enum(['high'])
46
+ .optional()
47
+ .describe('Set to "high" for maximum PNG pixel density'),
48
+ vizHeight: z
49
+ .number()
50
+ .int()
51
+ .positive()
52
+ .optional()
53
+ .describe('Rendered PDF height in pixels'),
54
+ vizWidth: z
55
+ .number()
56
+ .int()
57
+ .positive()
58
+ .optional()
59
+ .describe('Rendered PDF width in pixels'),
60
+ pdfPageType: pdfPageTypeSchema.optional().describe('PDF page size'),
61
+ pdfOrientation: z.enum(['Portrait', 'Landscape']).optional().describe('PDF orientation')
62
+ })
63
+ )
64
+ .output(
65
+ z.object({
66
+ viewId: z.string(),
67
+ format: z.enum(['csv', 'image', 'pdf']),
68
+ contentType: z.string(),
69
+ csvData: z.string().optional(),
70
+ contentBase64: z.string().optional()
71
+ })
72
+ )
73
+ .handleInvocation(async ctx => {
74
+ if (ctx.input.format !== 'image' && ctx.input.imageResolution !== undefined) {
75
+ throw tableauServiceError('imageResolution is only valid when format is "image".');
76
+ }
77
+ if (ctx.input.format !== 'pdf') {
78
+ if (
79
+ ctx.input.vizHeight !== undefined ||
80
+ ctx.input.vizWidth !== undefined ||
81
+ ctx.input.pdfPageType !== undefined ||
82
+ ctx.input.pdfOrientation !== undefined
83
+ ) {
84
+ throw tableauServiceError(
85
+ 'vizHeight, vizWidth, pdfPageType, and pdfOrientation are only valid when format is "pdf".'
86
+ );
87
+ }
88
+ }
89
+
90
+ let client = createClient(ctx.config, ctx.auth);
91
+ let result = await client.exportView(ctx.input.viewId, ctx.input.format, {
92
+ maxAgeMinutes: ctx.input.maxAgeMinutes,
93
+ filters: ctx.input.filters,
94
+ imageResolution: ctx.input.imageResolution,
95
+ vizHeight: ctx.input.vizHeight,
96
+ vizWidth: ctx.input.vizWidth,
97
+ pdfPageType: ctx.input.pdfPageType,
98
+ pdfOrientation: ctx.input.pdfOrientation
99
+ });
100
+
101
+ return {
102
+ output: {
103
+ viewId: ctx.input.viewId,
104
+ format: ctx.input.format,
105
+ contentType: result.contentType,
106
+ csvData: result.encoding === 'text' ? result.data : undefined,
107
+ contentBase64: result.encoding === 'base64' ? result.data : undefined
108
+ },
109
+ message: `Exported Tableau view \`${ctx.input.viewId}\` as ${ctx.input.format}.`
110
+ };
111
+ })
112
+ .build();
@@ -0,0 +1,47 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+
6
+ export let getSiteInfo = SlateTool.create(spec, {
7
+ name: 'Get Site Info',
8
+ key: 'get_site_info',
9
+ description: `Retrieve information about the current Tableau site, including name, URL, storage usage, and configuration settings.`,
10
+ tags: { readOnly: true }
11
+ })
12
+ .input(z.object({}))
13
+ .output(
14
+ z.object({
15
+ siteId: z.string(),
16
+ name: z.string().optional(),
17
+ contentUrl: z.string().optional(),
18
+ adminMode: z.string().optional(),
19
+ state: z.string().optional(),
20
+ storageQuota: z.number().optional(),
21
+ numCreators: z.number().optional(),
22
+ numExplorers: z.number().optional(),
23
+ numViewers: z.number().optional(),
24
+ revisionHistoryEnabled: z.boolean().optional()
25
+ })
26
+ )
27
+ .handleInvocation(async ctx => {
28
+ let client = createClient(ctx.config, ctx.auth);
29
+ let site = await client.getSiteInfo();
30
+
31
+ return {
32
+ output: {
33
+ siteId: site.id,
34
+ name: site.name,
35
+ contentUrl: site.contentUrl,
36
+ adminMode: site.adminMode,
37
+ state: site.state,
38
+ storageQuota: site.storageQuota != null ? Number(site.storageQuota) : undefined,
39
+ numCreators: site.numCreators != null ? Number(site.numCreators) : undefined,
40
+ numExplorers: site.numExplorers != null ? Number(site.numExplorers) : undefined,
41
+ numViewers: site.numViewers != null ? Number(site.numViewers) : undefined,
42
+ revisionHistoryEnabled: site.revisionHistoryEnabled
43
+ },
44
+ message: `Site **${site.name}** (state: ${site.state || 'active'}).`
45
+ };
46
+ })
47
+ .build();
@@ -0,0 +1,31 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+
6
+ export let getViewData = SlateTool.create(spec, {
7
+ name: 'Get View Data',
8
+ key: 'get_view_data',
9
+ description: `Export the underlying data from a Tableau view as CSV. Useful for retrieving the tabular data behind a dashboard visualization.`,
10
+ tags: { readOnly: true }
11
+ })
12
+ .input(
13
+ z.object({
14
+ viewId: z.string().describe('LUID of the view to export data from')
15
+ })
16
+ )
17
+ .output(
18
+ z.object({
19
+ csvData: z.string().describe('CSV content of the view data')
20
+ })
21
+ )
22
+ .handleInvocation(async ctx => {
23
+ let client = createClient(ctx.config, ctx.auth);
24
+ let csvData = await client.getViewData(ctx.input.viewId);
25
+
26
+ return {
27
+ output: { csvData },
28
+ message: `Exported CSV data from view \`${ctx.input.viewId}\`.`
29
+ };
30
+ })
31
+ .build();
@@ -0,0 +1,18 @@
1
+ export * from './list-workbooks';
2
+ export * from './manage-workbook';
3
+ export * from './list-datasources';
4
+ export * from './manage-datasource';
5
+ export * from './list-views';
6
+ export * from './get-view-data';
7
+ export * from './export-view';
8
+ export * from './manage-custom-views';
9
+ export * from './manage-users';
10
+ export * from './manage-groups';
11
+ export * from './manage-projects';
12
+ export * from './manage-permissions';
13
+ export * from './manage-jobs';
14
+ export * from './manage-favorites';
15
+ export * from './manage-flows';
16
+ export * from './manage-collections';
17
+ export * from './manage-alerts';
18
+ export * from './get-site-info';
@@ -0,0 +1,78 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+
6
+ export let listDatasources = SlateTool.create(spec, {
7
+ name: 'List Data Sources',
8
+ key: 'list_datasources',
9
+ description: `List and search data sources on the Tableau site. Supports pagination, filtering, and sorting.`,
10
+ tags: { readOnly: true }
11
+ })
12
+ .input(
13
+ z.object({
14
+ pageSize: z.number().optional().describe('Number of items per page'),
15
+ pageNumber: z.number().optional().describe('Page number (1-based)'),
16
+ filter: z.string().optional().describe('Filter expression (e.g., "name:eq:Sales Data")'),
17
+ sort: z.string().optional().describe('Sort expression (e.g., "name:asc")')
18
+ })
19
+ )
20
+ .output(
21
+ z.object({
22
+ datasources: z.array(
23
+ z.object({
24
+ datasourceId: z.string(),
25
+ name: z.string(),
26
+ description: z.string().optional(),
27
+ contentUrl: z.string().optional(),
28
+ type: z.string().optional(),
29
+ isCertified: z.boolean().optional(),
30
+ certificationNote: z.string().optional(),
31
+ createdAt: z.string().optional(),
32
+ updatedAt: z.string().optional(),
33
+ projectId: z.string().optional(),
34
+ projectName: z.string().optional(),
35
+ ownerId: z.string().optional()
36
+ })
37
+ ),
38
+ totalCount: z.number(),
39
+ pageNumber: z.number(),
40
+ pageSize: z.number()
41
+ })
42
+ )
43
+ .handleInvocation(async ctx => {
44
+ let client = createClient(ctx.config, ctx.auth);
45
+ let result = await client.queryDatasources({
46
+ pageSize: ctx.input.pageSize,
47
+ pageNumber: ctx.input.pageNumber,
48
+ filter: ctx.input.filter,
49
+ sort: ctx.input.sort
50
+ });
51
+
52
+ let pagination = result.pagination || {};
53
+ let datasources = (result.datasources?.datasource || []).map((d: any) => ({
54
+ datasourceId: d.id,
55
+ name: d.name,
56
+ description: d.description,
57
+ contentUrl: d.contentUrl,
58
+ type: d.type,
59
+ isCertified: d.isCertified,
60
+ certificationNote: d.certificationNote,
61
+ createdAt: d.createdAt,
62
+ updatedAt: d.updatedAt,
63
+ projectId: d.project?.id,
64
+ projectName: d.project?.name,
65
+ ownerId: d.owner?.id
66
+ }));
67
+
68
+ return {
69
+ output: {
70
+ datasources,
71
+ totalCount: Number(pagination.totalAvailable || 0),
72
+ pageNumber: Number(pagination.pageNumber || 1),
73
+ pageSize: Number(pagination.pageSize || datasources.length)
74
+ },
75
+ message: `Found **${datasources.length}** data sources (${pagination.totalAvailable || 0} total).`
76
+ };
77
+ })
78
+ .build();
@@ -0,0 +1,70 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+
6
+ export let listViews = SlateTool.create(spec, {
7
+ name: 'List Views',
8
+ key: 'list_views',
9
+ description: `List and search views across the Tableau site. Supports pagination, filtering, and sorting.`,
10
+ tags: { readOnly: true }
11
+ })
12
+ .input(
13
+ z.object({
14
+ pageSize: z.number().optional().describe('Number of items per page'),
15
+ pageNumber: z.number().optional().describe('Page number (1-based)'),
16
+ filter: z.string().optional().describe('Filter expression (e.g., "name:eq:Revenue")'),
17
+ sort: z.string().optional().describe('Sort expression (e.g., "name:asc")')
18
+ })
19
+ )
20
+ .output(
21
+ z.object({
22
+ views: z.array(
23
+ z.object({
24
+ viewId: z.string(),
25
+ name: z.string(),
26
+ contentUrl: z.string().optional(),
27
+ createdAt: z.string().optional(),
28
+ updatedAt: z.string().optional(),
29
+ viewUrlName: z.string().optional(),
30
+ workbookId: z.string().optional(),
31
+ ownerId: z.string().optional()
32
+ })
33
+ ),
34
+ totalCount: z.number(),
35
+ pageNumber: z.number(),
36
+ pageSize: z.number()
37
+ })
38
+ )
39
+ .handleInvocation(async ctx => {
40
+ let client = createClient(ctx.config, ctx.auth);
41
+ let result = await client.queryViews({
42
+ pageSize: ctx.input.pageSize,
43
+ pageNumber: ctx.input.pageNumber,
44
+ filter: ctx.input.filter,
45
+ sort: ctx.input.sort
46
+ });
47
+
48
+ let pagination = result.pagination || {};
49
+ let views = (result.views?.view || []).map((v: any) => ({
50
+ viewId: v.id,
51
+ name: v.name,
52
+ contentUrl: v.contentUrl,
53
+ createdAt: v.createdAt,
54
+ updatedAt: v.updatedAt,
55
+ viewUrlName: v.viewUrlName,
56
+ workbookId: v.workbook?.id,
57
+ ownerId: v.owner?.id
58
+ }));
59
+
60
+ return {
61
+ output: {
62
+ views,
63
+ totalCount: Number(pagination.totalAvailable || 0),
64
+ pageNumber: Number(pagination.pageNumber || 1),
65
+ pageSize: Number(pagination.pageSize || views.length)
66
+ },
67
+ message: `Found **${views.length}** views (${pagination.totalAvailable || 0} total).`
68
+ };
69
+ })
70
+ .build();
@@ -0,0 +1,88 @@
1
+ import { SlateTool } from 'slates';
2
+ import { z } from 'zod';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { normalizeBoolean } from '../lib/normalizers';
6
+
7
+ export let listWorkbooks = SlateTool.create(spec, {
8
+ name: 'List Workbooks',
9
+ key: 'list_workbooks',
10
+ description: `List and search workbooks on the Tableau site. Supports pagination, filtering, and sorting to find specific workbooks.`,
11
+ tags: { readOnly: true }
12
+ })
13
+ .input(
14
+ z.object({
15
+ pageSize: z
16
+ .number()
17
+ .optional()
18
+ .describe('Number of items per page (default 100, max 1000)'),
19
+ pageNumber: z.number().optional().describe('Page number to retrieve (1-based)'),
20
+ filter: z
21
+ .string()
22
+ .optional()
23
+ .describe('Filter expression (e.g., "name:eq:Sales Dashboard")'),
24
+ sort: z
25
+ .string()
26
+ .optional()
27
+ .describe('Sort expression (e.g., "name:asc" or "updatedAt:desc")')
28
+ })
29
+ )
30
+ .output(
31
+ z.object({
32
+ workbooks: z.array(
33
+ z.object({
34
+ workbookId: z.string(),
35
+ name: z.string(),
36
+ description: z.string().optional(),
37
+ contentUrl: z.string().optional(),
38
+ webpageUrl: z.string().optional(),
39
+ showTabs: z.boolean().optional(),
40
+ size: z.number().optional(),
41
+ createdAt: z.string().optional(),
42
+ updatedAt: z.string().optional(),
43
+ projectId: z.string().optional(),
44
+ projectName: z.string().optional(),
45
+ ownerId: z.string().optional()
46
+ })
47
+ ),
48
+ totalCount: z.number(),
49
+ pageNumber: z.number(),
50
+ pageSize: z.number()
51
+ })
52
+ )
53
+ .handleInvocation(async ctx => {
54
+ let client = createClient(ctx.config, ctx.auth);
55
+ let result = await client.queryWorkbooks({
56
+ pageSize: ctx.input.pageSize,
57
+ pageNumber: ctx.input.pageNumber,
58
+ filter: ctx.input.filter,
59
+ sort: ctx.input.sort
60
+ });
61
+
62
+ let pagination = result.pagination || {};
63
+ let workbooks = (result.workbooks?.workbook || []).map((w: any) => ({
64
+ workbookId: w.id,
65
+ name: w.name,
66
+ description: w.description,
67
+ contentUrl: w.contentUrl,
68
+ webpageUrl: w.webpageUrl,
69
+ showTabs: normalizeBoolean(w.showTabs),
70
+ size: w.size ? Number(w.size) : undefined,
71
+ createdAt: w.createdAt,
72
+ updatedAt: w.updatedAt,
73
+ projectId: w.project?.id,
74
+ projectName: w.project?.name,
75
+ ownerId: w.owner?.id
76
+ }));
77
+
78
+ return {
79
+ output: {
80
+ workbooks,
81
+ totalCount: Number(pagination.totalAvailable || 0),
82
+ pageNumber: Number(pagination.pageNumber || 1),
83
+ pageSize: Number(pagination.pageSize || workbooks.length)
84
+ },
85
+ message: `Found **${workbooks.length}** workbooks (${pagination.totalAvailable || 0} total).`
86
+ };
87
+ })
88
+ .build();