chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import type { AirtableCreateParams, AirtableCreateResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableCreateRecordsTool: ToolConfig<AirtableCreateParams, AirtableCreateResponse> = {
|
||||
id: 'airtable_create_records',
|
||||
name: 'Airtable Create Records',
|
||||
description: 'Write new records to an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
records: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of records to create, each with a `fields` object',
|
||||
// Example: [{ fields: { "Field 1": "Value1", "Field 2": "Value2" } }]
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`,
|
||||
method: 'POST',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ records: params.records }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records: data.records ?? [],
|
||||
metadata: {
|
||||
recordCount: (data.records ?? []).length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of created Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records created' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import type { AirtableDeleteParams, AirtableDeleteResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableDeleteRecordsTool: ToolConfig<AirtableDeleteParams, AirtableDeleteResponse> = {
|
||||
id: 'airtable_delete_records',
|
||||
name: 'Airtable Delete Records',
|
||||
description: 'Delete one or more records from an Airtable table by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
recordIds: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of record IDs to delete (each starts with "rec", e.g., ["recXXXXXXXXXXXXXX"]). Pass a single-element array to delete one record.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const base = `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`
|
||||
const ids = (params.recordIds ?? [])
|
||||
.map((id) => (id == null ? '' : String(id).trim()))
|
||||
.filter(Boolean)
|
||||
if (ids.length === 0) {
|
||||
throw new Error('At least one record ID is required to delete')
|
||||
}
|
||||
if (ids.length > 10) {
|
||||
throw new Error(
|
||||
`Airtable deletes at most 10 records per request (received ${ids.length}). Split the delete into batches of 10 or fewer.`
|
||||
)
|
||||
}
|
||||
const queryParams = new URLSearchParams()
|
||||
for (const id of ids) {
|
||||
queryParams.append('records[]', id as string)
|
||||
}
|
||||
return `${base}?${queryParams.toString()}`
|
||||
},
|
||||
method: 'DELETE',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const records = data.records ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
metadata: {
|
||||
recordCount: records.length,
|
||||
deletedRecordIds: records.map((r: { id: string }) => r.id),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of deleted Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
deleted: { type: 'boolean', description: 'Whether the record was deleted' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records deleted' },
|
||||
deletedRecordIds: { type: 'array', description: 'List of deleted record IDs' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import type { ToolConfig, ToolResponse } from '@/tools/types'
|
||||
|
||||
export interface AirtableGetBaseSchemaParams {
|
||||
accessToken: string
|
||||
baseId: string
|
||||
}
|
||||
|
||||
interface AirtableFieldSchema {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
options?: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface AirtableViewSchema {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface AirtableTableSchema {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
fields: AirtableFieldSchema[]
|
||||
views: AirtableViewSchema[]
|
||||
}
|
||||
|
||||
export interface AirtableGetBaseSchemaResponse extends ToolResponse {
|
||||
output: {
|
||||
tables: AirtableTableSchema[]
|
||||
metadata: {
|
||||
totalTables: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const airtableGetBaseSchemaTool: ToolConfig<
|
||||
AirtableGetBaseSchemaParams,
|
||||
AirtableGetBaseSchemaResponse
|
||||
> = {
|
||||
id: 'airtable_get_base_schema',
|
||||
name: 'Airtable Get Base Schema',
|
||||
description: 'Get the schema of all tables, fields, and views in an Airtable base',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.airtable.com/v0/meta/bases/${params.baseId}/tables`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const tables = (data.tables || []).map((table: Record<string, unknown>) => ({
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
description: table.description,
|
||||
fields: ((table.fields as Record<string, unknown>[]) || []).map((field) => ({
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
description: field.description,
|
||||
options: field.options,
|
||||
})),
|
||||
views: ((table.views as Record<string, unknown>[]) || []).map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
type: view.type,
|
||||
})),
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tables,
|
||||
metadata: {
|
||||
totalTables: tables.length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tables: {
|
||||
type: 'json',
|
||||
description: 'Array of table schemas with fields and views',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
fields: { type: 'json' },
|
||||
views: { type: 'json' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including total tables count',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { AirtableGetParams, AirtableGetResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableGetRecordTool: ToolConfig<AirtableGetParams, AirtableGetResponse> = {
|
||||
id: 'airtable_get_record',
|
||||
name: 'Airtable Get Record',
|
||||
description: 'Retrieve a single record from an Airtable table by its ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record ID to retrieve (starts with "rec", e.g., "recXXXXXXXXXXXXXX")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}/${params.recordId?.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data, // API returns the single record object
|
||||
metadata: {
|
||||
recordCount: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
description: 'Retrieved Airtable record',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records returned (always 1)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { airtableCreateRecordsTool } from '@/tools/airtable/create_records'
|
||||
import { airtableDeleteRecordsTool } from '@/tools/airtable/delete_records'
|
||||
import { airtableGetBaseSchemaTool } from '@/tools/airtable/get_base_schema'
|
||||
import { airtableGetRecordTool } from '@/tools/airtable/get_record'
|
||||
import { airtableListBasesTool } from '@/tools/airtable/list_bases'
|
||||
import { airtableListRecordsTool } from '@/tools/airtable/list_records'
|
||||
import { airtableListTablesTool } from '@/tools/airtable/list_tables'
|
||||
import { airtableUpdateMultipleRecordsTool } from '@/tools/airtable/update_multiple_records'
|
||||
import { airtableUpdateRecordTool } from '@/tools/airtable/update_record'
|
||||
import { airtableUpsertRecordsTool } from '@/tools/airtable/upsert_records'
|
||||
|
||||
export {
|
||||
airtableCreateRecordsTool,
|
||||
airtableDeleteRecordsTool,
|
||||
airtableGetBaseSchemaTool,
|
||||
airtableGetRecordTool,
|
||||
airtableListBasesTool,
|
||||
airtableListRecordsTool,
|
||||
airtableListTablesTool,
|
||||
airtableUpdateMultipleRecordsTool,
|
||||
airtableUpdateRecordTool,
|
||||
airtableUpsertRecordsTool,
|
||||
}
|
||||
|
||||
export * from './types'
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { AirtableListBasesParams, AirtableListBasesResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableListBasesTool: ToolConfig<AirtableListBasesParams, AirtableListBasesResponse> =
|
||||
{
|
||||
id: 'airtable_list_bases',
|
||||
name: 'Airtable List Bases',
|
||||
description: 'List all bases the authenticated user has access to',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
offset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination offset for retrieving additional bases',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = 'https://api.airtable.com/v0/meta/bases'
|
||||
if (params.offset) {
|
||||
return `${url}?offset=${encodeURIComponent(params.offset)}`
|
||||
}
|
||||
return url
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
bases: (data.bases ?? []).map(
|
||||
(base: { id: string; name: string; permissionLevel: string }) => ({
|
||||
id: base.id,
|
||||
name: base.name,
|
||||
permissionLevel: base.permissionLevel,
|
||||
})
|
||||
),
|
||||
metadata: {
|
||||
offset: data.offset ?? null,
|
||||
totalBases: (data.bases ?? []).length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
bases: {
|
||||
type: 'array',
|
||||
description: 'Array of Airtable bases with id, name, and permissionLevel',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Base ID (starts with "app")' },
|
||||
name: { type: 'string', description: 'Base name' },
|
||||
permissionLevel: {
|
||||
type: 'string',
|
||||
description: 'Permission level (none, read, comment, edit, create)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Pagination and count metadata',
|
||||
properties: {
|
||||
offset: { type: 'string', description: 'Offset for next page of results' },
|
||||
totalBases: { type: 'number', description: 'Number of bases returned' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { AirtableListParams, AirtableListResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableListRecordsTool: ToolConfig<AirtableListParams, AirtableListResponse> = {
|
||||
id: 'airtable_list_records',
|
||||
name: 'Airtable List Records',
|
||||
description: 'Read records from an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
maxRecords: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Maximum number of records to return (default: all records)',
|
||||
},
|
||||
filterFormula: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Formula to filter records (e.g., "({Field Name} = \'Value\')")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = `https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`
|
||||
const queryParams = new URLSearchParams()
|
||||
if (params.maxRecords) queryParams.append('maxRecords', Number(params.maxRecords).toString())
|
||||
if (params.filterFormula) {
|
||||
const encodedFormula = params.filterFormula.replace(/'/g, "'")
|
||||
queryParams.append('filterByFormula', encodedFormula)
|
||||
}
|
||||
const queryString = queryParams.toString()
|
||||
const finalUrl = queryString ? `${url}?${queryString}` : url
|
||||
return finalUrl
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records: data.records ?? [],
|
||||
metadata: {
|
||||
offset: data.offset ?? null,
|
||||
totalRecords: (data.records ?? []).length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of retrieved Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata including pagination offset and total records count',
|
||||
properties: {
|
||||
offset: { type: 'string', description: 'Pagination offset for next page' },
|
||||
totalRecords: { type: 'number', description: 'Number of records returned' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
AirtableField,
|
||||
AirtableListTablesParams,
|
||||
AirtableListTablesResponse,
|
||||
AirtableTable,
|
||||
} from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableListTablesTool: ToolConfig<
|
||||
AirtableListTablesParams,
|
||||
AirtableListTablesResponse
|
||||
> = {
|
||||
id: 'airtable_list_tables',
|
||||
name: 'Airtable List Tables',
|
||||
description: 'List all tables and their schema in an Airtable base',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => `https://api.airtable.com/v0/meta/bases/${params.baseId?.trim()}/tables`,
|
||||
method: 'GET',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
},
|
||||
|
||||
transformResponse: async (response, params) => {
|
||||
const data = await response.json()
|
||||
const tables: AirtableTable[] = (data.tables ?? []).map(
|
||||
(table: {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
primaryFieldId: string
|
||||
fields: AirtableField[]
|
||||
}) => ({
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
description: table.description ?? null,
|
||||
primaryFieldId: table.primaryFieldId,
|
||||
fields: (table.fields ?? []).map((field: AirtableField) => ({
|
||||
id: field.id,
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
description: field.description ?? null,
|
||||
options: field.options ?? null,
|
||||
})),
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
tables,
|
||||
metadata: {
|
||||
baseId: params?.baseId ?? '',
|
||||
totalTables: tables.length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
tables: {
|
||||
type: 'array',
|
||||
description: 'List of tables in the base with their schema',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Table ID (starts with "tbl")' },
|
||||
name: { type: 'string', description: 'Table name' },
|
||||
description: { type: 'string', description: 'Table description' },
|
||||
primaryFieldId: { type: 'string', description: 'ID of the primary field' },
|
||||
fields: {
|
||||
type: 'array',
|
||||
description: 'List of fields in the table',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Field ID (starts with "fld")' },
|
||||
name: { type: 'string', description: 'Field name' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Field type (singleLineText, multilineText, number, checkbox, singleSelect, multipleSelects, date, dateTime, attachment, linkedRecord, etc.)',
|
||||
},
|
||||
description: { type: 'string', description: 'Field description' },
|
||||
options: { type: 'json', description: 'Field-specific options (choices, etc.)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Base info and count metadata',
|
||||
properties: {
|
||||
baseId: { type: 'string', description: 'The base ID queried' },
|
||||
totalTables: { type: 'number', description: 'Number of tables in the base' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
// Common types
|
||||
interface AirtableRecord {
|
||||
id: string
|
||||
createdTime: string
|
||||
fields: Record<string, any>
|
||||
}
|
||||
|
||||
interface AirtableBase {
|
||||
id: string
|
||||
name: string
|
||||
permissionLevel: 'none' | 'read' | 'comment' | 'edit' | 'create'
|
||||
}
|
||||
|
||||
interface AirtableFieldOption {
|
||||
id: string
|
||||
name: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface AirtableField {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
description?: string
|
||||
options?: {
|
||||
choices?: AirtableFieldOption[]
|
||||
linkedTableId?: string
|
||||
isReversed?: boolean
|
||||
prefersSingleRecordLink?: boolean
|
||||
inverseLinkFieldId?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface AirtableTable {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
primaryFieldId: string
|
||||
fields: AirtableField[]
|
||||
}
|
||||
|
||||
interface AirtableView {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
}
|
||||
|
||||
interface AirtableBaseParams {
|
||||
accessToken: string
|
||||
baseId: string
|
||||
tableId: string
|
||||
}
|
||||
|
||||
// List Bases Types
|
||||
export interface AirtableListBasesParams {
|
||||
accessToken: string
|
||||
offset?: string
|
||||
}
|
||||
|
||||
export interface AirtableListBasesResponse extends ToolResponse {
|
||||
output: {
|
||||
bases: AirtableBase[]
|
||||
metadata: {
|
||||
offset?: string
|
||||
totalBases: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List Tables Types (Get Base Schema)
|
||||
export interface AirtableListTablesParams {
|
||||
accessToken: string
|
||||
baseId: string
|
||||
}
|
||||
|
||||
export interface AirtableListTablesResponse extends ToolResponse {
|
||||
output: {
|
||||
tables: AirtableTable[]
|
||||
metadata: {
|
||||
baseId: string
|
||||
totalTables: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List Records Types
|
||||
export interface AirtableListParams extends AirtableBaseParams {
|
||||
maxRecords?: number
|
||||
filterFormula?: string
|
||||
}
|
||||
|
||||
export interface AirtableListResponse extends ToolResponse {
|
||||
output: {
|
||||
records: AirtableRecord[]
|
||||
metadata: {
|
||||
offset?: string
|
||||
totalRecords: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get Record Types
|
||||
export interface AirtableGetParams extends AirtableBaseParams {
|
||||
recordId: string
|
||||
}
|
||||
|
||||
export interface AirtableGetResponse extends ToolResponse {
|
||||
output: {
|
||||
record: AirtableRecord
|
||||
metadata: {
|
||||
recordCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create Records Types
|
||||
export interface AirtableCreateParams extends AirtableBaseParams {
|
||||
records: Array<{ fields: Record<string, any> }>
|
||||
}
|
||||
|
||||
export interface AirtableCreateResponse extends ToolResponse {
|
||||
output: {
|
||||
records: AirtableRecord[]
|
||||
metadata: {
|
||||
recordCount: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Record Types (Single)
|
||||
export interface AirtableUpdateParams extends AirtableBaseParams {
|
||||
recordId: string
|
||||
fields: Record<string, any>
|
||||
}
|
||||
|
||||
export interface AirtableUpdateResponse extends ToolResponse {
|
||||
output: {
|
||||
record: AirtableRecord // Airtable returns the single updated record
|
||||
metadata: {
|
||||
recordCount: 1
|
||||
updatedFields: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Multiple Records Types
|
||||
export interface AirtableUpdateMultipleParams extends AirtableBaseParams {
|
||||
records: Array<{ id: string; fields: Record<string, any> }>
|
||||
}
|
||||
|
||||
export interface AirtableUpdateMultipleResponse extends ToolResponse {
|
||||
output: {
|
||||
records: AirtableRecord[] // Airtable returns the array of updated records
|
||||
metadata: {
|
||||
recordCount: number
|
||||
updatedRecordIds: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Records Types
|
||||
export interface AirtableDeleteParams extends AirtableBaseParams {
|
||||
recordIds: string[]
|
||||
}
|
||||
|
||||
interface AirtableDeletedRecord {
|
||||
id: string
|
||||
deleted: boolean
|
||||
}
|
||||
|
||||
export interface AirtableDeleteResponse extends ToolResponse {
|
||||
output: {
|
||||
records: AirtableDeletedRecord[]
|
||||
metadata: {
|
||||
recordCount: number
|
||||
deletedRecordIds: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert Records Types
|
||||
export interface AirtableUpsertParams extends AirtableBaseParams {
|
||||
records: Array<{ fields: Record<string, any> }>
|
||||
fieldsToMergeOn: string[]
|
||||
typecast?: boolean
|
||||
}
|
||||
|
||||
export interface AirtableUpsertResponse extends ToolResponse {
|
||||
output: {
|
||||
records: AirtableRecord[]
|
||||
createdRecords: string[]
|
||||
updatedRecords: string[]
|
||||
metadata: {
|
||||
recordCount: number
|
||||
createdCount: number
|
||||
updatedCount: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AirtableResponse =
|
||||
| AirtableListBasesResponse
|
||||
| AirtableListTablesResponse
|
||||
| AirtableListResponse
|
||||
| AirtableGetResponse
|
||||
| AirtableCreateResponse
|
||||
| AirtableUpdateResponse
|
||||
| AirtableUpdateMultipleResponse
|
||||
| AirtableDeleteResponse
|
||||
| AirtableUpsertResponse
|
||||
| AirtableListBasesResponse
|
||||
| AirtableGetBaseSchemaResponse
|
||||
|
||||
interface AirtableGetBaseSchemaResponse extends ToolResponse {
|
||||
output: {
|
||||
tables: Array<{
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
fields: Array<{ id: string; name: string; type: string; description?: string }>
|
||||
views: Array<{ id: string; name: string; type: string }>
|
||||
}>
|
||||
metadata: { totalTables: number }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type {
|
||||
AirtableUpdateMultipleParams,
|
||||
AirtableUpdateMultipleResponse,
|
||||
} from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableUpdateMultipleRecordsTool: ToolConfig<
|
||||
AirtableUpdateMultipleParams,
|
||||
AirtableUpdateMultipleResponse
|
||||
> = {
|
||||
id: 'airtable_update_multiple_records',
|
||||
name: 'Airtable Update Multiple Records',
|
||||
description: 'Update multiple existing records in an Airtable table',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
records: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of records to update, each with an `id` and a `fields` object',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ records: params.records }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const records = data.records ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
metadata: {
|
||||
recordCount: records.length,
|
||||
updatedRecordIds: records.map((r: { id: string }) => r.id),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of updated Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records updated' },
|
||||
updatedRecordIds: { type: 'array', description: 'List of updated record IDs' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import type { AirtableUpdateParams, AirtableUpdateResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableUpdateRecordTool: ToolConfig<AirtableUpdateParams, AirtableUpdateResponse> = {
|
||||
id: 'airtable_update_record',
|
||||
name: 'Airtable Update Record',
|
||||
description: 'Update an existing record in an Airtable table by ID',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
recordId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Record ID to update (starts with "rec", e.g., "recXXXXXXXXXXXXXX")',
|
||||
},
|
||||
fields: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'An object containing the field names and their new values',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}/${params.recordId?.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ fields: params.fields }),
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
record: data,
|
||||
metadata: {
|
||||
recordCount: 1,
|
||||
updatedFields: Object.keys(data.fields ?? {}),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
record: {
|
||||
type: 'json',
|
||||
description: 'Updated Airtable record',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Number of records updated (always 1)' },
|
||||
updatedFields: { type: 'array', description: 'List of field names that were updated' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { AirtableUpsertParams, AirtableUpsertResponse } from '@/tools/airtable/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const airtableUpsertRecordsTool: ToolConfig<AirtableUpsertParams, AirtableUpsertResponse> = {
|
||||
id: 'airtable_upsert_records',
|
||||
name: 'Airtable Upsert Records',
|
||||
description:
|
||||
'Update existing records or create new ones in an Airtable table, matching on the specified merge fields',
|
||||
version: '1.0.0',
|
||||
|
||||
oauth: {
|
||||
required: true,
|
||||
provider: 'airtable',
|
||||
},
|
||||
|
||||
params: {
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
baseId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Airtable base ID (starts with "app", e.g., "appXXXXXXXXXXXXXX")',
|
||||
},
|
||||
tableId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Table ID (starts with "tbl") or table name',
|
||||
},
|
||||
records: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Array of records to upsert, each with a `fields` object',
|
||||
},
|
||||
fieldsToMergeOn: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of field names used to match existing records (max 3). A record is updated when all merge fields match, otherwise it is created. Example: ["Name"]',
|
||||
},
|
||||
typecast: {
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'When true, Airtable automatically converts string values to the field type',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
`https://api.airtable.com/v0/${params.baseId?.trim()}/${params.tableId?.trim()}`,
|
||||
method: 'PATCH',
|
||||
headers: (params) => ({
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const mergeFields = (params.fieldsToMergeOn ?? [])
|
||||
.map((f) => (f == null ? '' : String(f).trim()))
|
||||
.filter(Boolean)
|
||||
if (mergeFields.length === 0) {
|
||||
throw new Error('At least one field to merge on is required for upsert')
|
||||
}
|
||||
if (mergeFields.length > 3) {
|
||||
throw new Error(
|
||||
`Airtable upsert accepts at most 3 fields to merge on (received ${mergeFields.length}).`
|
||||
)
|
||||
}
|
||||
const records = params.records ?? []
|
||||
if (records.length > 10) {
|
||||
throw new Error(
|
||||
`Airtable upserts at most 10 records per request (received ${records.length}). Split the upsert into batches of 10 or fewer.`
|
||||
)
|
||||
}
|
||||
const body: Record<string, unknown> = {
|
||||
performUpsert: { fieldsToMergeOn: mergeFields },
|
||||
records,
|
||||
}
|
||||
if (params.typecast != null) body.typecast = params.typecast
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const records = data.records ?? []
|
||||
const createdRecords = data.createdRecords ?? []
|
||||
const updatedRecords = data.updatedRecords ?? []
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
records,
|
||||
createdRecords,
|
||||
updatedRecords,
|
||||
metadata: {
|
||||
recordCount: records.length,
|
||||
createdCount: createdRecords.length,
|
||||
updatedCount: updatedRecords.length,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
records: {
|
||||
type: 'array',
|
||||
description: 'Array of upserted Airtable records',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Record ID' },
|
||||
createdTime: { type: 'string', description: 'Record creation timestamp' },
|
||||
fields: { type: 'json', description: 'Record field values' },
|
||||
},
|
||||
},
|
||||
},
|
||||
createdRecords: {
|
||||
type: 'array',
|
||||
description: 'IDs of records that were created',
|
||||
items: { type: 'string', description: 'Created record ID' },
|
||||
},
|
||||
updatedRecords: {
|
||||
type: 'array',
|
||||
description: 'IDs of records that were updated',
|
||||
items: { type: 'string', description: 'Updated record ID' },
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
description: 'Operation metadata',
|
||||
properties: {
|
||||
recordCount: { type: 'number', description: 'Total number of records returned' },
|
||||
createdCount: { type: 'number', description: 'Number of records created' },
|
||||
updatedCount: { type: 'number', description: 'Number of records updated' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user