Skip to content

Conversation

@pyramation
Copy link
Contributor

@pyramation pyramation commented Jan 1, 2026

feat: add graphile-sql-expression-validator plugin

Summary

Adds a new Graphile plugin that validates SQL expressions at the GraphQL layer before they reach the database. The plugin parses SQL expressions to AST, validates against allowed node types and functions, and deparses back to canonical SQL text.

This is part 2 of a 2-part change for constructive-io/constructive-planning#476. Part 1 is the schema change in constructive-db (PR #178) that adds the default_value_ast column.

Key features:

  • Detects fields tagged with @sqlExpression smart comment
  • Auto-discovers companion *_ast columns (e.g., default_valuedefault_value_ast)
  • Validates AST against forbidden node types (SelectStmt, InsertStmt, ColumnRef, etc.)
  • Validates function calls against an allowlist
  • Normalizes text to canonical deparsed form

Updates since last revision

Latest: Tests added to CI workflow

  • Added graphile/graphile-sql-expression-validator to the CI test matrix in .github/workflows/run-tests.yaml
  • 34 unit tests now run automatically on every PR

Previous updates:

  1. Plugin is opt-in: Removed from graphile-settings to avoid loading pgsql-parser/pgsql-deparser dependencies in servers that don't need SQL expression validation. Apps should add the plugin directly:
    import SqlExpressionValidatorPlugin from 'graphile-sql-expression-validator';
    // Add to appendPlugins array
  2. Unit test suite (34 tests): Tests for valid/invalid expressions, schema-qualified functions, canonicalization
  3. @rawSqlAstField smart comment: Override the default <column>_ast naming convention
  4. allowOwnedSchemas option: Auto-allows schema-qualified functions from schemas owned by the current database
  5. getAdditionalAllowedSchemas hook: Custom async function for dynamic schema resolution
  6. Updated dependencies: pgsql-parser and pgsql-deparser updated to v17.x

Review & Testing Checklist for Human

This is a medium-risk PR - unit tests exist but integration testing is still needed:

  • Verify plugin hook works: The GraphQLObjectType:fields:field hook with isRootMutation and pgFieldIntrospection may not correctly intercept mutations. Test with actual GraphQL mutations against collections_public.field
  • Test allowOwnedSchemas DB query: The query silently returns empty array on error. Verify the GraphQL role has SELECT access to collections_public.schema and that jwt_private.current_database_id() returns correctly
  • Validate AST validation logic: The ALLOWED_NODE_TYPES set is defined but only FORBIDDEN_NODE_TYPES is checked - confirm this is intended behavior

Recommended test plan:

  1. Deploy both this PR and constructive-db PR renamed #178 to a test environment
  2. Add SqlExpressionValidatorPlugin to your app's PostGraphile plugins
  3. Create a field via GraphQL mutation with defaultValue: "uuid_generate_v4()"
  4. Verify the default_value_ast column is populated with the parsed AST
  5. Verify the default_value text is the canonical deparsed form
  6. Attempt to create a field with defaultValue: "SELECT * FROM users" and verify it's rejected

Notes

  • Unit tests (34 passing) now run in CI - these test the core validation functions but not the Graphile plugin hook integration
  • The plugin is not bundled in graphile-settings - it must be explicitly added to apps that need it
  • The allowedSchemas option defaults to empty, meaning schema-qualified function calls will be rejected unless explicitly configured or allowOwnedSchemas is enabled
  • The allowOwnedSchemas feature fails silently (returns empty array) if the DB query fails - this is intentional for fail-closed security

Link to Devin run: https://app.devin.ai/sessions/7655646c517d4c74b0f7b827a102d494
Requested by: Dan Lynch (@pyramation)

- Create new plugin for SQL expression validation and AST normalization
- Parse SQL expressions to AST, validate against allowed node types and functions
- Deparse AST back to canonical SQL text
- Auto-discover paired fields (text + AST) via @sqlExpression smart comment
- Add plugin to graphile-settings bundle

Related to: constructive-io/constructive-planning#476
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

- Add @rawSqlAstField smart comment to specify custom AST column name
- Add allowOwnedSchemas option to auto-allow schemas from collections_public.schema
- Add getAdditionalAllowedSchemas hook for custom schema resolution
- Cache owned schemas per request to avoid redundant DB queries
- Update README with new features documentation
- Update pgsql-parser from ^13.18.0 to ^17.9.5
- Update pgsql-deparser from ^13.18.0 to ^17.15.0
- Match versions used elsewhere in the codebase
- Test valid expressions (constants, functions, casts, COALESCE, CASE, etc.)
- Test invalid expressions (SELECT, INSERT, UPDATE, DELETE, subqueries, column refs)
- Test schema-qualified function validation
- Test custom allowed functions
- Test canonicalization behavior
- Test AST validation

34 tests passing
The plugin is now opt-in per application to avoid loading pgsql-parser
and pgsql-deparser dependencies in servers that don't need SQL expression
validation. Apps that need this feature can add the plugin directly to
their PostGraphile configuration.
@pyramation pyramation merged commit e47e6c3 into main Jan 1, 2026
36 checks passed
@pyramation pyramation deleted the devin/1767234463-graphile-sql-expression-validator branch January 1, 2026 04:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants