
Security News
Static vs. Runtime Reachability: Insights from Latio’s On the Record Podcast
The Latio podcast explores how static and runtime reachability help teams prioritize exploitable vulnerabilities and streamline AppSec workflows.
MinuSQL (pronounced minuscule) is a lightweight, flexible SQL query builder and database abstraction layer for Node.js that supports both MySQL and PostgreSQL databases. It provides a minimalistic API for building SQL queries while maintaining type safety and security.
npm install minusql
This library acts as a wrapper around database drivers. To use it, you first create an instance of either a MySQL
or a Postgres
class, passing the database client (or a pool) from the mysql
or pg
libraries:
const mysql = require('mysql');
const { MySQL } = require('minusql');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'mydb'
});
const db = new MySQL(pool);
// Query example
const user = await db.users.selectOne({ id: 1 });
const { Pool } = require('pg');
const { Postgres } = require('minusql');
const pool = new Pool({
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'mydb'
});
const db = new Postgres(pool);
// Query example
const user = await db.users.selectOne({ id: 1 });
By default, MinuSQL automatically converts all identifiers to snake_case when building queries, and back to camelCase when handling results. You can disable this behavior by passing { convertCase: false }
to the constructor.
To perform queries on specific tables, simply access them as properties of the db
instance: for example, db.users
represents the users
table. To join multiple tables, call db.join()
(see below).
You can also explicitly call db.from('users')
to specify a table.
To perform raw queries, use db.exec(query, params)
.
To perform CRUD operations on a table, use one of the following methods:
db.table.select(where?, options?)
db.table.insert(rows, options?)
db.table.update(update, where?, options?)
db.table.delete(where?)
All of these methods return a Query
object, which will be executed as soon as you await
it (or call exec()
on it). You can also use the Query object to inspect the built query (the text
field) or call explain()
to construct an EXPLAIN query from it.
There are also two convenient aliases for common select types:
selectAll(options?)
is equivalent to select(null, options)
selectOne(where?, options?)
is equivalent to select(where, options).one()
// Select everything
const results = await db.users.selectAll();
// Simple query
const results = await db.users.select({ id: 1, name: 'John' });
// Complex query with operators and parameter substitution
const ageMin = 18;
const permissionList = ['edit', 'delete'];
const users = await db.users.select(['and',
['>', Symbol('age'), {$: ageMin}],
['=', Symbol('status'), 'active'],
['or',
{ role: 'admin' },
{ permissions: ['in', {$: permissionList}] },
],
]);
// With specific fields
const results = await db.users
.select(null, { fields: ['id', 'name'] });
// With ordering and limits
const results = await db.users
.select(null, {
order: 'createdAt DESC', // or [[Symbol('createdAt'), 'DESC']]
limit: 10,
offset: 0
});
The select(where?, options?)
method takes two arguments: the first defines the query condition and the second sets additional options.
You can use a raw query string for where
, but it's strongly discouraged as it won't be parameterized or escaped. Almost any condition can be expressed in a structured form instead.
Structured condition is defined as a recursive expression:
COALESCE
) or an operator (like <
or AND
). All following elements are its arguments (which are parsed as nested expressions).type
field to add an explicit type cast.key = value
and joined using the AND
operator. This is the same as ['AND', ['=', key1, value1], ['=', key2, value2], ...]
, just less verbose. If a value is itself an array, it's interpreted as if the key was inserted after the first element: { x: ['>', y] }
is the same as ['>', Symbol('x'), y]
. Keys are escaped as identifiers, and values may contain nested expressions.There are a few special behaviors for specific SQL operators:
['in', Symbol('x'), [1, 2, 3]]
is converted to "x" IN (1, 2, 3)
['not in', Symbol('x'), [1, 2, 3]]
is converted to "x" NOT IN (1, 2, 3)
['between', Symbol('x'), 1, 2]
is converted to "x" BETWEEN 1 AND 2
['not between', Symbol('x'), 1, 2]
is converted to "x" NOT BETWEEN 1 AND 2
['type', Symbol('x'), 'json']
is converted to json "x"
(the type is NOT escaped)['cast', Symbol('x'), 'json']
is converted to "x"::json
(the type is NOT escaped)['extract', Symbol('x'), 'month']
is converted to EXTRACT(month FROM x)
(note the order change; also the last argument is NOT escaped)['case', [cond1, then1], [cond2, then2], [default]]
is converted to CASE WHEN cond1 THEN then1 WHEN cond2 THEN then2 ELSE default END
Supported options are (all optional):
fields
: a raw string or an array of columns to selectgroup
: a raw string or an array of expressions to use in the GROUP BY
clausehaving
: a raw string or structured condition to use in the HAVING
clauseorder
: a raw string or an array of pairs [expression, 'ASC' | 'DESC'] to use in the ORDER BY
clauselimit
: a number to use in the LIMIT
clauseoffset
: a number to use in the OFFSET
clauseBy default, the resulting query returns an array of rows. To re-map it to more suitable data structures, see "Result Mapping" below.
// Single insert
await db.users.insert({
name: 'John', // Values will be parametrized by default (you can change this behavior by supplying "tranform" option)
age: 30
});
// Returning inserted ID
const result = await db.users.insert({
name: 'John',
age: 30,
}, { returnId: true }); // (PostgreSQL only, MySQL will always add insertId to output)
// result will contain the ID of the inserted row
// Batch insert with manually parametrized values
await db.users.insert(usersToInsert.map(user => ({
name: {$: user.name},
age: {$: user.age},
})), { transform: false });
// Upsert (handling conflicts)
await db.users.insert({
id: 888352,
name: 'John',
age: 30,
revision: 0,
joinedAt: Date.now() / 1000,
}, {
transform: {
joinedAt: 'timestamp', // Unixtime can be easily converted to timestamps
},
unique: ['id'], // Needed only for PostgreSQL (upserts on MySQL will work without it)
conflict: {
name: /update/, // Update name on conflict
age: /max/, // Update to largest of old and new value
revision: ['+', Symbol('revision'), 1], // Expressions are supported here as well
joinedAt: /fill/, // Update only if was null
},
});
insert
accepts two parameters: rows to insert (or a single row) and options.
Supported options:
fields
: an array of columns; if omitted, the first element's keys will be usedunique
(PostgreSQL only): for upserts, you need to specify a list of unique fieldsconflict
: for upserts, describes the conflict resolution strategy (see below)returnId
(PostgreSQL only): which column to return after insertion (set to true
to return column "id"). MySQL will always return id of the inserted row (along with some other information) as a insertId
field in the resulting row.Conflict resolution strategy is either false
(ignore all conflicts) or an object. Its keys correspond to columns that should be updated on conflict, and values are structured expressions to set them to.
For convenience, you can pass the following predefined RegExp patterns as aliases for common strategies:
/update/
: update to the new value on conflict/fill/
: only update if the old value is NULL
/inc/
: increment old value by 1/dec/
: decrement old value by 1/add/
: add new value to the old one/sub/
: subtract new value from the old one/max/
: select the maximum out of old value and the new one/min/
: select the minimum out of old value and the new one// Basic update with a simple where condition
await db.users.update(
{ age: 31 },
{ name: 'John' }
);
// Update with a complex where condition
await db.users.update(
{ status: 'inactive', lastSeen: new Date() },
['and',
['<', Symbol('lastLogin'), {$: oneMonthAgo}],
['=', Symbol('status'), 'active']
]
);
// Update with expressions
await db.users.update(
{
loginCount: ['+', Symbol('loginCount'), 1],
status: 'active'
},
{ id: 42 }
);
// Update all rows (be careful!)
await db.users.update(
{ isArchived: true },
null
);
The update(update, where?)
method takes two parameters:
update
: An object where keys are column names and values are either direct values or expressionswhere
: A condition to determine which rows to update (same format as in select
); if null
, all rows will be updatedThe update values can be:
where
conditions// Delete with a simple condition
await db.users.delete({ name: 'John' });
// Delete with a complex condition
await db.users.delete(['and',
['<', Symbol('lastLogin'), {$: sixMonthsAgo}],
['=', Symbol('status'), 'inactive']
]);
// Delete all rows (use with caution!)
await db.users.delete(null);
The delete(where?)
method takes a single parameter:
where
: A condition to determine which rows to delete (same format as in select
); if null
or omitted, all rows will be deletedMinuSQL provides various methods for transforming query results into different data structures:
// Get an array of results (default behavior)
const users = await db.users.select().toArray();
// Get an array of single column's values
const names = await db.users.select().toArray('name');
// Get just the first result or null if none found
const user = await db.users.select({ id: 1 }).one();
// Equivalent to using selectOne()
const user = await db.users.selectOne({ id: 1 });
// Get first result with transformation
const userName = await db.users.select({ id: 1 }).one('name');
// Map results to an object using a key
const usersById = await db.users.select().toObject('id');
// Result: { '1': {id: 1, name: 'John'}, '2': {id: 2, name: 'Jane'}, ... }
// Map to object with specific value
const nameById = await db.users.select().toObject('id', 'name');
// Result: { '1': 'John', '2': 'Jane', ... }
// Map to object using custom key function
const usersByFullName = await db.users.select().toObject(
user => `${user.firstName} ${user.lastName}`
);
// Group into arrays by a key
const usersByRole = await db.users.select().toObjectArray('role', 'name');
// Result: { 'admin': ['John', 'Jane'], 'user': ['Bob', 'Alice'], ... }
// Map instance
const userMap = await db.users.select().toMap('id');
// Result: Map { 1 => {id: 1, name: 'John'}, 2 => {id: 2, name: 'Jane'}, ... }
// Map with specific value
const nameMap = await db.users.select().toMap('id', 'name');
// Result: Map { 1 => 'John', 2 => 'Jane', ... }
// Group into Map of arrays
const roleMap = await db.users.select().toMapArray('role', 'name');
// Result: Map { 'admin' => ['John', 'Jane'], 'user' => ['Bob', 'Alice'], ... }
// Extract values to a Set
const allRoles = await db.users.select().toSet('role');
// Result: Set { 'admin', 'user', 'guest', ... }
// Process each row with a function
await db.users.select().forEach(user => {
console.log(`User ${user.name} is ${user.age} years old`);
});
// Map to class instances
class User {
static fromRow(row) {
const user = new User();
user.id = row.id;
user.name = row.name;
return user;
}
greet() {
return `Hello, ${this.name}!`;
}
}
const users = await db.users.select().toArray(User);
console.log(users[0].greet()); // "Hello, John!"
The Query object provides these mapping methods:
one(value?)
: Returns the first result or null if none found, optionally transformedtoArray(value?)
: Returns results as an array, optionally transforming each row using the field parametertoObject(key, value?)
: Maps results to an object using the specified key, optionally transforming valuestoObjectArray(key, value?)
: Groups results into arrays by keytoMap(key, value?)
: Maps results to a MaptoMapArray(key, value?)
: Groups results into arrays in a Map by keytoSet(value)
: Extracts unique values from the specified field into a SetforEach(fn)
: Executes a function for each result rowThe transformation parameter (value
) can be:
(row, index, allRows)
for custom transformationsfromRow()
static method if availablekey
parameter supports a subset of those types:
(row, index, allRows)
and result is used as key_
and used as keyNote that by default, all result keys are automatically converted from snake_case to camelCase unless convertCase: false
was set.
MinuSQL provides a simple way to work with transactions:
// Basic transaction
await db.begin(async (tx) => {
// The tx object is a transaction-specific database instance
await tx.users.insert({ name: 'John' });
await tx.profiles.insert({ userId: 1, bio: 'Hello' });
// If any query fails, the transaction will be automatically rolled back
// If all succeed, it will be committed automatically
});
const results = await db.join([
{ table: 'users', as: 'u' },
{ table: 'profiles', as: 'p', on: { 'u.id': Symbol('p.userId') } },
]).selectAll();
// Same as
const results = await db.users
.join('profiles p', { 'users.id': Symbol('p.userId') })
.selectAll();
To join multiple tables, call db.join
with the array of objects containing following fields:
table
: name of the table or another subqueryas
(Optional): alias to be used in AS
clausejoin
(Optional): join type to be used in JOIN
clause (if omitted, defaults to LEFT
)on
: any expression in the structured format to be used in ON
clauseInstead of object with join description, you can also use raw string, but it's discouraged.
Alternatively, you can call join
directly on a table: db.users.join({ table: 'profiles', ... })
. As a shorthand, you can also pass table name as the first argument and join condition as second.
Contributions are welcome! Please feel free to submit a Pull Request.
MIT
FAQs
A minimalistic ORM for MySQL and Postgres
The npm package minusql receives a total of 12 weekly downloads. As such, minusql popularity was classified as not popular.
We found that minusql demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
The Latio podcast explores how static and runtime reachability help teams prioritize exploitable vulnerabilities and streamline AppSec workflows.
Security News
The latest Opengrep releases add Apex scanning, precision rule tuning, and performance gains for open source static code analysis.
Security News
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.