feat(supabase): add Supabase REST API tool package with 10 tools
Query, insert, update, delete, upsert rows, call RPC functions, count rows, list tables, and search with ilike pattern matching via the PostgREST API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1cd44b4e97
commit
e42bbdff34
7 changed files with 1453 additions and 0 deletions
|
|
@ -13387,6 +13387,272 @@ blocks:
|
|||
description: "Sent message details"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Supabase Tools (database REST API for AI agents)
|
||||
# ---------------------------------------------------------------------------
|
||||
ops.supabaseQueryRows:
|
||||
type: utility
|
||||
description: "Query rows from a Supabase table with filtering, ordering, and pagination using PostgREST syntax."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST GET /rest/v1/{table} endpoint with filter params"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name to query"
|
||||
- name: select
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to select"
|
||||
- name: filter
|
||||
type: string
|
||||
optional: true
|
||||
description: "PostgREST filter string"
|
||||
- name: order
|
||||
type: string
|
||||
optional: true
|
||||
description: "Order by column(s)"
|
||||
- name: limit
|
||||
type: number
|
||||
optional: true
|
||||
description: "Maximum rows to return"
|
||||
- name: offset
|
||||
type: number
|
||||
optional: true
|
||||
description: "Rows to skip for pagination"
|
||||
outputs:
|
||||
- name: result
|
||||
type: QueryResult
|
||||
description: "Array of rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseGetRowById:
|
||||
type: utility
|
||||
description: "Get a single row from a Supabase table by its primary key value."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST GET /rest/v1/{table} with eq filter and limit=1"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: value
|
||||
type: string
|
||||
description: "Primary key value"
|
||||
- name: column
|
||||
type: string
|
||||
optional: true
|
||||
description: "Primary key column name"
|
||||
- name: select
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to select"
|
||||
outputs:
|
||||
- name: result
|
||||
type: RowResult
|
||||
description: "Single row or null"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseInsertRows:
|
||||
type: utility
|
||||
description: "Insert one or more rows into a Supabase table and return the inserted data."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST POST /rest/v1/{table} with Prefer: return=representation"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: rows
|
||||
type: array
|
||||
description: "Array of row objects to insert"
|
||||
- name: on_conflict
|
||||
type: string
|
||||
optional: true
|
||||
description: "Conflict resolution columns"
|
||||
- name: returning
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to return"
|
||||
outputs:
|
||||
- name: result
|
||||
type: InsertResult
|
||||
description: "Inserted rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseUpdateRows:
|
||||
type: utility
|
||||
description: "Update rows in a Supabase table matching a PostgREST filter condition."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST PATCH /rest/v1/{table} with filter params and Prefer: return=representation"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: filter
|
||||
type: string
|
||||
description: "PostgREST filter string (required)"
|
||||
- name: data
|
||||
type: object
|
||||
description: "Fields to update"
|
||||
- name: returning
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to return"
|
||||
outputs:
|
||||
- name: result
|
||||
type: UpdateResult
|
||||
description: "Updated rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseDeleteRows:
|
||||
type: utility
|
||||
description: "Delete rows from a Supabase table matching a PostgREST filter condition."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST DELETE /rest/v1/{table} with filter params and Prefer: return=representation"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: filter
|
||||
type: string
|
||||
description: "PostgREST filter string (required)"
|
||||
- name: returning
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to return"
|
||||
outputs:
|
||||
- name: result
|
||||
type: DeleteResult
|
||||
description: "Deleted rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseUpsertRows:
|
||||
type: utility
|
||||
description: "Insert or update rows in a Supabase table using merge-duplicates strategy."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST POST /rest/v1/{table} with Prefer: resolution=merge-duplicates"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: rows
|
||||
type: array
|
||||
description: "Array of row objects to upsert"
|
||||
- name: on_conflict
|
||||
type: string
|
||||
optional: true
|
||||
description: "Conflict resolution columns"
|
||||
- name: returning
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to return"
|
||||
outputs:
|
||||
- name: result
|
||||
type: UpsertResult
|
||||
description: "Upserted rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseCallRpc:
|
||||
type: utility
|
||||
description: "Call a Supabase PostgreSQL function (RPC) with optional parameters."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST POST /rest/v1/rpc/{function_name} endpoint"
|
||||
inputs:
|
||||
- name: function_name
|
||||
type: string
|
||||
description: "PostgreSQL function name"
|
||||
- name: params
|
||||
type: object
|
||||
optional: true
|
||||
description: "Function parameters"
|
||||
outputs:
|
||||
- name: result
|
||||
type: RpcResult
|
||||
description: "Function return value"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseCountRows:
|
||||
type: utility
|
||||
description: "Count rows in a Supabase table, optionally filtered by conditions."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST HEAD /rest/v1/{table} with Prefer: count=exact and parse content-range header"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name"
|
||||
- name: filter
|
||||
type: string
|
||||
optional: true
|
||||
description: "PostgREST filter string"
|
||||
outputs:
|
||||
- name: result
|
||||
type: CountResult
|
||||
description: "Row count and table name"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseListTables:
|
||||
type: utility
|
||||
description: "List all available tables in the Supabase database via the OpenAPI schema."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST GET /rest/v1/ endpoint and extract table names from paths"
|
||||
inputs: []
|
||||
outputs:
|
||||
- name: result
|
||||
type: ListTablesResult
|
||||
description: "Sorted list of table names"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
ops.supabaseSearchRows:
|
||||
type: utility
|
||||
description: "Search rows in a Supabase table by column+query pattern matching (ilike). Uses column and query inputs to build the ilike filter internally — no separate filter param needed."
|
||||
path: "supabase"
|
||||
domain_rules:
|
||||
- id: api_integration
|
||||
description: "Must call Supabase PostgREST GET /rest/v1/{table} with ilike.* filter built from column and query inputs"
|
||||
inputs:
|
||||
- name: table
|
||||
type: string
|
||||
description: "Table name to search"
|
||||
- name: column
|
||||
type: string
|
||||
description: "Column to apply ilike search on"
|
||||
- name: query
|
||||
type: string
|
||||
description: "Search term (wrapped in wildcards for contains matching)"
|
||||
- name: select
|
||||
type: string
|
||||
optional: true
|
||||
description: "Columns to select"
|
||||
- name: limit
|
||||
type: number
|
||||
optional: true
|
||||
description: "Maximum rows to return"
|
||||
- name: order
|
||||
type: string
|
||||
optional: true
|
||||
description: "Order by column(s)"
|
||||
outputs:
|
||||
- name: result
|
||||
type: SearchResult
|
||||
description: "Matching rows with count"
|
||||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
|
||||
# =============================================================================
|
||||
# VALIDATORS - Which validators to run against each block
|
||||
# =============================================================================
|
||||
|
|
|
|||
115
packages/tools/official/supabase/README.md
Normal file
115
packages/tools/official/supabase/README.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# @tpmjs/tools-supabase
|
||||
|
||||
Supabase REST API tools for AI agents. Query, insert, update, delete rows, call RPC functions, and more using the PostgREST API.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tpmjs/tools-supabase
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
Set these environment variables:
|
||||
|
||||
```bash
|
||||
SUPABASE_URL="https://your-project.supabase.co"
|
||||
SUPABASE_SERVICE_ROLE_KEY="your-service-role-key"
|
||||
```
|
||||
|
||||
The service role key bypasses Row Level Security (RLS) and grants full database access to AI agents.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { generateText } from 'ai';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { queryRows, insertRows } from '@tpmjs/tools-supabase';
|
||||
|
||||
const result = await generateText({
|
||||
model: openai('gpt-4o'),
|
||||
prompt: 'Get the first 5 users from the database',
|
||||
tools: { queryRows, insertRows },
|
||||
});
|
||||
```
|
||||
|
||||
### Query Rows
|
||||
|
||||
```typescript
|
||||
import { queryRows } from '@tpmjs/tools-supabase';
|
||||
|
||||
// Query with filtering and ordering
|
||||
await queryRows.execute({
|
||||
table: 'users',
|
||||
select: 'id,name,email',
|
||||
filter: 'age=gte.18&status=eq.active',
|
||||
order: 'created_at.desc',
|
||||
limit: 10,
|
||||
});
|
||||
```
|
||||
|
||||
### Insert Rows
|
||||
|
||||
```typescript
|
||||
import { insertRows } from '@tpmjs/tools-supabase';
|
||||
|
||||
// Insert a single row
|
||||
await insertRows.execute({
|
||||
table: 'users',
|
||||
rows: [{ name: 'Alice', email: 'alice@example.com' }],
|
||||
});
|
||||
|
||||
// Bulk insert
|
||||
await insertRows.execute({
|
||||
table: 'users',
|
||||
rows: [
|
||||
{ name: 'Bob', email: 'bob@example.com' },
|
||||
{ name: 'Carol', email: 'carol@example.com' },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `queryRows` | Query rows from a table with filtering, ordering, and pagination |
|
||||
| `getRowById` | Get a single row by its primary key value |
|
||||
| `insertRows` | Insert one or more rows into a table |
|
||||
| `updateRows` | Update rows matching a filter condition |
|
||||
| `deleteRows` | Delete rows matching a filter condition |
|
||||
| `upsertRows` | Insert or update rows using merge-duplicates strategy |
|
||||
| `callRpc` | Call a PostgreSQL function (RPC) with parameters |
|
||||
| `countRows` | Count rows in a table with optional filtering |
|
||||
| `listTables` | List all available tables in the database |
|
||||
| `searchRows` | Search rows using case-insensitive pattern matching |
|
||||
|
||||
## PostgREST Filter Syntax
|
||||
|
||||
Supabase uses PostgREST filter syntax for queries:
|
||||
|
||||
- **Equality:** `status=eq.active`
|
||||
- **Greater than:** `age=gt.18`
|
||||
- **Greater than or equal:** `age=gte.18`
|
||||
- **Less than:** `price=lt.100`
|
||||
- **Pattern matching:** `name=like.*smith*`
|
||||
- **Case-insensitive:** `email=ilike.*@gmail.com`
|
||||
- **Multiple filters:** `age=gte.18&status=eq.active` (AND)
|
||||
|
||||
See [PostgREST documentation](https://postgrest.org/en/stable/references/api/tables_views.html#horizontal-filtering-rows) for more operators.
|
||||
|
||||
## Safety Features
|
||||
|
||||
**Important:** The `updateRows` and `deleteRows` tools require a non-empty filter to prevent accidental full-table operations. This is a safety requirement to protect your data.
|
||||
|
||||
```typescript
|
||||
// ❌ This will throw an error
|
||||
await deleteRows.execute({ table: 'users', filter: '' });
|
||||
|
||||
// ✅ This is safe
|
||||
await deleteRows.execute({ table: 'users', filter: 'id=eq.123' });
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
107
packages/tools/official/supabase/package.json
Normal file
107
packages/tools/official/supabase/package.json
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
{
|
||||
"name": "@tpmjs/tools-supabase",
|
||||
"version": "0.1.0",
|
||||
"description": "Supabase REST API tools for AI agents — query, insert, update, delete rows, call RPC functions, and more",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"tpmjs",
|
||||
"ops",
|
||||
"ai",
|
||||
"supabase",
|
||||
"database",
|
||||
"postgres"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.49"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/supabase"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "ops",
|
||||
"frameworks": [
|
||||
"vercel-ai"
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "SUPABASE_URL",
|
||||
"description": "Supabase project URL (e.g. https://xyz.supabase.co)",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "SUPABASE_SERVICE_ROLE_KEY",
|
||||
"description": "Supabase service role key for full database access",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "queryRows",
|
||||
"description": "Query rows from a Supabase table with filtering, ordering, and pagination."
|
||||
},
|
||||
{
|
||||
"name": "getRowById",
|
||||
"description": "Get a single row from a Supabase table by its primary key value."
|
||||
},
|
||||
{
|
||||
"name": "insertRows",
|
||||
"description": "Insert one or more rows into a Supabase table and return the inserted data."
|
||||
},
|
||||
{
|
||||
"name": "updateRows",
|
||||
"description": "Update rows in a Supabase table matching a PostgREST filter condition."
|
||||
},
|
||||
{
|
||||
"name": "deleteRows",
|
||||
"description": "Delete rows from a Supabase table matching a PostgREST filter condition."
|
||||
},
|
||||
{
|
||||
"name": "upsertRows",
|
||||
"description": "Insert or update rows in a Supabase table using merge-duplicates strategy."
|
||||
},
|
||||
{
|
||||
"name": "callRpc",
|
||||
"description": "Call a Supabase PostgreSQL function (RPC) with optional parameters."
|
||||
},
|
||||
{
|
||||
"name": "countRows",
|
||||
"description": "Count rows in a Supabase table, optionally filtered by conditions."
|
||||
},
|
||||
{
|
||||
"name": "listTables",
|
||||
"description": "List all available tables in the Supabase database via the OpenAPI schema."
|
||||
},
|
||||
{
|
||||
"name": "searchRows",
|
||||
"description": "Search rows in a Supabase table using case-insensitive pattern matching."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
926
packages/tools/official/supabase/src/index.ts
Normal file
926
packages/tools/official/supabase/src/index.ts
Normal file
|
|
@ -0,0 +1,926 @@
|
|||
/**
|
||||
* @tpmjs/tools-supabase — Supabase REST API Tools for AI Agents
|
||||
*
|
||||
* Complete database management for AI agents: query, insert, update, delete rows,
|
||||
* call RPC functions, count records, and more using the PostgREST API.
|
||||
*
|
||||
* @requires SUPABASE_URL environment variable
|
||||
* @requires SUPABASE_SERVICE_ROLE_KEY environment variable
|
||||
*/
|
||||
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
// ─── Client Infrastructure ──────────────────────────────────────────────────
|
||||
|
||||
interface SupabaseConfig {
|
||||
url: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
function getConfig(): SupabaseConfig {
|
||||
const url = process.env.SUPABASE_URL;
|
||||
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!url) {
|
||||
throw new Error(
|
||||
'SUPABASE_URL environment variable is required. Get it from your Supabase project settings.'
|
||||
);
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
'SUPABASE_SERVICE_ROLE_KEY environment variable is required. Get it from your Supabase project settings.'
|
||||
);
|
||||
}
|
||||
|
||||
return { url, key };
|
||||
}
|
||||
|
||||
interface ApiRequestOptions {
|
||||
body?: unknown;
|
||||
headers?: Record<string, string>;
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
|
||||
async function apiRequest<T>(
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'HEAD',
|
||||
path: string,
|
||||
options?: ApiRequestOptions
|
||||
): Promise<T> {
|
||||
const config = getConfig();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
apikey: config.key,
|
||||
Authorization: `Bearer ${config.key}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
};
|
||||
|
||||
const url = new URL(`${config.url}${path}`);
|
||||
if (options?.params) {
|
||||
for (const [key, value] of Object.entries(options.params)) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (options?.body !== undefined) {
|
||||
requestOptions.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), requestOptions);
|
||||
|
||||
// Handle HEAD requests (used for counting)
|
||||
if (method === 'HEAD') {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Supabase HTTP error ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
// Return headers for HEAD requests
|
||||
return { headers: Object.fromEntries(response.headers.entries()) } as T;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`Supabase HTTP error ${response.status}: ${response.statusText}${errorText ? ` - ${errorText}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
// ─── Output Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface QueryResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RowResult {
|
||||
row: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface InsertResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UpdateResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface DeleteResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface UpsertResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface RpcResult {
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export interface CountResult {
|
||||
count: number;
|
||||
table: string;
|
||||
}
|
||||
|
||||
export interface ListTablesResult {
|
||||
tables: string[];
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
rows: Record<string, unknown>[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
// ─── Query Rows ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface QueryRowsInput {
|
||||
table: string;
|
||||
select?: string;
|
||||
filter?: string;
|
||||
order?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export const queryRows = tool({
|
||||
description:
|
||||
'Query rows from a Supabase table with filtering, ordering, and pagination using PostgREST syntax.',
|
||||
inputSchema: jsonSchema<QueryRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to query.',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
description: 'Columns to select (default: "*"). Example: "id,name,email".',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'PostgREST filter string. Example: "age=gte.18&status=eq.active".',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
description: 'Order by column(s). Example: "created_at.desc" or "name.asc".',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of rows to return (1-1000, default: 100).',
|
||||
},
|
||||
offset: {
|
||||
type: 'number',
|
||||
description: 'Number of rows to skip for pagination (default: 0).',
|
||||
},
|
||||
},
|
||||
required: ['table'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: QueryRowsInput): Promise<QueryResult> {
|
||||
try {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('limit must be between 1 and 1000');
|
||||
}
|
||||
|
||||
if (input.offset !== undefined && input.offset < 0) {
|
||||
throw new Error('offset must be non-negative');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('select', input.select || '*');
|
||||
|
||||
if (input.filter) {
|
||||
// Parse filter and add as individual params
|
||||
const filterParts = input.filter.split('&');
|
||||
for (const part of filterParts) {
|
||||
const [key, value] = part.split('=');
|
||||
if (key && value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (input.order) {
|
||||
params.append('order', input.order);
|
||||
}
|
||||
|
||||
if (input.limit !== undefined) {
|
||||
params.append('limit', String(input.limit));
|
||||
}
|
||||
|
||||
if (input.offset !== undefined) {
|
||||
params.append('offset', String(input.offset));
|
||||
}
|
||||
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}?${params.toString()}`;
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('GET', path);
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to query rows: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Get Row By ID ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface GetRowByIdInput {
|
||||
table: string;
|
||||
column?: string;
|
||||
value: string;
|
||||
select?: string;
|
||||
}
|
||||
|
||||
export const getRowById = tool({
|
||||
description: 'Get a single row from a Supabase table by its primary key value.',
|
||||
inputSchema: jsonSchema<GetRowByIdInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to query.',
|
||||
},
|
||||
column: {
|
||||
type: 'string',
|
||||
description: 'Primary key column name (default: "id").',
|
||||
},
|
||||
value: {
|
||||
type: 'string',
|
||||
description: 'The ID value to search for.',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
description: 'Columns to select (default: "*").',
|
||||
},
|
||||
},
|
||||
required: ['table', 'value'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetRowByIdInput): Promise<RowResult> {
|
||||
try {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!input.value || input.value.trim() === '') {
|
||||
throw new Error('value is required and must be non-empty');
|
||||
}
|
||||
|
||||
const column = input.column || 'id';
|
||||
const select = input.select || '*';
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('select', select);
|
||||
params.append(column, `eq.${input.value}`);
|
||||
params.append('limit', '1');
|
||||
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}?${params.toString()}`;
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('GET', path);
|
||||
|
||||
return {
|
||||
row: rows && rows.length > 0 ? rows[0] || null : null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get row by ID: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Insert Rows ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface InsertRowsInput {
|
||||
table: string;
|
||||
rows: Record<string, unknown>[];
|
||||
on_conflict?: string;
|
||||
returning?: string;
|
||||
}
|
||||
|
||||
export const insertRows = tool({
|
||||
description: 'Insert one or more rows into a Supabase table and return the inserted data.',
|
||||
inputSchema: jsonSchema<InsertRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to insert into.',
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
items: { type: 'object' },
|
||||
description: 'Array of row objects to insert (single or bulk).',
|
||||
},
|
||||
on_conflict: {
|
||||
type: 'string',
|
||||
description: 'Column name(s) for upsert conflict resolution.',
|
||||
},
|
||||
returning: {
|
||||
type: 'string',
|
||||
description: 'Columns to return (default: "*").',
|
||||
},
|
||||
},
|
||||
required: ['table', 'rows'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: InsertRowsInput): Promise<InsertResult> {
|
||||
try {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!Array.isArray(input.rows) || input.rows.length === 0) {
|
||||
throw new Error('rows must be a non-empty array');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (input.returning) {
|
||||
params.append('select', input.returning);
|
||||
}
|
||||
|
||||
if (input.on_conflict) {
|
||||
params.append('on_conflict', input.on_conflict);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const path = queryString
|
||||
? `/rest/v1/${encodeURIComponent(input.table)}?${queryString}`
|
||||
: `/rest/v1/${encodeURIComponent(input.table)}`;
|
||||
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('POST', path, {
|
||||
body: input.rows,
|
||||
headers: {
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to insert rows: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Update Rows ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UpdateRowsInput {
|
||||
table: string;
|
||||
filter: string;
|
||||
data: Record<string, unknown>;
|
||||
returning?: string;
|
||||
}
|
||||
|
||||
export const updateRows = tool({
|
||||
description:
|
||||
'Update rows in a Supabase table matching a filter condition. SAFETY: Filter is required to prevent full-table updates.',
|
||||
inputSchema: jsonSchema<UpdateRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to update.',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'PostgREST filter string (REQUIRED, must be non-empty). Example: "id=eq.123".',
|
||||
},
|
||||
data: {
|
||||
type: 'object',
|
||||
description: 'Object containing fields to update.',
|
||||
},
|
||||
returning: {
|
||||
type: 'string',
|
||||
description: 'Columns to return (default: "*").',
|
||||
},
|
||||
},
|
||||
required: ['table', 'filter', 'data'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: UpdateRowsInput): Promise<UpdateResult> {
|
||||
try {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!input.filter || input.filter.trim() === '') {
|
||||
throw new Error(
|
||||
'filter is required and must be non-empty to prevent accidental full-table updates. Use a specific filter like "id=eq.123".'
|
||||
);
|
||||
}
|
||||
|
||||
if (!input.data || typeof input.data !== 'object') {
|
||||
throw new Error('data is required and must be an object');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (input.returning) {
|
||||
params.append('select', input.returning);
|
||||
}
|
||||
|
||||
// Parse filter and add as individual params
|
||||
const filterParts = input.filter.split('&');
|
||||
for (const part of filterParts) {
|
||||
const [key, value] = part.split('=');
|
||||
if (key && value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}?${params.toString()}`;
|
||||
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('PATCH', path, {
|
||||
body: input.data,
|
||||
headers: {
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update rows: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Delete Rows ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DeleteRowsInput {
|
||||
table: string;
|
||||
filter: string;
|
||||
returning?: string;
|
||||
}
|
||||
|
||||
export const deleteRows = tool({
|
||||
description:
|
||||
'Delete rows from a Supabase table matching a filter condition. SAFETY: Filter is required to prevent full-table deletion.',
|
||||
inputSchema: jsonSchema<DeleteRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to delete from.',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'PostgREST filter string (REQUIRED, must be non-empty). Example: "id=eq.123".',
|
||||
},
|
||||
returning: {
|
||||
type: 'string',
|
||||
description: 'Columns to return from deleted rows (default: "*").',
|
||||
},
|
||||
},
|
||||
required: ['table', 'filter'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: DeleteRowsInput): Promise<DeleteResult> {
|
||||
try {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!input.filter || input.filter.trim() === '') {
|
||||
throw new Error(
|
||||
'filter is required and must be non-empty to prevent accidental full-table deletion. Use a specific filter like "id=eq.123".'
|
||||
);
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (input.returning) {
|
||||
params.append('select', input.returning);
|
||||
}
|
||||
|
||||
// Parse filter and add as individual params
|
||||
const filterParts = input.filter.split('&');
|
||||
for (const part of filterParts) {
|
||||
const [key, value] = part.split('=');
|
||||
if (key && value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}?${params.toString()}`;
|
||||
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('DELETE', path, {
|
||||
headers: {
|
||||
Prefer: 'return=representation',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete rows: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Upsert Rows ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UpsertRowsInput {
|
||||
table: string;
|
||||
rows: Record<string, unknown>[];
|
||||
on_conflict?: string;
|
||||
returning?: string;
|
||||
}
|
||||
|
||||
export const upsertRows = tool({
|
||||
description:
|
||||
'Insert or update rows in a Supabase table using merge-duplicates strategy for conflict resolution.',
|
||||
inputSchema: jsonSchema<UpsertRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to upsert into.',
|
||||
},
|
||||
rows: {
|
||||
type: 'array',
|
||||
items: { type: 'object' },
|
||||
description: 'Array of row objects to upsert.',
|
||||
},
|
||||
on_conflict: {
|
||||
type: 'string',
|
||||
description: 'Column name(s) for conflict resolution.',
|
||||
},
|
||||
returning: {
|
||||
type: 'string',
|
||||
description: 'Columns to return (default: "*").',
|
||||
},
|
||||
},
|
||||
required: ['table', 'rows'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: UpsertRowsInput): Promise<UpsertResult> {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!Array.isArray(input.rows) || input.rows.length === 0) {
|
||||
throw new Error('rows must be a non-empty array');
|
||||
}
|
||||
|
||||
// Validate each row is a non-null object with at least one field
|
||||
for (let i = 0; i < input.rows.length; i++) {
|
||||
const row = input.rows[i];
|
||||
if (!row || typeof row !== 'object' || Array.isArray(row)) {
|
||||
throw new Error(`rows[${i}] must be a non-null object with column-value pairs`);
|
||||
}
|
||||
if (Object.keys(row).length === 0) {
|
||||
throw new Error(`rows[${i}] must have at least one column-value pair`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (input.returning) {
|
||||
params.append('select', input.returning);
|
||||
}
|
||||
|
||||
if (input.on_conflict) {
|
||||
params.append('on_conflict', input.on_conflict);
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const path = queryString
|
||||
? `/rest/v1/${encodeURIComponent(input.table)}?${queryString}`
|
||||
: `/rest/v1/${encodeURIComponent(input.table)}`;
|
||||
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('POST', path, {
|
||||
body: input.rows,
|
||||
headers: {
|
||||
Prefer: 'return=representation,resolution=merge-duplicates',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('rows')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to upsert rows in "${input.table}": ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Call RPC ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CallRpcInput {
|
||||
function_name: string;
|
||||
params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const callRpc = tool({
|
||||
description: 'Call a Supabase PostgreSQL function (RPC) with parameters and return the result.',
|
||||
inputSchema: jsonSchema<CallRpcInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
function_name: {
|
||||
type: 'string',
|
||||
description: 'The PostgreSQL function name to call.',
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
description: 'Object containing function parameters.',
|
||||
},
|
||||
},
|
||||
required: ['function_name'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: CallRpcInput): Promise<RpcResult> {
|
||||
try {
|
||||
if (!input.function_name || input.function_name.trim() === '') {
|
||||
throw new Error('function_name is required and must be non-empty');
|
||||
}
|
||||
|
||||
const path = `/rest/v1/rpc/${encodeURIComponent(input.function_name)}`;
|
||||
|
||||
const data = await apiRequest<unknown>('POST', path, {
|
||||
body: input.params || {},
|
||||
});
|
||||
|
||||
return { data };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to call RPC function: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Count Rows ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CountRowsInput {
|
||||
table: string;
|
||||
filter?: string;
|
||||
}
|
||||
|
||||
export const countRows = tool({
|
||||
description:
|
||||
'Count rows in a Supabase table, optionally filtered by conditions, using exact count.',
|
||||
inputSchema: jsonSchema<CountRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to count rows in.',
|
||||
},
|
||||
filter: {
|
||||
type: 'string',
|
||||
description: 'Optional PostgREST filter string. Example: "age=gte.18&status=eq.active".',
|
||||
},
|
||||
},
|
||||
required: ['table'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: CountRowsInput): Promise<CountResult> {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (input.filter) {
|
||||
const filterParts = input.filter.split('&');
|
||||
for (const part of filterParts) {
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx === -1) {
|
||||
throw new Error(
|
||||
`Invalid filter segment "${part}". Expected format: "column=operator.value"`
|
||||
);
|
||||
}
|
||||
const key = part.substring(0, eqIdx);
|
||||
const value = part.substring(eqIdx + 1);
|
||||
if (key && value) {
|
||||
params.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
const pathSuffix = queryString ? `?${queryString}` : '';
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}${pathSuffix}`;
|
||||
|
||||
const response = await apiRequest<{ headers: Record<string, string> }>('HEAD', path, {
|
||||
headers: {
|
||||
Prefer: 'count=exact',
|
||||
},
|
||||
});
|
||||
|
||||
// Extract count from content-range header (format: "0-24/3573" or "*/0")
|
||||
const contentRange = response.headers['content-range'];
|
||||
let count = 0;
|
||||
|
||||
if (contentRange) {
|
||||
const match = contentRange.match(/\/(\d+)$/);
|
||||
if (match && match[1]) {
|
||||
count = Number.parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
table: input.table,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.startsWith('Invalid filter')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to count rows in "${input.table}": ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── List Tables ─────────────────────────────────────────────────────────────
|
||||
|
||||
export type ListTablesInput = {};
|
||||
|
||||
export const listTables = tool({
|
||||
description: 'List all available tables in the Supabase database via the OpenAPI schema.',
|
||||
inputSchema: jsonSchema<ListTablesInput>({
|
||||
type: 'object',
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(_input: ListTablesInput): Promise<ListTablesResult> {
|
||||
try {
|
||||
const path = '/rest/v1/';
|
||||
|
||||
// Fetch the OpenAPI spec from the root endpoint
|
||||
const spec = await apiRequest<{
|
||||
paths?: Record<string, unknown>;
|
||||
}>('GET', path);
|
||||
|
||||
const tables: string[] = [];
|
||||
|
||||
if (spec.paths) {
|
||||
for (const pathKey of Object.keys(spec.paths)) {
|
||||
// Path format: /{table_name} or /rpc/{function_name}
|
||||
// We want tables, not RPC functions
|
||||
if (pathKey.startsWith('/') && !pathKey.startsWith('/rpc/')) {
|
||||
const tableName = pathKey.substring(1); // Remove leading slash
|
||||
if (tableName && !tableName.includes('/')) {
|
||||
tables.push(tableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { tables: tables.sort() };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list tables: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Search Rows ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SearchRowsInput {
|
||||
table: string;
|
||||
column: string;
|
||||
query: string;
|
||||
select?: string;
|
||||
limit?: number;
|
||||
order?: string;
|
||||
}
|
||||
|
||||
export const searchRows = tool({
|
||||
description:
|
||||
'Search rows in a Supabase table using case-insensitive pattern matching (ilike operator).',
|
||||
inputSchema: jsonSchema<SearchRowsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
table: {
|
||||
type: 'string',
|
||||
description: 'The table name to search in.',
|
||||
},
|
||||
column: {
|
||||
type: 'string',
|
||||
description: 'The column name to search in.',
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'The search term to match (case-insensitive).',
|
||||
},
|
||||
select: {
|
||||
type: 'string',
|
||||
description: 'Columns to select (default: "*").',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Maximum number of rows to return (1-1000, default: 100).',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
description: 'Order by column(s). Example: "created_at.desc".',
|
||||
},
|
||||
},
|
||||
required: ['table', 'column', 'query'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SearchRowsInput): Promise<SearchResult> {
|
||||
if (!input.table || input.table.trim() === '') {
|
||||
throw new Error('table is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (!input.column || input.column.trim() === '') {
|
||||
throw new Error('column is required and must be non-empty');
|
||||
}
|
||||
|
||||
// Validate column name contains only valid characters (letters, digits, underscores)
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(input.column)) {
|
||||
throw new Error(
|
||||
`Invalid column name "${input.column}". Column names must start with a letter or underscore and contain only letters, digits, and underscores.`
|
||||
);
|
||||
}
|
||||
|
||||
if (!input.query || input.query.trim() === '') {
|
||||
throw new Error('query is required and must be non-empty');
|
||||
}
|
||||
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('limit must be between 1 and 1000');
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('select', input.select || '*');
|
||||
// PostgREST handles ilike safely via parameterized queries on the server side
|
||||
params.append(input.column, `ilike.*${input.query}*`);
|
||||
|
||||
if (input.order) {
|
||||
params.append('order', input.order);
|
||||
}
|
||||
|
||||
if (input.limit !== undefined) {
|
||||
params.append('limit', String(input.limit));
|
||||
}
|
||||
|
||||
const path = `/rest/v1/${encodeURIComponent(input.table)}?${params.toString()}`;
|
||||
const rows = await apiRequest<Record<string, unknown>[]>('GET', path);
|
||||
|
||||
return {
|
||||
rows: rows || [],
|
||||
count: (rows || []).length,
|
||||
};
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.startsWith('Invalid column') ||
|
||||
error.message.startsWith('query') ||
|
||||
error.message.startsWith('column') ||
|
||||
error.message.startsWith('table') ||
|
||||
error.message.startsWith('limit'))
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to search rows in "${input.table}": ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Default Export ─────────────────────────────────────────────────────────
|
||||
|
||||
export default {
|
||||
queryRows,
|
||||
getRowById,
|
||||
insertRows,
|
||||
updateRows,
|
||||
deleteRows,
|
||||
upsertRows,
|
||||
callRpc,
|
||||
countRows,
|
||||
listTables,
|
||||
searchRows,
|
||||
};
|
||||
11
packages/tools/official/supabase/tsconfig.json
Normal file
11
packages/tools/official/supabase/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/react-library.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"incremental": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
12
packages/tools/official/supabase/tsup.config.ts
Normal file
12
packages/tools/official/supabase/tsup.config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
target: 'es2022',
|
||||
treeshake: true,
|
||||
splitting: false,
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue