Created
September 17, 2025 01:38
-
-
Save bhouston/5836b7925186c7c9904e3fedd9fc5df3 to your computer and use it in GitHub Desktop.
ESLint rule to catch the use of Prisma's "include" queries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * ESLint rule to prevent the use of Prisma's `include` queries | |
| * This rule helps prevent accidental exposure of sensitive data by encouraging | |
| * explicit field selection using `select` instead of `include` | |
| */ | |
| export default { | |
| meta: { | |
| type: 'problem', | |
| docs: { | |
| description: | |
| 'Disallow Prisma include queries to prevent accidental data exposure', | |
| category: 'Security', | |
| recommended: true, | |
| }, | |
| fixable: null, | |
| schema: [], | |
| messages: { | |
| noPrismaInclude: | |
| 'Prisma `include` queries are not allowed. Use `select` instead to explicitly choose which fields to return. This prevents accidental exposure of sensitive data.', | |
| }, | |
| }, | |
| create(context) { | |
| // Helper function to check if a node is within a Prisma query | |
| function isWithinPrismaQuery(node) { | |
| let current = node; | |
| while (current) { | |
| if ( | |
| current.type === 'CallExpression' && | |
| current.callee && | |
| current.callee.type === 'MemberExpression' && | |
| current.callee.property && | |
| current.callee.property.type === 'Identifier' | |
| ) { | |
| const methodName = current.callee.property.name; | |
| const prismaMethods = [ | |
| 'findMany', | |
| 'findUnique', | |
| 'findFirst', | |
| 'create', | |
| 'update', | |
| 'upsert', | |
| 'createMany', | |
| 'updateMany', | |
| 'delete', | |
| 'deleteMany', | |
| ]; | |
| if (prismaMethods.includes(methodName)) { | |
| // Check if the callee object is likely a Prisma client | |
| const calleeObject = current.callee.object; | |
| if ( | |
| calleeObject && | |
| calleeObject.type === 'MemberExpression' && | |
| calleeObject.property && | |
| calleeObject.property.type === 'Identifier' | |
| ) { | |
| const modelName = calleeObject.property.name; | |
| // Common Prisma model names (you can extend this list) | |
| const commonModels = [ | |
| 'user', | |
| 'org', | |
| 'organization', | |
| 'project', | |
| 'asset', | |
| 'member', | |
| 'memberInvite', | |
| 'assetReport', | |
| 'userNotification', | |
| 'accessKey', | |
| 'session', | |
| 'refreshToken', | |
| ]; | |
| if (commonModels.includes(modelName.toLowerCase())) { | |
| return true; | |
| } | |
| } | |
| } | |
| } | |
| current = current.parent; | |
| } | |
| return false; | |
| } | |
| // Helper function to recursively find all 'include' properties in an object | |
| function findIncludeProperties(node, includeProperties = []) { | |
| if (node.type === 'ObjectExpression') { | |
| node.properties.forEach((prop) => { | |
| if ( | |
| prop.type === 'Property' && | |
| prop.key.type === 'Identifier' && | |
| prop.key.name === 'include' | |
| ) { | |
| includeProperties.push(prop); | |
| } | |
| // Recursively check nested objects (like in select statements) | |
| if (prop.value && prop.value.type === 'ObjectExpression') { | |
| findIncludeProperties(prop.value, includeProperties); | |
| } | |
| }); | |
| } | |
| return includeProperties; | |
| } | |
| return { | |
| // Match object expressions that contain an 'include' property | |
| ObjectExpression(node) { | |
| // Find all include properties (including nested ones) | |
| const includeProperties = findIncludeProperties(node); | |
| includeProperties.forEach((includeProperty) => { | |
| // Check if this is within a Prisma query | |
| if (isWithinPrismaQuery(node)) { | |
| context.report({ | |
| node: includeProperty, | |
| messageId: 'noPrismaInclude', | |
| }); | |
| } | |
| }); | |
| }, | |
| }; | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment