• Stars
    star
    124
  • Rank 288,207 (Top 6 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 6 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐Ÿฆ‘ Provides SQL tagged template strings and schema definition functions.

squid

SQL tagged template strings and schema definitions for JavaScript & TypeScript.

Build status npm version


The simple and safe way of writing SQL queries in node.js. Use postguard to validate SQL queries in your code against your table schemas at build time ๐Ÿš€

ย ย ย ย ๐Ÿ‘Œย ย Static typing made simple
ย ย ย ย ๐Ÿ›กย ย SQL injection prevention
ย ย ย ย ๐Ÿ”ฆย ย Static query validation using postguard
ย ย ย ย โšก๏ธย ย Almost no performance overhead

Parameters are SQL-injection-proofed by default. You can explicitly opt-out, by wrapping the parameter value in sql.raw().

Supports only Postgres right now, but it is easy to add support for MySQL, SQLite, ... as well. Create an issue or pull request if you need support for another database.

Why?

Why not use a query builder?

Query builders like Prisma or Knex.js seem like a good choice, but they all have one issue in common: They provide an abstraction that maps 1:1 to SQL, making you create SQL queries without writing SQL, but using their proprietary API.

You don't just require developers to learn both, SQL and the query builder's API, but the additional abstraction layer is also an additional source of error.

Why not use an ORM?

ORMs like Sequelize or TypeORM can get you started quickly, but will regularly lead to slow queries and can turn into a hassle in the long run. Read more about it here and here, for instance.

Installation

npm install squid

Usage

JavaScript

import { defineTable, sql, spreadInsert } from "squid/pg"
import database from "./database"

// Feel free to put the table schema in a different file
defineTable("users", {
  id: Schema.Number,
  name: Schema.String
})

export async function queryUserById(id) {
  const { rows } = await database.query(sql`
    SELECT * FROM users WHERE id = ${id}
  `)
  return rows.length > 0 ? rows[0] : null
}

TypeScript

// schema.ts
import { defineTable, Schema, NewTableRow, TableRow } from "squid"

export type NewUserRecord = NewTableRow<typeof usersTable>
export type UserRecord = TableRow<typeof usersTable>

const usersTable = defineTable("users", {
  id: Schema.Number,
  name: Schema.String
})
// users.ts
import { sql, spreadInsert } from "squid/pg"
import database from "./database"
import { NewUserRecord, UserRecord } from "./schema"

export async function createUser(record: NewUserRecord): Promise<UserRecord> {
  const { rows } = await database.query<UserRecord>(sql`
    INSERT INTO users ${spreadInsert(record)} RETURNING *
  `)
  return rows[0]
}

export async function queryUserById(id: string): Promise<UserRecord | null> {
  const { rows } = await database.query<UserRecord>(sql`
    SELECT * FROM users WHERE id = ${id}
  `)
  return rows[0] || null
}

We extend the pg driver's query() method types transparently, so you can pass a generic type parameter specifying the type of the result rows as you can see in the sample above.

The query() type parameter defaults to any, so you don't have to specify it. If it's set, the type of the rows result property will be inferred accordingly.

Query values

All expressions in the SQL template strings will be escaped properly automatically, so you don't need to worry about SQL injection attacks too much.

If you need to pass a value dynamically that should not be escaped, you can use sql.raw:

async function updateTimestamp(userID, timestamp = null) {
  await database.query(sql`
    UPDATE users
    SET timestamp = ${timestamp || sql.raw("NOW()")}
    WHERE id = ${userID}
  `)
}

Tag function

The sql template tag creates query objects compatible with pg, the super popular Postgres driver for node.

import { sql, spreadInsert } from "squid/pg"

sql`INSERT INTO users ${spreadInsert({ name: "Andy", age: 29 })}`
// => { text: "INSERT INTO users ("name", "age") VALUES ($1, $2)",
//      values: [ "Andy", 29 ] }

sql`SELECT * FROM users WHERE age < ${maxAge}`
// => { text: "SELECT * FROM users WHERE age < $1",
//      values: [ maxAge ] }

Import

All schema-related exports are database-driver-agnostic, so they can be imported from the main entrypoint squid:

import { defineTable, Schema, NewTableRow, TableRow } from "squid"

Non-schema-related exports are exported by the database-specific submodule squid/pg:

import { sql, spreadInsert } from "squid/pg"

For convenience squid/pg also exposes all the database-agnostic schema exports, so you can have one import declaration for everything:

import { defineTable, sql, spreadInsert, Schema, NewTableRow, TableRow } from "squid"

SQL injections

All values passed into the tagged template string are automatically escaped to prevent SQL injections. You have to explicitly opt-out of this behavior by using sql.raw() in case you want to dynamically modify the query.

The only attack vector left is forgetting to use the sql template tag. To rule out this potential source of error, there is also a way to explicitly escape a value passed to the template string: sql.safe().

An untagged template string using a value wrapped in sql.safe() will then result in an invalid query.

// This is fine
await database.query(sql`SELECT * FROM users LIMIT ${limit}`)

// Results in the same query as the previous example
await database.query(sql`SELECT * FROM users LIMIT ${sql.safe(limit)}`)

// Forgot the tag - SQL injection possible!
await database.query(`SELECT * FROM users LIMIT ${limit}`)

// Forgot the tag - This line will now throw, no SQLi possible
await database.query(`SELECT * FROM users LIMIT ${sql.safe(limit)}`)

API

sql`...`

Turns a template string into a postgres query object, escapes values automatically unless they are wrapped in sql.raw().

Example:

const limit = 50
await database.query(sql`SELECT * FROM users LIMIT ${50}`)

// same as:
await database.query({ text: "SELECT * FROM users LIMIT $1", values: [limit])

sql.raw(expression)

Wrap your SQL template string values in this call to prevent escaping. Be careful, though. This is essentially an SQL injection prevention opt-out.

Example:

await database.query(sql`
  UPDATE users SET last_login = ${loggingIn ? "NOW()" : "NULL"} WHERE id = ${userID}
`)

sql.safe(value)

Wraps a value in an object that just returns the (escaped) value.

Use it if you want to make sure that a query lacking the sql template tag cannot be executed. Otherwise a missing template string tag might lead to SQL injections.

spreadAnd({ [columnName: string]: any })

Check for equivalence of multiple column's values at once. Handy to keep long WHERE expressions short and concise.

Example:

const users = await database.query(sql`
  SELECT * FROM users WHERE ${spreadAnd({ name: "John", birthday: "1990-09-10" })}
`)

// same as:
// sql`SELECT * FROM users WHERE name = 'John' AND birthday = '1990-09-10'`

spreadInsert({ [columnName: string]: any })

Spread INSERT VALUES to keep the query sweet and short without losing explicity.

Example:

const users = await database.query(sql`
  INSERT INTO users ${spreadInsert({ name: "John", email: "[email protected]" })}
`)

// same as:
// sql`INSERT INTO users ("name", "email") VALUES ('John', '[email protected]')`
const users = await database.query(sql`
  INSERT INTO users ${spreadInsert(
    { name: "John", email: "[email protected]" },
    { name: "Travis", email: "[email protected]" }
  )}
`)

// same as:
// sql`INSERT INTO users ("name", "email") VALUES ('John', '[email protected]'), ('Travis', '[email protected]')`

spreadUpdate({ [columnName: string]: any })

Spread INSERT VALUES to keep the query sweet and short without losing explicity.

Example:

await database.query(sql`
  UPDATE users
  SET ${spreadUpdate({ name: "John", email: "[email protected]" })}
  WHERE id = 1
`)

// same as:
// sql`UPDATE users SET "name" = 'John', "email" = '[email protected]' WHERE id = 1`

defineTable(tableName: string, schema: { [columnName: string]: Schema.* })

Define a table's schema, so the queries can be validated at build time with postguard. When using TypeScript you can use TableRow<typeof table> and NewTableRow<typeof table> to derive TypeScript interfaces of your table records.

See dist/schema.d.ts for details.

Schema

Example:

defineTable("users", {
  id: Schema.Number,
  email: Schema.String,
  email_confirmed: Schema.Boolean,
  profile: Schema.JSON(
    Schema.Object({
      avatar_url: Schema.String,
      weblink: Schema.nullable(Schema.String)
    })
  ),
  created_at: Schema.default(Schema.Date),
  updated_at: Schema.nullable(Schema.Date),
  roles: Schema.Array(Schema.Enum(["admin", "user"]))
})

See dist/schema.d.ts for details.

TableRow / NewTableRow (TypeScript only)

Derive table record interfaces from the table schema. The type returned by TableRow is the kind of object a SELECT * will return, while NewTableRow returns an object that defines the shape of an object to be used for an INSERT with spreadInsert().

The difference between the two is that NewTableRow marks properties referring to columns defined as Schema.default() or Schema.nullable() as optional.

Example:

const usersTable = defineTable("users", {
  id: Schema.Number,
  email: Schema.String
})

type UserRecord = TableRow<typeof usersTable>
type NewUserRecord = NewTableRow<typeof usersTable>

See dist/schema.d.ts for details.

Performance

The performance impact of using the template string is neglectible. Benchmarked it once and it did 1000 queries in ~10ms on my MacBook Pro.

Debugging

Set the environment variable DEBUG to squid:* to enable debug logging for this package.

License

MIT

More Repositories

1

webpack-blocks

๐Ÿ“ฆ Configure webpack using functional feature blocks.
JavaScript
2,974
star
2

threads.js

๐Ÿงต Make web workers & worker threads as simple as a function call.
TypeScript
2,942
star
3

leakage

๐Ÿ› Memory leak testing for node.
JavaScript
1,583
star
4

pg-listen

๐Ÿ“ก PostgreSQL LISTEN & NOTIFY for node.js that finally works.
TypeScript
540
star
5

typed-emitter

๐Ÿ”ฉ Type-safe event emitter interface for TypeScript
JavaScript
250
star
6

use-inline-memo

โš›๏ธ React hook for memoizing values inline anywhere in a component
TypeScript
163
star
7

postguard

๐Ÿ› Statically validate Postgres SQL queries in JS / TS code and derive schemas.
TypeScript
161
star
8

laravel-js-localization

Simple, ease-to-use and flexible package for the Laravel web framework. Allows you to use localized messages of the Laravel webapp (see `resources/lang` directory) in your Javascript code.
PHP
143
star
9

ava-ts

๐Ÿš€ Fork of the AVA test runner with native typescript support
JavaScript
116
star
10

postcss-debug

Debug your postcss workflow with ease! Creates snapshots of your CSS files before/after each postcss plugin is run.
JavaScript
94
star
11

react-usestyles

๐Ÿ– Style components using React hooks. Abstracts the styling library away.
JavaScript
87
star
12

postcss-theme

PostCSS plugin to enable versatile theming.
JavaScript
87
star
13

react-stateful-fn

โš› Stateful functional components for React.
JavaScript
57
star
14

puppet-run

๐Ÿค– Run anything JavaScript in a headless Chrome from your command line
TypeScript
53
star
15

threadpool-js

Javascript thread pool implementation using web workers.
JavaScript
47
star
16

jquery-dim-background

jQuery plugin to dim the current page except for some user-defined top elements.
JavaScript
43
star
17

observable-fns

๐Ÿ•ต๏ธโ€โ™€๏ธ Light-weight observable implementation and functional utilities in TypeScript
TypeScript
41
star
18

npm-launch

๐Ÿš€ Minimalistic task runner on steroids!
JavaScript
39
star
19

drag-mock

Trigger HTML5 drag & drop events for testing
JavaScript
35
star
20

gear

๐Ÿ›  Experimental tool to bootstrap typed JavaScript code.
JavaScript
33
star
21

proposal-double-colon-types

๐Ÿค“ JS / Flow syntax proposal. Types ร  la Hindley-Milner.
20
star
22

http-event-stream

๐Ÿ“ก Modern spec-compliant Server Sent Events stream implementation.
TypeScript
19
star
23

ts

โš™๏ธ The CLI that TypeScript deserves.
TypeScript
18
star
24

key-store

๐Ÿ” Isomorphic encrypted key store written in TypeScript.
TypeScript
17
star
25

plow

๐Ÿ‘จโ€๐ŸŒพ Postgres migrations and seeding made easy
TypeScript
14
star
26

isomorphic-crypto

๐Ÿ”’ Isomorphic crypto package for node and the browser.
JavaScript
12
star
27

type-reflect

โ˜๏ธ TypeScript plugin providing access to type information at runtime
TypeScript
11
star
28

srv

๐Ÿ“ก Functional node server. Composable routing. Take a request, return a response.
TypeScript
9
star
29

rungpt

GPT client with local plugin framework, built by GPT-4
TypeScript
9
star
30

php-easygit

Manage Git repositories from within your PHP webapp. Commit, branch, clone, checkout, ...
PHP
7
star
31

zaster

๐Ÿ’ธ Headless multi-blockchain wallet and SDK.
TypeScript
7
star
32

react-commandments

๐Ÿ“– Thou shalt honor thy reactive code and keep it holy.
6
star
33

koa-router-index

Koa v2 middleware to create an index page for API servers.
JavaScript
5
star
34

gulp-elixir-modules

Elixir extension for handling frontend modules easily.
JavaScript
5
star
35

json-sql-import

Small PHP tool to import JSON data into database tables using transformation rules.
PHP
4
star
36

puppet-run-plugins

๐Ÿงฉ Plugins for puppet-run.
TypeScript
4
star
37

shutter-legacy

๐Ÿ“ธ Visual snapshot testing with no effort.
TypeScript
3
star
38

deep-assert

๐Ÿ” Better deep-equals comparison, supporting custom property assertions and pretty diffs.
TypeScript
3
star
39

ideabox

Place to collect techy ideas and get feedback.
1
star
40

bundle-decomposition-research

JavaScript
1
star
41

stellar-wallet

With the new Stellar wallet to the moon ๐Ÿš€
JavaScript
1
star