Erster Commit
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Weslley Araújo, Andrey Sidorov, Douglas Wilson, and contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+516
@@ -0,0 +1,516 @@
|
||||
# SQL Escaper
|
||||
|
||||
[](https://www.npmjs.com/package/sql-escaper)
|
||||
[](https://www.npmjs.com/package/sql-escaper)
|
||||
[](https://app.codecov.io/gh/mysqljs/sql-escaper)<br />
|
||||
[](https://github.com/mysqljs/sql-escaper/actions/workflows/ci_node.yml?query=branch%3Amain)
|
||||
[](https://github.com/mysqljs/sql-escaper/actions/workflows/ci_bun.yml?query=branch%3Amain)
|
||||
[](https://github.com/mysqljs/sql-escaper/actions/workflows/ci_deno.yml?query=branch%3Amain)
|
||||
|
||||
## Motivation
|
||||
|
||||
**SQL Escaper** is a rework of [**sqlstring**](https://github.com/mysqljs/sqlstring) (created by [**Douglas Wilson**](https://github.com/dougwilson)), by using an **AST**-based approach to parse and format SQL queries while maintaining its same API.
|
||||
|
||||
### Rework includes:
|
||||
|
||||
- **TypeScript** by default.
|
||||
- Support for `Uint8Array`, `BigInt`, and `Temporal`.
|
||||
- Support for both **CJS** and **ESM** exports.
|
||||
- Up to [**~40% faster**](#performance) compared to **sqlstring**.
|
||||
- Distinguishes when a keyword is used as value.
|
||||
- Distinguishes when a column has a keyword name.
|
||||
- Distinguishes between multiple clauses/keywords in the same query.
|
||||
- Reasonable conservative support for **Node.js v12** _(**sqlstring** supports **Node.js v0.6**)_.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> **SQL Escaper** has the same API as the original [**sqlstring**](https://github.com/mysqljs/sqlstring), so it can be used as a drop-in replacement. If **SQL Escaper** breaks any **API** usage compared to **sqlstring**, please, report it as a bug. [Pull Requests are welcome](./CONTRIBUTING.md).
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 🔐 **SQL Escaper** is intended to fix a potential [**SQL Injection vulnerability**](https://flattsecurity.medium.com/finding-an-unseen-sql-injection-by-bypassing-escape-functions-in-mysqljs-mysql-90b27f6542b4) reported in 2022. By combining the original [**sqlstring**](https://github.com/mysqljs/sqlstring) with [**mysqljs/mysql**](https://github.com/mysqljs/mysql) or [**MySQL2**](https://github.com/sidorares/node-mysql2), objects passed as values could be expanded into **SQL** fragments, potentially allowing attackers to manipulate query structure. See [sidorares/node-mysql2#4051](https://github.com/sidorares/node-mysql2/issues/4051) for details.
|
||||
>
|
||||
> Regardless of the `stringifyObjects` value, objects used outside of `SET` or `ON DUPLICATE KEY UPDATE` contexts are always stringified as `'[object Object]'`. This is a security measure to prevent [SQL Injection](https://flattsecurity.medium.com/finding-an-unseen-sql-injection-by-bypassing-escape-functions-in-mysqljs-mysql-90b27f6542b4) and is not interpreted as a breaking change for **sqlstring** usage.
|
||||
|
||||
---
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Node.js
|
||||
npm i sql-escaper
|
||||
```
|
||||
|
||||
```bash
|
||||
# Bun
|
||||
bun add sql-escaper
|
||||
```
|
||||
|
||||
```bash
|
||||
# Deno
|
||||
deno add npm:sql-escaper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### [MySQL2](https://github.com/sidorares/node-mysql2)
|
||||
|
||||
For **MySQL2**, it already uses **SQL Escaper** as its default escaping library since version `3.17.0`, so you just need to update it to the latest version:
|
||||
|
||||
```bash
|
||||
npm i mysql2@latest
|
||||
```
|
||||
|
||||
### [mysqljs/mysql](https://github.com/mysqljs/mysql)
|
||||
|
||||
You can use an overrides in your _package.json_:
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"mysql": "^2.18.1"
|
||||
},
|
||||
"overrides": {
|
||||
"sqlstring": "npm:sql-escaper"
|
||||
}
|
||||
```
|
||||
|
||||
- Next, clean the `node_modules` and reinstall the dependencies (`npm i`).
|
||||
- Please, note the minimum supported version of **Node.js** is `12`.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
For _up-to-date_ documentation, always follow the [**README.md**](https://github.com/mysqljs/sql-escaper?tab=readme-ov-file#readme) in the **GitHub** repository.
|
||||
|
||||
### Quickstart
|
||||
|
||||
```js
|
||||
import { escape, escapeId, format, raw } from 'sql-escaper';
|
||||
|
||||
escape("Hello 'World'", true);
|
||||
// => "'Hello \\'World\\''"
|
||||
|
||||
escapeId('table.column');
|
||||
// => '`table`.`column`'
|
||||
|
||||
format('SELECT * FROM ?? WHERE id = ?', ['users', 42]);
|
||||
// => 'SELECT * FROM `users` WHERE id = 42'
|
||||
|
||||
format('INSERT INTO users SET ?', [{ name: 'foo', email: 'bar@test.com' }]);
|
||||
// => "INSERT INTO users SET `name` = 'foo', `email` = 'bar@test.com'"
|
||||
|
||||
escape(raw('NOW()'), true);
|
||||
// => 'NOW()'
|
||||
```
|
||||
|
||||
### Import
|
||||
|
||||
#### ES Modules
|
||||
|
||||
```js
|
||||
import { escape, escapeId, format, raw } from 'sql-escaper';
|
||||
```
|
||||
|
||||
#### CommonJS
|
||||
|
||||
```js
|
||||
const { escape, escapeId, format, raw } = require('sql-escaper');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### escape
|
||||
|
||||
Escapes a value for safe use in SQL queries.
|
||||
|
||||
```ts
|
||||
escape(value: SqlValue, stringifyObjects?: boolean, timezone?: Timezone): string
|
||||
```
|
||||
|
||||
```js
|
||||
escape(undefined, true); // 'NULL'
|
||||
escape(null, true); // 'NULL'
|
||||
escape(true, true); // 'true'
|
||||
escape(false, true); // 'false'
|
||||
escape(5, true); // '5'
|
||||
escape("Hello 'World", true); // "'Hello \\'World'"
|
||||
```
|
||||
|
||||
#### Dates
|
||||
|
||||
Dates are converted to `YYYY-MM-DD HH:mm:ss.sss` format:
|
||||
|
||||
```js
|
||||
escape(new Date(2012, 4, 7, 11, 42, 3, 2), true);
|
||||
// => "'2012-05-07 11:42:03.002'"
|
||||
```
|
||||
|
||||
Invalid dates return `NULL`:
|
||||
|
||||
```js
|
||||
escape(new Date(NaN), true); // 'NULL'
|
||||
```
|
||||
|
||||
You can specify a timezone:
|
||||
|
||||
```js
|
||||
const date = new Date(Date.UTC(2012, 4, 7, 11, 42, 3, 2));
|
||||
|
||||
escape(date, true, 'Z'); // "'2012-05-07 11:42:03.002'"
|
||||
escape(date, true, '+01'); // "'2012-05-07 12:42:03.002'"
|
||||
escape(date, true, '-05:00'); // "'2012-05-07 06:42:03.002'"
|
||||
```
|
||||
|
||||
#### Temporal
|
||||
|
||||
[Temporal](https://tc39.es/proposal-temporal/) values are supported too.
|
||||
`Temporal.Instant` and `Temporal.ZonedDateTime` are absolute points in time and
|
||||
honor the `timezone` argument exactly like `Date` (millisecond precision):
|
||||
|
||||
```js
|
||||
const instant = Temporal.Instant.from('2012-05-07T11:42:03.002Z');
|
||||
|
||||
escape(instant, true, 'Z'); // "'2012-05-07 11:42:03.002'"
|
||||
escape(instant, true, '+0200'); // "'2012-05-07 13:42:03.002'"
|
||||
```
|
||||
|
||||
`Temporal.PlainDateTime`, `Temporal.PlainDate` and `Temporal.PlainTime` are
|
||||
wall-clock values and are emitted verbatim as `DATETIME` / `DATE` / `TIME`
|
||||
literals, ignoring `timezone`:
|
||||
|
||||
```js
|
||||
escape(Temporal.PlainDate.from('2012-05-07')); // "'2012-05-07'"
|
||||
escape(Temporal.PlainTime.from('11:42:03')); // "'11:42:03'"
|
||||
```
|
||||
|
||||
#### Buffers
|
||||
|
||||
Buffers are converted to hex strings:
|
||||
|
||||
```js
|
||||
escape(Buffer.from([0, 1, 254, 255]), true);
|
||||
// => "X'0001feff'"
|
||||
```
|
||||
|
||||
#### Objects
|
||||
|
||||
When `stringifyObjects` is set to a non-nullish value **(recommended)**, objects are stringified instead of being expanded into key-value pairs:
|
||||
|
||||
```js
|
||||
escape({ a: 'b' }, true);
|
||||
// => "'[object Object]'"
|
||||
```
|
||||
|
||||
Objects with a `toSqlString` method will have that method called:
|
||||
|
||||
```js
|
||||
escape({ toSqlString: () => 'NOW()' }, true);
|
||||
// => 'NOW()'
|
||||
```
|
||||
|
||||
Plain objects are converted to `key = value` pairs **(discouraged)**:
|
||||
|
||||
```js
|
||||
escape({ a: 'b', c: 'd' });
|
||||
// => "`a` = 'b', `c` = 'd'"
|
||||
```
|
||||
|
||||
Function properties in objects are ignored **(discouraged)**:
|
||||
|
||||
```js
|
||||
escape({ a: 'b', c: () => {} });
|
||||
// => "`a` = 'b'"
|
||||
```
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> Without `stringifyObjects`, plain objects are converted to `key = value` pairs, so an object reaching `escape` where a value is expected reshapes the query, for example:
|
||||
>
|
||||
> ```js
|
||||
> const userInput = { id: true }; // e.g. JSON.parse('{"id":true}')
|
||||
>
|
||||
> /** Unsafe 🔓 (value position) */
|
||||
> 'DELETE FROM entries WHERE id = ' + escape(userInput);
|
||||
> // => "DELETE FROM entries WHERE id = `id` = true"
|
||||
> // `id` = `id` is always true, so every row is deleted ❗️
|
||||
>
|
||||
> /** Valid ✅ (SET assignment) */
|
||||
> 'UPDATE users SET ' + escape({ name: 'foo', role: 'admin' });
|
||||
> // => "UPDATE users SET `name` = 'foo', `role` = 'admin'"
|
||||
> ```
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Instead, `format` only expands an object where it is safe (e.g., a `SET` assignment) and stringifies it everywhere else, so the same input cannot reshape the query:
|
||||
>
|
||||
> ```js
|
||||
> const userInput = { id: true }; // e.g. JSON.parse('{"id":true}')
|
||||
>
|
||||
> /** Unsafe 🔐 (value position) */
|
||||
> format('DELETE FROM entries WHERE id = ?', [userInput]);
|
||||
> // => "DELETE FROM entries WHERE id = '[object Object]'"
|
||||
> // stringified to an inert value
|
||||
>
|
||||
> /** Valid ✅ (SET assignment) */
|
||||
> format('UPDATE users SET ?', [{ name: 'foo', role: 'admin' }]);
|
||||
> // => "UPDATE users SET `name` = 'foo', `role` = 'admin'"
|
||||
> // expanded on purpose
|
||||
> ```
|
||||
|
||||
#### Arrays
|
||||
|
||||
Arrays are turned into comma-separated lists:
|
||||
|
||||
```js
|
||||
escape([1, 2, 'c'], true);
|
||||
// => "1, 2, 'c'"
|
||||
```
|
||||
|
||||
Nested arrays are turned into grouped lists (useful for bulk inserts):
|
||||
|
||||
```js
|
||||
escape(
|
||||
[
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
],
|
||||
true
|
||||
);
|
||||
// => '(1, 2, 3), (4, 5, 6)'
|
||||
```
|
||||
|
||||
#### Sets
|
||||
|
||||
Sets are treated like arrays, turning into comma-separated lists with natural deduplication:
|
||||
|
||||
```js
|
||||
escape(new Set([1, 2, 3]), true);
|
||||
// => '1, 2, 3'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### escapeId
|
||||
|
||||
Escapes an identifier (database, table, or column name).
|
||||
|
||||
```ts
|
||||
escapeId(value: SqlValue, forbidQualified?: boolean): string
|
||||
```
|
||||
|
||||
```js
|
||||
escapeId('id');
|
||||
// => '`id`'
|
||||
|
||||
escapeId('table.column');
|
||||
// => '`table`.`column`'
|
||||
|
||||
escapeId('i`d');
|
||||
// => '`i``d`'
|
||||
```
|
||||
|
||||
Qualified identifiers (with `.`) can be forbidden:
|
||||
|
||||
```js
|
||||
escapeId('id1.id2', true);
|
||||
// => '`id1.id2`'
|
||||
```
|
||||
|
||||
Arrays are turned into comma-separated identifier lists:
|
||||
|
||||
```js
|
||||
escapeId(['a', 'b', 't.c']);
|
||||
// => '`a`, `b`, `t`.`c`'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### format
|
||||
|
||||
Formats a SQL query by replacing `?` placeholders with escaped values and `??` with escaped identifiers.
|
||||
|
||||
```ts
|
||||
format(sql: string, values?: SqlValue | SqlValue[], stringifyObjects?: boolean, timezone?: Timezone): string
|
||||
```
|
||||
|
||||
```js
|
||||
format('SELECT * FROM ?? WHERE id = ?', ['users', 42]);
|
||||
// => 'SELECT * FROM `users` WHERE id = 42'
|
||||
|
||||
format('? and ?', ['a', 'b']);
|
||||
// => "'a' and 'b'"
|
||||
```
|
||||
|
||||
Triple (or more) question marks are ignored:
|
||||
|
||||
```js
|
||||
format('? or ??? and ?', ['foo', 'bar', 'fizz', 'buzz']);
|
||||
// => "'foo' or ??? and 'bar'"
|
||||
```
|
||||
|
||||
If no values are provided, the SQL is returned unchanged:
|
||||
|
||||
```js
|
||||
format('SELECT ??');
|
||||
// => 'SELECT ??'
|
||||
```
|
||||
|
||||
#### Objects in SET clauses
|
||||
|
||||
When `stringifyObjects` is falsy, objects used in `SET` or `ON DUPLICATE KEY UPDATE` contexts are automatically expanded into `key = value` pairs:
|
||||
|
||||
```js
|
||||
format('UPDATE users SET ?', [{ name: 'foo', email: 'bar@test.com' }]);
|
||||
// => "UPDATE users SET `name` = 'foo', `email` = 'bar@test.com'"
|
||||
|
||||
format(
|
||||
'INSERT INTO users (name, email) VALUES (?, ?) ON DUPLICATE KEY UPDATE ?',
|
||||
['foo', 'bar@test.com', { name: 'foo', email: 'bar@test.com' }]
|
||||
);
|
||||
// => "INSERT INTO users (name, email) VALUES ('foo', 'bar@test.com') ON DUPLICATE KEY UPDATE `name` = 'foo', `email` = 'bar@test.com'"
|
||||
```
|
||||
|
||||
When `stringifyObjects` is truthy, objects are always stringified:
|
||||
|
||||
```js
|
||||
format('UPDATE users SET ?', [{ name: 'foo' }], true);
|
||||
// => "UPDATE users SET '[object Object]'"
|
||||
```
|
||||
|
||||
#### Maps in SET clauses
|
||||
|
||||
Maps are treated like plain objects, preserving insertion order. In `SET` or `ON DUPLICATE KEY UPDATE` contexts, they are expanded into `key = value` pairs:
|
||||
|
||||
```js
|
||||
format('UPDATE users SET ?', [
|
||||
new Map([
|
||||
['name', 'foo'],
|
||||
['email', 'bar@test.com'],
|
||||
]),
|
||||
]);
|
||||
// => "UPDATE users SET `name` = 'foo', `email` = 'bar@test.com'"
|
||||
```
|
||||
|
||||
Outside of `SET` or `ON DUPLICATE KEY UPDATE`, a `Map` is stringified as `'[object Map]'`, the same security measure applied to objects:
|
||||
|
||||
```js
|
||||
format('SELECT * FROM users WHERE data = ?', [new Map([['id', 1]])]);
|
||||
// => "SELECT * FROM users WHERE data = '[object Map]'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### raw
|
||||
|
||||
Creates a raw SQL value that will not be escaped.
|
||||
|
||||
```ts
|
||||
raw(sql: string): Raw
|
||||
```
|
||||
|
||||
```js
|
||||
escape(raw('NOW()'), true);
|
||||
// => 'NOW()'
|
||||
```
|
||||
|
||||
Inside an expanded object, raw values are preserved **(discouraged)**:
|
||||
|
||||
```js
|
||||
escape({ id: raw('LAST_INSERT_ID()') });
|
||||
// => '`id` = LAST_INSERT_ID()'
|
||||
```
|
||||
|
||||
Only accepts strings:
|
||||
|
||||
```js
|
||||
raw(42); // throws TypeError
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TypeScript
|
||||
|
||||
You can import the available types:
|
||||
|
||||
```ts
|
||||
import type { Raw, SqlValue, Timezone } from 'sql-escaper';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Each benchmark formats `10,000` queries using `format` with `100` values, comparing **SQL Escaper** against the original [**sqlstring**](https://github.com/mysqljs/sqlstring) through [**hyperfine**](https://github.com/sharkdp/hyperfine):
|
||||
|
||||
| # | Benchmark | sqlstring | SQL Escaper | Difference |
|
||||
| --: | ---------------------------------------- | --------: | ----------: | ---------------: |
|
||||
| 1 | Select 100 values | 249.0 ms | 177.8 ms | **1.40x faster** |
|
||||
| 2 | Insert 100 values | 247.7 ms | 185.3 ms | **1.34x faster** |
|
||||
| 3 | Insert 100 strings requiring escape | 436.9 ms | 257.5 ms | **1.70x faster** |
|
||||
| 4 | Insert 100 dates | 611.9 ms | 415.1 ms | **1.47x faster** |
|
||||
| 5 | SET with 100 values | 258.8 ms | 207.4 ms | **1.25x faster** |
|
||||
| 6 | SET with 100 objects | 344.8 ms | 241.0 ms | **1.43x faster** |
|
||||
| 7 | ON DUPLICATE KEY UPDATE with 100 values | 462.0 ms | 362.7 ms | **1.27x faster** |
|
||||
| 8 | ON DUPLICATE KEY UPDATE with 100 objects | 559.4 ms | 437.8 ms | **1.28x faster** |
|
||||
| 9 | Multi-statement SET with 100 objects | 348.7 ms | 243.4 ms | **1.43x faster** |
|
||||
|
||||
- See detailed results and how the benchmarks are run in the [**benchmark**](https://github.com/mysqljs/sql-escaper/tree/main/benchmark) directory.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Benchmarks ran on [**GitHub Actions**](https://github.com/mysqljs/sql-escaper/blob/main/.github/workflows/ci_benchmark.yml) (`ubuntu-latest`) using **Node.js LTS**.
|
||||
> Results may vary depending on runner hardware and runtime version.
|
||||
|
||||
---
|
||||
|
||||
## Differences from sqlstring
|
||||
|
||||
- Requires **Node.js 12+** (the original [**sqlstring**](https://github.com/mysqljs/sqlstring) supports **Node.js** 0.6+)
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> The Node.js 12+ requirement is what allows **SQL Escaper** to leverage modern engine optimizations and achieve the [performance gains](#performance) over the original.
|
||||
|
||||
---
|
||||
|
||||
## Caution
|
||||
|
||||
> Based on the original [**sqlstring** documentation](https://github.com/mysqljs/sqlstring#readme).
|
||||
|
||||
- The escaping methods in this library only work when the [`NO_BACKSLASH_ESCAPES`](https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_backslash_escapes) SQL mode is disabled (which is the default state for MySQL servers).
|
||||
- This library performs **client-side escaping** to generate SQL strings. The syntax for `format` may look similar to a prepared statement, but it is not — the escaping rules from this module are used to produce the resulting SQL string.
|
||||
- When using `format`, **all** `?` placeholders are replaced, including those contained in comments and strings.
|
||||
- When structured user input is provided as the value to escape, care should be taken to validate the shape of the input, as the resulting escaped string may contain more than a single value.
|
||||
- `NaN` and `Infinity` are left as-is. MySQL does not support these values, and trying to insert them will trigger MySQL errors.
|
||||
- The string provided to `raw()` will **skip all escaping**, so be careful when passing in unvalidated input.
|
||||
|
||||
---
|
||||
|
||||
## Security Policy
|
||||
|
||||
[](https://github.com/mysqljs/sql-escaper/actions/workflows/ci_codeql.yml?query=branch%3Amain)
|
||||
|
||||
Please check the [**SECURITY.md**](https://github.com/mysqljs/sql-escaper/blob/main/SECURITY.md).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
See the [**Contributing Guide**](https://github.com/mysqljs/sql-escaper/blob/main/CONTRIBUTING.md) and please follow our [**Code of Conduct**](https://github.com/mysqljs/sql-escaper/blob/main/CODE_OF_CONDUCT.md) 🚀
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [](https://github.com/mysqljs/sql-escaper/graphs/contributors)
|
||||
- **SQL Escaper** is adapted from [**sqlstring**](https://github.com/mysqljs/sqlstring) ([**MIT**](https://github.com/mysqljs/sqlstring/blob/master/LICENSE)), modernizing it with high performance, TypeScript support and multi-runtime compatibility.
|
||||
- Special thanks to [**Douglas Wilson**](https://github.com/dougwilson) for the original **sqlstring** project and its [**contributors**](https://github.com/mysqljs/sqlstring/graphs/contributors).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
**SQL Escaper** is under the [**MIT License**](https://github.com/mysqljs/sql-escaper/blob/main/LICENSE).
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
|
||||
* MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
|
||||
*/
|
||||
import type { Raw, SqlValue, TemporalValue, Timezone } from './types.js';
|
||||
import { Buffer } from 'node:buffer';
|
||||
export type { Raw, SqlValue, TemporalValue, Timezone } from './types.js';
|
||||
export declare const dateToString: (date: Date, timezone: Timezone) => string;
|
||||
export declare const temporalToString: (value: TemporalValue, timezone?: Timezone) => string;
|
||||
export declare const escapeId: (value: SqlValue, forbidQualified?: boolean) => string;
|
||||
export declare const objectToValues: (object: Record<string, SqlValue> | Map<string, SqlValue>, timezone?: Timezone) => string;
|
||||
export declare const bufferToString: (buffer: Buffer) => string;
|
||||
export declare const arrayToList: (array: SqlValue[], timezone?: Timezone) => string;
|
||||
export declare const escape: (value: SqlValue, stringifyObjects?: boolean, timezone?: Timezone) => string;
|
||||
export declare const format: (sql: string, values?: SqlValue | SqlValue[], stringifyObjects?: boolean, timezone?: Timezone) => string;
|
||||
export declare const raw: (sql: string) => Raw;
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
|
||||
* MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.raw = exports.format = exports.escape = exports.arrayToList = exports.bufferToString = exports.objectToValues = exports.escapeId = exports.temporalToString = exports.dateToString = void 0;
|
||||
const node_buffer_1 = require("node:buffer");
|
||||
const CONTEXT_TRIGGER = new Uint8Array(128);
|
||||
const SET_CLAUSE_TERMINATORS_BY_FIRST = {};
|
||||
const SET_CLAUSE_TERMINATORS = [
|
||||
'where',
|
||||
'order',
|
||||
'group',
|
||||
'having',
|
||||
'limit',
|
||||
'union',
|
||||
'returning',
|
||||
'into',
|
||||
'for',
|
||||
'lock',
|
||||
'offset',
|
||||
'window',
|
||||
'procedure',
|
||||
'on',
|
||||
];
|
||||
const regex = {
|
||||
backtick: /`/g,
|
||||
dot: /\./g,
|
||||
timezone: /([+\-\s])(\d\d):?(\d\d)?/,
|
||||
escapeChars: /[\0\b\t\n\r\x1a"'\\]/g,
|
||||
};
|
||||
const charCode = {
|
||||
singleQuote: 39,
|
||||
backtick: 96,
|
||||
backslash: 92,
|
||||
dash: 45,
|
||||
slash: 47,
|
||||
asterisk: 42,
|
||||
exclamation: 33,
|
||||
plus: 43,
|
||||
questionMark: 63,
|
||||
comma: 44,
|
||||
openParen: 40,
|
||||
closeParen: 41,
|
||||
semicolon: 59,
|
||||
newline: 10,
|
||||
space: 32,
|
||||
tab: 9,
|
||||
carriageReturn: 13,
|
||||
};
|
||||
// Chars that open a string, identifier, or comment
|
||||
CONTEXT_TRIGGER[charCode.singleQuote] = 1;
|
||||
CONTEXT_TRIGGER[charCode.backtick] = 1;
|
||||
CONTEXT_TRIGGER[charCode.dash] = 1;
|
||||
CONTEXT_TRIGGER[charCode.slash] = 1;
|
||||
// Bucket terminators by their first character
|
||||
for (const word of SET_CLAUSE_TERMINATORS) {
|
||||
const first = word.charCodeAt(0);
|
||||
const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[first];
|
||||
if (bucket)
|
||||
bucket.push(word);
|
||||
else
|
||||
SET_CLAUSE_TERMINATORS_BY_FIRST[first] = [word];
|
||||
}
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const isRecord = (value) => typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!Array.isArray(value) &&
|
||||
!(value instanceof Set) &&
|
||||
!(value instanceof Map);
|
||||
const isWordChar = (code) => (code >= 65 && code <= 90) ||
|
||||
(code >= 97 && code <= 122) ||
|
||||
(code >= 48 && code <= 57) ||
|
||||
code === 95;
|
||||
const isWhitespace = (code) => code === charCode.space ||
|
||||
code === charCode.tab ||
|
||||
code === charCode.newline ||
|
||||
code === charCode.carriageReturn;
|
||||
const toLower = (code) => code | 32;
|
||||
const matchesWord = (sql, position, word, length) => {
|
||||
const wordLength = word.length;
|
||||
for (let offset = 0; offset < wordLength; offset++)
|
||||
if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
|
||||
return false;
|
||||
return ((position === 0 || !isWordChar(sql.charCodeAt(position - 1))) &&
|
||||
(position + wordLength >= length ||
|
||||
!isWordChar(sql.charCodeAt(position + wordLength))));
|
||||
};
|
||||
const skipSqlContext = (sql, position) => {
|
||||
const currentChar = sql.charCodeAt(position);
|
||||
const nextChar = sql.charCodeAt(position + 1);
|
||||
if (currentChar === charCode.singleQuote) {
|
||||
for (let cursor = position + 1; cursor < sql.length; cursor++) {
|
||||
if (sql.charCodeAt(cursor) === charCode.backslash)
|
||||
cursor++;
|
||||
else if (sql.charCodeAt(cursor) === charCode.singleQuote)
|
||||
return cursor + 1;
|
||||
}
|
||||
return sql.length;
|
||||
}
|
||||
if (currentChar === charCode.backtick) {
|
||||
const length = sql.length;
|
||||
for (let cursor = position + 1; cursor < length; cursor++) {
|
||||
if (sql.charCodeAt(cursor) !== charCode.backtick)
|
||||
continue;
|
||||
if (sql.charCodeAt(cursor + 1) === charCode.backtick) {
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
return cursor + 1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
if (currentChar === charCode.dash && nextChar === charCode.dash) {
|
||||
const afterDash = sql.charCodeAt(position + 2);
|
||||
if (Number.isNaN(afterDash) || afterDash <= charCode.space) {
|
||||
const lineBreak = sql.indexOf('\n', position + 2);
|
||||
return lineBreak === -1 ? sql.length : lineBreak + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
|
||||
const markerChar = sql.charCodeAt(position + 2);
|
||||
if (markerChar === charCode.exclamation || markerChar === charCode.plus)
|
||||
return -1;
|
||||
const commentEnd = sql.indexOf('*/', position + 2);
|
||||
return commentEnd === -1 ? sql.length : commentEnd + 2;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const findNextPlaceholder = (sql, start) => {
|
||||
const sqlLength = sql.length;
|
||||
for (let position = start; position < sqlLength; position++) {
|
||||
const code = sql.charCodeAt(position);
|
||||
if (code === charCode.questionMark)
|
||||
return position;
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, position);
|
||||
if (contextEnd !== -1)
|
||||
position = contextEnd - 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const isInSetAssignmentList = (sql, setEnd, placeholderPosition) => {
|
||||
const length = sql.length;
|
||||
let depth = 0;
|
||||
let sawContent = false;
|
||||
let lastWasComma = false;
|
||||
for (let i = setEnd; i < placeholderPosition;) {
|
||||
const code = sql.charCodeAt(i);
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, i);
|
||||
if (contextEnd !== -1) {
|
||||
i = contextEnd;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isWhitespace(code)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (code === charCode.openParen) {
|
||||
depth++;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (code === charCode.closeParen) {
|
||||
if (--depth < 0)
|
||||
return false;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (isWordChar(code)) {
|
||||
if (depth === 0 && !(code >= 48 && code <= 57)) {
|
||||
const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[code | 32];
|
||||
if (bucket)
|
||||
for (let t = 0; t < bucket.length; t++)
|
||||
if (matchesWord(sql, i, bucket[t], length))
|
||||
return false;
|
||||
}
|
||||
do {
|
||||
i++;
|
||||
} while (i < placeholderPosition && isWordChar(sql.charCodeAt(i)));
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
continue;
|
||||
}
|
||||
if (depth === 0) {
|
||||
if (code === charCode.semicolon)
|
||||
return false;
|
||||
if (code === charCode.comma) {
|
||||
lastWasComma = true;
|
||||
sawContent = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
}
|
||||
return depth === 0 && (!sawContent || lastWasComma);
|
||||
};
|
||||
const findSetKeyword = (sql, startFrom = 0) => {
|
||||
const length = sql.length;
|
||||
for (let position = startFrom; position < length; position++) {
|
||||
const code = sql.charCodeAt(position);
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, position);
|
||||
if (contextEnd !== -1) {
|
||||
position = contextEnd - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const lower = code | 32;
|
||||
if (lower === 115 && matchesWord(sql, position, 'set', length))
|
||||
return position + 3;
|
||||
if (lower === 107 && matchesWord(sql, position, 'key', length)) {
|
||||
let cursor = position + 3;
|
||||
while (cursor < length && isWhitespace(sql.charCodeAt(cursor)))
|
||||
cursor++;
|
||||
if (matchesWord(sql, cursor, 'update', length))
|
||||
return cursor + 6;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
|
||||
const isTemporal = (value) => Object.prototype.toString.call(value).startsWith('[object Temporal.');
|
||||
const hasSqlString = (value) => typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'toSqlString' in value &&
|
||||
typeof value.toSqlString === 'function';
|
||||
const escapeString = (value) => {
|
||||
const escapeChars = regex.escapeChars;
|
||||
escapeChars.lastIndex = 0;
|
||||
const first = escapeChars.exec(value);
|
||||
if (first === null)
|
||||
return `'${value}'`;
|
||||
const length = value.length;
|
||||
let result = "'" + value.slice(0, first.index);
|
||||
let chunkStart = first.index;
|
||||
for (let i = first.index; i < length; i++) {
|
||||
let escaped;
|
||||
switch (value.charCodeAt(i)) {
|
||||
case 0:
|
||||
escaped = '\\0';
|
||||
break;
|
||||
case 8:
|
||||
escaped = '\\b';
|
||||
break;
|
||||
case 9:
|
||||
escaped = '\\t';
|
||||
break;
|
||||
case 10:
|
||||
escaped = '\\n';
|
||||
break;
|
||||
case 13:
|
||||
escaped = '\\r';
|
||||
break;
|
||||
case 26:
|
||||
escaped = '\\Z';
|
||||
break;
|
||||
case 34:
|
||||
escaped = '\\"';
|
||||
break;
|
||||
case 39:
|
||||
escaped = "\\'";
|
||||
break;
|
||||
case 92:
|
||||
escaped = '\\\\';
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
result += value.slice(chunkStart, i) + escaped;
|
||||
chunkStart = i + 1;
|
||||
}
|
||||
return result + value.slice(chunkStart) + "'";
|
||||
};
|
||||
const pad2 = (value) => (value < 10 ? '0' + value : '' + value);
|
||||
const pad3 = (value) => value < 10 ? '00' + value : value < 100 ? '0' + value : '' + value;
|
||||
const pad4 = (value) => value < 10
|
||||
? '000' + value
|
||||
: value < 100
|
||||
? '00' + value
|
||||
: value < 1000
|
||||
? '0' + value
|
||||
: '' + value;
|
||||
const convertTimezone = (tz) => {
|
||||
if (tz === 'Z')
|
||||
return 0;
|
||||
const timezoneMatch = tz.match(regex.timezone);
|
||||
if (timezoneMatch)
|
||||
return ((timezoneMatch[1] === '-' ? -1 : 1) *
|
||||
(Number.parseInt(timezoneMatch[2], 10) +
|
||||
(timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) *
|
||||
60);
|
||||
return false;
|
||||
};
|
||||
const dateToString = (date, timezone) => {
|
||||
if (Number.isNaN(date.getTime()))
|
||||
return 'NULL';
|
||||
let year;
|
||||
let month;
|
||||
let day;
|
||||
let hour;
|
||||
let minute;
|
||||
let second;
|
||||
let millisecond;
|
||||
if (timezone === 'local') {
|
||||
year = date.getFullYear();
|
||||
month = date.getMonth() + 1;
|
||||
day = date.getDate();
|
||||
hour = date.getHours();
|
||||
minute = date.getMinutes();
|
||||
second = date.getSeconds();
|
||||
millisecond = date.getMilliseconds();
|
||||
}
|
||||
else {
|
||||
const timezoneOffsetMinutes = convertTimezone(timezone);
|
||||
let time = date.getTime();
|
||||
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
|
||||
time += timezoneOffsetMinutes * 60000;
|
||||
const adjustedDate = new Date(time);
|
||||
year = adjustedDate.getUTCFullYear();
|
||||
month = adjustedDate.getUTCMonth() + 1;
|
||||
day = adjustedDate.getUTCDate();
|
||||
hour = adjustedDate.getUTCHours();
|
||||
minute = adjustedDate.getUTCMinutes();
|
||||
second = adjustedDate.getUTCSeconds();
|
||||
millisecond = adjustedDate.getUTCMilliseconds();
|
||||
}
|
||||
// YYYY-MM-DD HH:mm:ss.mmm
|
||||
return escapeString(pad4(year) +
|
||||
'-' +
|
||||
pad2(month) +
|
||||
'-' +
|
||||
pad2(day) +
|
||||
' ' +
|
||||
pad2(hour) +
|
||||
':' +
|
||||
pad2(minute) +
|
||||
':' +
|
||||
pad2(second) +
|
||||
'.' +
|
||||
pad3(millisecond));
|
||||
};
|
||||
exports.dateToString = dateToString;
|
||||
const temporalToString = (value, timezone) => {
|
||||
if (typeof value.epochMilliseconds === 'number')
|
||||
return (0, exports.dateToString)(new Date(value.epochMilliseconds), timezone || 'local');
|
||||
if (value[Symbol.toStringTag] === 'Temporal.PlainDateTime')
|
||||
return escapeString(value.toString().replace('T', ' '));
|
||||
return escapeString(value.toString());
|
||||
};
|
||||
exports.temporalToString = temporalToString;
|
||||
const escapeId = (value, forbidQualified) => {
|
||||
if (Array.isArray(value)) {
|
||||
const length = value.length;
|
||||
let sql = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0)
|
||||
sql += ', ';
|
||||
sql += (0, exports.escapeId)(value[i], forbidQualified);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
const identifier = String(value);
|
||||
const hasJsonOperator = !forbidQualified && identifier.indexOf('->') !== -1;
|
||||
if (forbidQualified || hasJsonOperator) {
|
||||
if (identifier.indexOf('`') === -1)
|
||||
return `\`${identifier}\``;
|
||||
return `\`${identifier.replace(regex.backtick, '``')}\``;
|
||||
}
|
||||
if (identifier.indexOf('`') === -1 && identifier.indexOf('.') === -1)
|
||||
return `\`${identifier}\``;
|
||||
return `\`${identifier
|
||||
.replace(regex.backtick, '``')
|
||||
.replace(regex.dot, '`.`')}\``;
|
||||
};
|
||||
exports.escapeId = escapeId;
|
||||
const objectToValues = (object, timezone) => {
|
||||
let sql = '';
|
||||
if (object instanceof Map) {
|
||||
for (const [key, value] of object) {
|
||||
if (typeof value === 'function')
|
||||
continue;
|
||||
if (sql.length > 0)
|
||||
sql += ', ';
|
||||
sql += (0, exports.escapeId)(String(key));
|
||||
sql += ' = ';
|
||||
sql += (0, exports.escape)(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
for (const key in object) {
|
||||
if (!hasOwnProperty.call(object, key))
|
||||
continue;
|
||||
const value = object[key];
|
||||
if (typeof value === 'function')
|
||||
continue;
|
||||
if (sql.length > 0)
|
||||
sql += ', ';
|
||||
sql += (0, exports.escapeId)(key);
|
||||
sql += ' = ';
|
||||
sql += (0, exports.escape)(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
exports.objectToValues = objectToValues;
|
||||
const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
|
||||
exports.bufferToString = bufferToString;
|
||||
const arrayToList = (array, timezone) => {
|
||||
const length = array.length;
|
||||
let sql = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0)
|
||||
sql += ', ';
|
||||
const value = array[i];
|
||||
if (Array.isArray(value))
|
||||
sql += `(${(0, exports.arrayToList)(value, timezone)})`;
|
||||
else if (value instanceof Set)
|
||||
sql += `(${(0, exports.arrayToList)(Array.from(value), timezone)})`;
|
||||
else
|
||||
sql += (0, exports.escape)(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
exports.arrayToList = arrayToList;
|
||||
const escape = (value, stringifyObjects, timezone) => {
|
||||
if (value === undefined || value === null)
|
||||
return 'NULL';
|
||||
switch (typeof value) {
|
||||
case 'boolean':
|
||||
return value ? 'true' : 'false';
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
return value + '';
|
||||
case 'object': {
|
||||
if (isDate(value))
|
||||
return (0, exports.dateToString)(value, timezone || 'local');
|
||||
if (isTemporal(value))
|
||||
return (0, exports.temporalToString)(value, timezone);
|
||||
if (Array.isArray(value))
|
||||
return (0, exports.arrayToList)(value, timezone);
|
||||
if (value instanceof Set)
|
||||
return (0, exports.arrayToList)(Array.from(value), timezone);
|
||||
if (node_buffer_1.Buffer.isBuffer(value))
|
||||
return (0, exports.bufferToString)(value);
|
||||
if (value instanceof Uint8Array)
|
||||
return (0, exports.bufferToString)(node_buffer_1.Buffer.from(value));
|
||||
if (hasSqlString(value))
|
||||
return String(value.toSqlString());
|
||||
if (!(stringifyObjects === undefined || stringifyObjects === null))
|
||||
return escapeString(String(value));
|
||||
if (isRecord(value) || value instanceof Map)
|
||||
return (0, exports.objectToValues)(value, timezone);
|
||||
return escapeString(String(value));
|
||||
}
|
||||
case 'string':
|
||||
return escapeString(value);
|
||||
default:
|
||||
return escapeString(String(value));
|
||||
}
|
||||
};
|
||||
exports.escape = escape;
|
||||
const format = (sql, values, stringifyObjects, timezone) => {
|
||||
if (values === undefined || values === null)
|
||||
return sql;
|
||||
const valuesArray = Array.isArray(values) ? values : [values];
|
||||
const length = valuesArray.length;
|
||||
let setIndex = -2; // -2 = not yet computed, -1 = no SET found
|
||||
let nextSetIndex = -1; // -1 = no SET after setIndex
|
||||
let result = '';
|
||||
let chunkIndex = 0;
|
||||
let valuesIndex = 0;
|
||||
let placeholderPosition = findNextPlaceholder(sql, 0);
|
||||
while (valuesIndex < length && placeholderPosition !== -1) {
|
||||
// Count consecutive question marks to detect ? vs ?? vs ???+
|
||||
let placeholderEnd = placeholderPosition + 1;
|
||||
let escapedValue;
|
||||
while (sql.charCodeAt(placeholderEnd) === 63)
|
||||
placeholderEnd++;
|
||||
const placeholderLength = placeholderEnd - placeholderPosition;
|
||||
const currentValue = valuesArray[valuesIndex];
|
||||
if (placeholderLength > 2) {
|
||||
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
||||
continue;
|
||||
}
|
||||
if (placeholderLength === 2)
|
||||
escapedValue = (0, exports.escapeId)(currentValue);
|
||||
else if (typeof currentValue === 'number' ||
|
||||
typeof currentValue === 'bigint')
|
||||
escapedValue = `${currentValue}`;
|
||||
else if (typeof currentValue === 'object' &&
|
||||
currentValue !== null &&
|
||||
!stringifyObjects) {
|
||||
const expandable = !(Array.isArray(currentValue) ||
|
||||
currentValue instanceof Uint8Array ||
|
||||
currentValue instanceof Date ||
|
||||
hasSqlString(currentValue) ||
|
||||
isDate(currentValue)) &&
|
||||
(isRecord(currentValue) || currentValue instanceof Map);
|
||||
if (expandable) {
|
||||
// A SET assignment follows the keyword (a letter) or a comma continuing the list
|
||||
let previous = placeholderPosition - 1;
|
||||
while (previous >= chunkIndex && isWhitespace(sql.charCodeAt(previous)))
|
||||
previous--;
|
||||
const previousChar = previous >= chunkIndex ? toLower(sql.charCodeAt(previous)) : 0;
|
||||
if ((previousChar < 97 || previousChar > 122) &&
|
||||
previousChar !== charCode.comma)
|
||||
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
||||
else {
|
||||
// Lazy: resolve the first SET and its successor once
|
||||
if (setIndex === -2) {
|
||||
setIndex = findSetKeyword(sql);
|
||||
nextSetIndex = setIndex === -1 ? -1 : findSetKeyword(sql, setIndex);
|
||||
}
|
||||
// Nearest: advance to the SET closest before this placeholder
|
||||
while (nextSetIndex !== -1 && nextSetIndex <= placeholderPosition) {
|
||||
setIndex = nextSetIndex;
|
||||
nextSetIndex = findSetKeyword(sql, nextSetIndex);
|
||||
}
|
||||
if (setIndex !== -1 &&
|
||||
setIndex <= placeholderPosition &&
|
||||
isInSetAssignmentList(sql, setIndex, placeholderPosition))
|
||||
escapedValue = (0, exports.objectToValues)(currentValue, timezone);
|
||||
else
|
||||
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
||||
}
|
||||
}
|
||||
else
|
||||
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
||||
}
|
||||
else
|
||||
escapedValue = (0, exports.escape)(currentValue, stringifyObjects, timezone);
|
||||
result += sql.slice(chunkIndex, placeholderPosition);
|
||||
result += escapedValue;
|
||||
chunkIndex = placeholderEnd;
|
||||
valuesIndex++;
|
||||
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
||||
}
|
||||
if (chunkIndex === 0)
|
||||
return sql;
|
||||
if (chunkIndex < sql.length)
|
||||
return result + sql.slice(chunkIndex);
|
||||
return result;
|
||||
};
|
||||
exports.format = format;
|
||||
const raw = (sql) => {
|
||||
if (typeof sql !== 'string')
|
||||
throw new TypeError('argument sql must be a string');
|
||||
return {
|
||||
toSqlString: () => sql,
|
||||
};
|
||||
};
|
||||
exports.raw = raw;
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
const CONTEXT_TRIGGER = new Uint8Array(128);
|
||||
const SET_CLAUSE_TERMINATORS_BY_FIRST = {};
|
||||
const SET_CLAUSE_TERMINATORS = [
|
||||
"where",
|
||||
"order",
|
||||
"group",
|
||||
"having",
|
||||
"limit",
|
||||
"union",
|
||||
"returning",
|
||||
"into",
|
||||
"for",
|
||||
"lock",
|
||||
"offset",
|
||||
"window",
|
||||
"procedure",
|
||||
"on"
|
||||
];
|
||||
const regex = {
|
||||
backtick: /`/g,
|
||||
dot: /\./g,
|
||||
timezone: /([+\-\s])(\d\d):?(\d\d)?/,
|
||||
escapeChars: /[\0\b\t\n\r\x1a"'\\]/g
|
||||
};
|
||||
const charCode = {
|
||||
singleQuote: 39,
|
||||
backtick: 96,
|
||||
backslash: 92,
|
||||
dash: 45,
|
||||
slash: 47,
|
||||
asterisk: 42,
|
||||
exclamation: 33,
|
||||
plus: 43,
|
||||
questionMark: 63,
|
||||
comma: 44,
|
||||
openParen: 40,
|
||||
closeParen: 41,
|
||||
semicolon: 59,
|
||||
newline: 10,
|
||||
space: 32,
|
||||
tab: 9,
|
||||
carriageReturn: 13
|
||||
};
|
||||
CONTEXT_TRIGGER[charCode.singleQuote] = 1;
|
||||
CONTEXT_TRIGGER[charCode.backtick] = 1;
|
||||
CONTEXT_TRIGGER[charCode.dash] = 1;
|
||||
CONTEXT_TRIGGER[charCode.slash] = 1;
|
||||
for (const word of SET_CLAUSE_TERMINATORS) {
|
||||
const first = word.charCodeAt(0);
|
||||
const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[first];
|
||||
if (bucket) bucket.push(word);
|
||||
else SET_CLAUSE_TERMINATORS_BY_FIRST[first] = [word];
|
||||
}
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map);
|
||||
const isWordChar = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 95;
|
||||
const isWhitespace = (code) => code === charCode.space || code === charCode.tab || code === charCode.newline || code === charCode.carriageReturn;
|
||||
const toLower = (code) => code | 32;
|
||||
const matchesWord = (sql, position, word, length) => {
|
||||
const wordLength = word.length;
|
||||
for (let offset = 0; offset < wordLength; offset++)
|
||||
if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
|
||||
return false;
|
||||
return (position === 0 || !isWordChar(sql.charCodeAt(position - 1))) && (position + wordLength >= length || !isWordChar(sql.charCodeAt(position + wordLength)));
|
||||
};
|
||||
const skipSqlContext = (sql, position) => {
|
||||
const currentChar = sql.charCodeAt(position);
|
||||
const nextChar = sql.charCodeAt(position + 1);
|
||||
if (currentChar === charCode.singleQuote) {
|
||||
for (let cursor = position + 1; cursor < sql.length; cursor++) {
|
||||
if (sql.charCodeAt(cursor) === charCode.backslash) cursor++;
|
||||
else if (sql.charCodeAt(cursor) === charCode.singleQuote)
|
||||
return cursor + 1;
|
||||
}
|
||||
return sql.length;
|
||||
}
|
||||
if (currentChar === charCode.backtick) {
|
||||
const length = sql.length;
|
||||
for (let cursor = position + 1; cursor < length; cursor++) {
|
||||
if (sql.charCodeAt(cursor) !== charCode.backtick) continue;
|
||||
if (sql.charCodeAt(cursor + 1) === charCode.backtick) {
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
return cursor + 1;
|
||||
}
|
||||
return length;
|
||||
}
|
||||
if (currentChar === charCode.dash && nextChar === charCode.dash) {
|
||||
const afterDash = sql.charCodeAt(position + 2);
|
||||
if (Number.isNaN(afterDash) || afterDash <= charCode.space) {
|
||||
const lineBreak = sql.indexOf("\n", position + 2);
|
||||
return lineBreak === -1 ? sql.length : lineBreak + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
|
||||
const markerChar = sql.charCodeAt(position + 2);
|
||||
if (markerChar === charCode.exclamation || markerChar === charCode.plus)
|
||||
return -1;
|
||||
const commentEnd = sql.indexOf("*/", position + 2);
|
||||
return commentEnd === -1 ? sql.length : commentEnd + 2;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const findNextPlaceholder = (sql, start) => {
|
||||
const sqlLength = sql.length;
|
||||
for (let position = start; position < sqlLength; position++) {
|
||||
const code = sql.charCodeAt(position);
|
||||
if (code === charCode.questionMark) return position;
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, position);
|
||||
if (contextEnd !== -1) position = contextEnd - 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const isInSetAssignmentList = (sql, setEnd, placeholderPosition) => {
|
||||
const length = sql.length;
|
||||
let depth = 0;
|
||||
let sawContent = false;
|
||||
let lastWasComma = false;
|
||||
for (let i = setEnd; i < placeholderPosition; ) {
|
||||
const code = sql.charCodeAt(i);
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, i);
|
||||
if (contextEnd !== -1) {
|
||||
i = contextEnd;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (isWhitespace(code)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (code === charCode.openParen) {
|
||||
depth++;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (code === charCode.closeParen) {
|
||||
if (--depth < 0) return false;
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (isWordChar(code)) {
|
||||
if (depth === 0 && !(code >= 48 && code <= 57)) {
|
||||
const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[code | 32];
|
||||
if (bucket) {
|
||||
for (let t = 0; t < bucket.length; t++)
|
||||
if (matchesWord(sql, i, bucket[t], length)) return false;
|
||||
}
|
||||
}
|
||||
do {
|
||||
i++;
|
||||
} while (i < placeholderPosition && isWordChar(sql.charCodeAt(i)));
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
continue;
|
||||
}
|
||||
if (depth === 0) {
|
||||
if (code === charCode.semicolon) return false;
|
||||
if (code === charCode.comma) {
|
||||
lastWasComma = true;
|
||||
sawContent = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
sawContent = true;
|
||||
lastWasComma = false;
|
||||
i++;
|
||||
}
|
||||
return depth === 0 && (!sawContent || lastWasComma);
|
||||
};
|
||||
const findSetKeyword = (sql, startFrom = 0) => {
|
||||
const length = sql.length;
|
||||
for (let position = startFrom; position < length; position++) {
|
||||
const code = sql.charCodeAt(position);
|
||||
if (code < 128 && CONTEXT_TRIGGER[code]) {
|
||||
const contextEnd = skipSqlContext(sql, position);
|
||||
if (contextEnd !== -1) {
|
||||
position = contextEnd - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const lower = code | 32;
|
||||
if (lower === 115 && matchesWord(sql, position, "set", length))
|
||||
return position + 3;
|
||||
if (lower === 107 && matchesWord(sql, position, "key", length)) {
|
||||
let cursor = position + 3;
|
||||
while (cursor < length && isWhitespace(sql.charCodeAt(cursor))) cursor++;
|
||||
if (matchesWord(sql, cursor, "update", length)) return cursor + 6;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
const isDate = (value) => Object.prototype.toString.call(value) === "[object Date]";
|
||||
const isTemporal = (value) => Object.prototype.toString.call(value).startsWith("[object Temporal.");
|
||||
const hasSqlString = (value) => typeof value === "object" && value !== null && "toSqlString" in value && typeof value.toSqlString === "function";
|
||||
const escapeString = (value) => {
|
||||
const escapeChars = regex.escapeChars;
|
||||
escapeChars.lastIndex = 0;
|
||||
const first = escapeChars.exec(value);
|
||||
if (first === null) return `'${value}'`;
|
||||
const length = value.length;
|
||||
let result = "'" + value.slice(0, first.index);
|
||||
let chunkStart = first.index;
|
||||
for (let i = first.index; i < length; i++) {
|
||||
let escaped;
|
||||
switch (value.charCodeAt(i)) {
|
||||
case 0:
|
||||
escaped = "\\0";
|
||||
break;
|
||||
case 8:
|
||||
escaped = "\\b";
|
||||
break;
|
||||
case 9:
|
||||
escaped = "\\t";
|
||||
break;
|
||||
case 10:
|
||||
escaped = "\\n";
|
||||
break;
|
||||
case 13:
|
||||
escaped = "\\r";
|
||||
break;
|
||||
case 26:
|
||||
escaped = "\\Z";
|
||||
break;
|
||||
case 34:
|
||||
escaped = '\\"';
|
||||
break;
|
||||
case 39:
|
||||
escaped = "\\'";
|
||||
break;
|
||||
case 92:
|
||||
escaped = "\\\\";
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
result += value.slice(chunkStart, i) + escaped;
|
||||
chunkStart = i + 1;
|
||||
}
|
||||
return result + value.slice(chunkStart) + "'";
|
||||
};
|
||||
const pad2 = (value) => value < 10 ? "0" + value : "" + value;
|
||||
const pad3 = (value) => value < 10 ? "00" + value : value < 100 ? "0" + value : "" + value;
|
||||
const pad4 = (value) => value < 10 ? "000" + value : value < 100 ? "00" + value : value < 1e3 ? "0" + value : "" + value;
|
||||
const convertTimezone = (tz) => {
|
||||
if (tz === "Z") return 0;
|
||||
const timezoneMatch = tz.match(regex.timezone);
|
||||
if (timezoneMatch)
|
||||
return (timezoneMatch[1] === "-" ? -1 : 1) * (Number.parseInt(timezoneMatch[2], 10) + (timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) * 60;
|
||||
return false;
|
||||
};
|
||||
const dateToString = (date, timezone) => {
|
||||
if (Number.isNaN(date.getTime())) return "NULL";
|
||||
let year;
|
||||
let month;
|
||||
let day;
|
||||
let hour;
|
||||
let minute;
|
||||
let second;
|
||||
let millisecond;
|
||||
if (timezone === "local") {
|
||||
year = date.getFullYear();
|
||||
month = date.getMonth() + 1;
|
||||
day = date.getDate();
|
||||
hour = date.getHours();
|
||||
minute = date.getMinutes();
|
||||
second = date.getSeconds();
|
||||
millisecond = date.getMilliseconds();
|
||||
} else {
|
||||
const timezoneOffsetMinutes = convertTimezone(timezone);
|
||||
let time = date.getTime();
|
||||
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
|
||||
time += timezoneOffsetMinutes * 6e4;
|
||||
const adjustedDate = new Date(time);
|
||||
year = adjustedDate.getUTCFullYear();
|
||||
month = adjustedDate.getUTCMonth() + 1;
|
||||
day = adjustedDate.getUTCDate();
|
||||
hour = adjustedDate.getUTCHours();
|
||||
minute = adjustedDate.getUTCMinutes();
|
||||
second = adjustedDate.getUTCSeconds();
|
||||
millisecond = adjustedDate.getUTCMilliseconds();
|
||||
}
|
||||
return escapeString(
|
||||
pad4(year) + "-" + pad2(month) + "-" + pad2(day) + " " + pad2(hour) + ":" + pad2(minute) + ":" + pad2(second) + "." + pad3(millisecond)
|
||||
);
|
||||
};
|
||||
const temporalToString = (value, timezone) => {
|
||||
if (typeof value.epochMilliseconds === "number")
|
||||
return dateToString(new Date(value.epochMilliseconds), timezone || "local");
|
||||
if (value[Symbol.toStringTag] === "Temporal.PlainDateTime")
|
||||
return escapeString(value.toString().replace("T", " "));
|
||||
return escapeString(value.toString());
|
||||
};
|
||||
const escapeId = (value, forbidQualified) => {
|
||||
if (Array.isArray(value)) {
|
||||
const length = value.length;
|
||||
let sql = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) sql += ", ";
|
||||
sql += escapeId(value[i], forbidQualified);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
const identifier = String(value);
|
||||
const hasJsonOperator = !forbidQualified && identifier.indexOf("->") !== -1;
|
||||
if (forbidQualified || hasJsonOperator) {
|
||||
if (identifier.indexOf("`") === -1) return `\`${identifier}\``;
|
||||
return `\`${identifier.replace(regex.backtick, "``")}\``;
|
||||
}
|
||||
if (identifier.indexOf("`") === -1 && identifier.indexOf(".") === -1)
|
||||
return `\`${identifier}\``;
|
||||
return `\`${identifier.replace(regex.backtick, "``").replace(regex.dot, "`.`")}\``;
|
||||
};
|
||||
const objectToValues = (object, timezone) => {
|
||||
let sql = "";
|
||||
if (object instanceof Map) {
|
||||
for (const [key, value] of object) {
|
||||
if (typeof value === "function") continue;
|
||||
if (sql.length > 0) sql += ", ";
|
||||
sql += escapeId(String(key));
|
||||
sql += " = ";
|
||||
sql += escape(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
for (const key in object) {
|
||||
if (!hasOwnProperty.call(object, key)) continue;
|
||||
const value = object[key];
|
||||
if (typeof value === "function") continue;
|
||||
if (sql.length > 0) sql += ", ";
|
||||
sql += escapeId(key);
|
||||
sql += " = ";
|
||||
sql += escape(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
const bufferToString = (buffer) => `X${escapeString(buffer.toString("hex"))}`;
|
||||
const arrayToList = (array, timezone) => {
|
||||
const length = array.length;
|
||||
let sql = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (i > 0) sql += ", ";
|
||||
const value = array[i];
|
||||
if (Array.isArray(value)) sql += `(${arrayToList(value, timezone)})`;
|
||||
else if (value instanceof Set)
|
||||
sql += `(${arrayToList(Array.from(value), timezone)})`;
|
||||
else sql += escape(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
const escape = (value, stringifyObjects, timezone) => {
|
||||
if (value === void 0 || value === null) return "NULL";
|
||||
switch (typeof value) {
|
||||
case "boolean":
|
||||
return value ? "true" : "false";
|
||||
case "number":
|
||||
case "bigint":
|
||||
return value + "";
|
||||
case "object": {
|
||||
if (isDate(value)) return dateToString(value, timezone || "local");
|
||||
if (isTemporal(value)) return temporalToString(value, timezone);
|
||||
if (Array.isArray(value)) return arrayToList(value, timezone);
|
||||
if (value instanceof Set)
|
||||
return arrayToList(Array.from(value), timezone);
|
||||
if (Buffer.isBuffer(value)) return bufferToString(value);
|
||||
if (value instanceof Uint8Array)
|
||||
return bufferToString(Buffer.from(value));
|
||||
if (hasSqlString(value)) return String(value.toSqlString());
|
||||
if (!(stringifyObjects === void 0 || stringifyObjects === null))
|
||||
return escapeString(String(value));
|
||||
if (isRecord(value) || value instanceof Map)
|
||||
return objectToValues(value, timezone);
|
||||
return escapeString(String(value));
|
||||
}
|
||||
case "string":
|
||||
return escapeString(value);
|
||||
default:
|
||||
return escapeString(String(value));
|
||||
}
|
||||
};
|
||||
const format = (sql, values, stringifyObjects, timezone) => {
|
||||
if (values === void 0 || values === null) return sql;
|
||||
const valuesArray = Array.isArray(values) ? values : [values];
|
||||
const length = valuesArray.length;
|
||||
let setIndex = -2;
|
||||
let nextSetIndex = -1;
|
||||
let result = "";
|
||||
let chunkIndex = 0;
|
||||
let valuesIndex = 0;
|
||||
let placeholderPosition = findNextPlaceholder(sql, 0);
|
||||
while (valuesIndex < length && placeholderPosition !== -1) {
|
||||
let placeholderEnd = placeholderPosition + 1;
|
||||
let escapedValue;
|
||||
while (sql.charCodeAt(placeholderEnd) === 63) placeholderEnd++;
|
||||
const placeholderLength = placeholderEnd - placeholderPosition;
|
||||
const currentValue = valuesArray[valuesIndex];
|
||||
if (placeholderLength > 2) {
|
||||
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
||||
continue;
|
||||
}
|
||||
if (placeholderLength === 2) escapedValue = escapeId(currentValue);
|
||||
else if (typeof currentValue === "number" || typeof currentValue === "bigint")
|
||||
escapedValue = `${currentValue}`;
|
||||
else if (typeof currentValue === "object" && currentValue !== null && !stringifyObjects) {
|
||||
const expandable = !(Array.isArray(currentValue) || currentValue instanceof Uint8Array || currentValue instanceof Date || hasSqlString(currentValue) || isDate(currentValue)) && (isRecord(currentValue) || currentValue instanceof Map);
|
||||
if (expandable) {
|
||||
let previous = placeholderPosition - 1;
|
||||
while (previous >= chunkIndex && isWhitespace(sql.charCodeAt(previous)))
|
||||
previous--;
|
||||
const previousChar = previous >= chunkIndex ? toLower(sql.charCodeAt(previous)) : 0;
|
||||
if ((previousChar < 97 || previousChar > 122) && previousChar !== charCode.comma)
|
||||
escapedValue = escape(currentValue, true, timezone);
|
||||
else {
|
||||
if (setIndex === -2) {
|
||||
setIndex = findSetKeyword(sql);
|
||||
nextSetIndex = setIndex === -1 ? -1 : findSetKeyword(sql, setIndex);
|
||||
}
|
||||
while (nextSetIndex !== -1 && nextSetIndex <= placeholderPosition) {
|
||||
setIndex = nextSetIndex;
|
||||
nextSetIndex = findSetKeyword(sql, nextSetIndex);
|
||||
}
|
||||
if (setIndex !== -1 && setIndex <= placeholderPosition && isInSetAssignmentList(sql, setIndex, placeholderPosition))
|
||||
escapedValue = objectToValues(currentValue, timezone);
|
||||
else escapedValue = escape(currentValue, true, timezone);
|
||||
}
|
||||
} else escapedValue = escape(currentValue, true, timezone);
|
||||
} else escapedValue = escape(currentValue, stringifyObjects, timezone);
|
||||
result += sql.slice(chunkIndex, placeholderPosition);
|
||||
result += escapedValue;
|
||||
chunkIndex = placeholderEnd;
|
||||
valuesIndex++;
|
||||
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
|
||||
}
|
||||
if (chunkIndex === 0) return sql;
|
||||
if (chunkIndex < sql.length) return result + sql.slice(chunkIndex);
|
||||
return result;
|
||||
};
|
||||
const raw = (sql) => {
|
||||
if (typeof sql !== "string")
|
||||
throw new TypeError("argument sql must be a string");
|
||||
return {
|
||||
toSqlString: () => sql
|
||||
};
|
||||
};
|
||||
export {
|
||||
arrayToList,
|
||||
bufferToString,
|
||||
dateToString,
|
||||
escape,
|
||||
escapeId,
|
||||
format,
|
||||
objectToValues,
|
||||
raw,
|
||||
temporalToString
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export type Raw = {
|
||||
toSqlString(): string;
|
||||
};
|
||||
/** Avoids the global `Temporal` namespace so consumers don't need TS's ESNext.Temporal lib. */
|
||||
export type TemporalValue = {
|
||||
readonly [Symbol.toStringTag]: `Temporal.${string}`;
|
||||
readonly epochMilliseconds?: number;
|
||||
toString(): string;
|
||||
};
|
||||
export type SqlValue = string | number | bigint | boolean | Date | TemporalValue | Buffer | Uint8Array | Raw | Record<string, unknown> | SqlValue[] | Set<SqlValue> | Map<string, SqlValue> | null | undefined;
|
||||
export type Timezone = 'local' | 'Z' | (string & NonNullable<unknown>);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "sql-escaper",
|
||||
"version": "1.5.1",
|
||||
"description": "🛡️ Faster SQL escape and format for JavaScript (Node.js, Bun, and Deno).",
|
||||
"main": "./lib/index.js",
|
||||
"module": "./lib/index.mjs",
|
||||
"types": "./lib/index.d.ts",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/mysqljs/sql-escaper.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/mysqljs/sql-escaper/issues"
|
||||
},
|
||||
"author": "https://github.com/mysqljs",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "cd benchmark && npm ci && bash index.sh",
|
||||
"build:esm": "esbuild src/index.ts --outfile=lib/index.mjs --platform=node --target=node12 --format=esm",
|
||||
"build": "rm -rf ./lib && tsc && npm run build:esm",
|
||||
"test:node": "poku test -r=\"compact\"",
|
||||
"test:bun": "bun --bun poku -r=\"compact\" test",
|
||||
"test:coverage": "mcr --import tsx --config mcr.config.ts npm run test:node",
|
||||
"lint": "biome lint --error-on-warnings && prettier --check .",
|
||||
"lint:fix": "biome lint --write && prettier --write .github/workflows/*.yml .",
|
||||
"update": "pu minor && npm i && (npm audit fix || true) && npm run lint:fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.7.1",
|
||||
"@js-temporal/polyfill": "^0.5.1",
|
||||
"@types/node": "^26.1.1",
|
||||
"esbuild": "^0.28.1",
|
||||
"monocart-coverage-reports": "^2.12.12",
|
||||
"packages-update": "^2.0.0",
|
||||
"poku": "^4.5.0",
|
||||
"prettier": "^3.9.5",
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./lib/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"sql",
|
||||
"escape",
|
||||
"format",
|
||||
"sqlstring",
|
||||
"sql-injection",
|
||||
"sanitize",
|
||||
"query",
|
||||
"mysql",
|
||||
"node",
|
||||
"nodejs",
|
||||
"bun",
|
||||
"deno",
|
||||
"typescript",
|
||||
"fast"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user