← Back to README

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:

  1. Runs TypeScript compiler for each package
  2. Generates ES modules with .js extensions
  3. Runs Prettier formatting (unless --no-format is used)
  4. 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 (no any types 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 any types: All code must be strictly typed

  • Prefer type over interface: Use interface only for extensible contracts

  • ESM imports: Always include .js extension 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 features
  • fix: - Bug fixes
  • refactor: - Code refactoring
  • test: - Test additions or changes
  • docs: - Documentation changes
  • chore: - Build process or tooling changes

5.3 Pull Requests

  1. Create feature branch:

    git checkout -b feat/my-feature
    
  2. Make changes and test:

    ./scripts/format-all.sh
    ./scripts/lint-all.sh
    ./scripts/build.sh
    npm test
    
  3. Push and create PR:

    git push -u origin feat/my-feature
    # Create PR on GitHub
    
  4. 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:

  1. Make changes to source files
  2. Run linter: ./scripts/lint-all.sh --fix
  3. Build: ./scripts/build.sh --no-format (skip formatting for speed)
  4. Run specific tests: npm test --workspace @tinqerjs/tinqer -- --grep "your feature"
  5. Iterate until tests pass
  6. Run full test suite: npm test | tee .tests/run-<timestamp>.txt
  7. Format code: ./scripts/format-all.sh (or ./scripts/build.sh without --no-format)
  8. Final build: ./scripts/build.sh (lint-first: run ./scripts/lint-all.sh before building)
  9. 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 typecheck to check types without building
  • Enable verbose logging in tests with DEBUG=* npm test

← Back to README