Development Guide
Guide for contributing to Tinqer, running tests, and troubleshooting.
Table of Contents
1. Getting Started
1.1 Prerequisites
- Node.js 22.13+ in the 22.x line, or Node.js 24+ (Node.js 26 is used for verification)
- npm 11.19+ (workspaces, release-age filtering and version-pinned install-script approvals)
- Docker + Docker Compose (optional, for PostgreSQL integration tests via
devenv/docker-compose.yml)
1.2 Installation
Both the library and documentation site enforce a seven-day minimum npm release
age through their project .npmrc. Strict engine checks reject npm versions
that cannot enforce the policy. Keep these safeguards enabled when refreshing
dependencies. The library approves native setup scripts only for the exact
reviewed better-sqlite3 and esbuild versions in package.json; review and
update those pins when upgrading either dependency.
All workspaces compile tests with TypeScript 6 into their ignored .tests/
directory and run that JavaScript in Mocha against the current dist/ build.
Query tests inspect callback source, so an alternative runtime transpiler can
change the behavior being tested. npm test compiles the tests before running;
npm run build checks both source and tests under the strict project settings.
Rebuild the library after source changes. For continuous test editing, run
npm run test:build -- --watch in the package alongside npm run test:watch.
# Clone the repository
git clone https://github.com/tinqerjs/tinqer.git
cd tinqer
# Install dependencies
npm install
# Build all packages
./scripts/build.sh
1.3 Project Structure
tinqer/
├── packages/
│ ├── tinqer/ # Core library
│ │ ├── src/
│ │ │ ├── expressions/ # Expression tree types
│ │ │ ├── linq/ # Public query DSL (Queryable/TerminalQuery)
│ │ │ ├── parser/ # Lambda parsing (OXC) + AST visitor
│ │ │ ├── plans/ # Plan handles (defineSelect/defineUpdate/etc.)
│ │ │ ├── policies/ # Row filters / policy helpers
│ │ │ ├── query-tree/ # Operation node types
│ │ │ └── visitors/ # Operation visitors (AST -> query tree)
│ │ └── tests/ # Core library tests
│ │
│ ├── pg-promise-adapter/ # PostgreSQL adapter (pg-promise)
│ │ ├── src/
│ │ │ ├── generators/ # SQL clause generators (from/where/select/etc.)
│ │ │ ├── expression-generator.ts # Expression -> SQL generator
│ │ │ ├── sql-generator.ts # Operation tree -> SQL orchestrator
│ │ │ └── index.ts # Public adapter API (execute*, toSql)
│ │ └── tests/ # Unit tests (SQL generation + runtime behavior)
│ │
│ ├── better-sqlite3-adapter/ # SQLite adapter (better-sqlite3)
│ │ ├── src/
│ │ │ ├── generators/ # SQL clause generators (from/where/select/etc.)
│ │ │ ├── expression-generator.ts # Expression -> SQL generator
│ │ │ ├── sql-generator.ts # Operation tree -> SQL orchestrator
│ │ │ └── index.ts # Public adapter API (execute*, toSql)
│ │ └── tests/ # Unit tests (SQL generation + runtime behavior)
│ │
│ ├── pg-promise-adapter-integration/ # PostgreSQL integration tests (requires a running DB)
│ │ └── tests/
│ └── better-sqlite3-adapter-integration/ # SQLite integration tests
│ └── tests/
│
├── devenv/ # Local dev environment helpers
│ ├── docker-compose.yml # PostgreSQL for integration tests
│ └── run.sh # Convenience wrapper around docker compose
│
├── scripts/ # Build and utility scripts
│ ├── build.sh # Main build script
│ ├── clean.sh # Clean build artifacts
│ ├── lint-all.sh # Lint all packages
│ └── format-all.sh # Format with Prettier
│
└── docs/ # Documentation
2. Building
2.1 Build Commands
# Standard build with formatting
./scripts/build.sh
# Build without formatting (faster during development)
./scripts/build.sh --no-format
# Build specific package
cd packages/tinqer
npm run build
Build Process:
- Runs TypeScript compiler for each package
- Generates ES modules with
.jsextensions - Runs Prettier formatting (unless
--no-formatis used) - Outputs to
dist/directories
2.2 Clean Build
# Remove build artifacts
./scripts/clean.sh
# Remove build artifacts and node_modules
./scripts/clean.sh --all
3. Testing
3.1 Running Tests
# Run all tests
npm test
# Save full test output (gitignored)
npm test | tee .tests/run-<timestamp>.txt
# Run tests for a specific workspace
npm test --workspace @tinqerjs/tinqer
npm test --workspace @tinqerjs/pg-promise-adapter
npm test --workspace @tinqerjs/better-sqlite3-adapter
npm test --workspace @tinqerjs/better-sqlite3-adapter-integration
# Grep within a specific workspace (Mocha)
npm test --workspace @tinqerjs/tinqer -- --grep "WHERE operations"
# PostgreSQL integration tests (requires a running Postgres)
./devenv/run.sh up
TINQER_PG_INTEGRATION=1 npm test --workspace @tinqerjs/pg-promise-adapter-integration
3.2 Test Organization
Core Library Tests (packages/tinqer/tests/):
- Parser tests: Lambda expression parsing
- AST visitor tests: AST to expression tree conversion
- Queryable tests: Query builder API
- Type tests: TypeScript type inference
Integration Tests:
- PostgreSQL integration:
pg-promise-adapter-integration/tests/ - SQLite integration:
better-sqlite3-adapter-integration/tests/ - Full end-to-end query execution tests
- Database-specific feature tests
3.3 Writing Tests
Unit Test Example:
import { describe, it } from "mocha";
import { strict as assert } from "assert";
import { createSchema, defineSelect } from "@tinqerjs/tinqer";
import { toSql } from "@tinqerjs/pg-promise-adapter";
describe("SQL Generation", () => {
it("should generate SQL with WHERE clause", () => {
interface Schema {
users: { id: number; name: string; age: number };
}
const schema = createSchema<Schema>();
const result = toSql(
defineSelect(schema, (q) => q.from("users").where((u) => u.age >= 18)),
{},
);
// Assert SQL and parameters
assert.ok(result.sql.includes("WHERE"));
assert.ok(result.params);
});
});
Integration Test Example:
import { describe, it, beforeEach } from "mocha";
import { strict as assert } from "assert";
import { createSchema } from "@tinqerjs/tinqer";
import { executeSelect } from "@tinqerjs/pg-promise-adapter";
import { db } from "./shared-db.js";
const schema = createSchema<Schema>();
describe("PostgreSQL Integration", () => {
beforeEach(async () => {
await db.none("TRUNCATE TABLE users RESTART IDENTITY CASCADE");
await db.none("INSERT INTO users (name, age) VALUES ('Alice', 30), ('Bob', 25)");
});
it("should execute SELECT query", async () => {
const results = await executeSelect(
db,
schema,
(q, params: { minAge: number }) =>
q
.from("users")
.where((u) => u.age >= params.minAge)
.select((u) => u.name),
{ minAge: 25 },
);
assert.deepEqual(results, ["Alice", "Bob"]);
});
});
Test Database Setup:
PostgreSQL integration tests use a shared connection (packages/pg-promise-adapter-integration/tests/shared-db.ts):
import pgPromise from "pg-promise";
const pgp = pgPromise();
const connectionString =
process.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/tinqer_test";
export const db = pgp(connectionString);
SQLite tests use isolated in-memory databases:
import Database from "better-sqlite3";
describe("SQLite Tests", () => {
let db: Database.Database;
beforeEach(() => {
db = new Database(":memory:");
// Create schema and seed data
});
afterEach(() => {
db.close();
});
});
4. Code Quality
4.1 Linting
# Lint all packages
./scripts/lint-all.sh
# Lint with auto-fix
./scripts/lint-all.sh --fix
# Lint specific package
cd packages/tinqer
npm run lint
npm run lint:fix
ESLint Configuration:
@typescript-eslint/no-explicit-any: error (noanytypes allowed)@typescript-eslint/prefer-const: error- Strict type checking enabled
4.2 Formatting
# Format all files with Prettier
./scripts/format-all.sh
# Check formatting without changes
./scripts/format-all.sh --check
# Format specific package
cd packages/tinqer
npm run format
IMPORTANT: Always run ./scripts/format-all.sh before committing.
5. Contributing
5.1 Coding Standards
TypeScript Guidelines:
-
No
anytypes: All code must be strictly typed -
Prefer
typeoverinterface: Useinterfaceonly for extensible contracts -
ESM imports: Always include
.jsextension in imports// Correct import { Queryable } from "./linq/queryable.js"; // Incorrect import { Queryable } from "./linq/queryable"; -
Pure functions: Prefer stateless functions with explicit dependency injection
-
No dynamic imports: Always use static imports
Code Organization:
- Export functions from modules when possible
- Use classes only for stateful connections or complex state management
- Keep files focused and single-purpose
- Write comprehensive JSDoc comments for public APIs
5.2 Commit Guidelines
# Before committing:
./scripts/format-all.sh # Format code
./scripts/lint-all.sh # Check linting
./scripts/build.sh # Build all packages
npm test # Run all tests
# Commit with descriptive message
git add .
git commit -m "feat: add support for window functions"
Commit Message Format:
feat:- New featuresfix:- Bug fixesrefactor:- Code refactoringtest:- Test additions or changesdocs:- Documentation changeschore:- Build process or tooling changes
5.3 Pull Requests
-
Create feature branch:
git checkout -b feat/my-feature -
Make changes and test:
./scripts/format-all.sh ./scripts/lint-all.sh ./scripts/build.sh npm test -
Push and create PR:
git push -u origin feat/my-feature # Create PR on GitHub -
PR Requirements:
- All tests passing
- Code formatted and linted
- Documentation updated
- Clear description of changes
- Type safety maintained
6. Troubleshooting
6.1 Common Issues
Issue: Build Fails with Module Resolution Errors
Error: Cannot find module './queryable.js'
Solution: Ensure all imports include .js extension:
// Incorrect
import { Queryable } from "./queryable";
// Correct
import { Queryable } from "./queryable.js";
Issue: Tests Fail with Connection Pool Destroyed
Error: Connection pool has been destroyed
Solution: Use shared database connection, don’t call pgp.end() in tests:
// Correct
import { db } from "./shared-db.js";
// Incorrect - don't create new pgp instances in tests
const pgp = pgPromise();
const db = pgp({...});
pgp.end(); // This destroys the global pool!
Issue: SQLite Boolean Type Errors
TypeError: SQLite3 can only bind numbers, strings, bigints, buffers, and null
Solution: Use number type (0/1) for boolean columns in SQLite schemas:
// Correct for SQLite
interface Schema {
users: {
is_active: number; // Use 0 for false, 1 for true
};
}
// Incorrect for SQLite
interface Schema {
users: {
is_active: boolean; // SQLite doesn't have boolean type
};
}
6.2 Parser Errors
Issue: Unsupported AST Node Type
Error: Unsupported AST node type: TemplateLiteral
Solution: Use params pattern for dynamic values:
// Incorrect - template literal in lambda
.where(u => u.name === `User ${userId}`)
// Correct - use params with executeSelect
await executeSelect(
db,
schema,
(q, params: { name: string }) =>
q.from("users").where((u) => u.name === params.name),
{ name: `User ${userId}` },
);
Issue: Unknown Identifier
Error: Unknown identifier 'externalVar'
Solution: Pass external variables via params object:
// Incorrect - closure variable
const minAge = 18;
.where(u => u.age >= minAge)
// Correct - params pattern with executeSelect
await executeSelect(
db,
schema,
(q, params: { minAge: number }) =>
q.from("users").where((u) => u.age >= params.minAge),
{ minAge: 18 },
);
6.3 Type Errors
Issue: Type Inference Not Working
// Type inference fails without schema context
const schema = createSchema(); // No schema type provided
// Types will be 'unknown' without schema
const result = await executeSelect(
db,
schema,
(q) => q.from("users"), // Type is Queryable<unknown>
{},
);
Solution: Provide explicit schema type to createSchema:
interface Schema {
users: { id: number; name: string };
}
const schema = createSchema<Schema>();
// Now fully typed from schema
const result = await executeSelect(
db,
schema,
(q) => q.from("users"), // Fully typed: Queryable<{ id: number; name: string }>
{},
);
Issue: Property Does Not Exist
Property 'email' does not exist on type '{ id: number; name: string }'
Solution: Ensure schema definition includes all columns:
interface Schema {
users: {
id: number;
name: string;
email: string; // Add missing column
};
}
Development Workflow
Typical Development Cycle:
- Make changes to source files
- Run linter:
./scripts/lint-all.sh --fix - Build:
./scripts/build.sh --no-format(skip formatting for speed) - Run specific tests:
npm test --workspace @tinqerjs/tinqer -- --grep "your feature" - Iterate until tests pass
- Run full test suite:
npm test | tee .tests/run-<timestamp>.txt - Format code:
./scripts/format-all.sh(or./scripts/build.shwithout--no-format) - Final build:
./scripts/build.sh(lint-first: run./scripts/lint-all.shbefore building) - Commit changes
Debugging Tips:
- Use
npm test --workspace <workspace> -- --grep "pattern"to focus on specific tests - Check
.tests/directory for saved test output (gitignored) - Use
npm run typecheckto check types without building - Enable verbose logging in tests with
DEBUG=* npm test