npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pg-lightquery

v0.4.5

Published

postgres module

Readme

pg-lightquery

A modern, type-safe PostgreSQL query builder for Node.js with TypeScript support.

Why Choose pg-lightquery?

🎯 Better Developer Experience

  • Query Inspection: See generated SQL and parameters before execution
  • Type Safety: Full TypeScript support with auto-completion
  • Zero Learning Curve: Intuitive API that mirrors your mental model
  • Chained Operations: Fluent CTE-based multi-table inserts and updates
  • Related Tables Registry: Simplified management of table relationships

🚀 Superior Architecture

  • Deferred Execution: Build queries separately, execute when ready
  • Composition Over Inheritance: Clean, testable code structure
  • Security First: Separate concerns for column validation and data projection
  • Enhanced TableBase: Built-in support for complex table relationships

⚡ Advanced Features

  • Smart Operators: Built-in support for LIKE, IN, date ranges, JSON queries
  • Complex Joins: Filter by joined/aggregated columns with full type safety
  • Transaction Builder: Fluent interface for multi-query transactions
  • Chained Insert/Update Builder: Type-safe CTE operations for complex multi-table operations
  • Optional Audit Fields: Automatic lastChangedBy tracking with configurable defaults
  • Minimal Dependencies: Only 4 core dependencies

Installation

npm install pg-lightquery

Quick Start

1. Setup Connection

import PostgresConnection from 'pg-lightquery';

PostgresConnection.initialize({
	host: 'localhost',
	port: 5432,
	user: 'your_user',
	password: 'your_password',
	database: 'your_database',
});

2. Define Your Schema

import {ColumnDefinition, TableDefinition} from 'pg-lightquery';

export const usersColumns = {
	id: {type: 'INTEGER', primaryKey: true, autoIncrement: true},
	name: {type: 'TEXT', notNull: true},
	email: {type: 'TEXT', unique: true},
	createdAt: {type: 'TIMESTAMP WITHOUT TIME ZONE', notNull: true, default: 'NOW()'},
	lastChangedBy: {type: 'TEXT', notNull: false}, // Optional audit field
} as const;

export type UsersSchema = {
	[K in keyof typeof usersColumns]: ColumnDefinition;
};

const usersTable: TableDefinition<UsersSchema> = {
	tableName: 'users',
	schema: {columns: usersColumns},
};

3. Create Your Table Class

import {TableBase, EnhancedTableBase} from 'pg-lightquery';

// Basic table class
class UsersTable extends TableBase<UsersSchema> {
	constructor() {
		super(usersTable);
	}

	// Type-safe insert with query inspection
	insertUser(userData: {name: string; email: string}) {
		return this.insert({
			allowedColumns: ['name', 'email'],
			options: {
				data: userData,
				returnField: 'id',
			},
		});
	}

	// Flexible select with smart operators
	selectUsers(filters?: {name?: string; email?: string}) {
		return this.select({
			allowedColumns: '*',
			options: {
				where: filters,
				columnsToReturn: ['id', 'name', 'email', 'createdAt'],
			},
		});
	}

	// Safe update with required WHERE clause
	updateUser(data: {name?: string; email?: string}, where: {id?: number; email?: string}) {
		return this.update({
			allowedColumns: ['name', 'email'],
			options: {
				data,
				where,
				returnField: 'id',
			},
		});
	}
}

// Enhanced table class with related tables support
class EnhancedUsersTable extends EnhancedTableBase<UsersSchema> {
	constructor() {
		super(usersTable);

		// Register related tables for chained operations
		this.registerRelatedTable('posts', {tableDefinition: postsTable});
		this.registerRelatedTable('addresses', {tableDefinition: addressesTable});
	}

	// Complex multi-table insert with CTE support
	createUserWithProfile(userData: {name: string; email: string}, includePost = false, includeAddress = false) {
		const postData = {title: 'Welcome Post', content: 'Welcome to our platform!'};
		const addressData = {street: '123 Default St', city: 'Default City'};

		return this.createChainedInsert()
			.insert('new_user', this.db, userData, {returnField: '*'})
			.insertWithReferenceIf(includePost, 'user_post', this.getRelatedTable('posts'), postData, {
				from: 'new_user',
				field: 'id',
				to: 'userId',
			})
			.insertWithReferenceIf(includeAddress, 'user_address', this.getRelatedTable('addresses'), addressData, {
				from: 'new_user',
				field: 'id',
				to: 'userId',
			})
			.selectFrom('new_user')
			.build();
	}
}

const users = new UsersTable();
const enhancedUsers = new EnhancedUsersTable();

4. Use It

// Query inspection before execution
const insertQuery = users.insertUser({name: 'John', email: '[email protected]'});
console.log('SQL:', insertQuery.query.sqlText);
console.log('Values:', insertQuery.query.values);

// Execute when ready
const newUser = await insertQuery.execute();

// Or execute immediately
const allUsers = await users.selectUsers().execute();

// Update with WHERE clause (required for safety)
const updateQuery = users.updateUser({name: 'John Updated'}, {id: 1});
const updatedUser = await updateQuery.execute();

// Complex multi-table operations
const userWithProfile = await enhancedUsers
	.createUserWithProfile(
		{name: 'John', email: '[email protected]'},
		true, // include post
		true // include address
	)
	.execute();

Core Features

🔍 Query Inspection

Every query returns a QueryResult object with .query and .execute() methods:

const query = users.selectUsers({name: 'John'});

// Inspect before execution
console.log(query.query.sqlText); // "SELECT ... FROM users WHERE name = $1"
console.log(query.query.values); // ["John"]

// Execute when ready
const results = await query.execute();

🔗 Chained Insert & Update Builder

Build complex multi-table operations with automatic CTE handling, now with full support for updates:

import {createChainedInsert} from 'pg-lightquery';

// Simple chained insert
const result = createChainedInsert()
	.insert('new_user', usersDb, userData, {returnField: '*'})
	.insertWithReference('user_post', postsDb, postData, {
		from: 'new_user',
		field: 'id',
		to: 'userId',
	})
	.selectFrom('new_user')
	.build();

// Conditional inserts
const result = createChainedInsert()
	.insert('new_user', usersDb, userData, {returnField: '*'})
	.insertWithReferenceIf(hasAddress, 'user_address', addressesDb, addressData, {
		from: 'new_user',
		field: 'id',
		to: 'userId',
	})
	.selectFrom('new_user')
	.build();

// UPDATE operations in transactions
const updateResult = createChainedInsert()
	.update('updated_user', usersDb, {name: 'Updated Name'}, {id: userId}, {returnField: '*'})
	.update('updated_post', postsDb, {title: 'Updated Title'}, {id: postId}, {returnField: '*'})
	.selectFrom('updated_user')
	.build();

// Mixed INSERT and UPDATE operations
const mixedResult = createChainedInsert()
	.insert('new_user', usersDb, newUserData, {returnField: '*'})
	.updateWithReference(
		'updated_post',
		postsDb,
		{content: 'Post updated by new user'},
		{id: existingPostId},
		{from: 'new_user', field: 'id', to: 'userId'},
		{returnField: '*'}
	)
	.selectFrom('new_user')
	.build();

// Execute the chained operation
const result = await mixedResult.execute();

🔄 Advanced Update Scenarios

// Update multiple related tables in a transaction
const result = createChainedInsert()
	.update('charge_update', chargesDb, {status: 'COMPLETED', amount: 1500}, {idCharge: chargeId}, {returnField: '*'})
	.updateTable('deal_update', 'dealsTable', {status: 'COMPLETED'}, {idDeal: dealId}, {returnField: '*'})
	.updateTable(
		'expense_update',
		'otherExpensesTable',
		{status: 'COMPLETED'},
		{idExpense: expenseId},
		{returnField: '*'}
	)
	.selectFrom('charge_update')
	.build();

// Conditional updates based on business logic
const shouldUpdateDeal = dealStatus === 'PENDING';
const shouldUpdateExpense = expenseAmount > 0;

const conditionalResult = createChainedInsert()
	.insert('new_charge', chargesDb, chargeData, {returnField: '*'})
	.updateIf(shouldUpdateDeal, 'deal_update', dealsDb, {status: 'ACTIVE'}, {idDeal: dealId})
	.updateIf(shouldUpdateExpense, 'expense_update', expensesDb, {amount: newAmount}, {idExpense: expenseId})
	.selectFrom('new_charge')
	.build();

