{"id":29419843,"url":"https://github.com/denull/minusql","last_synced_at":"2025-07-12T01:12:14.219Z","repository":{"id":286815141,"uuid":"962328527","full_name":"denull/minusql","owner":"denull","description":null,"archived":false,"fork":false,"pushed_at":"2025-04-10T11:28:08.000Z","size":96,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-17T05:04:52.030Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/denull.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-04-08T01:59:14.000Z","updated_at":"2025-04-10T11:28:12.000Z","dependencies_parsed_at":"2025-04-08T14:39:24.889Z","dependency_job_id":null,"html_url":"https://github.com/denull/minusql","commit_stats":null,"previous_names":["denull/minusql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/denull/minusql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denull%2Fminusql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denull%2Fminusql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denull%2Fminusql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denull%2Fminusql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/denull","download_url":"https://codeload.github.com/denull/minusql/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/denull%2Fminusql/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264922908,"owners_count":23683705,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-07-12T01:12:12.932Z","updated_at":"2025-07-12T01:12:14.211Z","avatar_url":"https://github.com/denull.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MinuSQL\n\nMinuSQL (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.\n\n## Features\n\n- Support for both MySQL and PostgreSQL\n- No dependencies\n- Fluent query builder interface\n- Automatic case conversion (snake_case ↔ camelCase)\n- Parameterized queries for security\n- Transaction support\n- Flexible result mapping\n- Built-in support for common SQL operations (SELECT, INSERT, UPDATE, DELETE)\n- JOIN operations with various join types\n- Conflict resolution for INSERT operations\n- Type-safe query building\n- EXPLAIN query support\n\n## Installation\n\n```bash\nnpm install minusql\n```\n\n## Quick Start\n\nThis 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:\n\n### MySQL\n\n```javascript\nconst mysql = require('mysql');\nconst { MySQL } = require('minusql');\n\nconst pool = mysql.createPool({\n  host: 'localhost',\n  user: 'root',\n  password: 'password',\n  database: 'mydb'\n});\n\nconst db = new MySQL(pool);\n\n// Query example\nconst user = await db.users.selectOne({ id: 1 });\n```\n\n### PostgreSQL\n\n```javascript\nconst { Pool } = require('pg');\nconst { Postgres } = require('minusql');\n\nconst pool = new Pool({\n  host: 'localhost',\n  user: 'postgres',\n  password: 'password',\n  database: 'mydb'\n});\n\nconst db = new Postgres(pool);\n\n// Query example\nconst user = await db.users.selectOne({ id: 1 });\n```\n\nBy 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.\n\n## API Documentation\n\nTo 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).\n\nYou can also explicitly call `db.from('users')` to specify a table.\n\nTo perform raw queries, use `db.exec(query, params)`.\n\n### Query Building\n\nTo perform CRUD operations on a table, use one of the following methods:\n- `db.table.select(where?, options?)`\n- `db.table.insert(rows, options?)`\n- `db.table.update(update, where?)`\n- `db.table.delete(where?)`\n\nAll 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.\n\nThere are also two convenient aliases for common select types:\n- `selectAll(options?)` is equivalent to `select(null, options)`\n- `selectOne(where?, options?)` is equivalent to `select(where, options).one()`\n\n#### SELECT Queries\n\n```javascript\n// Select everything\nconst results = await db.users.selectAll();\n\n// Simple query\nconst results = await db.users.select({ id: 1, name: 'John' });\n\n// Complex query with operators and parameter substitution\nconst ageMin = 18;\nconst permissionList = ['edit', 'delete'];\nconst users = await db.users.select(['and',\n  ['\u003e', Symbol('age'), {$: ageMin}],\n  ['=', Symbol('status'), 'active'],\n  ['or',\n    { role: 'admin' },\n    { permissions: ['in', {$: permissionList}] },\n  ],\n]);\n\n// With specific fields\nconst results = await db.users\n  .select(null, { fields: ['id', 'name'] });\n\n// With ordering and limits\nconst results = await db.users\n  .select(null, { \n    order: 'createdAt DESC', // or [[Symbol('createdAt'), 'DESC']]\n    limit: 10,\n    offset: 0\n  });\n```\n\nThe `select(where?, options?)` method takes two arguments: the first defines the query condition and the second sets additional options.\n\nYou 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.\n\nStructured condition is defined as a recursive expression:\n- If it's an **array**, its first element should be a SQL function name (like `COALESCE`) or an operator (like `\u003c` or `AND`). All following elements are its arguments (which are parsed as nested expressions).\n- If it's an **object with a $ field**, it contains a parameter which will be passed along with the query. This prevents SQL injections and improves performance, and is recommended for all user-supplied data. You can also use the `type` field to add an explicit type cast.\n- If it's an **object**, its key-value pairs are converted to expressions in the form of `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: ['\u003e', y] }` is the same as `['\u003e', Symbol('x'), y]`. Keys are escaped as identifiers, and values may contain nested expressions.\n- If it's a **Symbol** instance, it refers to a database column and is therefore escaped as an identifier.\n- Otherwise (if it's a primitive value, like a number or string), it's escaped and inserted into the query.\n\nThere are a few special behaviors for specific SQL operators:\n- `['in', Symbol('x'), [1, 2, 3]]` is converted to `\"x\" IN (1, 2, 3)`\n- `['not in', Symbol('x'), [1, 2, 3]]` is converted to `\"x\" NOT IN (1, 2, 3)`\n- `['between', Symbol('x'), 1, 2]` is converted to `\"x\" BETWEEN 1 AND 2`\n- `['not between', Symbol('x'), 1, 2]` is converted to `\"x\" NOT BETWEEN 1 AND 2`\n- `['type', Symbol('x'), 'json']` is converted to `json \"x\"` (the type is NOT escaped)\n- `['cast', Symbol('x'), 'json']` is converted to `\"x\"::json` (the type is NOT escaped)\n- `['extract', Symbol('x'), 'month']` is converted to `EXTRACT(month FROM x)` (note the order change; also the last argument is NOT escaped)\n- `['case', [cond1, then1], [cond2, then2], [default]]` is converted to `CASE WHEN cond1 THEN then1 WHEN cond2 THEN then2 ELSE default END`\n\nSupported options are (all optional):\n- `fields`: a raw string or an array of columns to select\n- `group`: a raw string or an array of expressions to use in the `GROUP BY` clause\n- `having`: a raw string or structured condition to use in the `HAVING` clause\n- `order`: a raw string or an array of pairs [expression, 'ASC' | 'DESC'] to use in the `ORDER BY` clause\n- `limit`: a number to use in the `LIMIT` clause\n- `offset`: a number to use in the `OFFSET` clause\n\nBy default, the resulting query returns an array of rows. To re-map it to more suitable data structures, see \"Result Mapping\" below.\n\n#### INSERT Queries\n\n```javascript\n// Single insert\nawait db.users.insert({\n  name: 'John', // Values will be parametrized by default (you can change this behavior by supplying \"tranform\" option)\n  age: 30\n});\n\n// Returning inserted ID\nconst result = await db.users.insert({\n  name: 'John',\n  age:  30,\n}, { returnId: true }); // (PostgreSQL only, MySQL will always add insertId to output)\n// result will contain the ID of the inserted row\n\n// Batch insert with manually parametrized values\nawait db.users.insert(usersToInsert.map(user =\u003e ({\n  name: {$: user.name},\n  age:  {$: user.age},\n})), { transform: false });\n\n// Upsert (handling conflicts)\nawait db.users.insert({\n  id:         888352,\n  name:       'John',\n  age:        30,\n  revision:   0,\n  joinedAt:   Date.now() / 1000,\n}, {\n  transform: {\n    joinedAt: 'timestamp', // Unixtime can be easily converted to timestamps\n  },\n  unique: ['id'], // Needed only for PostgreSQL (upserts on MySQL will work without it)\n  conflict: {\n    name:     /update/, // Update name on conflict\n    age:      /max/,    // Update to largest of old and new value\n    revision: ['+', Symbol('revision'), 1], // Expressions are supported here as well\n    joinedAt: /fill/,   // Update only if was null\n  },\n});\n\n```\n\n`insert` accepts two parameters: rows to insert (or a single row) and options.\n\nSupported options:\n- `fields`: an array of columns; if omitted, the first element's keys will be used\n- `unique` (PostgreSQL only): for upserts, you need to specify a list of unique fields\n- `conflict`: for upserts, describes the conflict resolution strategy (see below)\n- `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.\n\nConflict 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.\n\nFor convenience, you can pass the following predefined RegExp patterns as aliases for common strategies:\n- `/update/`: update to the new value on conflict\n- `/fill/`: only update if the old value is `NULL`\n- `/inc/`: increment old value by 1\n- `/dec/`: decrement old value by 1\n- `/add/`: add new value to the old one\n- `/sub/`: subtract new value from the old one\n- `/max/`: select the maximum out of old value and the new one\n- `/min/`: select the minimum out of old value and the new one\n\n#### UPDATE Queries\n\n```javascript\n// Basic update with a simple where condition\nawait db.users.update(\n  { age: 31 },\n  { name: 'John' }\n);\n\n// Update with a complex where condition\nawait db.users.update(\n  { status: 'inactive', lastSeen: new Date() },\n  ['and', \n    ['\u003c', Symbol('lastLogin'), {$: oneMonthAgo}],\n    ['=', Symbol('status'), 'active']\n  ]\n);\n\n// Update with expressions\nawait db.users.update(\n  { \n    loginCount: ['+', Symbol('loginCount'), 1],\n    status: 'active'\n  },\n  { id: 42 }\n);\n\n// Update all rows (be careful!)\nawait db.users.update(\n  { isArchived: true },\n  null\n);\n```\n\nThe `update(update, where?)` method takes two parameters:\n1. `update`: An object where keys are column names and values are either direct values or expressions\n2. `where`: A condition to determine which rows to update (same format as in `select`); if `null`, all rows will be updated\n\nThe update values can be:\n- Simple values (strings, numbers, booleans, etc.)\n- Expressions using the same syntax as in `where` conditions\n- SQL functions and operators in array format\n\n#### DELETE Queries\n\n```javascript\n// Delete with a simple condition\nawait db.users.delete({ name: 'John' });\n\n// Delete with a complex condition\nawait db.users.delete(['and',\n  ['\u003c', Symbol('lastLogin'), {$: sixMonthsAgo}],\n  ['=', Symbol('status'), 'inactive']\n]);\n\n// Delete all rows (use with caution!)\nawait db.users.delete(null);\n```\n\nThe `delete(where?)` method takes a single parameter:\n- `where`: A condition to determine which rows to delete (same format as in `select`); if `null` or omitted, all rows will be deleted\n\n### Result Mapping\n\nMinuSQL provides various methods for transforming query results into different data structures:\n\n```javascript\n// Get an array of results (default behavior)\nconst users = await db.users.select().toArray();\n\n// Get an array of single column's values\nconst names = await db.users.select().toArray('name');\n\n\n// Get just the first result or null if none found\nconst user = await db.users.select({ id: 1 }).one();\n// Equivalent to using selectOne()\nconst user = await db.users.selectOne({ id: 1 });\n\n// Get first result with transformation\nconst userName = await db.users.select({ id: 1 }).one('name');\n\n// Map results to an object using a key\nconst usersById = await db.users.select().toObject('id');\n// Result: { '1': {id: 1, name: 'John'}, '2': {id: 2, name: 'Jane'}, ... }\n\n// Map to object with specific value\nconst nameById = await db.users.select().toObject('id', 'name');\n// Result: { '1': 'John', '2': 'Jane', ... }\n\n// Map to object using custom key function\nconst usersByFullName = await db.users.select().toObject(\n  user =\u003e `${user.firstName} ${user.lastName}`\n);\n\n// Group into arrays by a key\nconst usersByRole = await db.users.select().toObjectArray('role', 'name');\n// Result: { 'admin': ['John', 'Jane'], 'user': ['Bob', 'Alice'], ... }\n\n// Map instance\nconst userMap = await db.users.select().toMap('id');\n// Result: Map { 1 =\u003e {id: 1, name: 'John'}, 2 =\u003e {id: 2, name: 'Jane'}, ... }\n\n// Map with specific value\nconst nameMap = await db.users.select().toMap('id', 'name');\n// Result: Map { 1 =\u003e 'John', 2 =\u003e 'Jane', ... }\n\n// Group into Map of arrays\nconst roleMap = await db.users.select().toMapArray('role', 'name');\n// Result: Map { 'admin' =\u003e ['John', 'Jane'], 'user' =\u003e ['Bob', 'Alice'], ... }\n\n// Extract values to a Set\nconst allRoles = await db.users.select().toSet('role');\n// Result: Set { 'admin', 'user', 'guest', ... }\n\n// Process each row with a function\nawait db.users.select().forEach(user =\u003e {\n  console.log(`User ${user.name} is ${user.age} years old`);\n});\n\n// Map to class instances\nclass User {\n  static fromRow(row) {\n    const user = new User();\n    user.id = row.id;\n    user.name = row.name;\n    return user;\n  }\n  \n  greet() {\n    return `Hello, ${this.name}!`;\n  }\n}\n\nconst users = await db.users.select().toArray(User);\nconsole.log(users[0].greet()); // \"Hello, John!\"\n```\n\nThe Query object provides these mapping methods:\n- `one(value?)`: Returns the first result or null if none found, optionally transformed\n- `toArray(value?)`: Returns results as an array, optionally transforming each row using the field parameter\n- `toObject(key, value?)`: Maps results to an object using the specified key, optionally transforming values\n- `toObjectArray(key, value?)`: Groups results into arrays by key\n- `toMap(key, value?)`: Maps results to a Map\n- `toMapArray(key, value?)`: Groups results into arrays in a Map by key\n- `toSet(value)`: Extracts unique values from the specified field into a Set\n- `forEach(fn)`: Executes a function for each result row\n\nThe transformation parameter (`value`) can be:\n- A **string**: extracts that property from each row\n- A **function**: called with `(row, index, allRows)` for custom transformations\n- A **class**: tries to instantiate objects from rows, using `fromRow()` static method if available\n- An **object**: for extracting/transforming multiple properties (recursively)\n- An **array**: for extracting multiple properties as an array (recursively)\n\n`key` parameter supports a subset of those types:\n- A **string**: property to be used as key\n- A **function**: called with `(row, index, allRows)` and result is used as key\n- An **array**: elements will be joined with `_` and used as key\n\nNote that by default, all result keys are automatically converted from snake_case to camelCase unless `convertCase: false` was set.\n\n### Transactions\n\nMinuSQL provides a simple way to work with transactions:\n\n```javascript\n// Basic transaction\nawait db.begin(async (tx) =\u003e {\n  // The tx object is a transaction-specific database instance\n  await tx.users.insert({ name: 'John' });\n  await tx.profiles.insert({ userId: 1, bio: 'Hello' });\n  \n  // If any query fails, the transaction will be automatically rolled back\n  // If all succeed, it will be committed automatically\n});\n```\n\n### JOIN Operations\n\n```javascript\nconst results = await db.join([\n  { table: 'users', as: 'u' },\n  { table: 'profiles', as: 'p', on: { 'u.id': Symbol('p.userId') } },\n]).selectAll();\n\n// Same as\nconst results = await db.users\n  .join('profiles p', { 'users.id': Symbol('p.userId') })\n  .selectAll();\n```\n\nTo join multiple tables, call `db.join` with the array of objects containing following fields:\n- `table`: name of the table or another subquery\n- `as` (Optional): alias to be used in `AS` clause\n- `join` (Optional): join type to be used in `JOIN` clause (if omitted, defaults to `LEFT`)\n- `on`: any expression in the structured format to be used in `ON` clause\n\nInstead of object with join description, you can also use raw string, but it's discouraged.\n\nAlternatively, 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.\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nMIT","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenull%2Fminusql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdenull%2Fminusql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdenull%2Fminusql/lists"}