Files
simstudioai--sim/apps/sim/tools/supabase/introspect.ts
T
wehub-resource-sync d25d482dc2
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) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

173 lines
5.6 KiB
TypeScript

import {
INTROSPECT_TABLE_OUTPUT_PROPERTIES,
type SupabaseColumnSchema,
type SupabaseIntrospectParams,
type SupabaseIntrospectResponse,
type SupabaseTableSchema,
} from '@/tools/supabase/types'
import { supabaseBaseUrl } from '@/tools/supabase/utils'
import type { ToolConfig } from '@/tools/types'
/**
* Tool for introspecting Supabase database schema.
*
* PostgREST (which powers `/rest/v1`) has no generic "run arbitrary SQL"
* endpoint, so schema introspection is derived from the project's
* auto-generated OpenAPI spec (`GET /rest/v1/` with an OpenAPI `Accept`
* header) rather than a live `information_schema` query. Primary-key
* detection is a best-effort naming heuristic (`id` column), and
* foreign-key detection only succeeds if the table owner has added a
* matching `references table.column` SQL comment — the OpenAPI spec does
* not expose constraint metadata directly. Index information is not
* available via this API at all. `nullable` is also best-effort: PostgREST
* only lists a column under `required` when it's NOT NULL *and* has no
* default, so a NOT NULL column with a default is reported as nullable.
*/
export const introspectTool: ToolConfig<SupabaseIntrospectParams, SupabaseIntrospectResponse> = {
id: 'supabase_introspect',
name: 'Supabase Introspect',
description:
'Introspect Supabase database schema from its OpenAPI spec to get table and column structures (best-effort primary/foreign key detection)',
version: '1.0.0',
params: {
projectId: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase project ID (e.g., jdrkgepadsdopsntdlom)',
},
schema: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Database schema to introspect (defaults to all user schemas, commonly "public")',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Supabase service role secret key',
},
},
request: {
url: (params) => `${supabaseBaseUrl(params.projectId)}/rest/v1/`,
method: 'GET',
headers: (params) => ({
apikey: params.apiKey,
Authorization: `Bearer ${params.apiKey}`,
Accept: 'application/openapi+json',
...(params.schema ? { 'Accept-Profile': params.schema } : {}),
}),
},
transformResponse: async (response: Response, params?: SupabaseIntrospectParams) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to introspect database: ${errorText}`)
}
const openApiSpec = await response.json()
const tables = parseOpenApiSpec(openApiSpec, params?.schema)
return {
success: true,
output: {
message: `Successfully introspected ${tables.length} table(s) from database schema`,
tables,
schemas: [...new Set(tables.map((t) => t.schema))],
},
}
},
outputs: {
message: { type: 'string', description: 'Operation status message' },
tables: {
type: 'array',
description: 'Array of table schemas with columns, keys, and indexes',
items: {
type: 'object',
properties: INTROSPECT_TABLE_OUTPUT_PROPERTIES,
},
},
schemas: { type: 'array', description: 'List of schemas found in the database' },
},
}
/**
* Parse a PostgREST-generated OpenAPI spec into table schemas.
*
* `isPrimaryKey` is a naming heuristic (`id` column) — PostgREST does not
* expose real constraint metadata in the spec. `isForeignKey`/`references`
* only populate when the table owner has added a `references table.column`
* SQL comment on the column. `indexes` is always empty: index definitions
* are not part of the OpenAPI spec.
*/
function parseOpenApiSpec(spec: any, filterSchema?: string): SupabaseTableSchema[] {
const tables: SupabaseTableSchema[] = []
const definitions = spec.definitions || spec.components?.schemas || {}
for (const [tableName, tableDef] of Object.entries(definitions)) {
if (tableName.startsWith('_') || tableName === 'Error') continue
const definition = tableDef as any
const properties = definition.properties || {}
const required = definition.required || []
const columns: SupabaseColumnSchema[] = []
const primaryKey: string[] = []
const foreignKeys: Array<{
column: string
referencesTable: string
referencesColumn: string
}> = []
for (const [colName, colDef] of Object.entries(properties)) {
const col = colDef as any
const isPK = colName === 'id'
const fkMatch = col.description?.match(/references\s+(\w+)\.(\w+)/)
const column: SupabaseColumnSchema = {
name: colName,
type: col.format || col.type || 'unknown',
nullable: !required.includes(colName),
default: col.default || null,
isPrimaryKey: isPK,
isForeignKey: !!fkMatch,
}
if (fkMatch) {
column.references = { table: fkMatch[1], column: fkMatch[2] }
foreignKeys.push({
column: colName,
referencesTable: fkMatch[1],
referencesColumn: fkMatch[2],
})
}
if (isPK) {
primaryKey.push(colName)
}
columns.push(column)
}
tables.push({
name: tableName,
// The OpenAPI spec doesn't map tables to schemas, so this can only
// reflect the schema that was actually requested (or "public" when
// introspecting the default schema) — not necessarily the table's
// true schema in a multi-schema database.
schema: filterSchema || 'public',
columns,
primaryKey,
foreignKeys,
indexes: [],
})
}
return tables
}