// Update with references from previous operations
const referenceResult = createChainedInsert()
	.insert('new_entity', entitiesDb, entityData, {returnField: '*'})
	.updateWithReference(
		'linked_record',
		recordsDb,
		{lastModifiedBy: 'system'},
		{id: recordId},
		{from: 'new_entity', field: 'id', to: 'entityId'},
		{returnField: '*'}
	)
	.selectFrom('new_entity')
	.build();

// Generated SQL for mixed operations:
// WITH new_entity AS (
//   INSERT INTO entities (...) VALUES (...) RETURNING *
// ),
// linked_record AS (
//   UPDATE records
//   SET "lastModifiedBy" = $1, "entityId" = (SELECT "id" FROM new_entity)
//   WHERE "id" = $2
//   RETURNING *
// )
// SELECT * FROM new_entity;

🏗️ Enhanced TableBase

Simplify complex table relationships with built-in registry:

class PlacesTable extends EnhancedTableBase<PlacesSchema> {
	constructor() {
		super(placesTable);

		// Register related tables
		this.registerRelatedTable('places_contacts', {tableDefinition: placesContactsTable});
		this.registerRelatedTable('places_contacts_billing', {tableDefinition: placesContactsBillingTable});
	}

	insertPlaceWithRelations(data: PlacesData, idContact: number, isBilling = false) {
		return this.createChainedInsert()
			.insert('place', this.db, data)
			.insertWithReference(
				'place_contact',
				'places_contacts',
				{idContact},
				{
					from: 'place',
					field: 'idPlace',
					to: 'idPlace',
				}
			)
			.insertWithReferenceIf(
				isBilling,
				'billing',
				'places_contacts_billing',
				{},
				{
					from: 'place_contact',
					field: 'idPlaceContact',
					to: 'idPlaceContact',
				}
			)
			.selectFrom('place')
			.build();
	}

	// Update operations with registered tables
	updatePlaceAndContacts(placeId: number, placeData: Partial<PlacesData>, contactData?: any) {
		return this.createChainedInsert()
			.update('updated_place', this.db, placeData, {idPlace: placeId}, {returnField: '*'})
			.updateTableIf(
				!!contactData,
				'updated_contact',
				'places_contacts',
				contactData || {},
				{idPlace: placeId},
				{returnField: '*'}
			)
			.selectFrom('updated_place')
			.build();
	}
}

🎨 Smart Query Operators

Built-in support for common SQL patterns:

// Pattern matching
await users.selectUsers({'name.like': 'John%'}).execute();

// Multiple values
await users.selectUsers({'id.in': [1, 2, 3]}).execute();

// Date ranges
await users
	.selectUsers({
		'createdAt.startDate': '2023-01-01',
		'createdAt.endDate': '2023-12-31',
	})
	.execute();

// JSON fields
await users.selectUsers({'settings.theme': 'dark'}).execute();

// NOT conditions
await users.selectUsers({'email.not': null}).execute();

🔐 Security & Projection Control

Separate concerns for security (what can be filtered) and projection (what gets returned):

// Public API: Limited filtering, safe data return
const publicUsers = users.select({
	allowedColumns: ['id', 'name'], // Can only filter by these
	options: {
		where: {name: 'John'},
		columnsToReturn: ['id', 'name', 'email'], // But can return these
	},
});

// Admin API: Full access
const adminUsers = users.select({
	allowedColumns: '*', // Can filter by anything
	options: {
		columnsToReturn: '*', // Can return everything
	},
});

🔄 Transaction Builder

Fluent interface for complex transactions:

const user1 = users.insertUser({name: 'Alice', email: '[email protected]'});
const user2 = users.insertUser({name: 'Bob', email: '[email protected]'});

const transaction = users.transaction().add(user1.query).add(user2.query);

// Inspect the entire transaction
console.log('Queries:', transaction.queries.length);

// Execute all or none
const results = await transaction.execute();

✏️ Safe Update Operations

Built-in safety features to prevent accidental mass updates:

// Basic update with required WHERE clause
const updateQuery = users.updateUser({name: 'John Updated', email: '[email protected]'}, {id: 1});

// Inspect before execution
console.log(updateQuery.query.sqlText);
// OUTPUT: UPDATE users SET "name" = $1, "email" = $2, "lastChangedBy" = $3 WHERE "id" = $4

// Execute when ready
const updatedUser = await updateQuery.execute();

🛡️ Safety Features

Required WHERE clause - Prevents accidental mass updates:

