Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions skills/nearform-sql/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
name: nearform-sql
description: "Use this skill when writing, reviewing, or refactoring database queries with @nearform/sql — Nearform's tagged-template library that produces SQL-injection-safe parameterized queries for pg, mysql, and mysql2. Covers installation, the SQL tag (.text/.sql/.values/.debug), helpers (glue, map, unsafe, quoteIdent), Fastify integration, dynamic/bulk queries, and security best practices. Trigger terms: SQL, query, parameterized query, SQL injection, pg, postgres, mysql, @nearform/sql, glue, quoteIdent."
metadata:
author: "NearForm"
version: 1.0.0
tags:
- category/code-generation
- tool/postgres
- tool/mysql
- domain/engineering
- domain/security
---

# @nearform/sql

`@nearform/sql` is a tagged-template library that turns ES6 template literals into
**parameterized** queries, immune to SQL injection by construction. It works with
`pg` (PostgreSQL), `mysql`, and `mysql2`. You build a query with the `` SQL`...` ``
tag and pass the resulting statement object straight to your driver.

> **Source:** this skill ships in this repo (under `skills/nearform-sql/`) and is a
> port of the same skill maintained in the
> [`nearform/skills`](https://github.com/nearform/skills) marketplace
> (`plugins/nearform-sql/`). The marketplace version may receive updates first;
> this repo tracks them.

## When to use this skill

Reach for this skill whenever you:

- Write or review code that builds SQL queries in Node.js.
- See a raw SQL string assembled with `+`, `${...}` inside a plain template string, or
`string.replace` — that is a SQL-injection red flag; rewrite it with the `` SQL`` `` tag.
- Need dynamic queries: conditional `WHERE`, dynamic `SET`, bulk `INSERT`, or `IN (...)` lists.
- Interpolate identifiers (table/column names) into a query.

> For the full API reference see [`references/api-cheatsheet.md`](references/api-cheatsheet.md).
> For framework integration and dynamic-query recipes see [`references/patterns.md`](references/patterns.md).

## Install & import

```sh
npm install @nearform/sql
```

```js
import SQL from '@nearform/sql' // ESM — prefer this for new code
```

```js
const SQL = require('@nearform/sql') // CommonJS — also supported
```

The package itself is published as CommonJS (no `exports` map), but it interops
cleanly with ESM through a **default import** — use `import SQL from '@nearform/sql'`,
then call `SQL.glue`, `SQL.map`, etc. off that default.

Statements are fully typed — the package ships `SQL.d.ts`, and helpers like
`SQL.map<T>(array, mapFunc?)` are generic, so TypeScript infers the element type.

## The golden rule

**Every runtime value goes through `${...}` inside the `` SQL`` `` tag.** The tag captures
each interpolated value as a bound parameter — it never injects it into the query text.

```js
const username = "Robert'); DROP TABLE students;--"

const sql = SQL`SELECT * FROM users WHERE username = ${username}`
// sql.text -> "SELECT * FROM users WHERE username = $1" (pg)
// sql.sql -> "SELECT * FROM users WHERE username = ?" (mysql)
// sql.values -> ["Robert'); DROP TABLE students;--"] (bound, harmless)
```

- **Never** build the query with string concatenation or an untagged template literal.
- `SQL.unsafe(value)` and `append(..., { unsafe: true })` interpolate literally and
**bypass this protection** — use them only for values you fully control, never for
user input. See the cheatsheet.

## Core usage

The statement object is driver-agnostic. Choose the property your client expects, or
pass the whole object — `pg`, `mysql`, and `mysql2` all read what they need from it.

```js
const user = { username: 'alice', email: 'alice@example.com' }

const sql = SQL`
INSERT INTO users (username, email)
VALUES (${user.username}, ${user.email})
`

// PostgreSQL (pg) — uses sql.text ($1, $2) + sql.values
await pgClient.query(sql)

// MySQL (mysql / mysql2) — uses sql.sql (?, ?) + sql.values
mysqlConnection.query(sql)
```

| Property | Use it for | Placeholder style |
|----------|------------|-------------------|
| `sql.text` | PostgreSQL (`pg`) | `$1, $2, …` |
| `sql.sql` | MySQL (`mysql`, `mysql2`) | `?, ?, …` |
| `sql.values` | The bound values array (both) | — |
| `sql.debug` | **Logging/debugging only** — never execute it | inlined (unsafe) |

## Composing queries

Nest `` SQL`` `` tags directly — the preferred way to build queries from fragments.
Parameterization is preserved across the nesting.

```js
const where = SQL`WHERE active = ${true}`
const sql = SQL`SELECT id, email FROM users ${where} ORDER BY created_at DESC`
```

For repeated fragments, use the helpers:

- **`SQL.glue(pieces, separator)`** — join an array of statements (dynamic `SET`/`WHERE`, bulk `VALUES`).
- **`SQL.map(array, mapFunc?)`** — expand an array into bound values (`IN (...)` lists, bulk insert).

```js
const ids = [1, 2, 3]
const sql = SQL`SELECT * FROM users WHERE id IN (${SQL.map(ids)})`
```

See [`references/patterns.md`](references/patterns.md) for complete dynamic-query, Fastify,
and migration recipes.

> `append()` still works but is **deprecated** — prefer nesting tags.

## Best practices checklist

- ✅ Wrap **every** query in the `` SQL`` `` tag; pass interpolated values via `${...}`.
- ✅ Never concatenate strings or use untagged template literals for SQL.
- ⚠️ **No `undefined`** — the tag throws on `undefined`. Coerce nullable fields to `null`:
```js
SQL`INSERT INTO users (name, address) VALUES (${user.name}, ${user.address || null})`
```
- ✅ Use **`SQL.quoteIdent(name)`** for dynamic identifiers (table/column names) — never
interpolate them as values.
- ⚠️ Use **`SQL.unsafe(value)`** only for trusted, non-user values; it bypasses injection protection.
- ✅ Use `sql.debug` for logs only — **never** send it to the driver as the executed query.
- ✅ Enforce the tag in your codebase with [`eslint-plugin-sql`](https://www.npmjs.com/package/eslint-plugin-sql)
and the `thebearingedge.vscode-sql-lit` VS Code extension for syntax highlighting inside the tag.
158 changes: 158 additions & 0 deletions skills/nearform-sql/references/api-cheatsheet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# @nearform/sql — API cheatsheet

Reference for `@nearform/sql`. Import with `import SQL from '@nearform/sql'`
(ESM) or `const SQL = require('@nearform/sql')` (CommonJS).

## Statement object

Building a query with the `` SQL`` `` tag returns a **statement** with these getters:

| Property | Type | Description |
|----------|------|-------------|
| `.text` | `string` | PostgreSQL form, placeholders `$1, $2, …`. Pass to `pg`. |
| `.sql` | `string` | MySQL form, placeholders `?, ?, …`. Pass to `mysql` / `mysql2`. |
| `.values` | `any[]` | The escaped/bound values, in order. |
| `.debug` | `string` | A formatted but **unsafe** statement with values inlined. For debugging/logging only — never execute it. |

All three major drivers accept the statement object directly:

```js
pgClient.query(sql) // reads .text + .values
mysqlConnection.query(sql) // reads .sql + .values
mysql2Connection.query(sql) // reads .sql + .values
```

## The `SQL` tag

```js
SQL`SELECT * FROM users WHERE id = ${userId} AND active = ${true}`
```

Each `${value}` becomes a bound parameter. This is the only injection-safe way to put
runtime values into a query.

## `SQL.glue(pieces, separator)`

`glue(pieces: StatementLike[], separator: string): SqlStatement`

Joins an array of statements with a separator. Useful for dynamic `SET`, `WHERE`, and bulk `VALUES`.

```js
const updates = []
updates.push(SQL`name = ${username}`)
updates.push(SQL`email = ${email}`)

const sql = SQL`UPDATE users SET ${SQL.glue(updates, ' , ')} WHERE id = ${userId}`
```

Bulk insert:

```js
const users = [
{ id: 1, name: 'something' },
{ id: 2, name: 'something-else' }
]

const sql = SQL`INSERT INTO users (id, name) VALUES
${SQL.glue(
users.map(user => SQL`(${user.id},${user.name})`),
' , '
)}
`
```

## `SQL.map(array, mapFunc?)`

`map<T>(array: T[], mapFunc?: (item: T) => unknown): SqlStatement`

Expands an array into bound values. Ideal for `IN (...)` lists.

```js
const ids = [1, 2, 3]
const values = SQL.map(ids)
const sql = SQL`SELECT * FROM users WHERE id IN (${values})`
```

With a custom mapper:

```js
const objArray = [{ id: 1, name: 'name1' }, { id: 2, name: 'name2' }]
const values = SQL.map(objArray, (item) => item.id)
const sql = SQL`SELECT * FROM users WHERE id IN (${values})`
```

## `SQL.quoteIdent(value)`

`quoteIdent(value: string): SqlStatement`

Safely quotes an **identifier** (table/column/schema name). Mimics PostgreSQL's
`quote_ident` and MySQL's `quote_identifier`:

- PostgreSQL: wraps in double quotes `"…"` with escaping.
- MySQL: wraps in backticks `` `…` `` with escaping.

Use it whenever an identifier is dynamic — you cannot bind an identifier as a value.

```js
const table = 'users'

const sql = SQL`
UPDATE ${SQL.quoteIdent(table)}
SET username = ${username}
WHERE id = ${userId}
`
```

## `SQL.unsafe(value)` ⚠️

`unsafe<T>(value: T): SqlStatement`

Interpolates the value **literally**, as-is, into the query text — bypassing
parameterization. Returns a thin wrapper that the `` SQL`` `` tag recognizes and
inlines verbatim.

> ⚠️ **Security:** `unsafe` interprets interpolated values as literals. It can introduce
> SQL-injection vulnerabilities. Use it **only** for values you fully control (constants,
> enums you validated), **never** for user input.

```js
const username = 'john'
const userId = 1

const sql = SQL`
UPDATE users
SET username = '${SQL.unsafe(username)}'
WHERE id = ${userId}
`
```

## `append(statement, options?)` — DEPRECATED

`append(statement: StatementLike, options?: { unsafe?: boolean }): SqlStatement`

Appends to an existing statement. **Deprecated** — prefer nesting `` SQL`` `` tags instead:

```js
// Preferred
const from = SQL`FROM users`
const sql = SQL`SELECT * ${from}`

// Legacy (deprecated)
const sql = SQL`UPDATE users SET name = ${username}, email = ${email}`
sql.append(SQL`, login = ${dynamicName}`, { unsafe: true })
sql.append(SQL`WHERE id = ${userId}`)
```

## Gotcha: no `undefined`

The tag **throws on `undefined`** — `undefined` is a JavaScript concept, not a SQL one.
Coerce nullable fields to `null`:

```js
const user = { name: 'foo bar' } // no `address`

const sql = SQL`
INSERT INTO users (name, address)
VALUES (${user.name}, ${user.address || null})
`
```
Loading
Loading