import { z } from "zod"; import type { ZodType } from "zod"; import type { ToolBinder } from "../core/binder"; /** * Collection Bindings * * This module provides standardized tool bindings for Collections, representing * SQL table-like structures with CRUD + Search operations compatible with TanStack DB. * * Key Features: * - Generic collection bindings that work with any entity type * - Standardized tool naming: `COLLECTION_{COLLECTION}_*` * - Compatible with TanStack DB query-collection * - Full TypeScript support with proper type constraints * - Support for filtering, sorting, and pagination * - Simple id and title fields for human-readable identification */ /** * Base schema for collection entities * All collection entities must have an id, title, and audit trail fields */ export const BaseCollectionEntitySchema = z.object({ id: z.string().describe("Unique identifier for the entity"), title: z.string().describe("Human-readable title for the entity"), description: z.string().nullish().describe("Description of the entity"), created_at: z.string().datetime(), updated_at: z.string().datetime(), created_by: z.string().optional(), updated_by: z.string().optional(), }); /** * Type helper for BaseCollectionEntitySchema */ export type BaseCollectionEntitySchemaType = typeof BaseCollectionEntitySchema; /** * Comparison expression schema for filtering */ const ComparisonExpressionSchema = z.object({ field: z.array(z.string()), operator: z.enum(["eq", "gt", "gte", "lt", "lte", "in", "like", "contains"]), value: z.unknown(), }); /** * Where expression type for filtering * Recursive to allow nested logical operators */ export type WhereExpression = | z.infer | { operator: "and" | "or" | "not"; conditions: WhereExpression[]; }; /** * Where expression schema for filtering * Supports TanStack DB predicate push-down patterns * Recursive to allow nested logical operators */ export const WhereExpressionSchema: z.ZodType = z.lazy(() => z.union([ ComparisonExpressionSchema, z.object({ operator: z.enum(["and", "or", "not"]), conditions: z.array(WhereExpressionSchema), }), ]), ); /** * Order by expression for sorting */ export const OrderByExpressionSchema = z.object({ field: z.array(z.string()), direction: z.enum(["asc", "desc"]), nulls: z.enum(["first", "last"]).optional(), }); /** * List/Query input schema for collections * Compatible with TanStack DB LoadSubsetOptions */ export const CollectionListInputSchema = z.object({ where: WhereExpressionSchema.optional().describe("Filter expression"), orderBy: z .array(OrderByExpressionSchema) .optional() .describe("Sort expressions"), limit: z .number() .int() .min(1) .max(1000) .optional() .describe("Maximum number of items to return"), offset: z .number() .int() .min(0) .optional() .describe("Number of items to skip"), }); /** * Factory function to create list output schema for a specific collection type */ export function createCollectionListOutputSchema( entitySchema: T, ) { return z.object({ items: z.array(entitySchema).describe("Array of collection items"), totalCount: z .number() .int() .min(0) .optional() .describe("Total number of matching items (if available)"), hasMore: z .boolean() .optional() .describe("Whether there are more items available"), }); } /** * Get by ID input schema */ export const CollectionGetInputSchema = z.object({ id: z.string().describe("ID of the entity to retrieve"), }); /** * Factory function to create get output schema */ export function createCollectionGetOutputSchema( entitySchema: T, ) { return z.object({ item: entitySchema .nullable() .describe("The retrieved item, or null if not found"), }); } /** * Factory function to create insert input schema */ export function createCollectionInsertInputSchema< T extends z.ZodObject, >(entitySchema: T) { // Remove id field since it may be auto-generated by the server return z.object({ data: entitySchema .partial() .describe("Data for the new entity (id may be auto-generated)"), }); } /** * Factory function to create insert output schema */ export function createCollectionInsertOutputSchema( entitySchema: T, ) { return z.object({ item: entitySchema.describe("The created entity with generated id"), }); } /** * Factory function to create update input schema */ export function createCollectionUpdateInputSchema( entitySchema: T, ) { return z.object({ id: z.string().describe("ID of the entity to update"), data: (entitySchema as unknown as z.ZodObject) .partial() .describe("Partial entity data to update"), }); } /** * Factory function to create update output schema */ export function createCollectionUpdateOutputSchema( entitySchema: T, ) { return z.object({ item: entitySchema.describe("The updated entity"), }); } /** * Delete input schema */ export const CollectionDeleteInputSchema = z.object({ id: z.string().describe("ID of the entity to delete"), }); /** * Factory function to create delete output schema */ export function createCollectionDeleteOutputSchema( entitySchema: T, ) { return z.object({ item: entitySchema.describe("The deleted entity"), }); } /** * Options for creating collection bindings */ export interface CollectionBindingOptions { /** * If true, only LIST and GET operations will be included (read-only collection) * @default false */ readOnly?: boolean; } /** * Creates generic collection bindings for a specific entity type * * This function generates standardized tool bindings that work with any collection/table * by accepting a custom entity schema and collection name. The bindings provide: * - COLLECTION_{NAME}_LIST - Query/search entities with filtering and sorting (required) * - COLLECTION_{NAME}_GET - Get a single entity by ID (required) * - COLLECTION_{NAME}_CREATE - Create a new entity (optional, excluded if readOnly=true) * - COLLECTION_{NAME}_UPDATE - Update an existing entity (optional, excluded if readOnly=true) * - COLLECTION_{NAME}_DELETE - Delete an entity (optional, excluded if readOnly=true) * * @param collectionName - The name of the collection/table (e.g., "users", "products", "orders") * @param entitySchema - The Zod schema for the entity type (must extend BaseCollectionEntitySchema) * @param options - Optional configuration for the collection bindings * @returns Array of tool bindings for Collection CRUD + Query operations * * @example * ```typescript * const UserSchema = z.object({ * id: z.string(), * title: z.string(), * created_at: z.string().datetime(), * updated_at: z.string().datetime(), * created_by: z.string().optional(), * updated_by: z.string().optional(), * email: z.string().email(), * }); * * // Full CRUD collection * const USER_COLLECTION_BINDING = createCollectionBindings("users", UserSchema); * * // Read-only collection (only LIST and GET) * const READONLY_COLLECTION_BINDING = createCollectionBindings("products", ProductSchema, { readOnly: true }); * ``` */ export function createCollectionBindings< TEntitySchema extends z.ZodObject, TName extends string, >( collectionName: TName, entitySchema: TEntitySchema, options?: CollectionBindingOptions, ) { const upperName = collectionName.toUpperCase() as Uppercase; const readOnly = options?.readOnly ?? false; const bindings: ToolBinder[] = [ { name: `COLLECTION_${upperName}_LIST` as const, inputSchema: CollectionListInputSchema, outputSchema: createCollectionListOutputSchema(entitySchema), }, { name: `COLLECTION_${upperName}_GET` as const, inputSchema: CollectionGetInputSchema, outputSchema: createCollectionGetOutputSchema(entitySchema), }, ]; // Only include mutation operations if not read-only if (!readOnly) { bindings.push( { name: `COLLECTION_${upperName}_CREATE` as const, inputSchema: createCollectionInsertInputSchema(entitySchema), outputSchema: createCollectionInsertOutputSchema(entitySchema), opt: true, }, { name: `COLLECTION_${upperName}_UPDATE` as const, inputSchema: createCollectionUpdateInputSchema(entitySchema), outputSchema: createCollectionUpdateOutputSchema(entitySchema), opt: true, }, { name: `COLLECTION_${upperName}_DELETE` as const, inputSchema: CollectionDeleteInputSchema, outputSchema: createCollectionDeleteOutputSchema(entitySchema), opt: true, }, ); } return bindings as unknown as CollectionBinding< TEntitySchema, Uppercase >; } // Helper type for tool definition type ToolBinding< Name extends string, Input, Output, Opt extends boolean = false, > = { name: Name; inputSchema: ZodType; outputSchema: ZodType; } & (Opt extends true ? { opt: true } : unknown); /** * Type helper to extract the collection binding type */ export type CollectionBinding< TEntitySchema, TUpperName extends Uppercase = Uppercase, TEntity = TEntitySchema extends z.ZodObject ? z.infer : TEntitySchema, > = [ ToolBinding< `COLLECTION_${TUpperName}_LIST`, CollectionListInput, CollectionListOutput >, ToolBinding< `COLLECTION_${TUpperName}_GET`, CollectionGetInput, CollectionGetOutput >, ToolBinding< `COLLECTION_${TUpperName}_CREATE`, CollectionInsertInput, CollectionInsertOutput, true >, ToolBinding< `COLLECTION_${TUpperName}_UPDATE`, CollectionUpdateInput, CollectionUpdateOutput, true >, ToolBinding< `COLLECTION_${TUpperName}_DELETE`, CollectionDeleteInput, CollectionDeleteOutput, true >, ]; /** * Type helper to extract tool names from a collection binding */ export type CollectionTools< TEntitySchema extends z.ZodObject, TUpperName extends Uppercase = Uppercase, > = CollectionBinding[number]["name"]; // Export types for TypeScript usage export type CollectionListInput = z.infer; export type CollectionGetInput = z.infer; export type CollectionDeleteInput = z.infer; export type OrderByExpression = z.infer; /** * Type helper for insert input with generic item type */ export type CollectionInsertInput = { data: Partial; }; /** * Type helper for update input with generic item type */ export type CollectionUpdateInput = { id: string; data: Partial; }; /** * Type helper for list output with generic item type */ export type CollectionListOutput = { items: T[]; totalCount?: number; hasMore?: boolean; }; /** * Type helper for get output with generic item type */ export type CollectionGetOutput = { item: T | null; }; /** * Type helper for insert output with generic item type */ export type CollectionInsertOutput = { item: T; }; /** * Type helper for update output with generic item type */ export type CollectionUpdateOutput = { item: T; }; /** * Type helper for delete output with generic item type */ export type CollectionDeleteOutput = { item: T; }; /** * Base collection entity type - inferred from BaseCollectionEntitySchema */ export type BaseCollectionEntity = z.infer;