// ❌ This will throw an error
users.updateUser({name: 'New Name'}, {}); // Empty WHERE clause
// Error: WHERE clause is required for UPDATE operations

// ✅ Explicit mass update (use with caution)
users.updateUser({lastLoginAt: new Date()}, {}, {allowUpdateAll: true});

🎯 Advanced Update Options

// Update with smart operators in WHERE clause
await users.updateUser({status: 'inactive'}, {'email.like': '%@oldcompany.com'}).execute();

// Update with RETURNING clause
const updatedUser = await users.updateUser({name: 'Updated Name'}, {id: 1}, {returnField: 'id'}).execute();

// Track who made the change
await users.updateUser({name: 'Admin Updated'}, {id: 1}, {idUser: 'admin-123'}).execute();

🔍 Update Query Inspection

const updateQuery = users.updateUser({name: 'John', email: '[email protected]'}, {id: 1});

// Full query inspection
console.log('SQL:', updateQuery.query.sqlText);
console.log('Parameters:', updateQuery.query.values);
console.log('Parameter count:', updateQuery.query.values.length);

// Generated SQL:
// UPDATE users
// SET "name" = $1, "email" = $2, "lastChangedBy" = $3
// WHERE "id" = $4
//
// Parameters: ['John', '[email protected]', 'SERVER', 1]

📊 Optional Audit Fields

Automatic lastChangedBy tracking with configurable defaults:

// Automatic audit field addition (when present in schema)
const insertQuery = users.insertUser({name: 'John', email: '[email protected]'});
// Automatically includes lastChangedBy: 'SERVER' if field exists in schema

// Custom audit tracking
const insertQuery = users.insertUser({name: 'John', email: '[email protected]'}, {idUser: 'admin-123'});
// Uses custom idUser value for lastChangedBy

// Update with audit tracking
const updateQuery = users.updateUser({name: 'Updated'}, {id: 1}, {idUser: 'user-456'});
// Tracks who made the change

🎯 Complex Joins with Type Safety

Filter by joined/aggregated columns that don't exist in your base table:

// Define custom schema for joined results
interface UserWithPosts {
	id: number;
	name: string;
	email: string;
	posts: Post[];
}

const userWithPostsSchema = {
	id: {type: 'INTEGER'},
	name: {type: 'TEXT'},
	email: {type: 'TEXT'},
	posts: {type: 'JSONB'},
} as const;

class UsersTable extends TableBase<UsersSchema> {
	selectUsersWithPosts(filters?: any) {
		const sql = `
			SELECT u.id, u.name, u.email, 
				   json_agg(p.*) as posts
			FROM users u
			LEFT JOIN posts p ON u.id = p.user_id
			GROUP BY u.id, u.name, u.email
			WHERE 1=1
		`;

		return this.selectWithCustomSchema<UserWithPosts, typeof userWithPostsSchema>({
			allowedColumns: ['id', 'name', 'posts'],
			predefinedSQL: {sqlText: sql},
			options: {where: filters},
		});
	}
}

// Now you can filter by joined columns!
const activeUsers = await users
	.selectUsersWithPosts({
		'posts.not': null, // Filter by posts (doesn't exist in users table)
		'name.like': 'John%', // Combined with regular columns
	})
	.execute();

Testing Made Easy

describe('User Operations', () => {
	it('generates correct SQL', () => {
		const query = users.selectUsers({name: 'John'});

		expect(query.query.sqlText).toContain('SELECT');
		expect(query.query.sqlText).toContain('WHERE "name" = $1');
		expect(query.query.values).toEqual(['John']);
	});

	it('executes and returns data', async () => {
		const result = await users.selectUsers({id: 1}).execute();
		expect(result).toHaveLength(1);
	});

	it('handles chained inserts correctly', async () => {
		const chainedInsert = createChainedInsert()
			.insert('new_user', usersDb, userData, {returnField: '*'})
			.insertWithReference('user_post', postsDb, postData, {
				from: 'new_user',
				field: 'id',
				to: 'userId',
			})
			.selectFrom('new_user')
			.build();

		expect(chainedInsert.queries[0].sqlText).toContain('WITH new_user AS');
		expect(chainedInsert.queries[0].sqlText).toContain('INSERT INTO users');
		expect(chainedInsert.queries[0].sqlText).toContain('INSERT INTO posts');
	});
});

