← 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 18+ (for ESM support)
  • npm 8+
  • TypeScript 5.3+
  • PostgreSQL 12+ (for PostgreSQL adapter development)
  • SQLite 3.35+ (for SQLite adapter development)

1.2 Installation

# Clone the repository
git clone https://github.com/webpods-org/tinqer.git
cd tinqer

# Install dependencies
npm install

# Build all packages
./scripts/build.sh

1.3 Project Structure

tinqer/
├── packages/
│   ├── tinqer/                          # Core library
│   │   ├── src/
│   │   │   ├── parser/                  # Lambda expression parser (OXC)
│   │   │   ├── converter/               # AST to expression tree converter
│   │   │   ├── queryable/               # Queryable API
│   │   │   ├── visitors/                # SQL generation visitors
│   │   │   └── types/                   # TypeScript type definitions
│   │   └── tests/                       # Core library tests
│   │
│   ├── tinqer-sql-pg-promise/           # PostgreSQL adapter
│   │   ├── src/
│   │   │   ├── adapter.ts               # PostgreSQL SQL adapter
│   │   │   ├── execute.ts               # Execution functions
│   │   │   └── visitors/                # PostgreSQL-specific visitors
│   │   └── tests/                       # Integration tests
│   │
│   ├── tinqer-sql-better-sqlite3/       # SQLite adapter
│   │   ├── src/
│   │   │   ├── adapter.ts               # SQLite SQL adapter
│   │   │   ├── execute.ts               # Execution functions
│   │   │   └── visitors/                # SQLite-specific visitors
│   │   └── tests/                       # Integration tests
│   │
│   └── tinqer-sql-*/                    # Integration test packages
│
├── 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

# Run tests in watch mode
npm run test:watch

# Run specific tests by pattern
npm run test:grep -- "WHERE operations"
npm run test:grep -- "INSERT"
npm run test:grep -- "JOIN"

# Run tests for specific package
cd packages/tinqer
npm test

3.2 Test Organization

Core Library Tests (packages/tinqer/tests/):

  • Parser tests: Lambda expression parsing
  • Converter tests: AST to expression tree conversion
  • Queryable tests: Query builder API
  • Type tests: TypeScript type inference

Integration Tests (packages/tinqer-sql-*/tests/):

  • PostgreSQL integration: tinqer-sql-pg-promise-integration/tests/
  • SQLite integration: tinqer-sql-better-sqlite3-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 } from "@webpods/tinqer";
import { selectStatement } from "@webpods/tinqer-sql-pg-promise";

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 = selectStatement(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 "@webpods/tinqer";
import { executeSelectSimple } from "@webpods/tinqer-sql-pg-promise";
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 executeSelectSimple(db, schema, (q) =>
      q
        .from("users")
        .where((u) => u.age >= 25)
        .select((u) => u.name),
    );

    assert.deepEqual(results, ["Alice", "Bob"]);
  });
});

Test Database Setup:

PostgreSQL tests use shared connection (packages/tinqer-sql-pg-promise-integration/tests/shared-db.ts):

import pgPromise from "pg-promise";

const pgp = pgPromise();
export const db = pgp({
  host: "localhost",
  port: 5432,
  database: "tinqer_test",
  user: "tinqer_test",
  password: "tinqer_test",
});

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 "./queryable/queryable.js";
    
    // Incorrect
    import { Queryable } from "./queryable/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 executeSelectSimple
await executeSelectSimple(
  db,
  schema,
  (q, p) =>
    q.from("users").where((u) => u.name === p.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 executeSelectSimple
await executeSelectSimple(
  db,
  schema,
  (q, p) =>
    q.from("users").where((u) => u.age >= p.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 run test:grep -- "your feature"
  5. Iterate until tests pass
  6. Run full test suite: npm test
  7. Format code: ./scripts/format-all.sh
  8. Final build: ./scripts/build.sh
  9. Commit changes

Debugging Tips:

  • Use npm run test:grep -- "pattern" to focus on specific tests
  • Check .tests/ directory for saved test output (gitignored)
  • Use TypeScript’s tsc --noEmit to check types without building
  • Enable verbose logging in tests with DEBUG=* npm test

← Back to README