Skip to main content
Glama
explicitTableIds.ts8.08 kB
import type { TSESTree } from "@typescript-eslint/utils"; import { createRule } from "../util.js"; import { ReportFixFunction, RuleContext, } from "@typescript-eslint/utils/ts-eslint"; import { AST_NODE_TYPES } from "@typescript-eslint/utils"; import type ts from "typescript"; /** * Rule to enforce explicit table names in database calls * (db.get, db.replace, db.patch, db.delete) */ export const explicitTableIds = createRule({ name: "explicit-table-ids", meta: { type: "suggestion", docs: { description: "Database operations should include an explicit table name as the first argument.", }, messages: { "missing-table-name": "Database {{method}} call should include an explicit table name as the first argument. Expected: db.{{method}}({{tableName}}, ...) ", "missing-table-name-no-inference": "Database {{method}} call should include an explicit table name as the first argument. Expected: db.{{method}}(<tableName>, ...).", }, schema: [], fixable: "code", }, defaultOptions: [], create: (context) => { const filename = context.filename; const isGenerated = filename.includes("_generated"); if (isGenerated) { return {}; } const services = context.sourceCode.parserServices; if ( !services?.program || !services.esTreeNodeToTSNodeMap || typeof services.esTreeNodeToTSNodeMap.get !== "function" ) { // Type information not available return {}; } const checker = services.program.getTypeChecker(); const tsNodeMap = services.esTreeNodeToTSNodeMap; // Get DatabaseReader and DatabaseWriter types for proper subtype checking // We need to find these types in the type system let anyDatabaseReader: ts.Type | null = null; let anyDatabaseWriter: ts.Type | null = null; try { // Try to get the database types from the program const sourceFiles = services.program.getSourceFiles(); for (const sf of sourceFiles) { if (sf.fileName.includes("_generated/server")) { const sourceFileSymbol = checker.getSymbolAtLocation(sf); if (sourceFileSymbol) { const exports = checker.getExportsOfModule(sourceFileSymbol); for (const exp of exports) { const type = checker.getTypeOfSymbolAtLocation(exp, sf); const typeString = checker.typeToString(type); if ( typeString.includes("DatabaseReader") || typeString.includes("GenericDatabaseReader") ) { // Get the type that has the methods we care about const dbProp = type.getProperty("db"); if (dbProp) { const dbType = checker.getTypeOfSymbolAtLocation(dbProp, sf); anyDatabaseReader = dbType; } } if ( typeString.includes("DatabaseWriter") || typeString.includes("GenericDatabaseWriter") ) { const dbProp = type.getProperty("db"); if (dbProp) { const dbType = checker.getTypeOfSymbolAtLocation(dbProp, sf); anyDatabaseWriter = dbType; } } } } break; } } } catch { // If we can't get the types, we'll fall back to pattern matching } return { CallExpression(node: TSESTree.CallExpression) { // Check if it's a property access (db.get, db.replace, etc.) if (node.callee.type !== AST_NODE_TYPES.MemberExpression) { return; } const memberExpr = node.callee; if (memberExpr.property.type !== AST_NODE_TYPES.Identifier) { return; } const methodName = memberExpr.property.name; const validMethods = ["get", "replace", "patch", "delete"]; if (!validMethods.includes(methodName)) { return; } // Check if the object is a database by checking its type or pattern const objectTsNode = tsNodeMap.get(memberExpr.object); const objectType = checker.getTypeAtLocation(objectTsNode); // Use proper subtype checking if we have the database types available let isDatabaseType = false; if (anyDatabaseReader || anyDatabaseWriter) { isDatabaseType = (anyDatabaseReader !== null && methodName === "get" && checker.isTypeAssignableTo(objectType, anyDatabaseReader)) || (anyDatabaseWriter !== null && checker.isTypeAssignableTo(objectType, anyDatabaseWriter)); } else { // Fall back to string matching if we couldn't get the types const typeString = checker.typeToString(objectType); isDatabaseType = typeString.includes("DatabaseReader") || typeString.includes("DatabaseWriter") || typeString.includes("GenericDatabaseReader") || typeString.includes("GenericDatabaseWriter"); } // Also check for common patterns like ctx.db const isCtxDb = memberExpr.object.type === AST_NODE_TYPES.MemberExpression && memberExpr.object.property.type === AST_NODE_TYPES.Identifier && memberExpr.object.property.name === "db"; if (!isDatabaseType && !isCtxDb) { return; } // Check the number of arguments to determine if it's unmigrated const args = node.arguments; const isUnmigrated = (methodName === "get" && args.length === 1) || (methodName === "replace" && args.length === 2) || (methodName === "patch" && args.length === 2) || (methodName === "delete" && args.length === 1); if (!isUnmigrated) { return; } // Try to get type information for the first argument const tsNode = tsNodeMap.get(args[0]); const type = checker.getTypeAtLocation(tsNode); let tableName: string | null = null; // Try to extract table name from Id<"tableName"> type if (type.aliasSymbol?.name === "Id") { // Type with alias type arguments (internal TypeScript API) const typeWithArgs = type as ts.Type & { aliasTypeArguments?: readonly ts.Type[]; }; const typeArgs = typeWithArgs.aliasTypeArguments; if (typeArgs && typeArgs.length === 1) { const tableType = typeArgs[0]; // Check if it's a string literal type if (tableType.isStringLiteral && tableType.isStringLiteral()) { tableName = tableType.value; } else if (tableType.flags & (1 << 7)) { // StringLiteral flag = 128 = 1 << 7 // Fallback for different TypeScript versions const stringLiteralType = tableType as ts.StringLiteralType; tableName = stringLiteralType.value; } } } // Report the issue if (tableName) { context.report({ node, messageId: "missing-table-name", data: { method: methodName, tableName: JSON.stringify(tableName), }, fix: createTableNameFix(context, node, tableName), }); } else { context.report({ node, messageId: "missing-table-name-no-inference", data: { method: methodName, }, }); } }, }; }, }); /** * Creates a fix that inserts the table name as the first argument */ function createTableNameFix( context: RuleContext<string, unknown[]>, call: TSESTree.CallExpression, tableName: string, ): ReportFixFunction { return (fixer) => { const firstArg = call.arguments[0]; if (!firstArg) return null; const tableNameString = JSON.stringify(tableName); return fixer.insertTextBefore(firstArg, `${tableNameString}, `); }; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server