@commandable/integration-data 0.0.1 → 0.0.4
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 +18 -15
- 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 +219 -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/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 +248 -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-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 +47 -55
- package/integrations/google-sheet/__tests__/usage_parity.test.ts +3 -29
- package/integrations/google-sheet/__tests__/write_handlers.test.ts +64 -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 +37 -36
- package/integrations/google-slides/__tests__/usage_parity.test.ts +3 -28
- package/integrations/google-slides/__tests__/write_handlers.test.ts +65 -58
- 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
|
@@ -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/drive`
|
|
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 Drive 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 the Drive folders/files you want to access 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.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const body = {
|
|
3
|
+
name: input.name,
|
|
4
|
+
mimeType: input.mimeType,
|
|
5
|
+
}
|
|
6
|
+
if (input.parentId)
|
|
7
|
+
body.parents = [input.parentId]
|
|
8
|
+
|
|
9
|
+
const res = await integration.fetch('/files', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
body,
|
|
12
|
+
})
|
|
13
|
+
return await res.json()
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const body = {
|
|
3
|
+
name: input.name,
|
|
4
|
+
mimeType: 'application/vnd.google-apps.folder',
|
|
5
|
+
}
|
|
6
|
+
if (input.parentId)
|
|
7
|
+
body.parents = [input.parentId]
|
|
8
|
+
|
|
9
|
+
const res = await integration.fetch('/files', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
body,
|
|
12
|
+
})
|
|
13
|
+
return await res.json()
|
|
14
|
+
}
|
|
15
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}`, {
|
|
3
|
+
method: 'DELETE',
|
|
4
|
+
})
|
|
5
|
+
if (res.status === 204)
|
|
6
|
+
return { success: true, status: 204 }
|
|
7
|
+
try {
|
|
8
|
+
return await res.json()
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return { success: res.ok, status: res.status }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
async (input) => {
|
|
2
|
+
const params = new URLSearchParams()
|
|
3
|
+
params.set('addParents', input.addParents)
|
|
4
|
+
if (input.removeParents)
|
|
5
|
+
params.set('removeParents', input.removeParents)
|
|
6
|
+
|
|
7
|
+
const res = await integration.fetch(`/files/${encodeURIComponent(input.fileId)}?${params.toString()}`, {
|
|
8
|
+
method: 'PATCH',
|
|
9
|
+
})
|
|
10
|
+
return await res.json()
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "google-drive",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"tools": [
|
|
5
|
+
{
|
|
6
|
+
"name": "create_folder",
|
|
7
|
+
"description": "Create a folder in Google Drive.",
|
|
8
|
+
"inputSchema": "schemas/create_folder.json",
|
|
9
|
+
"handler": "handlers/create_folder.js",
|
|
10
|
+
"scope": "write"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "create_file",
|
|
14
|
+
"description": "Create a Drive file (including Google Docs/Sheets/Slides) optionally inside a parent folder.",
|
|
15
|
+
"inputSchema": "schemas/create_file.json",
|
|
16
|
+
"handler": "handlers/create_file.js",
|
|
17
|
+
"scope": "write"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "move_file",
|
|
21
|
+
"description": "Move a file to a different parent folder (add/remove parents).",
|
|
22
|
+
"inputSchema": "schemas/move_file.json",
|
|
23
|
+
"handler": "handlers/move_file.js",
|
|
24
|
+
"scope": "write"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "get_file",
|
|
28
|
+
"description": "Get a Drive file’s metadata by fileId.",
|
|
29
|
+
"inputSchema": "schemas/get_file.json",
|
|
30
|
+
"handler": "handlers/get_file.js",
|
|
31
|
+
"scope": "read"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "delete_file",
|
|
35
|
+
"description": "Permanently delete a Drive file or folder by fileId.",
|
|
36
|
+
"inputSchema": "schemas/delete_file.json",
|
|
37
|
+
"handler": "handlers/delete_file.js",
|
|
38
|
+
"scope": "write"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"required": ["name", "mimeType"],
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string" },
|
|
8
|
+
"mimeType": { "type": "string", "description": "Drive mimeType (e.g. application/vnd.google-apps.document)" },
|
|
9
|
+
"parentId": { "type": "string", "description": "Optional parent folder fileId" }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"required": ["name"],
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string" },
|
|
8
|
+
"parentId": { "type": "string", "description": "Optional parent folder fileId" }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"required": ["fileId", "addParents"],
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"fileId": { "type": "string" },
|
|
8
|
+
"addParents": { "type": "string", "description": "Comma-separated parent IDs to add" },
|
|
9
|
+
"removeParents": { "type": "string", "description": "Comma-separated parent IDs to remove (optional)" }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
@@ -1,128 +1,120 @@
|
|
|
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 read tests using
|
|
4
|
+
// LIVE Google Sheets read tests using credentials
|
|
6
5
|
// Required env vars:
|
|
7
|
-
// -
|
|
8
|
-
// - COMMANDABLE_MANAGED_OAUTH_SECRET_KEY
|
|
9
|
-
// - GSHEETS_TEST_CONNECTION_ID (managed OAuth connection for provider 'google-sheet')
|
|
10
|
-
// - GSHEETS_TEST_SPREADSHEET_ID (an accessible spreadsheet 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
|
-
'GSHEETS_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-sheet read handlers (live)', () => {
|
|
23
|
-
let
|
|
13
|
+
let sheets: ReturnType<typeof createToolbox>
|
|
14
|
+
let drive: ReturnType<typeof createToolbox>
|
|
24
15
|
let sheetTitle: string | undefined
|
|
16
|
+
let folderId: string | undefined
|
|
17
|
+
let spreadsheetId: string | undefined
|
|
25
18
|
|
|
26
19
|
beforeAll(async () => {
|
|
27
|
-
const
|
|
20
|
+
const env = process.env as Record<string, string | undefined>
|
|
21
|
+
const credentialStore = createCredentialStore(async () => ({
|
|
22
|
+
token: env.GOOGLE_TOKEN || '',
|
|
23
|
+
serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '',
|
|
24
|
+
}))
|
|
25
|
+
const proxy = createProxy(credentialStore)
|
|
26
|
+
sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
|
|
27
|
+
drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const integrationNode = { id: 'node-gsheets', type: 'google-sheet', label: 'Google Sheets', connectionId: GSHEETS_TEST_CONNECTION_ID } as any
|
|
29
|
+
// Create dedicated folder + spreadsheet for this run
|
|
30
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Sheets ${Date.now()}` })
|
|
31
|
+
folderId = folder?.id
|
|
32
|
+
expect(folderId).toBeTruthy()
|
|
34
33
|
|
|
35
|
-
const
|
|
36
|
-
|
|
34
|
+
const created = await drive.write('create_file')({
|
|
35
|
+
name: `CmdTest Sheet ${Date.now()}`,
|
|
36
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
37
|
+
parentId: folderId,
|
|
38
|
+
})
|
|
39
|
+
spreadsheetId = created?.id
|
|
40
|
+
expect(spreadsheetId).toBeTruthy()
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const build = new Function('integration', `return (${tool!.handlerCode});`)
|
|
43
|
-
return build(integration) as (input: any) => Promise<any>
|
|
42
|
+
try {
|
|
43
|
+
const get_spreadsheet = sheets.read('get_spreadsheet')
|
|
44
|
+
const meta = await get_spreadsheet({ spreadsheetId })
|
|
45
|
+
sheetTitle = meta?.sheets?.[0]?.properties?.title
|
|
44
46
|
}
|
|
47
|
+
catch {}
|
|
48
|
+
}, 60000)
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const meta = await get_spreadsheet({ spreadsheetId })
|
|
52
|
-
sheetTitle = meta?.sheets?.[0]?.properties?.title
|
|
53
|
-
}
|
|
54
|
-
catch {}
|
|
55
|
-
}
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
if (!folderId)
|
|
52
|
+
return
|
|
53
|
+
await safeCleanup(async () => spreadsheetId ? drive.write('delete_file')({ fileId: spreadsheetId }) : Promise.resolve())
|
|
54
|
+
await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
|
|
56
55
|
}, 60000)
|
|
57
56
|
|
|
58
57
|
it('get_spreadsheet returns metadata', async () => {
|
|
59
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
60
58
|
if (!spreadsheetId)
|
|
61
59
|
return expect(true).toBe(true)
|
|
62
|
-
const handler =
|
|
60
|
+
const handler = sheets.read('get_spreadsheet')
|
|
63
61
|
const result = await handler({ spreadsheetId })
|
|
64
62
|
expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
|
|
65
63
|
}, 30000)
|
|
66
64
|
|
|
67
65
|
it('get_values returns a value range', async () => {
|
|
68
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
69
66
|
if (!spreadsheetId)
|
|
70
67
|
return expect(true).toBe(true)
|
|
71
|
-
const handler =
|
|
68
|
+
const handler = sheets.read('get_values')
|
|
72
69
|
const range = sheetTitle ? `${sheetTitle}!A1:B5` : 'A1:B5'
|
|
73
70
|
const result = await handler({ spreadsheetId, range })
|
|
74
71
|
expect(result?.range || result?.values).toBeTruthy()
|
|
75
72
|
}, 30000)
|
|
76
73
|
|
|
77
74
|
it('batch_get_values returns multiple ranges', async () => {
|
|
78
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
79
75
|
if (!spreadsheetId)
|
|
80
76
|
return expect(true).toBe(true)
|
|
81
|
-
const handler =
|
|
77
|
+
const handler = sheets.read('batch_get_values')
|
|
82
78
|
const aTitle = sheetTitle || 'Sheet1'
|
|
83
79
|
const result = await handler({ spreadsheetId, ranges: [`${aTitle}!A1:A3`, `${aTitle}!B1:B3`] })
|
|
84
80
|
expect(Array.isArray(result?.valueRanges)).toBe(true)
|
|
85
81
|
}, 30000)
|
|
86
82
|
|
|
87
83
|
it('get_spreadsheet_by_data_filter returns metadata', async () => {
|
|
88
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
89
84
|
if (!spreadsheetId)
|
|
90
85
|
return expect(true).toBe(true)
|
|
91
|
-
const handler =
|
|
86
|
+
const handler = sheets.read('get_spreadsheet_by_data_filter')
|
|
92
87
|
const aTitle = sheetTitle || 'Sheet1'
|
|
93
88
|
const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:A1` }] })
|
|
94
89
|
expect(result?.spreadsheetId || result?.sheets).toBeTruthy()
|
|
95
90
|
}, 30000)
|
|
96
91
|
|
|
97
92
|
it('get_values_by_data_filter returns values', async () => {
|
|
98
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
99
93
|
if (!spreadsheetId)
|
|
100
94
|
return expect(true).toBe(true)
|
|
101
|
-
const handler =
|
|
95
|
+
const handler = sheets.read('get_values_by_data_filter')
|
|
102
96
|
const aTitle = sheetTitle || 'Sheet1'
|
|
103
97
|
const result = await handler({ spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!A1:B2` }] })
|
|
104
98
|
expect(Array.isArray(result?.valueRanges)).toBe(true)
|
|
105
99
|
}, 30000)
|
|
106
100
|
|
|
107
101
|
it('search_developer_metadata returns results or empty', async () => {
|
|
108
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
109
102
|
if (!spreadsheetId)
|
|
110
103
|
return expect(true).toBe(true)
|
|
111
|
-
const handler =
|
|
104
|
+
const handler = sheets.read('search_developer_metadata')
|
|
112
105
|
const result = await handler({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
|
|
113
106
|
expect(result !== undefined).toBe(true)
|
|
114
107
|
}, 30000)
|
|
115
108
|
|
|
116
109
|
it('get_developer_metadata retrieves by id when available', async () => {
|
|
117
|
-
const spreadsheetId = env.GSHEETS_TEST_SPREADSHEET_ID
|
|
118
110
|
if (!spreadsheetId)
|
|
119
111
|
return expect(true).toBe(true)
|
|
120
|
-
const search =
|
|
112
|
+
const search = sheets.read('search_developer_metadata')
|
|
121
113
|
const list = await search({ spreadsheetId, dataFilters: [{ developerMetadataLookup: { visibility: 'DOCUMENT' } }] })
|
|
122
114
|
const first = list?.matchedDeveloperMetadata?.[0]?.developerMetadata || list?.developerMetadata?.[0]
|
|
123
115
|
if (!first?.metadataId)
|
|
124
116
|
return expect(true).toBe(true)
|
|
125
|
-
const getdm =
|
|
117
|
+
const getdm = sheets.read('get_developer_metadata')
|
|
126
118
|
const got = await getdm({ spreadsheetId, metadataId: first.metadataId })
|
|
127
119
|
expect(got?.metadataId).toBe(first.metadataId)
|
|
128
120
|
}, 30000)
|
|
@@ -1,35 +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-sheet 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
|
-
// Matches buildRead('x'), buildWrite('x'), buildAdmin('x'), buildHandler('x'), buildReadHandler('x'), buildWriteHandler('x')
|
|
27
|
-
const nameRe = new RegExp(`build(?:Read|Write|Admin)?(?:Handler)?\\(\\s*['\"\`]${escapeRegExp(name)}['\"\`]\\s*\\)`, 'm')
|
|
28
|
-
const found = fileContents.some(src => nameRe.test(src))
|
|
29
|
-
if (!found)
|
|
30
|
-
missing.push(name)
|
|
31
|
-
}
|
|
32
|
-
|
|
5
|
+
it('every manifest tool is referenced in tests', () => {
|
|
6
|
+
const missing = getMissingToolUsages({ integrationName: 'google-sheet', importMetaUrl: import.meta.url })
|
|
33
7
|
expect(missing, `Missing handler usages in tests: ${missing.join(', ')}`).toEqual([])
|
|
34
8
|
})
|
|
35
9
|
})
|
|
@@ -1,69 +1,61 @@
|
|
|
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
|
+
}))
|
|
31
|
+
const proxy = createProxy(credentialStore)
|
|
32
|
+
sheets = createToolbox('google-sheet', proxy, createIntegrationNode('google-sheet', { label: 'Google Sheets', credentialId: 'google-sheet-creds' }))
|
|
33
|
+
drive = createToolbox('google-drive', proxy, createIntegrationNode('google-drive', { label: 'Google Drive', credentialId: 'google-drive-creds' }))
|
|
34
|
+
|
|
35
|
+
const folder = await drive.write('create_folder')({ name: `CmdTest Sheets Write ${Date.now()}` })
|
|
36
|
+
ctx.folderId = folder?.id
|
|
37
|
+
expect(ctx.folderId).toBeTruthy()
|
|
38
|
+
|
|
39
|
+
const created = await drive.write('create_file')({
|
|
40
|
+
name: `CmdTest Sheet ${Date.now()}`,
|
|
41
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
42
|
+
parentId: ctx.folderId,
|
|
38
43
|
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const tools = loadIntegrationTools('google-sheet')
|
|
42
|
-
expect(tools).toBeTruthy()
|
|
44
|
+
ctx.spreadsheetId = created?.id
|
|
45
|
+
expect(ctx.spreadsheetId).toBeTruthy()
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
ctx.spreadsheetId = GSHEETS_TEST_SPREADSHEET_ID
|
|
47
|
+
const createdDest = await drive.write('create_file')({
|
|
48
|
+
name: `CmdTest Sheet Dest ${Date.now()}`,
|
|
49
|
+
mimeType: 'application/vnd.google-apps.spreadsheet',
|
|
50
|
+
parentId: ctx.folderId,
|
|
51
|
+
})
|
|
52
|
+
ctx.destSpreadsheetId = createdDest?.id
|
|
53
|
+
expect(ctx.destSpreadsheetId).toBeTruthy()
|
|
53
54
|
|
|
54
55
|
// Try to detect a default sheet title
|
|
55
56
|
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
57
|
try {
|
|
66
|
-
const get_spreadsheet =
|
|
58
|
+
const get_spreadsheet = sheets.read('get_spreadsheet')
|
|
67
59
|
const meta = await get_spreadsheet({ spreadsheetId: ctx.spreadsheetId })
|
|
68
60
|
sheetTitle = meta?.sheets?.[0]?.properties?.title
|
|
69
61
|
}
|
|
@@ -71,16 +63,27 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
71
63
|
}
|
|
72
64
|
}, 60000)
|
|
73
65
|
|
|
66
|
+
afterAll(async () => {
|
|
67
|
+
await safeCleanup(async () => {
|
|
68
|
+
const delete_file = drive.write('delete_file')
|
|
69
|
+
if (ctx.spreadsheetId)
|
|
70
|
+
await delete_file({ fileId: ctx.spreadsheetId })
|
|
71
|
+
if (ctx.destSpreadsheetId)
|
|
72
|
+
await delete_file({ fileId: ctx.destSpreadsheetId })
|
|
73
|
+
})
|
|
74
|
+
await safeCleanup(async () => ctx.folderId ? drive.write('delete_file')({ fileId: ctx.folderId }) : Promise.resolve())
|
|
75
|
+
}, 60_000)
|
|
76
|
+
|
|
74
77
|
it('append_values appends then clear_values clears', async () => {
|
|
75
78
|
if (!ctx.spreadsheetId)
|
|
76
79
|
return expect(true).toBe(true)
|
|
77
80
|
|
|
78
|
-
const append_values =
|
|
81
|
+
const append_values = sheets.write('append_values')
|
|
79
82
|
const aTitle = sheetTitle || 'Sheet1'
|
|
80
83
|
const appendRes = await append_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1`, values: [[`CmdTest ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
|
|
81
84
|
expect(appendRes?.updates || appendRes?.tableRange || appendRes?.spreadsheetId).toBeTruthy()
|
|
82
85
|
|
|
83
|
-
const clear_values =
|
|
86
|
+
const clear_values = sheets.write('clear_values')
|
|
84
87
|
const clearRes = await clear_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!A1:A10` })
|
|
85
88
|
expect(clearRes?.clearedRange || clearRes?.spreadsheetId).toBeTruthy()
|
|
86
89
|
}, 60000)
|
|
@@ -89,7 +92,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
89
92
|
if (!ctx.spreadsheetId)
|
|
90
93
|
return expect(true).toBe(true)
|
|
91
94
|
const aTitle = sheetTitle || 'Sheet1'
|
|
92
|
-
const update_values =
|
|
95
|
+
const update_values = sheets.write('update_values')
|
|
93
96
|
const res = await update_values({ spreadsheetId: ctx.spreadsheetId, range: `${aTitle}!B1:B1`, values: [[`CmdTestU ${Date.now()}`]], valueInputOption: 'USER_ENTERED' })
|
|
94
97
|
expect(res?.updatedRange || res?.spreadsheetId).toBeTruthy()
|
|
95
98
|
}, 60000)
|
|
@@ -98,7 +101,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
98
101
|
if (!ctx.spreadsheetId)
|
|
99
102
|
return expect(true).toBe(true)
|
|
100
103
|
const aTitle = sheetTitle || 'Sheet1'
|
|
101
|
-
const batch_update_values =
|
|
104
|
+
const batch_update_values = sheets.write('batch_update_values')
|
|
102
105
|
const res = await batch_update_values({ spreadsheetId: ctx.spreadsheetId, data: [
|
|
103
106
|
{ range: `${aTitle}!C1:C1`, values: [[`CmdTestB1 ${Date.now()}`]] },
|
|
104
107
|
{ range: `${aTitle}!C2:C2`, values: [[`CmdTestB2 ${Date.now()}`]] },
|
|
@@ -110,7 +113,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
110
113
|
if (!ctx.spreadsheetId)
|
|
111
114
|
return expect(true).toBe(true)
|
|
112
115
|
const aTitle = sheetTitle || 'Sheet1'
|
|
113
|
-
const batch_update_values_by_data_filter =
|
|
116
|
+
const batch_update_values_by_data_filter = sheets.write('batch_update_values_by_data_filter')
|
|
114
117
|
const res = await batch_update_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, data: [
|
|
115
118
|
{ dataFilter: { a1Range: `${aTitle}!D1:D1` }, values: [[`CmdTestDF ${Date.now()}`]] },
|
|
116
119
|
], valueInputOption: 'USER_ENTERED' })
|
|
@@ -121,7 +124,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
121
124
|
if (!ctx.spreadsheetId)
|
|
122
125
|
return expect(true).toBe(true)
|
|
123
126
|
const aTitle = sheetTitle || 'Sheet1'
|
|
124
|
-
const batch_clear_values =
|
|
127
|
+
const batch_clear_values = sheets.write('batch_clear_values')
|
|
125
128
|
const res = await batch_clear_values({ spreadsheetId: ctx.spreadsheetId, ranges: [`${aTitle}!A1:A2`, `${aTitle}!B1:B2`] })
|
|
126
129
|
expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
|
|
127
130
|
}, 60000)
|
|
@@ -130,7 +133,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
130
133
|
if (!ctx.spreadsheetId)
|
|
131
134
|
return expect(true).toBe(true)
|
|
132
135
|
const aTitle = sheetTitle || 'Sheet1'
|
|
133
|
-
const batch_clear_values_by_data_filter =
|
|
136
|
+
const batch_clear_values_by_data_filter = sheets.write('batch_clear_values_by_data_filter')
|
|
134
137
|
const res = await batch_clear_values_by_data_filter({ spreadsheetId: ctx.spreadsheetId, dataFilters: [{ a1Range: `${aTitle}!E1:E2` }] })
|
|
135
138
|
expect(Boolean(res?.spreadsheetId) || Array.isArray(res?.clearedRanges)).toBe(true)
|
|
136
139
|
}, 60000)
|
|
@@ -139,7 +142,7 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
139
142
|
if (!ctx.spreadsheetId)
|
|
140
143
|
return expect(true).toBe(true)
|
|
141
144
|
const aTitle = sheetTitle || 'Sheet1'
|
|
142
|
-
const batch_update =
|
|
145
|
+
const batch_update = sheets.write('batch_update')
|
|
143
146
|
const res = await batch_update({ spreadsheetId: ctx.spreadsheetId, requests: [
|
|
144
147
|
{ findReplace: { find: '___unlikely___', replacement: '___unlikely___', matchCase: true, searchByRegex: false, includeFormulas: false, range: { sheetId: 0 } } },
|
|
145
148
|
], includeSpreadsheetInResponse: false })
|
|
@@ -147,12 +150,12 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
147
150
|
}, 60000)
|
|
148
151
|
|
|
149
152
|
it('copy_to_spreadsheet copies a sheet when destination provided', async () => {
|
|
150
|
-
if (!ctx.spreadsheetId || !
|
|
153
|
+
if (!ctx.spreadsheetId || !ctx.destSpreadsheetId)
|
|
151
154
|
return expect(true).toBe(true)
|
|
152
|
-
const copy_to_spreadsheet =
|
|
155
|
+
const copy_to_spreadsheet = sheets.write('copy_to_spreadsheet')
|
|
153
156
|
// Attempt to copy sheet with id 0 (typical for first sheet). If it fails, skip.
|
|
154
157
|
try {
|
|
155
|
-
const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId:
|
|
158
|
+
const res = await copy_to_spreadsheet({ spreadsheetId: ctx.spreadsheetId, sheetId: 0, destinationSpreadsheetId: ctx.destSpreadsheetId })
|
|
156
159
|
expect(res?.sheetId !== undefined || res?.spreadsheetId).toBeTruthy()
|
|
157
160
|
}
|
|
158
161
|
catch {
|
|
@@ -160,12 +163,10 @@ suite('google-sheet write handlers (live)', () => {
|
|
|
160
163
|
}
|
|
161
164
|
}, 60000)
|
|
162
165
|
|
|
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.
|
|
166
|
+
it('create_spreadsheet creates a spreadsheet (self-cleaning)', async () => {
|
|
167
|
+
const created = await sheets.write('create_spreadsheet')({ properties: { title: `CmdTest Sheet Tool ${Date.now()}` } })
|
|
168
|
+
const id = created?.spreadsheetId
|
|
169
|
+
expect(typeof id).toBe('string')
|
|
170
|
+
await safeCleanup(async () => id ? drive.write('delete_file')({ fileId: id }) : Promise.resolve())
|
|
170
171
|
}, 60000)
|
|
171
172
|
})
|