Collections
Zodgres provides a simple and intuitive API for querying collections using Postgres.js ES6 Tagged Template Strings.
Creating Records
Create single or multiple records in a collection.
For complex SQL operations, use insert()
with raw SQL.
Single Record
const users = db.collection('users', {
id: z.number().optional(),
name: z.string(),
age: z.number().optional(),
});
// Open database connection and run migrations
await db.open();
const user = await users.create({
name: 'John',
age: 30,
});
Before using collections: Make sure to call await db.open()
after defining all your collections. This establishes the database connection and runs migrations before you can perform any operations.
Multiple Records
const newUsers = await users.create([
{ name: 'Alice' },
{ name: 'Bob', age: 25 },
]);
Validation
Data is validated against your Zod schema:
// ❌ Validation error
await users.create({
name: '',
age: -5,
});
Retrieving Records
Retrieve records from a collection using SQL template literals.
All Records
const allUsers = await users.select();
const allUsers = await users.select`*`;
With Conditions
const user = await users.select`* WHERE name = ${'John'}`;
const adults = await users.select`* WHERE age >= ${18}`;
Specific Columns
const names = await users.select`name`;
const adultNames = await users.select`name WHERE age > ${25}`;
Ordering and Limiting
const orderedUsers = await users.select`* ORDER BY age DESC`;
const topUsers = await users.select`* ORDER BY id LIMIT ${5}`;
Aggregations
const countResult = await users.select<{ count: string }>`COUNT(*)`;
const avgAge = await users.select<{ avg: string }>`AVG(age) WHERE age IS NOT NULL`;
Single Record
Select a single record. Automatically adds LIMIT 1
and returns the first result or undefined
.
const firstUser = await users.selectOne`*`;
const user = await users.selectOne`* WHERE name = ${'John'}`;
Updating Records
Update records and return the number of updated records. If you want to return the updated data, use the RETURNING
clause.
Basic Updates
const updated = await users.update`name = ${'John'} WHERE id = ${1}`;
console.log(updated.count); // 1
const multi = await users.update`age = ${25} WHERE name = ${'Bob'}`;
console.log(updated.count); // 1
Multiple Fields
You may pass an object to the first argument to update multiple fields at once:
const updated = await users.update`
${{ name: 'Jane', age: 31 }}
WHERE id = ${2}
`;
Or, you may fill in the fields manually:
const updated = await users.update`
name = ${'Jane'}, age = ${31}
WHERE id = ${2}
`;
Returning Data
You may use the RETURNING
clause to return the updated data:
const updated = await users.update`
${{ name: 'Jane', age: 31 }}
WHERE id = ${2}
RETURNING *
`;
All Records
const allUpdated = await users.update`status = ${'active'}`;
console.log(allUpdated.count); // 100
Deleting Records
Delete records from a collection.
All Records
const deletedCount = await users.delete();
With Conditions
const deletedCount = await users.delete`WHERE age < ${18}`;
With RETURNING
const deletedUsers = await users.delete<{ id: number, name: string }>`
WHERE name = ${'John'} RETURNING *
`;
Counting Records
Count records in a collection.
const totalUsers = await users.count();
const adultCount = await users.count`WHERE age >= ${18}`;
const activeAdults = await users.count`
WHERE age >= ${18} AND status = ${'active'}
`;
Raw SQL Insert
Insert records using raw SQL and return the inserted data.
The insert()
method uses raw SQL, unlike create()
which accepts structured objects.
const inserted = await users.insert`(name, age) VALUES (${'John'}, ${30})`;
const inserted = await users.insert`(name) VALUES (${'Jane'})`;
const multiple = await users.insert`
(name, age) VALUES
(${'Alice'}, ${25}),
(${'Bob'}, ${30})
`;
Parameter Safety
Template literals automatically handle parameter escaping - thanks to Postgres.js.
const searchTerm = "'; DROP TABLE users; --";
// ✅ Safe - parameters are properly escaped
const results = await users.select`* WHERE name = ${searchTerm}`;
Error Handling
Handle query errors gracefully:
try {
const user = await users.create({
name: 'John',
age: 30,
});
} catch (error) {
if (error.code === '23505') {
// Unique constraint violation
console.error('Duplicate key violation');
} else if (error instanceof z.ZodError) {
// Validation error
console.error('Invalid data:', error.errors);
} else {
console.error('Database error:', error.message);
}
}
Type Safety
All query results are properly typed based on your schema:
const users = db.collection('users', {
id: z.number().optional(),
name: z.string(),
age: z.number().optional(),
});
const allUsers = await users.select();
// Type: Array<{ id: number, name: string, age: number | undefined }>
allUsers.forEach(user => {
console.log(user.name); // ✅ TypeScript knows this is a string
console.log(user.age); // ✅ TypeScript knows this can be undefined
// console.log(user.email); // ❌ TypeScript error - not in schema
});
Performance Tips
Batch Operations
// ✅ Better - single query
const newUsers = await users.create([
{ name: 'User 1' },
{ name: 'User 2' },
]);
// ❌ Slower - multiple queries
for (const userData of userDataArray) {
await users.create(userData);
}
Select Only Needed Columns
// ✅ Better - only needed columns
const userNames = await users.select`name WHERE age > ${18}`;
// ❌ Slower - unnecessary data
const activeUsers = await users.select`* WHERE age > ${18}`;
const names = activeUsers.map(u => u.name);