├── .nvmrc ├── .gitignore ├── jest.config.js ├── index.ts ├── .npmignore ├── __snapshots__ ├── filesInPackage.spec.ts.snap └── core.spec.ts.snap ├── filesInPackage.spec.ts ├── core.spec.ts ├── package.json ├── LICENSE ├── .circleci └── config.yml ├── mysql.spec.ts ├── core.ts ├── pg.spec.ts ├── pg.ts ├── mysql.ts ├── README.md └── tsconfig.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v11.9.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | testMatch: ["**/*.spec.ts"] 5 | }; 6 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import * as mysql from "./mysql"; 2 | import * as pg from "./pg"; 3 | import * as core from "./core"; 4 | 5 | export { core, mysql, pg }; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.ts 2 | *.spec.js 3 | *.ts 4 | !*.d.ts 5 | *.spec.d.ts 6 | __snapshots__ 7 | .vscode 8 | jest.config.js 9 | .circleci 10 | test-results 11 | .nvmrc -------------------------------------------------------------------------------- /__snapshots__/filesInPackage.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Files that will be published to the repo 1`] = ` 4 | Array [ 5 | "package.json", 6 | "core.d.ts", 7 | "core.js", 8 | "index.d.ts", 9 | "index.js", 10 | "LICENSE", 11 | "mysql.d.ts", 12 | "mysql.js", 13 | "pg.d.ts", 14 | "pg.js", 15 | "README.md", 16 | "tsconfig.json", 17 | ] 18 | `; 19 | -------------------------------------------------------------------------------- /filesInPackage.spec.ts: -------------------------------------------------------------------------------- 1 | import { spawnSync } from "child_process"; 2 | 3 | test("Files that will be published to the repo", () => { 4 | const result = spawnSync("npm", ["pack", "--dry-run"], { encoding: "utf8" }); 5 | const lines = result.stderr.split("\n"); 6 | const begin = lines.findIndex(line => line.includes("Tarball Contents")); 7 | const end = lines.findIndex(line => line.includes("Tarball Details")); 8 | const files = lines.slice(begin + 1, end).map(line => { 9 | return line.split(/\s+/)[3]; 10 | }); 11 | expect(files).toMatchSnapshot(); 12 | }); 13 | -------------------------------------------------------------------------------- /core.spec.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "./core"; 2 | 3 | test("parses a query", () => { 4 | const nodes = parse<{ age: number }, {}>` 5 | SELECT first_name, last_name, age FROM table 6 | WHERE first_name = ${"Gal"} 7 | AND age = ${x => x.age} 8 | `; 9 | 10 | expect(nodes).toMatchSnapshot(); 11 | }); 12 | 13 | test("composes correctly", () => { 14 | const allUsers = parse<{}, {}>` 15 | SELECT * FROM users 16 | `; 17 | const nodes = parse<{}, {}>` 18 | SELECT * FROM (${allUsers}) all_users; 19 | `; 20 | expect(nodes).toMatchSnapshot(); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cuery", 3 | "version": "3.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "test": "jest", 9 | "prepublishOnly": "npm run build && npm run test" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@types/expect": "^1.20.3", 16 | "@types/jest": "^24.0.11", 17 | "@types/mysql": "^2.15.5", 18 | "@types/pg": "^7.4.14", 19 | "expect": "^24.1.0", 20 | "jest": "^24.5.0", 21 | "jest-junit": "^6.3.0", 22 | "mysql": "^2.16.0", 23 | "pg": "^7.9.0", 24 | "prettier": "^1.16.4", 25 | "ts-jest": "^24.0.0", 26 | "ts-node": "^8.0.2", 27 | "typescript": "^3.3.1" 28 | }, 29 | "peerDependencies": { 30 | "mysql": "^2.16.0", 31 | "pg": "^7.9.0" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /__snapshots__/core.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`composes correctly 1`] = ` 4 | Array [ 5 | Object { 6 | "type": "string", 7 | "value": " 8 | SELECT * FROM (", 9 | }, 10 | Object { 11 | "type": "string", 12 | "value": " 13 | SELECT * FROM users 14 | ", 15 | }, 16 | Object { 17 | "type": "string", 18 | "value": ") all_users; 19 | ", 20 | }, 21 | ] 22 | `; 23 | 24 | exports[`parses a query 1`] = ` 25 | Array [ 26 | Object { 27 | "type": "string", 28 | "value": " 29 | SELECT first_name, last_name, age FROM table 30 | WHERE first_name = ", 31 | }, 32 | Object { 33 | "type": "primitive", 34 | "value": "Gal", 35 | }, 36 | Object { 37 | "type": "string", 38 | "value": " 39 | AND age = ", 40 | }, 41 | Object { 42 | "type": "computed", 43 | "value": [Function], 44 | }, 45 | Object { 46 | "type": "string", 47 | "value": " 48 | ", 49 | }, 50 | ] 51 | `; 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gal Schlezinger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | - image: postgres:10 12 | environment: 13 | POSTGRES_PASSWORD: password 14 | - image: mysql:5.7 15 | environment: 16 | MYSQL_ROOT_PASSWORD: password 17 | 18 | # Specify service dependencies here if necessary 19 | # CircleCI maintains a library of pre-built images 20 | # documented at https://circleci.com/docs/2.0/circleci-images/ 21 | # - image: circleci/mongo:3.4.4 22 | 23 | working_directory: ~/repo 24 | 25 | steps: 26 | - checkout 27 | 28 | # Download and cache dependencies 29 | - restore_cache: 30 | keys: 31 | - v1-dependencies-{{ checksum "package.json" }} 32 | # fallback to using the latest cache if no exact match is found 33 | - v1-dependencies- 34 | 35 | - run: npm ci 36 | 37 | - save_cache: 38 | paths: 39 | - node_modules 40 | key: v1-dependencies-{{ checksum "package.json" }} 41 | 42 | - run: npm run build 43 | 44 | # run tests! 45 | - run: 46 | name: test 47 | command: npm test -- --ci --reporters=default --reporters=jest-junit 48 | environment: 49 | JEST_JUNIT_OUTPUT: ./test-results/jest/results.xml 50 | 51 | - store_test_results: 52 | path: ./test-results 53 | -------------------------------------------------------------------------------- /mysql.spec.ts: -------------------------------------------------------------------------------- 1 | import expect from "expect"; 2 | import * as Mysql from "mysql"; 3 | import { sql, createSqlWithDefaults } from "./mysql"; 4 | 5 | let connection: Mysql.Connection; 6 | 7 | beforeAll(() => { 8 | connection = connect(); 9 | }); 10 | 11 | afterAll(() => { 12 | connection.end(); 13 | }); 14 | 15 | test("with default options", async () => { 16 | const sql = createSqlWithDefaults({ connection }); 17 | 18 | const simpleQuery = sql<{ name: string }, { providedName: string }>` 19 | SELECT ${p => p.name} AS providedName FROM DUAL 20 | `; 21 | 22 | const result = await simpleQuery.execute({ name: "Gal" }); 23 | 24 | expect(result[0].providedName).toBe("Gal"); 25 | }); 26 | 27 | test("simple query", async () => { 28 | const simpleQuery = sql<{ name: string }, { providedName: string }>` 29 | SELECT ${p => p.name} AS providedName FROM DUAL 30 | `; 31 | 32 | const result = await simpleQuery.execute({ name: "Gal" }, { connection }); 33 | 34 | expect(result[0].providedName).toBe("Gal"); 35 | }); 36 | 37 | test("composition", async () => { 38 | const simpleQuery = sql<{ name: string }, { providedName: string }>` 39 | SELECT ${p => p.name} AS name FROM DUAL 40 | `; 41 | const composition = sql<{ name: string }, { uppercased: string }>` 42 | SELECT UPPER(name) AS uppercased FROM (${simpleQuery}) simple_query 43 | `; 44 | 45 | const result = await composition.execute({ name: "Gal" }, { connection }); 46 | 47 | expect(result[0].uppercased).toBe("GAL"); 48 | }); 49 | 50 | function connect() { 51 | const connection = Mysql.createConnection({ 52 | host: "localhost", 53 | user: "root", 54 | password: "password" 55 | }); 56 | 57 | return connection; 58 | } 59 | -------------------------------------------------------------------------------- /core.ts: -------------------------------------------------------------------------------- 1 | type Primitive = string | boolean | number; 2 | export type Param = (data: T) => any; 3 | export type AstNode = 4 | | { 5 | type: "computed"; 6 | value: Param; 7 | } 8 | | { 9 | type: "primitive"; 10 | value: Primitive; 11 | } 12 | | { 13 | type: "string"; 14 | value: string; 15 | }; 16 | 17 | export abstract class Query { 18 | nodes: AstNode[]; 19 | constructor(nodes: AstNode[]) { 20 | this.nodes = nodes; 21 | } 22 | } 23 | 24 | class Raw { 25 | value: Primitive; 26 | 27 | constructor(value: Primitive) { 28 | this.value = value; 29 | } 30 | 31 | toString() { 32 | return String(this.value); 33 | } 34 | } 35 | 36 | export function raw(primitive: Primitive) { 37 | return new Raw(primitive); 38 | } 39 | 40 | export type SqlFunctionParam = 41 | | AstNode[] 42 | | Query 43 | | Param 44 | | Raw 45 | | Primitive; 46 | 47 | export function parse( 48 | strings: TemplateStringsArray, 49 | ...params: SqlFunctionParam[] 50 | ): AstNode[] { 51 | const nodes = [] as AstNode[]; 52 | 53 | for (const index in strings) { 54 | const string = strings[index]; 55 | const param = params[index]; 56 | 57 | nodes.push({ type: "string", value: string }); 58 | 59 | if (!param) { 60 | } else if (param instanceof Query) { 61 | nodes.push(...param.nodes); 62 | } else if (param instanceof Raw) { 63 | nodes.push({ type: "string", value: param.toString() }); 64 | } else if (Array.isArray(param)) { 65 | nodes.push(...param); 66 | } else if (typeof param === "function") { 67 | nodes.push({ type: "computed", value: param }); 68 | } else { 69 | nodes.push({ type: "primitive", value: param }); 70 | } 71 | } 72 | 73 | return nodes; 74 | } 75 | -------------------------------------------------------------------------------- /pg.spec.ts: -------------------------------------------------------------------------------- 1 | import { PostgresQuery, raw, sql, createSqlWithDefaults } from "./pg"; 2 | import Pg from "pg"; 3 | 4 | let pool: Pg.Pool; 5 | 6 | beforeAll(async () => { 7 | pool = new Pg.Pool({ 8 | user: "postgres", 9 | host: "localhost", 10 | port: 5432, 11 | password: "password", 12 | database: "postgres" 13 | }); 14 | }); 15 | 16 | afterAll(async () => { 17 | await pool.end(); 18 | }); 19 | 20 | test("with default", async () => { 21 | const sql = createSqlWithDefaults({ pool }); 22 | 23 | const simpleQuery = sql<{ name: string }, { name: string }>` 24 | SELECT ${p => p.name} AS name 25 | `; 26 | 27 | const result = await simpleQuery.execute({ name: "Gal" }); 28 | 29 | expect(result[0].name).toBe("Gal"); 30 | }); 31 | 32 | test("simple query", async () => { 33 | const simpleQuery = sql<{ name: string }, { name: string }>` 34 | SELECT ${p => p.name} AS name 35 | `; 36 | 37 | const result = await simpleQuery.execute({ name: "Gal" }, { pool }); 38 | 39 | expect(result[0].name).toBe("Gal"); 40 | }); 41 | 42 | test("composition", async () => { 43 | const simpleQuery = sql<{ name: string }, { name: string }>` 44 | SELECT ${p => p.name} AS name 45 | `; 46 | 47 | const composition = sql<{ name: string }, { uppercased: string }>` 48 | SELECT UPPER(simple.name) AS uppercased 49 | FROM (${simpleQuery}) simple 50 | `; 51 | 52 | const result = await composition.execute({ name: "Gal" }, { pool }); 53 | 54 | expect(result[0].uppercased).toBe("GAL"); 55 | }); 56 | 57 | test("a query composition function", async () => { 58 | function limit(query: PostgresQuery) { 59 | return sql` 60 | SELECT * 61 | FROM (${query}) LIMITED__QUERY__${raw(Math.floor(Math.random() * 99999))} 62 | LIMIT ${p => p.limit} 63 | OFFSET ${p => p.offset} 64 | `; 65 | } 66 | 67 | const simpleQuery = sql<{}, { name: string }>` 68 | SELECT * 69 | FROM (VALUES ('hello'), ('world')) names(name) 70 | `; 71 | 72 | const limited = limit(simpleQuery); 73 | 74 | const first = await limited.execute({ limit: 1, offset: 0 }, { pool }); 75 | const last = await limited.execute({ limit: 1, offset: 1 }, { pool }); 76 | 77 | expect(first[0].name).toBe("hello"); 78 | expect(last[0].name).toBe("world"); 79 | }); 80 | -------------------------------------------------------------------------------- /pg.ts: -------------------------------------------------------------------------------- 1 | import { Query, SqlFunctionParam, parse, Param, AstNode } from "./core"; 2 | import * as Pg from "pg"; 3 | 4 | type CompiledQuery = { 5 | queryString: string; 6 | params: Param[]; 7 | }; 8 | 9 | type ExecuteOptions = { 10 | pool: Pg.Pool; 11 | }; 12 | 13 | export class PostgresQuery extends Query { 14 | compiledQuery?: CompiledQuery; 15 | 16 | compile() { 17 | if (!this.compiledQuery) { 18 | this.compiledQuery = formatAst(this.nodes); 19 | } 20 | 21 | return this.compiledQuery; 22 | } 23 | 24 | async execute(data: Input, options: ExecuteOptions) { 25 | const queryData = this.compile(); 26 | const params = queryData.params.map(fn => fn(data)); 27 | const results = await options.pool.query(queryData.queryString, params); 28 | return results.rows as Output[]; 29 | } 30 | } 31 | 32 | export class PostgresQueryWithDefault extends PostgresQuery< 33 | Input, 34 | Output 35 | > { 36 | defaultOptions: ExecuteOptions; 37 | 38 | constructor(nodes: AstNode[], defaultOptions: ExecuteOptions) { 39 | super(nodes); 40 | this.defaultOptions = defaultOptions; 41 | } 42 | 43 | async execute(data: Input, options?: ExecuteOptions) { 44 | return super.execute(data, options || this.defaultOptions); 45 | } 46 | } 47 | 48 | function formatAst(nodes: AstNode[]): CompiledQuery { 49 | const params = [] as Param[]; 50 | let queryString = ""; 51 | 52 | for (const node of nodes) { 53 | switch (node.type) { 54 | case "string": 55 | queryString += node.value; 56 | break; 57 | case "primitive": 58 | params.push(() => node.value); 59 | queryString += `$${params.length}`; 60 | break; 61 | case "computed": 62 | params.push(node.value); 63 | queryString += `$${params.length}`; 64 | break; 65 | } 66 | } 67 | 68 | return { queryString, params }; 69 | } 70 | 71 | export function sql( 72 | strings: TemplateStringsArray, 73 | ...params: SqlFunctionParam[] 74 | ) { 75 | const nodes = parse(strings, ...params); 76 | return new PostgresQuery(nodes); 77 | } 78 | 79 | export function createSqlWithDefaults(defaultOptions: ExecuteOptions) { 80 | return function sql( 81 | strings: TemplateStringsArray, 82 | ...params: SqlFunctionParam[] 83 | ) { 84 | const nodes = parse(strings, ...params); 85 | return new PostgresQueryWithDefault(nodes, defaultOptions); 86 | }; 87 | } 88 | 89 | export { raw } from "./core"; 90 | -------------------------------------------------------------------------------- /mysql.ts: -------------------------------------------------------------------------------- 1 | import * as Mysql from "mysql"; 2 | import { parse, Param, AstNode, Query, SqlFunctionParam } from "./core"; 3 | 4 | type CompiledQuery = { 5 | queryString: string; 6 | params: Param[]; 7 | }; 8 | 9 | type ExecuteOptions = { 10 | connection: Mysql.Connection; 11 | }; 12 | 13 | export class MysqlQuery extends Query { 14 | compiledQuery?: CompiledQuery; 15 | 16 | compile() { 17 | if (!this.compiledQuery) { 18 | this.compiledQuery = formatAst(this.nodes); 19 | } 20 | 21 | return this.compiledQuery; 22 | } 23 | 24 | execute(data: Input, options: ExecuteOptions) { 25 | const formattedQuery = format(data, this.compile()); 26 | return new Promise((res, rej) => { 27 | options.connection.query(formattedQuery, (err, result) => { 28 | if (err) return rej(err); 29 | const output = result as Output[]; 30 | return res(output); 31 | }); 32 | }); 33 | } 34 | } 35 | 36 | export class MysqlQueryWithDefaults extends MysqlQuery< 37 | Input, 38 | Output 39 | > { 40 | defaultOptions: ExecuteOptions; 41 | 42 | constructor(nodes: AstNode[], defaultOptions: ExecuteOptions) { 43 | super(nodes); 44 | this.defaultOptions = defaultOptions; 45 | } 46 | 47 | async execute(data: Input, options?: ExecuteOptions) { 48 | return super.execute(data, options || this.defaultOptions); 49 | } 50 | } 51 | 52 | function formatAst(nodes: AstNode[]) { 53 | let queryString = ""; 54 | const params: Param[] = []; 55 | 56 | for (const node of nodes) { 57 | switch (node.type) { 58 | case "string": 59 | queryString += node.value; 60 | break; 61 | case "computed": 62 | queryString += "?"; 63 | params.push(node.value); 64 | break; 65 | case "primitive": 66 | queryString += "?"; 67 | params.push(function() { 68 | return node.value; 69 | }); 70 | break; 71 | } 72 | } 73 | 74 | return { queryString, params }; 75 | } 76 | 77 | export function sql( 78 | strings: TemplateStringsArray, 79 | ...params: SqlFunctionParam[] 80 | ) { 81 | const nodes = parse(strings, ...params); 82 | return new MysqlQuery(nodes); 83 | } 84 | 85 | export function format( 86 | data: A, 87 | { queryString, params }: CompiledQuery 88 | ) { 89 | return Mysql.format(queryString, params.map(fn => fn(data))); 90 | } 91 | 92 | export function createSqlWithDefaults(options: ExecuteOptions) { 93 | return function sql( 94 | strings: TemplateStringsArray, 95 | ...params: SqlFunctionParam[] 96 | ) { 97 | const nodes = parse(strings, ...params); 98 | return new MysqlQueryWithDefaults(nodes, options); 99 | }; 100 | } 101 | 102 | export { raw } from "./core"; 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cuery - Composable SQL Querying [![CircleCI status](https://circleci.com/gh/Schniz/cuery.svg?style=svg)](https://circleci.com/gh/Schniz/cuery) 2 | 3 | > A composable SQL query builder based inspired by 4 | > [styled-components :nail_care:](https://styled-components.com) :sparkles: 5 | 6 | :dancer: Replace weird `$1` or `?` in your queries with simple functions! 7 | 8 | :star: PostgreSQL and MySQL support! 9 | 10 | :lock: Type safety (and autocompletion) with TypeScript 11 | 12 | ## Why 13 | 14 | In 2016, I wrote a blog post about 15 | [composing SQL queries](https://medium.com/@galstar/composable-sql-in-javascript-db51d9cae017) 16 | and published this library as a reference. The years passed, and there are much 17 | cooler ways of doing it, so this is the new way - using template literals. 18 | 19 | # Installation 20 | 21 | For PostgreSQL users: 22 | 23 | ```bash 24 | yarn add cuery pg 25 | # or 26 | npm install --save cuery pg 27 | ``` 28 | 29 | For MySQL users: 30 | 31 | ```bash 32 | yarn add cuery mysql 33 | # or 34 | npm install --save cuery mysql 35 | ``` 36 | 37 | # API 38 | 39 | Import the modules for the database you use: 40 | 41 | - `cuery/pg` for PostgreSQL 42 | - `cuery/mysql` for MySQL 43 | 44 | Both modules export the same two basic functions: 45 | 46 | ### `sql` template literal 47 | 48 | The `sql` template literal is meant for constructing an SQL query. It accepts functions, that will be acted as "getters" from the object you supply to the execute function, and compose other SQL queries too. 49 | 50 | The two generics are meant for type safety, so you would declare your input and output types co-located with your query, just like a function: `(input: Input) => Output`. 51 | 52 | It returns an SQL query, that later can be `execute`d with the options needed, such as a `pool` (or a `connection` in MySQL) 53 | 54 | ```ts 55 | const returnsNumber = sql< 56 | {}, // Takes no parameters as input 57 | { age: number } 58 | >` // Returns a number as output 59 | SELECT 27 AS age 60 | `; 61 | 62 | const takesNumberAndReturnsIt = sql< 63 | { age: number }, // Takes a number as input 64 | { age: number } 65 | >` // Returns a number as output 66 | SELECT ${p => p.age} AS age 67 | `; 68 | 69 | (await takesNumberAndReturnsIt.execute({ age: 27 }, { pool: new Pg.Pool() }))[0] 70 | .age === 27; 71 | ``` 72 | 73 | ### `createSqlWithDefaults(defaults)` 74 | 75 | This function returns an `sql` template literal function, that defaults to a specific execute options. 76 | Normally, it would be stored in a specific file in your project, that contains the information about the database connection, so you won't need to pass it all around your application. 77 | 78 | ```ts 79 | const sql = createSqlWithDefaults({ pool: new Pg.Pool() }); 80 | const query = sql<{}, { age: number }>`SELECT 27 AS age`; 81 | (await query.execute({}))[0].age === 27; 82 | ``` 83 | 84 | ### `raw` 85 | 86 | This function is a helper function to say that the primitive passed into this function should be stringified and be added "as is" to the query. This is unsafe by nature, but when used correctly can have good implications like generating table names. 87 | 88 | ```ts 89 | sql<{}, {}>`SELECT 27 AS ${raw("age")}`; 90 | ``` 91 | 92 | # Usage 93 | 94 | ### PostgreSQL 95 | 96 | ```ts 97 | import { sql } from "cuery/pg"; 98 | 99 | const usersQuery = sql`SELECT name, age FROM users`; 100 | const usersWithNameQuery = sql<{ name: string }, { name: string; age: number }>` 101 | SELECT name, age FROM (${usersQuery}) 102 | WHERE name = ${params => params.name} 103 | `; 104 | 105 | // pool = new Pg.Pool() 106 | 107 | const rows = await usersWithNameQuery.execute({ name: "John" }, { pool }); 108 | rows[0].age; // Type safe! 109 | ``` 110 | 111 | ### MySQL 112 | 113 | ```ts 114 | import { sql } from "cuery/mysql"; 115 | 116 | const usersQuery = sql`SELECT name, age FROM users`; 117 | const usersWithNameQuery = sql<{ name: string }, { name: string; age: number }>` 118 | SELECT name, age FROM (${usersQuery}) 119 | WHERE name = ${params => params.name} 120 | `; 121 | 122 | // connection = create a new mysql connection 123 | 124 | const rows = await usersWithNameQuery.execute({ name: "John" }, { connection }); 125 | rows[0].age; // Type safe! 126 | ``` 127 | 128 | ## Transformations 129 | 130 | You can declare helper methods that do magic on your queries, like `limit`: 131 | 132 | ```ts 133 | function limit(query: Query) { 134 | return sql` 135 | SELECT * 136 | FROM (${query}) LIMITED__QUERY__${raw(Math.floor(Math.random() * 99999))} 137 | LIMIT ${p => p.limit} 138 | OFFSET ${p => p.offset} 139 | `; 140 | } 141 | 142 | // then you can just compose your queries! 143 | 144 | const users = sql< 145 | {}, 146 | { name: string; age: number } 147 | >`SELECT name, age FROM users`; 148 | const usersWithLimit = limit(users); 149 | execute(usersWithLimit, { limit: 10, offset: 10 }); // start with offset of 10, then take 10 records. 150 | ``` 151 | 152 | # Running tests 153 | 154 | ```bash 155 | docker run --rm -d -p 5432:5432 -e POSTGRES_PASSWORD=password postgres:10 156 | docker run --rm -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password mysql:5.7 157 | npm test 158 | ``` 159 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "dom", 8 | "es2018" 9 | ] /* Specify library files to be included in the compilation. */, 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | "types": [ 49 | "jest" 50 | ] /* Type declaration files to be included in compilation. */, 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | } 65 | } 66 | --------------------------------------------------------------------------------