Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function ConnectorSelectorField({
const context = useMemo<SelectorContext>(() => {
const ctx: SelectorContext = {}
if (credentialId) ctx.oauthCredential = credentialId
if (field.mimeType) ctx.mimeType = field.mimeType

for (const depFieldId of getDependsOnFields(field.dependsOn)) {
const depField = configFields.find((f) => f.id === depFieldId)
Expand All @@ -49,7 +50,7 @@ export function ConnectorSelectorField({
}

return ctx
}, [credentialId, field.dependsOn, sourceConfig, configFields, canonicalModes])
}, [credentialId, field.mimeType, field.dependsOn, sourceConfig, configFields, canonicalModes])

const depsResolved = useMemo(() => {
if (!field.dependsOn) return true
Expand Down
65 changes: 38 additions & 27 deletions apps/sim/connectors/google-docs/google-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { toError } from '@sim/utils/errors'
import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils'
import { googleDocsConnectorMeta } from '@/connectors/google-docs/meta'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
import { joinTagArray, parseTagDate } from '@/connectors/utils'
import {
buildDriveParentsClause,
joinTagArray,
parseMultiValue,
parseTagDate,
} from '@/connectors/utils'

const logger = createLogger('GoogleDocsConnector')

Expand Down Expand Up @@ -152,10 +157,8 @@ function fileToStub(file: DriveFile): ExternalDocument {
function buildQuery(sourceConfig: Record<string, unknown>): string {
const parts: string[] = ['trashed = false', "mimeType = 'application/vnd.google-apps.document'"]

const folderId = sourceConfig.folderId as string | undefined
if (folderId?.trim()) {
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
}
const parentsClause = buildDriveParentsClause(parseMultiValue(sourceConfig.folderId))
if (parentsClause) parts.push(parentsClause)

return parts.join(' and ')
}
Expand Down Expand Up @@ -298,38 +301,46 @@ export const googleDocsConnector: ConnectorConfig = {
accessToken: string,
sourceConfig: Record<string, unknown>
): Promise<{ valid: boolean; error?: string }> => {
const folderId = sourceConfig.folderId as string | undefined
const folderIds = parseMultiValue(sourceConfig.folderId)
const maxDocs = sourceConfig.maxDocs as string | undefined

if (maxDocs && (Number.isNaN(Number(maxDocs)) || Number(maxDocs) <= 0)) {
return { valid: false, error: 'Max documents must be a positive number' }
}

try {
if (folderId?.trim()) {
const url = `https://www.googleapis.com/drive/v3/files/${folderId.trim()}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
if (folderIds.length > 0) {
for (const folderId of folderIds) {
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
},
},
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' }
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return {
valid: false,
error: `Folder "${folderId}" not found. Check the folder ID and permissions.`,
}
}
return {
valid: false,
error: `Failed to access folder "${folderId}": ${response.status}`,
}
}
return { valid: false, error: `Failed to access folder: ${response.status}` }
}

const folder = await response.json()
if (folder.mimeType !== 'application/vnd.google-apps.folder') {
return { valid: false, error: 'The provided ID is not a folder' }
const folder = await response.json()
if (folder.mimeType !== 'application/vnd.google-apps.folder') {
return { valid: false, error: `"${folderId}" is not a folder` }
}
}
} else {
const url =
Expand Down
19 changes: 17 additions & 2 deletions apps/sim/connectors/google-docs/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,26 @@ export const googleDocsConnectorMeta: ConnectorMeta = {
},

configFields: [
{
id: 'folderSelector',
title: 'Folders',
type: 'selector',
selectorKey: 'google.drive',
mimeType: 'application/vnd.google-apps.folder',
canonicalParamId: 'folderId',
mode: 'basic',
multi: true,
placeholder: 'Select one or more folders (optional)',
required: false,
},
{
id: 'folderId',
title: 'Folder ID',
title: 'Folder IDs',
type: 'short-input',
placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)',
canonicalParamId: 'folderId',
mode: 'advanced',
multi: true,
placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)',
required: false,
},
{
Expand Down
62 changes: 35 additions & 27 deletions apps/sim/connectors/google-drive/google-drive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/document
import { googleDriveConnectorMeta } from '@/connectors/google-drive/meta'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
import {
buildDriveParentsClause,
CONNECTOR_MAX_FILE_BYTES,
ConnectorFileTooLargeError,
htmlToPlainText,
joinTagArray,
markSkipped,
parseMultiValue,
parseTagDate,
readBodyWithLimit,
sizeLimitSkipReason,
Expand Down Expand Up @@ -137,10 +139,8 @@ interface DriveFile {
function buildQuery(sourceConfig: Record<string, unknown>): string {
const parts: string[] = ['trashed = false']

const folderId = sourceConfig.folderId as string | undefined
if (folderId?.trim()) {
parts.push(`'${folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")}' in parents`)
}
const parentsClause = buildDriveParentsClause(parseMultiValue(sourceConfig.folderId))
if (parentsClause) parts.push(parentsClause)

const fileType = (sourceConfig.fileType as string) || 'all'
switch (fileType) {
Expand Down Expand Up @@ -324,7 +324,7 @@ export const googleDriveConnector: ConnectorConfig = {
accessToken: string,
sourceConfig: Record<string, unknown>
): Promise<{ valid: boolean; error?: string }> => {
const folderId = sourceConfig.folderId as string | undefined
const folderIds = parseMultiValue(sourceConfig.folderId)
const maxFiles = sourceConfig.maxFiles as string | undefined

if (maxFiles && (Number.isNaN(Number(maxFiles)) || Number(maxFiles) <= 0)) {
Expand All @@ -333,31 +333,39 @@ export const googleDriveConnector: ConnectorConfig = {

// Verify access to Drive API
try {
if (folderId?.trim()) {
// Verify the folder exists and is accessible
const url = `https://www.googleapis.com/drive/v3/files/${folderId.trim()}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
if (folderIds.length > 0) {
// Verify each folder exists, is accessible, and is actually a folder
for (const folderId of folderIds) {
const url = `https://www.googleapis.com/drive/v3/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
},
},
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' }
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return {
valid: false,
error: `Folder "${folderId}" not found. Check the folder ID and permissions.`,
}
}
return {
valid: false,
error: `Failed to access folder "${folderId}": ${response.status}`,
}
}
return { valid: false, error: `Failed to access folder: ${response.status}` }
}

const folder = await response.json()
if (folder.mimeType !== 'application/vnd.google-apps.folder') {
return { valid: false, error: 'The provided ID is not a folder' }
const folder = await response.json()
if (folder.mimeType !== 'application/vnd.google-apps.folder') {
return { valid: false, error: `"${folderId}" is not a folder` }
}
}
} else {
// Verify basic Drive access by listing one file
Expand Down
19 changes: 17 additions & 2 deletions apps/sim/connectors/google-drive/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,26 @@ export const googleDriveConnectorMeta: ConnectorMeta = {
},

configFields: [
{
id: 'folderSelector',
title: 'Folders',
type: 'selector',
selectorKey: 'google.drive',
mimeType: 'application/vnd.google-apps.folder',
canonicalParamId: 'folderId',
mode: 'basic',
multi: true,
placeholder: 'Select one or more folders (optional)',
required: false,
},
{
id: 'folderId',
title: 'Folder ID',
title: 'Folder IDs',
type: 'short-input',
placeholder: 'e.g. 1aBcDeFgHiJkLmNoPqRsTuVwXyZ (optional)',
canonicalParamId: 'folderId',
mode: 'advanced',
multi: true,
placeholder: 'e.g. 1aBcDeFg…, 2cDeFgHi… (comma-separated for multiple)',
required: false,
},
{
Expand Down
79 changes: 45 additions & 34 deletions apps/sim/connectors/google-forms/google-forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { getErrorMessage, toError } from '@sim/utils/errors'
import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils'
import { googleFormsConnectorMeta, MAX_RESPONSES_PER_FORM } from '@/connectors/google-forms/meta'
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
import { joinTagArray, parseTagDate } from '@/connectors/utils'
import {
buildDriveParentsClause,
joinTagArray,
parseMultiValue,
parseTagDate,
} from '@/connectors/utils'

const logger = createLogger('GoogleFormsConnector')

Expand Down Expand Up @@ -448,16 +453,14 @@ function renderFormDocument(form: FormStructure, responses: FormResponse[]): str
}

/**
* Builds the Drive `q` query that selects form files, optionally scoped to a
* folder. Single quotes and backslashes in the folder ID are escaped to prevent
* query injection.
* Builds the Drive `q` query that selects form files, optionally scoped to one
* or more folders. Single quotes and backslashes in folder IDs are escaped to
* prevent query injection.
*/
function buildDriveQuery(folderId?: string): string {
function buildDriveQuery(folderIds: string[]): string {
const parts = ['trashed = false', `mimeType = '${FORM_MIME_TYPE}'`]
if (folderId?.trim()) {
const escaped = folderId.trim().replace(/\\/g, '\\\\').replace(/'/g, "\\'")
parts.push(`'${escaped}' in parents`)
}
const parentsClause = buildDriveParentsClause(folderIds)
if (parentsClause) parts.push(parentsClause)
return parts.join(' and ')
}

Expand All @@ -479,9 +482,9 @@ export const googleFormsConnector: ConnectorConfig = {
return { documents: [], hasMore: false }
}

const folderId = sourceConfig.folderId as string | undefined
const folderIds = parseMultiValue(sourceConfig.folderId)
const queryParams = new URLSearchParams({
q: buildDriveQuery(folderId),
q: buildDriveQuery(folderIds),
pageSize: String(DRIVE_PAGE_SIZE),
orderBy: 'modifiedTime desc',
fields: 'nextPageToken,files(id,name,mimeType,modifiedTime,createdTime,webViewLink,owners)',
Expand All @@ -493,7 +496,7 @@ export const googleFormsConnector: ConnectorConfig = {
const url = `${DRIVE_API_BASE}/files?${queryParams.toString()}`

logger.info('Listing Google Forms', {
folderId: folderId?.trim() || 'all',
folderId: folderIds.length > 0 ? folderIds.join(',') : 'all',
contentScope,
cursor: cursor ?? 'initial',
})
Expand Down Expand Up @@ -667,7 +670,7 @@ export const googleFormsConnector: ConnectorConfig = {
accessToken: string,
sourceConfig: Record<string, unknown>
): Promise<{ valid: boolean; error?: string }> => {
const folderId = sourceConfig.folderId as string | undefined
const folderIds = parseMultiValue(sourceConfig.folderId)
const maxForms = sourceConfig.maxForms as string | undefined
const maxResponsesPerForm = sourceConfig.maxResponsesPerForm as string | undefined

Expand All @@ -683,30 +686,38 @@ export const googleFormsConnector: ConnectorConfig = {
}

try {
if (folderId?.trim()) {
const url = `${DRIVE_API_BASE}/files/${encodeURIComponent(folderId.trim())}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
if (folderIds.length > 0) {
for (const folderId of folderIds) {
const url = `${DRIVE_API_BASE}/files/${encodeURIComponent(folderId)}?fields=id,name,mimeType&supportsAllDrives=true`
const response = await fetchWithRetry(
url,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
},
},
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return { valid: false, error: 'Folder not found. Check the folder ID and permissions.' }
VALIDATE_RETRY_OPTIONS
)

if (!response.ok) {
if (response.status === 404) {
return {
valid: false,
error: `Folder "${folderId}" not found. Check the folder ID and permissions.`,
}
}
return {
valid: false,
error: `Failed to access folder "${folderId}": ${response.status}`,
}
}
return { valid: false, error: `Failed to access folder: ${response.status}` }
}

const folder = await response.json()
if (folder.mimeType !== FOLDER_MIME_TYPE) {
return { valid: false, error: 'The provided ID is not a folder' }
const folder = await response.json()
if (folder.mimeType !== FOLDER_MIME_TYPE) {
return { valid: false, error: `"${folderId}" is not a folder` }
}
}
} else {
const url = `${DRIVE_API_BASE}/files?pageSize=1&q=${encodeURIComponent(`mimeType = '${FORM_MIME_TYPE}'`)}&fields=files(id)&supportsAllDrives=true&includeItemsFromAllDrives=true`
Expand Down
Loading
Loading