@commandable/integration-data 0.0.1 → 0.0.5
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.
- package/dist/credentials-index.d.ts +4 -21
- package/dist/credentials-index.d.ts.map +1 -1
- package/dist/credentials-index.js +407 -215
- package/dist/credentials-index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +38 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +70 -16
- package/dist/loader.js.map +1 -1
- package/integrations/__tests__/liveHarness.ts +84 -0
- package/integrations/__tests__/usageParity.ts +54 -0
- package/integrations/airtable/__tests__/get_handlers.test.ts +43 -31
- package/integrations/airtable/__tests__/usage_parity.test.ts +3 -29
- package/integrations/airtable/__tests__/write_and_admin_handlers.test.ts +20 -17
- package/integrations/airtable/credentials.json +21 -16
- package/integrations/github/__tests__/get_handlers.test.ts +101 -108
- package/integrations/github/__tests__/usage_parity.test.ts +15 -27
- package/integrations/github/__tests__/write_handlers.test.ts +223 -306
- package/integrations/github/credentials.json +40 -15
- package/integrations/github/credentials_hint_classic_pat.md +8 -0
- package/integrations/github/credentials_hint_fine_grained_pat.md +9 -0
- package/integrations/github/handlers/create_commit.js +2 -17
- package/integrations/github/manifest.json +2 -2
- package/integrations/google-calendar/__tests__/get_handlers.test.ts +21 -13
- package/integrations/google-calendar/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +24 -17
- package/integrations/google-calendar/credentials.json +50 -29
- package/integrations/google-calendar/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-calendar/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/__tests__/get_handlers.test.ts +87 -61
- package/integrations/google-docs/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-docs/__tests__/write_handlers.test.ts +251 -245
- package/integrations/google-docs/credentials.json +50 -29
- package/integrations/google-docs/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-docs/credentials_hint_service_account.md +10 -0
- package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
- package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
- package/integrations/google-drive/__tests__/handlers.test.ts +102 -0
- package/integrations/google-drive/credentials.json +57 -0
- package/integrations/google-drive/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-drive/credentials_hint_service_account.md +10 -0
- package/integrations/google-drive/handlers/create_file.js +15 -0
- package/integrations/google-drive/handlers/create_folder.js +15 -0
- package/integrations/google-drive/handlers/delete_file.js +14 -0
- package/integrations/google-drive/handlers/get_file.js +7 -0
- package/integrations/google-drive/handlers/move_file.js +12 -0
- package/integrations/google-drive/manifest.json +42 -0
- package/integrations/google-drive/schemas/create_file.json +12 -0
- package/integrations/google-drive/schemas/create_folder.json +11 -0
- package/integrations/google-drive/schemas/delete_file.json +10 -0
- package/integrations/google-drive/schemas/get_file.json +10 -0
- package/integrations/google-drive/schemas/move_file.json +12 -0
- package/integrations/google-sheet/__tests__/get_handlers.test.ts +48 -55
- package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +65 -63
- package/integrations/google-sheet/credentials.json +50 -29
- package/integrations/google-sheet/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-sheet/credentials_hint_service_account.md +10 -0
- package/integrations/google-slides/__tests__/get_handlers.test.ts +38 -36
- package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -59
- package/integrations/google-slides/credentials.json +50 -29
- package/integrations/google-slides/credentials_hint_oauth_token.md +8 -0
- package/integrations/google-slides/credentials_hint_service_account.md +10 -0
- package/integrations/notion/__tests__/get_handlers.test.ts +18 -15
- package/integrations/notion/__tests__/usage_parity.test.ts +3 -28
- package/integrations/notion/__tests__/write_and_admin_handlers.test.ts +56 -60
- package/integrations/notion/credentials.json +22 -17
- package/integrations/trello/__tests__/get_handlers.test.ts +58 -73
- package/integrations/trello/__tests__/usage_parity.test.ts +3 -28
- package/integrations/trello/__tests__/write_and_admin_handlers.test.ts +49 -67
- package/integrations/trello/credentials.json +26 -21
- package/integrations/trello/handlers/close_board.js +6 -0
- package/integrations/trello/handlers/create_board.js +11 -0
- package/integrations/trello/handlers/delete_board.js +13 -0
- package/integrations/trello/manifest.json +21 -0
- package/integrations/trello/schemas/close_board.json +10 -0
- package/integrations/trello/schemas/create_board.json +12 -0
- package/integrations/trello/schemas/delete_board.json +10 -0
- package/package.json +1 -1
|
@@ -1,69 +1,62 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
|
|
4
3
|
|
|
5
|
-
// LIVE Google Sheets write tests using
|
|
4
|
+
// LIVE Google Sheets write tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
8
|
-
// -
|
|
9
|
-
// - GSHEETS_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-sheet')
|
|
10
|
-
// - GSHEETS_TEST_SPREADSHEET_ID (target spreadsheet ID with write access)
|
|
6
|
+
// - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
|
|
7
|
+
// - GOOGLE_SHEETS_TEST_SPREADSHEET_ID (target spreadsheet ID with write access)
|
|
11
8
|
|
|
12
9
|
interface Ctx {
|
|
13
10
|
spreadsheetId?: string
|
|
11
|
+
folderId?: string
|
|
12
|
+
destSpreadsheetId?: string
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
const
|
|
17
|
-
const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
|
|
18
|
-
const suite = hasEnv(
|
|
19
|
-
'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
|
|
20
|
-
'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
|
|
21
|
-
'GSHEETS_TEST_CONNECTION_ID',
|
|
22
|
-
'GSHEETS_TEST_SPREADSHEET_ID',
|
|
23
|
-
)
|
|
15
|
+
const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
|
|
24
16
|
? describe
|
|
25
17
|
: describe.skip
|
|
26
18
|
|
|
27
19
|
suite('google-sheet write handlers (live)', () => {
|
|
28
20
|
const ctx: Ctx = {}
|
|
29
|
-
let
|
|
21
|
+
let sheets: ReturnType<typeof createToolbox>
|
|
22
|
+
let drive: ReturnType<typeof createToolbox>
|
|
30
23
|
let sheetTitle: string | undefined
|
|
31
24
|
|
|
32
25
|
beforeAll(async () => {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
const env = process.env as Record<string, string | undefined>
|
|
27
|
+
const credentialStore = createCredentialStore(async () => ({
|
|
28
|
+
token: env.GOOGLE_TOKEN || '',
|
|
29
|
+
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
30
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
31
|
+
}))
|
|
32
|
+
const proxy = createProxy(credentialStore)
|
|
33
|
+
sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
|
|
34
|
+
drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
|
|
35
|
+
|
|
36
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Sheets Write ${Date.now()}` })
|
|
37
|
+
ctx.folderId = folder?.id
|
|
38
|
+
expect(ctx.folderId).toBeTruthy()
|
|
39
|
+
|
|
40
|
+
const created = await drive.write('create_file')({
|
|
41
|
+
name: `CmdTest Sheet ${Date.now()}`,
|
|
42
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
43
|
+
parentId: ctx.folderId,
|
|
38
44
|
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const tools = loadIntegrationTools('google-sheet')
|
|
42
|
-
expect(tools).toBeTruthy()
|
|
45
|
+
ctx.spreadsheetId = created?.id
|
|
46
|
+
expect(ctx.spreadsheetId).toBeTruthy()
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ctx.spreadsheetId = GSHEETS_TEST_SPREADSHEET_ID
|
|
48
|
+
const createdDest = await drive.write('create_file')({
|
|
49
|
+
name: `CmdTest Sheet Dest ${Date.now()}`,
|
|
50
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
51
|
+
parentId: ctx.folderId,
|
|
52
|
+
})
|
|
53
|
+
ctx.destSpreadsheetId = createdDest?.id
|
|
54
|
+
expect(ctx.destSpreadsheetId).toBeTruthy()
|
|
53
55
|
|
|
54
56
|
// Try to detect a default sheet title
|
|
55
57
|
if (ctx.spreadsheetId) {
|
|
56
|
-
const proxy2 = new IntegrationProxy({
|
|
57
|
-
managedOAuthBaseUrl: COMMANDABLE_MANAGED_OAUTH_BASE_URL,
|
|
58
|
-
managedOAuthSecretKey: COMMANDABLE_MANAGED_OAUTH_SECRET_KEY,
|
|
59
|
-
})
|
|
60
|
-
const node2 = { id: 'node-gsheets', type: 'google-sheet', label: 'Google Sheets', connectionId: GSHEETS_TEST_CONNECTION_ID } as any
|
|
61
|
-
const tools2 = loadIntegrationTools('google-sheet')!
|
|
62
|
-
const tool = tools2.read.find(t => t.name === 'get_spreadsheet')!
|
|
63
|
-
const build = new Function('integration', `return (${tool.handlerCode});`)
|
|
64
|
-
const integration = { fetch: (path: string, init?: RequestInit) => proxy2.call(node2, path, init) }
|
|
65
58
|
try {
|
|
66
|
-
const get_spreadsheet =
|
|
59
|
+
const get_spreadsheet = sheets.read('get_spreadsheet')
|
|
67
60
|
const meta = await get_spreadsheet({ spreadsheetId: ctx.spreadsheetId })
|
|
68
61
|
sheetTitle = meta?.sheets?.[0]?.properties?.title
|
|
69
62
|
}
|
|
@@ -71,16 +64,27 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
71
64
|
}
|
|
72
65
|
}, 60000)
|
|
73
66
|
|
|
67
|
+
afterAll(async () => {
|
|
68
|
+
await safeCleanup(async () => {
|
|
69
|
+
const delete_file = drive.write('delete_file')
|
|
70
|
+
if (ctx.spreadsheetId)
|
|
71
|
+
await delete_file({ fileId: ctx.spreadsheetId })
|
|
72
|
+
if (ctx.destSpreadsheetId)
|
|
73
|
+
await delete_file({ fileId: ctx.destSpreadsheetId })
|
|
74
|
+
})
|
|
75
|
+
await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
|
|
76
|
+
}, 60_000)
|
|
77
|
+
|
|
74
78
|
it('append_values appends then clear_values clears', async () => {
|
|
75
79
|
if (!ctx.spreadsheetId)
|
|
76
80
|
return expect(true).toBe(true)
|
|
77
81
|
|
|
78
|
-
const append_values =
|
|
82
|
+
const append_values = sheets.write('append_values')
|
|
79
83
|
const aTitle = sheetTitle || 'Sheet1'
|
|
80
84
|
const appendRes = await append_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1`, values: [[`CmdTest ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
|
|
81
85
|
expect(appendRes?.updates || appendRes?.tableRange || appendRes?.spreadsheetId).toBeTruthy()
|
|
82
86
|
|
|
83
|
-
const clear_values =
|
|
87
|
+
const clear_values = sheets.write('clear_values')
|
|
84
88
|
const clearRes = await clear_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1:A10` })
|
|
85
89
|
expect(clearRes?.clearedRange || clearRes?.spreadsheetId).toBeTruthy()
|
|
86
90
|
}, 60000)
|
|
@@ -89,7 +93,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
89
93
|
if (!ctx.spreadsheetId)
|
|
90
94
|
return expect(true).toBe(true)
|
|
91
95
|
const aTitle = sheetTitle || 'Sheet1'
|
|
92
|
-
const update_values =
|
|
96
|
+
const update_values = sheets.write('update_values')
|
|
93
97
|
const res = await update_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!B1:B1`, values: [[`CmdTestU ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
|
|
94
98
|
expect(res?.updatedRange || res?.spreadsheetId).toBeTruthy()
|
|
95
99
|
}, 60000)
|
|
@@ -98,7 +102,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
98
102
|
if (!ctx.spreadsheetId)
|
|
99
103
|
return expect(true).toBe(true)
|
|
100
104
|
const aTitle = sheetTitle || 'Sheet1'
|
|
101
|
-
const batch_update_values =
|
|
105
|
+
const batch_update_values = sheets.write('batch_update_values')
|
|
102
106
|
const res = await batch_update_values({ spreadsheetId: ctx.spreadsheetId, data: [
|
|
103
107
|
{ range: `${aTitle}!C1:C1`, values: [[`CmdTestB1 ${Date.now()}`]] },
|
|
104
108
|
{ range: `${aTitle}!C2:C2`, values: [[`CmdTestB2 ${Date.now()}`]] },
|
|
@@ -110,7 +114,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
110
114
|
if (!ctx.spreadsheetId)
|
|
111
115
|
return expect(true).toBe(true)
|
|
112
116
|
const aTitle = sheetTitle || 'Sheet1'
|
|
113
|
-
const batch_update_values_by_data_filter =
|
|
117
|
+
const batch_update_values_by_data_filter = sheets.write('batch_update_values_by_data_filter')
|
|
114
118
|
const res = await batch_update_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, data: [
|
|
115
119
|
{ dataFilter: { a1Range: `${aTitle}!D1:D1` }, values: [[`CmdTestDF ${Date.now()}`]] },
|
|
116
120
|
], valueInputOption: 'USER_ENTERED' })
|
|
@@ -121,7 +125,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
121
125
|
if (!ctx.spreadsheetId)
|
|
122
126
|
return expect(true).toBe(true)
|
|
123
127
|
const aTitle = sheetTitle || 'Sheet1'
|
|
124
|
-
const batch_clear_values =
|
|
128
|
+
const batch_clear_values = sheets.write('batch_clear_values')
|
|
125
129
|
const res = await batch_clear_values({ spreadsheetId: ctx.spreadsheetId, ranges: [`${aTitle}!A1:A2`, `${aTitle}!B1:B2`] })
|
|
126
130
|
expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
|
|
127
131
|
}, 60000)
|
|
@@ -130,7 +134,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
130
134
|
if (!ctx.spreadsheetId)
|
|
131
135
|
return expect(true).toBe(true)
|
|
132
136
|
const aTitle = sheetTitle || 'Sheet1'
|
|
133
|
-
const batch_clear_values_by_data_filter =
|
|
137
|
+
const batch_clear_values_by_data_filter = sheets.write('batch_clear_values_by_data_filter')
|
|
134
138
|
const res = await batch_clear_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!E1:E2` }] })
|
|
135
139
|
expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
|
|
136
140
|
}, 60000)
|
|
@@ -139,7 +143,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
139
143
|
if (!ctx.spreadsheetId)
|
|
140
144
|
return expect(true).toBe(true)
|
|
141
145
|
const aTitle = sheetTitle || 'Sheet1'
|
|
142
|
-
const batch_update =
|
|
146
|
+
const batch_update = sheets.write('batch_update')
|
|
143
147
|
const res = await batch_update({ spreadsheetId: ctx.spreadsheetId, requests: [
|
|
144
148
|
{ findReplace: { find: '___unlikely___', replacement: '___unlikely___', matchCase: true, searchByRegex: false, includeFormulas: false, range: { sheetId: 0 } } },
|
|
145
149
|
], includeSpreadsheetInResponse: false })
|
|
@@ -147,12 +151,12 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
147
151
|
}, 60000)
|
|
148
152
|
|
|
149
153
|
it('copy_to_spreadsheet copies a sheet when destination provided', async () => {
|
|
150
|
-
if (!ctx.spreadsheetId || !
|
|
154
|
+
if (!ctx.spreadsheetId || !ctx.destSpreadsheetId)
|
|
151
155
|
return expect(true).toBe(true)
|
|
152
|
-
const copy_to_spreadsheet =
|
|
156
|
+
const copy_to_spreadsheet = sheets.write('copy_to_spreadsheet')
|
|
153
157
|
// Attempt to copy sheet with id 0 (typical for first sheet). If it fails, skip.
|
|
154
158
|
try {
|
|
155
|
-
const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId:
|
|
159
|
+
const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: ctx.destSpreadsheetId })
|
|
156
160
|
expect(res?.sheetId !== undefined || res?.spreadsheetId).toBeTruthy()
|
|
157
161
|
}
|
|
158
162
|
catch {
|
|
@@ -160,12 +164,10 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
160
164
|
}
|
|
161
165
|
}, 60000)
|
|
162
166
|
|
|
163
|
-
it('create_spreadsheet creates a spreadsheet
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(res?.spreadsheetId).toBeTruthy()
|
|
169
|
-
// Note: No delete here to avoid Drive scope; test leaves an artifact when enabled.
|
|
167
|
+
it('create_spreadsheet creates a spreadsheet (self-cleaning)', async () => {
|
|
168
|
+
const created = await sheets.write('create_spreadsheet')({ properties: { title: `CmdTest Sheet Tool ${Date.now()}` } })
|
|
169
|
+
const id = created?.spreadsheetId
|
|
170
|
+
expect(typeof id).toBe('string')
|
|
171
|
+
await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
|
|
170
172
|
}, 60000)
|
|
171
173
|
})
|
|
@@ -1,36 +1,57 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
|
|
2
|
+
"variants": {
|
|
3
|
+
"service_account": {
|
|
4
|
+
"label": "Service Account (recommended)",
|
|
5
|
+
"schema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"serviceAccountJson": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"title": "Service Account JSON",
|
|
11
|
+
"description": "Full service account key JSON (contents of the downloaded JSON file from Google Cloud)."
|
|
12
|
+
},
|
|
13
|
+
"subject": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"title": "Subject / impersonated user (optional)",
|
|
16
|
+
"description": "Optional user email to impersonate when using Google Workspace domain-wide delegation."
|
|
17
|
+
},
|
|
18
|
+
"scopes": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"title": "OAuth scopes (optional)",
|
|
21
|
+
"description": "Optional override for OAuth scopes. Defaults to spreadsheets.",
|
|
22
|
+
"items": { "type": "string" }
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": ["serviceAccountJson"],
|
|
26
|
+
"additionalProperties": false
|
|
9
27
|
},
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
28
|
+
"injection": {
|
|
29
|
+
"headers": {
|
|
30
|
+
"Authorization": "Bearer {{token}}"
|
|
31
|
+
}
|
|
14
32
|
},
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
33
|
+
"preprocess": "google_service_account"
|
|
34
|
+
},
|
|
35
|
+
"oauth_token": {
|
|
36
|
+
"label": "OAuth Access Token (short-lived)",
|
|
37
|
+
"schema": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"token": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"title": "OAuth Access Token",
|
|
43
|
+
"description": "Short-lived Google OAuth access token with spreadsheets scope."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"required": ["token"],
|
|
47
|
+
"additionalProperties": false
|
|
19
48
|
},
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"items": { "type": "string" }
|
|
49
|
+
"injection": {
|
|
50
|
+
"headers": {
|
|
51
|
+
"Authorization": "Bearer {{token}}"
|
|
52
|
+
}
|
|
25
53
|
}
|
|
26
|
-
},
|
|
27
|
-
"required": [],
|
|
28
|
-
"additionalProperties": false
|
|
29
|
-
},
|
|
30
|
-
"injection": {
|
|
31
|
-
"headers": {
|
|
32
|
-
"Authorization": "Bearer {{token}}"
|
|
33
54
|
}
|
|
34
|
-
}
|
|
55
|
+
},
|
|
56
|
+
"default": "service_account"
|
|
35
57
|
}
|
|
36
|
-
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Obtain a short-lived Google OAuth access token:
|
|
2
|
+
|
|
3
|
+
1. Use the Google OAuth 2.0 Playground (`https://developers.google.com/oauthplayground/`) or your own OAuth flow
|
|
4
|
+
2. Select the scope: `https://www.googleapis.com/auth/spreadsheets`
|
|
5
|
+
3. Exchange the authorization code for an access token
|
|
6
|
+
4. Paste the access token here
|
|
7
|
+
|
|
8
|
+
Note: OAuth access tokens are short-lived (typically 1 hour). For long-running use, prefer the Service Account variant.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Set up a Google Cloud Service Account:
|
|
2
|
+
|
|
3
|
+
1. Open the [Google Cloud Console](https://console.cloud.google.com/)
|
|
4
|
+
2. Enable the **Google Sheets API** for your project
|
|
5
|
+
3. Go to **IAM & Admin → Service Accounts** and create a new service account
|
|
6
|
+
4. Under **Keys**, click **Add Key → Create new key → JSON** and download the file
|
|
7
|
+
5. Paste the full contents of the JSON file here
|
|
8
|
+
6. Share your target spreadsheets with the service account's `client_email`
|
|
9
|
+
|
|
10
|
+
For Google Workspace users: optionally configure domain-wide delegation and set `subject` to the user's email.
|
|
@@ -1,68 +1,70 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
import { loadIntegrationTools } from '../../../src/integrations/dataLoader.js'
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createCredentialStore, createIntegrationNode, createProxy, createToolbox, hasEnv, safeCleanup } from '../../__tests__/liveHarness.js'
|
|
4
3
|
|
|
5
|
-
// LIVE Google Slides read tests using
|
|
4
|
+
// LIVE Google Slides read tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
8
|
-
// - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
|
|
9
|
-
// - GSLIDES_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-slides')
|
|
10
|
-
// - GSLIDES_TEST_PRESENTATION_ID (an accessible presentation ID)
|
|
6
|
+
// - Either GOOGLE_TOKEN, OR GOOGLE_SERVICE_ACCOUNT_JSON
|
|
11
7
|
|
|
12
|
-
const
|
|
13
|
-
const hasEnv = (...keys: string[]) => keys.every(k => !!env[k] && env[k].trim().length > 0)
|
|
14
|
-
const suite = hasEnv(
|
|
15
|
-
'COMMANDABLE_MANAGED_OAUTH_BASE_URL',
|
|
16
|
-
'COMMANDABLE_MANAGED_OAUTH_SECRET_KEY',
|
|
17
|
-
'GSLIDES_TEST_CONNECTION_ID',
|
|
18
|
-
)
|
|
8
|
+
const suite = (hasEnv('GOOGLE_TOKEN') || hasEnv('GOOGLE_SERVICE_ACCOUNT_JSON'))
|
|
19
9
|
? describe
|
|
20
10
|
: describe.skip
|
|
21
11
|
|
|
22
12
|
suite('google-slides read handlers (live)', () => {
|
|
23
|
-
let
|
|
13
|
+
let slides: ReturnType<typeof createToolbox>
|
|
14
|
+
let drive: ReturnType<typeof createToolbox>
|
|
15
|
+
let folderId: string | undefined
|
|
16
|
+
let presentationId: string | undefined
|
|
24
17
|
|
|
25
18
|
beforeAll(async () => {
|
|
26
|
-
const
|
|
19
|
+
const env = process.env as Record<string, string | undefined>
|
|
20
|
+
const credentialStore = createCredentialStore(async () => ({
|
|
21
|
+
token: env.GOOGLE_TOKEN || '',
|
|
22
|
+
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
23
|
+
subject: env.GOOGLE_IMPERSONATE_SUBJECT || '',
|
|
24
|
+
}))
|
|
25
|
+
const proxy = createProxy(credentialStore)
|
|
26
|
+
slides = createToolbox('google-slides', proxy, createIntegrationNode('google-slides', { label: 'Google Slides', credentialId: 'google-slides-creds' }))
|
|
27
|
+
drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const integrationNode = { id: 'node-gslides', type: 'google-slides', label: 'Google Slides', connectionId: GSLIDES_TEST_CONNECTION_ID } as any
|
|
29
|
+
// Create dedicated folder + presentation for this run
|
|
30
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Slides ${Date.now()}` })
|
|
31
|
+
folderId = folder?.id
|
|
32
|
+
expect(folderId).toBeTruthy()
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const created = await drive.write('create_file')({
|
|
35
|
+
name: `CmdTest Slides ${Date.now()}`,
|
|
36
|
+
mimeType: 'application/vnd.google-apps.presentation',
|
|
37
|
+
parentId: folderId,
|
|
38
|
+
})
|
|
39
|
+
presentationId = created?.id
|
|
40
|
+
expect(presentationId).toBeTruthy()
|
|
41
|
+
}, 60000)
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return build(integration) as (input: any) => Promise<any>
|
|
43
|
-
}
|
|
43
|
+
afterAll(async () => {
|
|
44
|
+
if (!folderId)
|
|
45
|
+
return
|
|
46
|
+
await safeCleanup(async () => presentationId ? drive.write('delete_file')({ fileId: presentationId }) : Promise.resolve())
|
|
47
|
+
await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
|
|
44
48
|
}, 60000)
|
|
45
49
|
|
|
46
50
|
it('get_presentation returns metadata', async () => {
|
|
47
|
-
const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
|
|
48
51
|
if (!presentationId)
|
|
49
52
|
return expect(true).toBe(true)
|
|
50
|
-
const handler =
|
|
53
|
+
const handler = slides.read('get_presentation')
|
|
51
54
|
const result = await handler({ presentationId })
|
|
52
55
|
expect(result?.presentationId || Array.isArray(result?.slides)).toBeTruthy()
|
|
53
56
|
}, 30000)
|
|
54
57
|
|
|
55
58
|
it('get_page_thumbnail returns URL data', async () => {
|
|
56
|
-
const presentationId = env.GSLIDES_TEST_PRESENTATION_ID
|
|
57
59
|
if (!presentationId)
|
|
58
60
|
return expect(true).toBe(true)
|
|
59
61
|
// First query the presentation to discover a page id
|
|
60
|
-
const getPresentation =
|
|
62
|
+
const getPresentation = slides.read('get_presentation')
|
|
61
63
|
const meta = await getPresentation({ presentationId })
|
|
62
64
|
const firstSlide = meta?.slides?.[0]
|
|
63
65
|
if (!firstSlide?.objectId)
|
|
64
66
|
return expect(true).toBe(true)
|
|
65
|
-
const handler =
|
|
67
|
+
const handler = slides.read('get_page_thumbnail')
|
|
66
68
|
const result = await handler({ presentationId, 'pageObjectId': firstSlide.objectId, 'thumbnailProperties.thumbnailSize': 'MEDIUM', 'thumbnailProperties.mimeType': 'PNG' })
|
|
67
69
|
expect(typeof result?.contentUrl === 'string' || typeof result?.thumbnailUrl === 'string').toBe(true)
|
|
68
70
|
}, 30000)
|
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
1
|
import { describe, expect, it } from 'vitest'
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
function escapeRegExp(str: string): string {
|
|
8
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
9
|
-
}
|
|
2
|
+
import { getMissingToolUsages } from '../../__tests__/usageParity.js'
|
|
10
3
|
|
|
11
4
|
describe('google-slides static usage parity', () => {
|
|
12
|
-
it('every manifest tool is referenced in tests
|
|
13
|
-
const
|
|
14
|
-
const toolNames = (manifest.tools as any[]).map(t => t.name)
|
|
15
|
-
|
|
16
|
-
const testsDir = fileURLToPath(new URL('.', import.meta.url))
|
|
17
|
-
expect(existsSync(testsDir)).toBe(true)
|
|
18
|
-
const testFiles = readdirSync(testsDir)
|
|
19
|
-
.filter(f => /\.test\.(t|j)s$/.test(f) && !f.includes('usage_parity.test'))
|
|
20
|
-
.map(f => resolve(testsDir, f))
|
|
21
|
-
|
|
22
|
-
const fileContents = testFiles.map(f => readFileSync(f, 'utf8'))
|
|
23
|
-
|
|
24
|
-
const missing: string[] = []
|
|
25
|
-
for (const name of toolNames) {
|
|
26
|
-
const nameRe = new RegExp(`build(?:Read|Write|Admin)?(?:Handler)?\\(\\s*['\"\`]${escapeRegExp(name)}['\"\`]\\s*\\)`, 'm')
|
|
27
|
-
const found = fileContents.some(src => nameRe.test(src))
|
|
28
|
-
if (!found)
|
|
29
|
-
missing.push(name)
|
|
30
|
-
}
|
|
31
|
-
|
|
5
|
+
it('every manifest tool is referenced in tests', () => {
|
|
6
|
+
const missing = getMissingToolUsages({ integrationName: 'google-slides', importMetaUrl: import.meta.url })
|
|
32
7
|
expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
|
|
33
8
|
})
|
|
34
9
|
})
|