Prisma schema directive parser that extracts, validates, and transforms query parameters with built-in SQL injection prevention.
To define optimized, parameterized queries directly in Prisma schema and generate type-safe API endpoints. This parser extracts those directives, validates them, and prepares them for code generation.
model Post {
id Int @id
title String
status String
views Int
/// @optimize {
/// "method": "findMany",
/// "query": {
/// "where": {
/// "status": "$status",
/// "views": { "gte": "$minViews" }
/// }
/// }
/// }
}The parser extracts this, validates fields, sanitizes parameter names.
yarn add @dee-wan-cms/schema-parserimport { processModelDirectives } from '@dee-wan-cms/schema-parser';
import { getDMMF } from '@prisma/internals';
const dmmf = await getDMMF({ datamodel: schemaString });
const model = dmmf.datamodel.models.find((m) => m.name === 'Post');
const result = processModelDirectives(model, dmmf.datamodel, {
skipInvalid: true,
});
result.directives.forEach((directive) => {
console.log('Method:', directive.method);
console.log('Parameters:', directive.parameters.all);
});Automatically sanitizes parameter names, strips SQL keywords, and validates against dangerous patterns.
import { sanitizeParamName } from '@dee-wan-cms/schema-parser';
sanitizeParamName('DROP TABLE users'); // → 'param_abc123' (safe hash)
sanitizeParamName('user-id'); // → 'userid'
sanitizeParamName('SELECT'); // → 'param_def456' (safe hash)const query = {
where: {
status: '$status',
views: { gte: '$minViews' },
},
};
const { params, processedQuery } = extractParamsFromQuery(query, model);
// params = [
// { name: 'status', type: 'string', path: ['where', 'status'], position: 1 },
// { name: 'minViews', type: 'number', path: ['where', 'views', 'gte'], position: 2 }
// ]
// processedQuery = {
// where: {
// status: '__DYNAMIC_status__',
// views: { gte: '__DYNAMIC_minViews__' }
// }
// }Validates that referenced fields exist on the model:
const result = processModelDirectives(model, datamodel);
if (result.errors.length > 0) {
result.errors.forEach((err) => {
console.error(`${err.level}: ${err.message}`);
// error: Field 'nonExistent' does not exist on model 'Post'
});
}Main entry point. Parses all directives from a model's documentation.
Parameters:
model: DMMF.Model- Prisma model from DMMFdatamodel: DMMF.Datamodel- Full datamodel for validationconfig?: DirectivePipelineConfigskipInvalid: boolean- Skip invalid directives vs throw (default:true)
Returns: ModelDirectiveResult
{
modelName: string
directives: DirectiveProps[]
errors: DirectiveError[]
}Example:
const result = processModelDirectives(model, datamodel, {
skipInvalid: false, // Throw on validation errors
});Process directives for all models at once.
Returns: Map<string, ModelDirectiveResult>
const results = processAllDirectives(dmmf.datamodel.models, dmmf.datamodel);
for (const [modelName, result] of results) {
console.log(`${modelName}: ${result.directives.length} directives`);
}Extract and transform dynamic parameters from a query object.
Returns: ExtractedParams
{
processedQuery: Record<string, unknown>
params: ParameterDefinition[]
inputSchema: InputSchema
dynamicKeys: string[]
staticValues: unknown[]
}Sanitize a parameter name for safe use.
Rules:
- Strips SQL keywords
- Converts to alphanumeric + underscore
- Adds
param_prefix if starts with digit - Max 64 characters
- Deterministic hashing for collisions
sanitizeParamName('user-id'); // 'userid'
sanitizeParamName('123abc'); // 'param_123abc'
sanitizeParamName('DROP_TABLE_users'); // 'users' (keywords stripped)interface DirectiveProps {
method: string;
modelName: string;
query: ProcessedQuery;
parameters: ParameterSet;
context: {
model: DMMF.Model;
datamodel: DMMF.Datamodel;
allModels: DMMF.Model[];
enums: DMMF.DatamodelEnum[];
};
}
interface ParameterDefinition {
name: string; // Sanitized name
originalName?: string; // Original from directive
type: 'string' | 'number' | 'boolean' | 'object';
path: string[]; // Path in query object
required: boolean;
position: number; // 1-indexed
}/// @optimize {
/// "method": "findMany",
/// "query": {
/// "where": {
/// "AND": [
/// { "status": "$status" },
/// {
/// "OR": [
/// { "views": { "gte": "$minViews" } },
/// { "published": true }
/// ]
/// }
/// ]
/// }
/// }
/// }const query = {
where: {
OR: [{ status: '$s1' }, { status: '$s2' }],
},
};
// Parameters get distinct names and positions
// Even if referencing the same fieldSame parameter name across query? Gets deduplicated automatically:
const query = {
where: {
status: '$status',
OR: [{ status: '$status' }], // Same $status
},
};
// Only one parameter definition created
// Both locations reference the same internal markerThe parser infers parameter types from context:
// Numeric keywords
{
take: '$limit';
} // type: 'number'
{
skip: '$offset';
} // type: 'number'
// Field-based inference
{
where: {
id: '$id';
}
} // type: 'number' (Post.id is Int)
{
where: {
title: '$t';
}
} // type: 'string' (Post.title is String)
// Comparison operators
{
views: {
gte: '$min';
}
} // type: 'number'
// Pattern matching
{
userId: '$uid';
} // type: 'number' (contains 'id')
{
isActive: '$active';
} // type: 'boolean' (contains 'is')import {
validateDynamicParams,
extractParamValue,
} from '@dee-wan-cms/schema-parser';
const params = { status: 'active', minViews: 100 };
try {
validateDynamicParams(params, directive.parameters.all);
// Parameters are valid
} catch (error) {
console.error('Missing parameters:', error.message);
// Missing required parameters: status (path: where.status), minViews (path: where.views.gte)
}
// Extract specific parameter value
const status = extractParamValue(params, ['where', 'status']);- SQL Keyword Filtering: Strips/hashes dangerous keywords
- Prototype Pollution Prevention: Blocks
__proto__,constructor,prototype - Input Validation: Validates field existence against schema
- Type Safety: Strong TypeScript types throughout
- Array Index Validation: Prevents negative indices
// ✅ DO: Use the parser's sanitization
const safe = sanitizeParamName(userInput);
// ❌ DON'T: Use raw user input as parameter names
const unsafe = `$${userInput}`; // NEVER
// ✅ DO: Validate against schema
const result = processModelDirectives(model, datamodel, {
skipInvalid: false, // Throw on invalid fields
});
// ✅ DO: Check for errors
if (result.errors.length > 0) {
// Handle validation errors
}- Recursion Depth: Deep nesting (>50 levels) works but is slow
- Input Size: Very large JSON (>1MB) may cause performance issues
- Parameter Count: No hard limit, but >1000 parameters is impractical
// Lenient: Skip invalid directives, collect errors
const lenient = processModelDirectives(model, datamodel, {
skipInvalid: true,
});
console.log(
`Parsed ${lenient.directives.length}, errors: ${lenient.errors.length}`,
);
// Strict: Throw on first error
try {
const strict = processModelDirectives(model, datamodel, {
skipInvalid: false,
});
} catch (error) {
console.error('Validation failed:', error.message);
}/// @optimize {
/// "method": "findMany",
/// "query": {
/// "take": "$limit",
/// "skip": "$offset",
/// "orderBy": { "createdAt": "desc" }
/// }
/// }/// @optimize {
/// "method": "findMany",
/// "query": {
/// "where": {
/// "author": {
/// "email": "$authorEmail"
/// }
/// },
/// "include": { "author": true }
/// }
/// }/// @optimize {
/// "method": "findUnique",
/// "query": {
/// "where": {
/// "id": "$postId"
/// }
/// }
/// }/// @optimize {
/// "method": "findFirst",
/// "query": {
/// "where": {
/// "status": "$status",
/// "publishedAt": { "lte": "$maxDate" }
/// },
/// "orderBy": { "publishedAt": "desc" }
/// }
/// }/// @optimize {
/// "method": "count",
/// "query": {
/// "where": {
/// "archived": false,
/// "views": { "gte": "$minViews" }
/// }
/// }
/// }/// @optimize {
/// "method": "aggregate",
/// "query": {
/// "where": {
/// "status": "$status"
/// },
/// "_sum": { "views": true },
/// "_avg": { "views": true }
/// }
/// }import {
normalizeParamPath,
pathToDotNotation,
} from '@dee-wan-cms/schema-parser';
const path = ['where', '[0]', 'status', 'in'];
normalizeParamPath(path); // ['where', 'status']
pathToDotNotation(path); // 'where.status'import { organizeParameters } from '@dee-wan-cms/schema-parser';
const organized = organizeParameters(params);
// {
// all: ParameterDefinition[] // All params, sorted by position
// required: ParameterDefinition[] // Only required
// optional: ParameterDefinition[] // Only optional
// typeMap: { [name]: type } // Quick type lookup
// }The directive references a field not in your Prisma model. Check spelling and model definition.
Runtime validation failed. Ensure all parameters in the query are provided:
const params = { status: 'active' }; // Missing minViews!
validateDynamicParams(params, directive.parameters.all); // ThrowsInternal error - parameters aren't sequentially positioned. This shouldn't happen; file an issue.
git clone https://github.com/your-org/dee-wan-cms.git
cd packages/schema-parser
npm install
npm testRequirements:
- Node 18+
- Tests must pass
- Coverage must not decrease
- Follow existing code style (functional, no OOP)
MIT
Built for Dee-wan-cms - edge-first CMS
Part of the @dee-wan-cms/* ecosystem.