Skip to content

dee-wan-cms/schema-parser

Repository files navigation

@dee-wan-cms/schema-parser

Prisma schema directive parser that extracts, validates, and transforms query parameters with built-in SQL injection prevention.

Why?

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.

Installation

yarn add @dee-wan-cms/schema-parser

Quick Start

import { 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);
});

Core Features

🔒 SQL Injection Prevention

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)

🎯 Dynamic Parameter Extraction

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__' }
//   }
// }

Field Validation

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'
  });
}

API Reference

processModelDirectives(model, datamodel, config?)

Main entry point. Parses all directives from a model's documentation.

Parameters:

  • model: DMMF.Model - Prisma model from DMMF
  • datamodel: DMMF.Datamodel - Full datamodel for validation
  • config?: DirectivePipelineConfig
    • skipInvalid: 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
});

processAllDirectives(models, datamodel, config?)

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`);
}

extractParamsFromQuery(query, model)

Extract and transform dynamic parameters from a query object.

Returns: ExtractedParams

{
  processedQuery: Record<string, unknown>
  params: ParameterDefinition[]
  inputSchema: InputSchema
  dynamicKeys: string[]
  staticValues: unknown[]
}

sanitizeParamName(name)

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)

Type Definitions

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
}

Advanced Usage

Complex Queries with Logical Operators

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "where": {
///       "AND": [
///         { "status": "$status" },
///         {
///           "OR": [
///             { "views": { "gte": "$minViews" } },
///             { "published": true }
///           ]
///         }
///       ]
///     }
///   }
/// }

Array Parameters

const query = {
  where: {
    OR: [{ status: '$s1' }, { status: '$s2' }],
  },
};

// Parameters get distinct names and positions
// Even if referencing the same field

Parameter Deduplication

Same 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 marker

Custom Type Inference

The 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')

Runtime Parameter Validation

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']);

Security

Built-in Protections

  1. SQL Keyword Filtering: Strips/hashes dangerous keywords
  2. Prototype Pollution Prevention: Blocks __proto__, constructor, prototype
  3. Input Validation: Validates field existence against schema
  4. Type Safety: Strong TypeScript types throughout
  5. Array Index Validation: Prevents negative indices

Best Practices

// ✅ 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
}

Known Limitations

  1. Recursion Depth: Deep nesting (>50 levels) works but is slow
  2. Input Size: Very large JSON (>1MB) may cause performance issues
  3. Parameter Count: No hard limit, but >1000 parameters is impractical

Configuration

Error Handling Modes

// 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);
}

Common Patterns

Pagination

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "take": "$limit",
///     "skip": "$offset",
///     "orderBy": { "createdAt": "desc" }
///   }
/// }

Filtering with Relations

/// @optimize {
///   "method": "findMany",
///   "query": {
///     "where": {
///       "author": {
///         "email": "$authorEmail"
///       }
///     },
///     "include": { "author": true }
///   }
/// }

Single Record Lookup

/// @optimize {
///   "method": "findUnique",
///   "query": {
///     "where": {
///       "id": "$postId"
///     }
///   }
/// }

Conditional Filtering

/// @optimize {
///   "method": "findFirst",
///   "query": {
///     "where": {
///       "status": "$status",
///       "publishedAt": { "lte": "$maxDate" }
///     },
///     "orderBy": { "publishedAt": "desc" }
///   }
/// }

Counting Records

/// @optimize {
///   "method": "count",
///   "query": {
///     "where": {
///       "archived": false,
///       "views": { "gte": "$minViews" }
///     }
///   }
/// }

Aggregation

/// @optimize {
///   "method": "aggregate",
///   "query": {
///     "where": {
///       "status": "$status"
///     },
///     "_sum": { "views": true },
///     "_avg": { "views": true }
///   }
/// }

Utilities

Path Normalization

import {
  normalizeParamPath,
  pathToDotNotation,
} from '@dee-wan-cms/schema-parser';

const path = ['where', '[0]', 'status', 'in'];
normalizeParamPath(path); // ['where', 'status']
pathToDotNotation(path); // 'where.status'

Parameter Organization

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
// }

Troubleshooting

"Field 'X' does not exist on model"

The directive references a field not in your Prisma model. Check spelling and model definition.

"Missing required parameters"

Runtime validation failed. Ensure all parameters in the query are provided:

const params = { status: 'active' }; // Missing minViews!
validateDynamicParams(params, directive.parameters.all); // Throws

"Parameter position gap detected"

Internal error - parameters aren't sequentially positioned. This shouldn't happen; file an issue.

Contributing

git clone https://github.com/your-org/dee-wan-cms.git
cd packages/schema-parser
npm install
npm test

Requirements:

  • Node 18+
  • Tests must pass
  • Coverage must not decrease
  • Follow existing code style (functional, no OOP)

License

MIT

Credits

Built for Dee-wan-cms - edge-first CMS

Part of the @dee-wan-cms/* ecosystem.

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published