Performance & Dependencies

  • Minimal footprint: Only 4 dependencies (pg, mocklogs, sql-ddl-to-json-schema, uuid)
  • Parameterized queries: Built-in SQL injection protection
  • Efficient execution: Deferred execution prevents unnecessary queries
  • TypeScript optimized: Full type inference and checking
  • CTE optimization: Efficient multi-table operations with proper parameter handling

Compared to Other Libraries

| Feature | pg-lightquery | Prisma | TypeORM | Raw SQL | | -------------------- | ----------------- | ---------- | ---------- | --------- | | Type Safety | ✅ Full | ✅ Full | ⚠️ Partial | ❌ None | | Query Inspection | ✅ Built-in | ❌ No | ❌ No | ✅ Manual | | Chained Inserts | ✅ Type-safe | ❌ No | ❌ No | ⚠️ Manual | | Bundle Size | ✅ Small | ❌ Large | ❌ Large | ✅ None | | Complex Joins | ✅ Type-safe | ⚠️ Limited | ⚠️ Limited | ✅ Manual | | Update Safety | ✅ Required WHERE | ⚠️ Manual | ⚠️ Manual | ⚠️ Manual | | Audit Fields | ✅ Automatic | ❌ Manual | ❌ Manual | ⚠️ Manual | | Learning Curve | ✅ Minimal | ❌ Steep | ❌ Steep | ✅ None | | Flexibility | ✅ High | ⚠️ Medium | ⚠️ Medium | ✅ Full |

API Reference

Core Types

// All methods return QueryResult<T>
interface QueryResult<T> {
	query: QueryObject; // Inspect SQL and parameters
	execute(): Promise<T>; // Execute when ready
}

interface QueryObject {
	sqlText: string; // Generated SQL
	values: any[]; // Parameterized values
}

Chained Insert & Update Builder

// Create a new chained insert builder
const builder = createChainedInsert();

// INSERT operations
builder.insert(cteName, table, data, options);
builder.insertWithReference(cteName, table, data, reference, options);
builder.insertWithReferenceIf(condition, cteName, table, data, reference, options);

// UPDATE operations
builder.update(cteName, table, data, where, options);
builder.updateWithReference(cteName, table, data, where, reference, options);
builder.updateIf(condition, cteName, table, data, where, options);
builder.updateWithReferenceIf(condition, cteName, table, data, where, reference, options);

// Set final SELECT
builder.selectFrom(cteName, columns);

// Build and execute
const result = builder.build();
const data = await result.execute();

Enhanced TableBase

class MyTable extends EnhancedTableBase<MySchema> {
	constructor() {
		super(tableDefinition);

		// Register related tables
		this.registerRelatedTable('related_table', {tableDefinition: relatedTableDef});
	}

	// Use chained inserts with registered tables
	complexInsertOperation() {
		return this.createChainedInsert()
			.insertIntoTable('main', 'main_table', data)
			.insertIntoTableWithReference('related', 'related_table', relatedData, reference)
			.selectFrom('main')
			.build();
	}

	// Use chained updates with registered tables
	complexUpdateOperation() {
		return this.createChainedInsert()
			.updateTable('main_update', 'main_table', data, where)
			.updateTableWithReference('related_update', 'related_table', relatedData, where, reference)
			.updateTableIf(condition, 'conditional_update', 'other_table', data, where)
			.selectFrom('main_update')
			.build();
	}
}

Query Operators

// Available operators for WHERE conditions
type QueryOperators = {
	'field.like': string; // LIKE pattern matching
	'field.in': any[]; // IN clause
	'field.not': any; // NOT EQUAL
	'field.startDate': string; // Date >= value
	'field.endDate': string; // Date <= value
	'field.orderBy': 'ASC' | 'DESC'; // ORDER BY
	'field.null': boolean; // IS NULL / IS NOT NULL
	// JSON field access
	'jsonField.property': any; // JSON -> 'property' = value
};

Audit Field Configuration

// Optional lastChangedBy field in schema
const schema = {
	// ... other fields
	lastChangedBy: {
		type: 'TEXT',
		notNull: false, // Optional field
	},
};

// Automatic addition with default value
const insertQuery = table.insert(data); // Uses 'SERVER' as default

// Custom audit tracking
const insertQuery = table.insert(data, {idUser: 'custom-user-id'});

Contributing

Contributions are welcome! Please check out our GitHub repository.

License

ISC License