vite-plugin-gqlite
A Vite plugin that transforms gqlite template literals into optimized SQLite queries at build time. This plugin enables you to write GraphQL-like queries that are automatically converted to efficient SQL queries, preventing N+1 problems and SQL injection vulnerabilities.
Features
- 🚀 Build-time GraphQL to SQL transformation
- 🛡️ SQL injection prevention through parameterized queries
- ⚡ N+1 problem prevention with optimized joins
- 🔍 TypeScript support with full type safety
- 📍 Source map generation for debugging
- 🎯 Named query requirement for better organization
- 🔧 Configurable function names and file patterns
- 📦 Persisted query mode with stable operation IDs and compact server maps
Installation
bun add @gqlite/tag
bun add -d @gqlite/vite-plugin @gqlite/gql2sql graphql
Database Setup
For automatic schema generation, you'll need a database connection that implements the DatabaseConnection interface:
interface DatabaseConnection {
all(query: string): Promise<any[]>;
}
import { Database } from 'bun:sqlite';
const db = new Database('./database.sqlite');
const dbConnection = {
async all(query: string) {
return db.query(query).all();
}
};
import Database from 'better-sqlite3';
const db = new Database('./database.sqlite');
const dbConnection = {
async all(query: string) {
return db.prepare(query).all();
}
};
Quick Start
1. Configure the plugin in your vite.config.ts:
import { defineConfig } from 'vite';
import { createSchemaFromDatabase, gqlitePlugin } from '@gqlite/vite-plugin';
import Database from 'better-sqlite3';
const db = new Database('./database.sqlite');
const dbConnection = {
async all(sql) {
return db.prepare(sql).all();
},
};
const schema = await createSchemaFromDatabase(dbConnection);
export default defineConfig({
plugins: [
gqlitePlugin({
schema,
})
]
});
2. Use gqlite in your code:
import { gqlite } from '@gqlite/tag';
const users = gqlite<GQL.GetUsers>(db)`
query GetUsers {
users {
id
name
email
}
}
`;
const temp = db.query("SELECT t0.id AS id, t0.name AS name, t0.email AS email FROM users AS t0", {});
const users = gqlite.transformResult(temp);
Syntax Requirements
Named Queries Only
All GraphQL queries must have a name. Unnamed queries will not be transformed:
const users = gqlite<GQL.GetUsers>(db)`
query GetUsers {
users { id name }
}
`;
const users = gqlite<GQL.GetUsers>(db)`
query {
users { id name }
}
`;
Type Annotations
The plugin expects the TypeScript generic syntax for better code organization:
const users = gqlite<GQL.GetUsers>(db)`query GetUsers { ... }`;
const users = gqlite(db)`query GetUsers { ... }`;
@flatten Relation Projection
Use @flatten on root many-to-one or one-to-one relation fields to inline selected child scalar fields into parent rows. Child aliases become output field names.
const posts = gqlite<GQL.GetPostsWithAuthors>(db)`
query GetPostsWithAuthors {
posts {
id
title
user @flatten {
authorName: name
authorEmail: email
}
}
}
`;
@flatten is rejected for array relations (one-to-many / many-to-many) and nested relation children.
Advanced Examples
Queries with Parameters
const userId = 123;
const userPosts = gqlite<GQL.GetUserPosts>(db)`
query GetUserPosts {
user(id: ${userId}) {
id
name
posts {
id
title
content
}
}
}
`;
Complex Nested Relations
const usersWithPosts = gqlite<GQL.GetUsersWithPosts>(db)`
query GetUsersWithPosts {
users {
id
name
posts {
id
title
user {
name
}
}
}
}
`;
Configuration Options
interface GqlitePluginOptions {
schema: Schema;
functionName?: string;
include?: string[];
exclude?: string[];
generateTypes?: boolean;
typesDir?: string;
generateGraphqlSchema?: boolean;
graphqlSchemaOutputPath?: string;
sqlExecutor?: (params: {
paramMapping: Record<string, string>;
escapedSql: string;
dbVariable: string;
}) => string;
persistedQueries?: boolean | PersistedQueryOptions;
}
interface PersistedQueryOptions {
registryPath?: string;
manifestPath?: string;
compactMapPath?: string;
clientFunctionName?: string;
schemaHash?: string;
compilerVersion?: string;
}
Persisted Query Mode
Persisted mode keeps GraphQL and SQL out of the browser bundle. The plugin collects gqlite<T>(db) template literals, assigns stable integer operation IDs, writes server artifacts, and replaces client code with ID calls.
gqlitePlugin({
schema,
persistedQueries: {
registryPath: ".gqlite/operation-registry.json",
manifestPath: ".gqlite/persisted-manifest.json",
compactMapPath: ".gqlite/persisted-map.ts",
clientFunctionName: "gqliteQuery",
},
});
Source code:
const usersPromise = gqlite<GQL.ListUsers>(db)`
query ListUsers {
users { id name email }
}
`;
Generated client code:
const usersPromise = gqliteQuery(1);
Generated artifacts:
operation-registry.json stores stable integer IDs for canonical GraphQL operations.
persisted-manifest.json is a readable debug manifest with SQL and parameter names.
persisted-map.ts exports compact runtime data for executePersistedQuery().
For a complete app, see examples/gqlite-persisted.
Custom Function Name
gqlitePlugin({
schema: mySchema,
functionName: 'myCustomGql'
})
const data = myCustomGql<GQL.Query>(db)`query GetData { ... }`;
File Filtering
gqlitePlugin({
schema: mySchema,
include: ['.ts', '.tsx'],
exclude: ['node_modules', 'dist']
})
Custom SQL Executor
By default, the plugin generates code that calls db.query(sql, params). You can customize this behavior using the sqlExecutor option:
gqlitePlugin({
schema: mySchema,
sqlExecutor: ({ paramMapping, escapedSql, dbVariable }) => {
const paramObject = Object.entries(paramMapping)
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
return `${dbVariable}.execute(${escapedSql}, { ${paramObject} })`;
}
})
Examples of Custom SQL Executors
For databases that use .prepare() and .all():
sqlExecutor: ({ paramMapping, escapedSql, dbVariable }) => {
const paramObject = Object.entries(paramMapping)
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
return `${dbVariable}.prepare(${escapedSql}).all({ ${paramObject} })`;
}
For async database calls:
sqlExecutor: ({ paramMapping, escapedSql, dbVariable }) => {
const paramObject = Object.entries(paramMapping)
.map(([key, value]) => `${key}: ${value}`)
.join(", ");
return `await ${dbVariable}.query(${escapedSql}, { ${paramObject} })`;
}
For custom parameter formatting:
sqlExecutor: ({ paramMapping, escapedSql, dbVariable }) => {
const paramArray = Object.entries(paramMapping)
.map(([_, value]) => value)
.join(", ");
return `${dbVariable}.query(${escapedSql}, [${paramArray}])`;
}
Understanding the Parameters
The sqlExecutor function receives these parameters:
-
paramMapping: Maps parameter names to their JavaScript expressions
-
escapedSql: The generated SQL query as a JSON-escaped string
-
dbVariable: The variable name of the database instance
The function should return a string of JavaScript code that will be inserted into the transformed output.
Schema Definition
You can generate schemas automatically from your existing SQLite database using the built-in schema generator:
Automatic Schema Generation (Recommended)
import { createSchemaFromDatabase, gqlitePlugin } from '@gqlite/vite-plugin';
const dbConnection = {
all: (query: string) => db.prepare(query).all()
};
const schema = await createSchemaFromDatabase(dbConnection);
export default defineConfig({
plugins: [
gqlitePlugin({ schema })
]
});
Manual Schema Definition (Alternative)
If you prefer to define schemas manually or need custom configurations:
const schema = {
users: {
id: { type: "INTEGER", nullable: false },
name: { type: "TEXT", nullable: false },
email: { type: "TEXT", nullable: false },
createdAt: { type: "DATETIME", nullable: true }
},
posts: {
id: { type: "INTEGER", nullable: false },
title: { type: "TEXT", nullable: false },
content: { type: "TEXT", nullable: true },
userId: {
type: "INTEGER",
nullable: false,
relation: {
table: "users",
foreignKey: "userId",
type: "many-to-one"
}
},
user: {
type: "OBJECT",
relation: {
table: "users",
foreignKey: "userId",
type: "many-to-one"
}
}
},
users: {
posts: {
type: "ARRAY",
relation: {
table: "posts",
foreignKey: "userId",
type: "one-to-many"
}
}
}
};
Schema Generator Features
The createSchemaFromDatabase function automatically:
- Detects column types and maps SQLite types to schema types
- Identifies foreign keys and creates many-to-one relationships
- Generates convenience fields (e.g.,
userId → user relation field)
- Creates one-to-many relationships in referenced tables
- Detects one-to-one relationships from unique foreign keys
- Detects many-to-many relationships through junction tables, including payload-column junction tables
- Preserves relation source/target columns so non-
id joins can be generated correctly
- Handles nullable columns correctly (primary keys are never nullable)
GraphQL Schema Generation
The plugin automatically generates GraphQL schema files that provide IDE validation and autocompletion support for your GraphQL queries. This feature is enabled by default and creates a comprehensive schema based on your SQLite database structure.
Generated Schema Features
- Complete Type Definitions: All database tables become GraphQL types
- WHERE Input Types: Support for filtering with operators (
eq, ne, gt, gte, lt, lte, like, ilike)
- Query Root: Automatically generated Query type with all table queries
- Relationship Support: Proper GraphQL field definitions for table relations
- Pagination Support: Built-in
limit and offset parameters
- Ordering Support:
orderBy parameter for result sorting
- Directive Support:
directive @flatten on FIELD for supported flattened relation projections
Configuration
gqlitePlugin({
schema: mySchema,
generateGraphqlSchema: true,
graphqlSchemaOutputPath: './src/schema.graphql',
})
Generated Schema Example
For a simple blog schema, the plugin generates:
type Users {
id: Int!
username: String!
email: String!
posts: [Posts!]!
}
type Posts {
id: Int!
title: String!
content: String!
authorId: Int!
author: Users!
}
input UsersWhereInput {
id: IntWhereInput
username: StringWhereInput
email: StringWhereInput
}
input StringWhereInput {
eq: String
ne: String
like: String
ilike: String
}
type Query {
users(
where: UsersWhereInput
orderBy: [String!]
limit: Int
offset: Int
): [Users!]!
posts(
where: PostsWhereInput
orderBy: [String!]
limit: Int
offset: Int
): [Posts!]!
}
IDE Integration
The generated schema provides:
- Syntax Highlighting: Proper GraphQL syntax highlighting in your queries
- Autocompletion: Field name suggestions as you type
- Type Validation: Real-time validation of query structure
- Documentation: Hover information for fields and types
Disabling Schema Generation
gqlitePlugin({
schema: mySchema,
generateGraphqlSchema: false
})
Security Features
SQL Injection Prevention
All user inputs are automatically parameterized:
const userInput = "'; DROP TABLE users; --";
const result = gqlite<GQL.GetUser>(db)`
query GetUser {
user(id: ${userInput}) {
id name
}
}
`;
N+1 Problem Prevention
Nested queries are automatically optimized with JOINs:
const usersWithPosts = gqlite<GQL.GetUsersWithPosts>(db)`
query GetUsersWithPosts {
users {
id
name
posts { // Automatically joined, not fetched in a loop
id
title
}
}
}
`;
Development
Install Dependencies
bun install
Run Tests
bun test
Build
bun run build
How It Works
- Parse: The plugin parses your source code to find
gqlite template literals
- Extract: GraphQL queries and parameters are extracted from template literals
- Convert: GraphQL queries are converted to optimized SQL using the schema
- Execute: SQL execution code is generated using the default or custom
sqlExecutor
- Transform: The original code is replaced with parameterized SQL calls
- Generate: Source maps are generated for debugging support
TypeScript Support
The plugin generates TypeScript-compatible code and preserves all type information. It works seamlessly with:
.ts and .tsx files
- JSX syntax
- TypeScript generics
- Interface definitions
- Type annotations