crudely-typed
Advanced tools
Comparing version 0.1.0 to 0.1.1
@@ -17,3 +17,3 @@ export interface QueryResult { | ||
constructor(schema: SchemaT, tableName: keyof SchemaT); | ||
select<Cols extends null | keyof TableT = null, WhereCols extends keyof TableT | SQLAny<keyof TableT & string> = never, Joins extends Record<string, keyof TableT> = never, IsSingular extends boolean = false>(opts?: { | ||
select<Cols extends null | keyof TableT = null, WhereCols extends keyof TableT | SQLAny<keyof TableT & string> = never, Joins extends Record<string, keyof LooseKey3<SchemaT, Table, 'foreignKeys'>> = never, IsSingular extends boolean = false>(opts?: { | ||
columns?: Cols[]; | ||
@@ -25,3 +25,3 @@ where?: WhereCols[]; | ||
}): (...args: [Extract<WhereCols, string>, WhereCols extends SQLAny<infer C> ? C : never] extends [never, never] ? [db: Queryable] : [db: Queryable, where: Resolve<Resolve<Pick<LooseKey3<SchemaT, Table, "$type">, Extract<WhereCols, string> & keyof LooseKey3<SchemaT, Table, "$type">>> & { [K in (WhereCols extends SQLAny<infer C> ? C : never) & string]: Set<LooseKey3<SchemaT, Table, "$type">[K & keyof LooseKey3<SchemaT, Table, "$type">]> | readonly LooseKey3<SchemaT, Table, "$type">[K & keyof LooseKey3<SchemaT, Table, "$type">][]; }>]) => [Cols, Joins] extends [null, never] ? Promise<Result<LooseKey3<SchemaT, Table, "$type">, IsSingular>> : [Cols] extends [null] ? Promise<Result<LooseKey3<SchemaT, Table, "$type"> & Resolve<Join<LooseKey<SchemaT, Table>, Joins>>, IsSingular>> : Promise<Result<Resolve<Resolve<Pick<LooseKey3<SchemaT, Table, "$type">, Cols & keyof LooseKey3<SchemaT, Table, "$type">>> & Join<LooseKey<SchemaT, Table>, Joins>>, IsSingular>>; | ||
selectByPrimaryKey<Cols extends null | keyof TableT = null, Joins extends Record<string, keyof TableT> = never>(opts?: { | ||
selectByPrimaryKey<Cols extends null | keyof TableT = null, Joins extends Record<string, keyof LooseKey3<SchemaT, Table, 'foreignKeys'>> = never>(opts?: { | ||
columns?: Cols[]; | ||
@@ -32,6 +32,6 @@ join?: Joins; | ||
disallowColumns?: DisallowedColumns[]; | ||
}): (db: Queryable, row: Omit<LooseKey3<SchemaT, Table, "$input">, DisallowedColumns & keyof LooseKey3<SchemaT, Table, "$input">>) => Promise<LooseKey3<SchemaT, Table, "$type">>; | ||
}): (db: Queryable, row: [DisallowedColumns] extends [never] ? LooseKey3<SchemaT, Table, "$input"> : Omit<LooseKey3<SchemaT, Table, "$input">, DisallowedColumns & keyof LooseKey3<SchemaT, Table, "$input">>) => Promise<LooseKey3<SchemaT, Table, "$type">>; | ||
insertMultiple<DisallowedColumns extends keyof TableT = never>(opts?: { | ||
disallowColumns?: DisallowedColumns[]; | ||
}): (db: Queryable, rows: Omit<LooseKey3<SchemaT, Table, "$input">, DisallowedColumns & keyof LooseKey3<SchemaT, Table, "$input">>[]) => Promise<LooseKey3<SchemaT, Table, "$type">[]>; | ||
}): (db: Queryable, rows: [DisallowedColumns] extends [never] ? readonly LooseKey3<SchemaT, Table, "$input">[] : readonly Omit<LooseKey3<SchemaT, Table, "$input">, DisallowedColumns & keyof LooseKey3<SchemaT, Table, "$input">>[]) => Promise<LooseKey3<SchemaT, Table, "$type">[]>; | ||
update<SetCols extends null | keyof TableT = null, WhereCols extends keyof TableT | SQLAny<keyof TableT & string> = never, IsSingular extends boolean = false>(opts?: { | ||
@@ -38,0 +38,0 @@ set?: SetCols[]; |
"use strict"; | ||
/* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
return new (P || (P = Promise))(function (resolve, reject) { | ||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } | ||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } | ||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } | ||
step((generator = generator.apply(thisArg, _arguments || [])).next()); | ||
}); | ||
}; | ||
var __generator = (this && this.__generator) || function (thisArg, body) { | ||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; | ||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; | ||
function verb(n) { return function (v) { return step([n, v]); }; } | ||
function step(op) { | ||
if (f) throw new TypeError("Generator is already executing."); | ||
while (_) try { | ||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; | ||
if (y = 0, t) op = [op[0] & 2, t.value]; | ||
switch (op[0]) { | ||
case 0: case 1: t = op; break; | ||
case 4: _.label++; return { value: op[1], done: false }; | ||
case 5: _.label++; y = op[1]; op = [0]; continue; | ||
case 7: op = _.ops.pop(); _.trys.pop(); continue; | ||
default: | ||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } | ||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } | ||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } | ||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } | ||
if (t[2]) _.ops.pop(); | ||
_.trys.pop(); continue; | ||
} | ||
op = body.call(thisArg, _); | ||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } | ||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; | ||
} | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.any = exports.TableBuilder = exports.TypedSQL = void 0; | ||
var TypedSQL = /** @class */ (function () { | ||
function TypedSQL(schema) { | ||
class TypedSQL { | ||
constructor(schema) { | ||
this.schema = schema; | ||
} | ||
TypedSQL.prototype.table = function (tableName) { | ||
table(tableName) { | ||
return new TableBuilder(this.schema, tableName); | ||
}; | ||
return TypedSQL; | ||
}()); | ||
} | ||
} | ||
exports.TypedSQL = TypedSQL; | ||
var TableBuilder = /** @class */ (function () { | ||
function TableBuilder(schema, tableName) { | ||
class TableBuilder { | ||
constructor(schema, tableName) { | ||
this.schema = schema; | ||
this.tableName = tableName; | ||
} | ||
TableBuilder.prototype.select = function (opts) { | ||
select(opts) { | ||
var _a, _b, _c, _d, _e; | ||
var where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
var whereCols = where.filter(function (col) { return !isSQLAny(col); }); | ||
var whereAnyCols = where.filter(isSQLAny); | ||
const where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
const whereCols = where.filter(col => !isSQLAny(col)); | ||
const whereAnyCols = where.filter(isSQLAny); | ||
// TODO: eliminate Select<> entirely. | ||
return new Select(this.schema[this.tableName], this.tableName, ((_b = opts === null || opts === void 0 ? void 0 : opts.columns) !== null && _b !== void 0 ? _b : null), whereCols, whereAnyCols, ((_c = opts === null || opts === void 0 ? void 0 : opts.orderBy) !== null && _c !== void 0 ? _c : null), ((_d = opts === null || opts === void 0 ? void 0 : opts.join) !== null && _d !== void 0 ? _d : null), (_e = opts === null || opts === void 0 ? void 0 : opts.limitOne) !== null && _e !== void 0 ? _e : false).build(); | ||
}; | ||
} | ||
// TODO: disallow this method if primaryKey=null | ||
TableBuilder.prototype.selectByPrimaryKey = function (opts) { | ||
return this.select(__assign(__assign({}, opts), { where: [this.schema[this.tableName].primaryKey], limitOne: true })); | ||
}; | ||
TableBuilder.prototype.insert = function (opts) { | ||
selectByPrimaryKey(opts) { | ||
return this.select(Object.assign(Object.assign({}, opts), { where: [this.schema[this.tableName].primaryKey], limitOne: true })); | ||
} | ||
insert(opts) { | ||
var _a; | ||
return new Insert(this.schema[this.tableName], this.tableName, ((_a = opts === null || opts === void 0 ? void 0 : opts.disallowColumns) !== null && _a !== void 0 ? _a : null)).build(); | ||
}; | ||
TableBuilder.prototype.insertMultiple = function (opts) { | ||
} | ||
insertMultiple(opts) { | ||
var _a; | ||
return new InsertMultiple(this.schema[this.tableName], this.tableName, ((_a = opts === null || opts === void 0 ? void 0 : opts.disallowColumns) !== null && _a !== void 0 ? _a : null)).build(); | ||
}; | ||
TableBuilder.prototype.update = function (opts) { | ||
} | ||
// TODO: this should include a disallowColumns for dynamic set | ||
update(opts) { | ||
var _a, _b, _c; | ||
var where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
var whereCols = where.filter(function (col) { return !isSQLAny(col); }); | ||
var whereAnyCols = where.filter(isSQLAny); | ||
const where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
const whereCols = where.filter(col => !isSQLAny(col)); | ||
const whereAnyCols = where.filter(isSQLAny); | ||
return new Update(this.tableName, whereCols, whereAnyCols, ((_b = opts === null || opts === void 0 ? void 0 : opts.set) !== null && _b !== void 0 ? _b : null), ((_c = opts === null || opts === void 0 ? void 0 : opts.limitOne) !== null && _c !== void 0 ? _c : false)).build(); | ||
}; | ||
TableBuilder.prototype.updateByPrimaryKey = function (opts) { | ||
return this.update(__assign(__assign({}, opts), { where: [this.schema[this.tableName].primaryKey], limitOne: true })); | ||
}; | ||
TableBuilder.prototype.delete = function (opts) { | ||
} | ||
updateByPrimaryKey(opts) { | ||
return this.update(Object.assign(Object.assign({}, opts), { where: [this.schema[this.tableName].primaryKey], limitOne: true })); | ||
} | ||
delete(opts) { | ||
var _a, _b; | ||
var where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
var whereCols = where.filter(function (col) { return !isSQLAny(col); }); | ||
var whereAnyCols = where.filter(isSQLAny); | ||
const where = ((_a = opts === null || opts === void 0 ? void 0 : opts.where) !== null && _a !== void 0 ? _a : []); | ||
const whereCols = where.filter(col => !isSQLAny(col)); | ||
const whereAnyCols = where.filter(isSQLAny); | ||
return new Delete(this.tableName, whereCols, whereAnyCols, ((_b = opts === null || opts === void 0 ? void 0 : opts.limitOne) !== null && _b !== void 0 ? _b : false)).build(); | ||
}; | ||
TableBuilder.prototype.deleteByPrimaryKey = function () { | ||
} | ||
deleteByPrimaryKey() { | ||
return this.delete({ | ||
@@ -110,5 +61,4 @@ where: [this.schema[this.tableName].primaryKey], | ||
}); | ||
}; | ||
return TableBuilder; | ||
}()); | ||
} | ||
} | ||
exports.TableBuilder = TableBuilder; | ||
@@ -122,4 +72,4 @@ function any(column) { | ||
} | ||
var Select = /** @class */ (function () { | ||
function Select(tableSchema, table, cols, whereCols, whereAnyCols, order, joins, isSingular) { | ||
class Select { | ||
constructor(tableSchema, table, cols, whereCols, whereAnyCols, order, joins, isSingular) { | ||
this.tableSchema = tableSchema; | ||
@@ -134,92 +84,71 @@ this.table = table; | ||
} | ||
Select.prototype.build = function () { | ||
var _this = this; | ||
var what = ['*']; | ||
build() { | ||
let what = ['*']; | ||
if (this.cols) { | ||
what = this.cols; | ||
} | ||
var query = "SELECT ".concat(what.join(', '), " FROM ").concat(this.table); | ||
var joins = []; | ||
let query = `SELECT ${what.join(', ')} FROM ${this.table}`; | ||
let joins = []; | ||
if (this.joins) { | ||
var joinNameCols = Object.entries(this.joins); | ||
query = "SELECT ".concat(what.map(function (c) { return "t1.".concat(c); }).join(', '), ", "); | ||
const joinNameCols = Object.entries(this.joins); | ||
query = `SELECT ${what.map(c => `t1.${c}`).join(', ')}, `; | ||
query += joinNameCols | ||
.map(function (_a, i) { | ||
var joinName = _a[0]; | ||
return "to_jsonb(t".concat(i + 2, ".*) as ").concat(joinName); | ||
}) | ||
.map(([joinName], i) => `to_jsonb(t${i + 2}.*) as ${joinName}`) | ||
.join(', '); | ||
query += " FROM ".concat(this.table, " as t1"); | ||
joins = joinNameCols.map(function (_a, i) { | ||
var _ = _a[0], col = _a[1]; | ||
var fkey = _this.tableSchema.foreignKeys[col]; | ||
var n = i + 2; | ||
return " JOIN ".concat(fkey.table, " AS t").concat(n, " ON t1.").concat(col, " = t").concat(n, ".").concat(fkey.column); | ||
query += ` FROM ${this.table} as t1`; | ||
joins = joinNameCols.map(([_, col], i) => { | ||
const fkey = this.tableSchema.foreignKeys[col]; | ||
const n = i + 2; | ||
return ` JOIN ${fkey.table} AS t${n} ON t1.${col} = t${n}.${fkey.column}`; | ||
}); | ||
query += joins.join(''); | ||
} | ||
var whereKeys = []; | ||
var whereClauses = []; | ||
var tab = this.joins ? 't1.' : ''; | ||
const whereKeys = []; | ||
const whereClauses = []; | ||
const tab = this.joins ? 't1.' : ''; | ||
if (this.whereCols) { | ||
for (var _i = 0, _a = this.whereCols; _i < _a.length; _i++) { | ||
var col = _a[_i]; | ||
for (const col of this.whereCols) { | ||
whereKeys.push(col); | ||
var n = whereKeys.length; | ||
const n = whereKeys.length; | ||
// XXX pg-promise requires a cast here for UUID columns (${tab}${col}::text) | ||
// while node-postgres does not require it. | ||
whereClauses.push("".concat(tab).concat(col, " = $").concat(n)); | ||
whereClauses.push(`${tab}${col} = $${n}`); | ||
} | ||
} | ||
if (this.whereAnyCols) { | ||
for (var _b = 0, _c = this.whereAnyCols; _b < _c.length; _b++) { | ||
var anyCol = _c[_b]; | ||
var col = anyCol.__any; | ||
for (const anyCol of this.whereAnyCols) { | ||
const col = anyCol.__any; | ||
whereKeys.push(col); | ||
var n = whereKeys.length; | ||
whereClauses.push("".concat(tab).concat(col, " = ANY($").concat(n, ")")); | ||
const n = whereKeys.length; | ||
whereClauses.push(`${tab}${col} = ANY($${n})`); | ||
} | ||
} | ||
if (whereClauses.length) { | ||
query += " WHERE ".concat(whereClauses.join(' AND ')); | ||
query += ` WHERE ${whereClauses.join(' AND ')}`; | ||
} | ||
if (this.order) { | ||
var orderClause = this.order.map(function (_a) { | ||
var col = _a[0], dir = _a[1]; | ||
return "".concat(col, " ").concat(dir); | ||
}); | ||
query += " ORDER BY ".concat(orderClause); | ||
const orderClause = this.order.map(([col, dir]) => `${col} ${dir}`); | ||
query += ` ORDER BY ${orderClause}`; | ||
} | ||
return function (db, whereObj) { return __awaiter(_this, void 0, void 0, function () { | ||
var where, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
where = whereKeys.map(function (col) { | ||
return whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]; | ||
}); | ||
return [4 /*yield*/, db.query(query, where)]; | ||
case 1: | ||
result = _a.sent(); | ||
if (this.isSingular) { | ||
if (result.rowCount === 0) { | ||
return [2 /*return*/, null]; | ||
} | ||
else if (result.rowCount === 1) { | ||
return [2 /*return*/, result.rows[0]]; | ||
} | ||
// TODO: is it helpful or harmful to add a LIMIT 1 to the query? | ||
throw new Error('Got multiple results for singular query'); | ||
} | ||
return [2 /*return*/, result.rows]; | ||
return async (db, whereObj) => { | ||
const where = whereKeys.map(col => whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]); | ||
const result = await db.query(query, where); | ||
if (this.isSingular) { | ||
if (result.rowCount === 0) { | ||
return null; | ||
} | ||
}); | ||
}); }; | ||
}; | ||
return Select; | ||
}()); | ||
var Insert = /** @class */ (function () { | ||
function Insert(tableSchema, table, disallowedColumns) { | ||
else if (result.rowCount === 1) { | ||
return result.rows[0]; | ||
} | ||
// TODO: is it helpful or harmful to add a LIMIT 1 to the query? | ||
throw new Error('Got multiple results for singular query'); | ||
} | ||
return result.rows; | ||
}; | ||
} | ||
} | ||
class Insert { | ||
constructor(tableSchema, table, disallowedColumns) { | ||
this.tableSchema = tableSchema; | ||
@@ -229,42 +158,33 @@ this.table = table; | ||
} | ||
Insert.prototype.build = function () { | ||
var _this = this; | ||
build() { | ||
// TODO: define an interface for this | ||
var allColumns = this.tableSchema.columns; | ||
var disallowedColumns = this.disallowedColumns; | ||
var allowedColumns = disallowedColumns | ||
? allColumns.filter(function (col) { return !disallowedColumns.includes(col); }) | ||
const allColumns = this.tableSchema.columns; | ||
const disallowedColumns = this.disallowedColumns; | ||
const allowedColumns = disallowedColumns | ||
? allColumns.filter(col => !disallowedColumns.includes(col)) | ||
: allColumns; | ||
return function (db, obj) { return __awaiter(_this, void 0, void 0, function () { | ||
var illegalCols, keys, placeholders, colsSql, placeholderSql, query, vals, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (disallowedColumns) { | ||
illegalCols = disallowedColumns.filter(function (col) { return obj[col] !== undefined; }); | ||
if (illegalCols.length > 0) { | ||
throw new Error("Cannot insert disallowed column(s) ".concat(illegalCols)); | ||
} | ||
} | ||
keys = allowedColumns.filter(function (col) { return obj[col] !== undefined; }); | ||
placeholders = keys.map(function (_col, i) { return "$".concat(i + 1); }); | ||
colsSql = keys.join(', '); | ||
placeholderSql = placeholders.join(', '); | ||
query = "INSERT INTO ".concat(this.table, "(").concat(colsSql, ") VALUES (").concat(placeholderSql, ") RETURNING *"); | ||
vals = keys.map(function (col) { return obj[col]; }); | ||
return [4 /*yield*/, db.query(query, vals)]; | ||
case 1: | ||
result = _a.sent(); | ||
if (result.rowCount === 0) { | ||
return [2 /*return*/, null]; // should be an error? | ||
} | ||
return [2 /*return*/, result.rows[0]]; | ||
return async (db, obj) => { | ||
if (disallowedColumns) { | ||
const illegalCols = disallowedColumns.filter(col => obj[col] !== undefined); | ||
if (illegalCols.length > 0) { | ||
throw new Error(`Cannot insert disallowed column(s) ${illegalCols}`); | ||
} | ||
}); | ||
}); }; | ||
}; | ||
return Insert; | ||
}()); | ||
var InsertMultiple = /** @class */ (function () { | ||
function InsertMultiple(tableSchema, table, disallowedColumns) { | ||
} | ||
const keys = allowedColumns.filter(col => obj[col] !== undefined); | ||
const placeholders = keys.map((_col, i) => `$${i + 1}`); | ||
// TODO: quoting for table / column names everywhere | ||
const colsSql = keys.join(', '); | ||
const placeholderSql = placeholders.join(', '); | ||
const query = `INSERT INTO ${this.table}(${colsSql}) VALUES (${placeholderSql}) RETURNING *`; | ||
const vals = keys.map(col => obj[col]); | ||
const result = await db.query(query, vals); | ||
if (result.rowCount === 0) { | ||
return null; // should be an error? | ||
} | ||
return result.rows[0]; | ||
}; | ||
} | ||
} | ||
class InsertMultiple { | ||
constructor(tableSchema, table, disallowedColumns) { | ||
this.tableSchema = tableSchema; | ||
@@ -274,53 +194,39 @@ this.table = table; | ||
} | ||
InsertMultiple.prototype.build = function () { | ||
var _this = this; | ||
var allColumns = this.tableSchema.columns; | ||
var disallowedColumns = this.disallowedColumns; | ||
var allowedColumns = disallowedColumns | ||
? allColumns.filter(function (col) { return !disallowedColumns.includes(col); }) | ||
build() { | ||
const allColumns = this.tableSchema.columns; | ||
const disallowedColumns = this.disallowedColumns; | ||
const allowedColumns = disallowedColumns | ||
? allColumns.filter(col => !disallowedColumns.includes(col)) | ||
: allColumns; | ||
return function (db, rows) { return __awaiter(_this, void 0, void 0, function () { | ||
var illegalCols, keys, colsSql, placeholder, insertSqls, vals, _loop_1, _i, rows_1, row, placeholderSql, query, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (disallowedColumns) { | ||
illegalCols = disallowedColumns.filter(function (col) { | ||
return rows.some(function (row) { return row[col] !== undefined; }); | ||
}); | ||
if (illegalCols.length > 0) { | ||
throw new Error("Cannot insert disallowed column(s) ".concat(illegalCols)); | ||
} | ||
} | ||
if (rows.length === 0) { | ||
return [2 /*return*/, []]; // TODO(danvk): consider throwing in this case | ||
} | ||
keys = allowedColumns.filter(function (col) { return rows[0][col] !== undefined; }); | ||
colsSql = keys.join(', '); | ||
placeholder = 1; | ||
insertSqls = []; | ||
vals = []; | ||
_loop_1 = function (row) { | ||
insertSqls.push('(' + keys.map(function (_col, i) { return "$".concat(placeholder + i); }).join(',') + ')'); | ||
placeholder += keys.length; | ||
vals = vals.concat(keys.map(function (k) { return row[k]; })); | ||
}; | ||
for (_i = 0, rows_1 = rows; _i < rows_1.length; _i++) { | ||
row = rows_1[_i]; | ||
_loop_1(row); | ||
} | ||
placeholderSql = insertSqls.join(', '); | ||
query = "INSERT INTO ".concat(this.table, "(").concat(colsSql, ") VALUES ").concat(placeholderSql, " RETURNING *"); | ||
return [4 /*yield*/, db.query(query, vals)]; | ||
case 1: | ||
result = _a.sent(); | ||
return [2 /*return*/, result.rows]; | ||
return async (db, rows) => { | ||
if (disallowedColumns) { | ||
const illegalCols = disallowedColumns.filter(col => rows.some(row => row[col] !== undefined)); | ||
if (illegalCols.length > 0) { | ||
throw new Error(`Cannot insert disallowed column(s) ${illegalCols}`); | ||
} | ||
}); | ||
}); }; | ||
}; | ||
return InsertMultiple; | ||
}()); | ||
var Update = /** @class */ (function () { | ||
function Update(table, whereCols, whereAnyCols, setCols, isSingular) { | ||
} | ||
if (rows.length === 0) { | ||
return []; // TODO(danvk): consider throwing in this case | ||
} | ||
const keys = allowedColumns.filter(col => rows[0][col] !== undefined); | ||
const colsSql = keys.join(', '); | ||
let placeholder = 1; | ||
const insertSqls = []; | ||
let vals = []; | ||
for (const row of rows) { | ||
insertSqls.push('(' + keys.map((_col, i) => `$${placeholder + i}`).join(',') + ')'); | ||
placeholder += keys.length; | ||
vals = vals.concat(keys.map(k => row[k])); | ||
} | ||
// TODO: quoting for table / column names everywhere | ||
const placeholderSql = insertSqls.join(', '); | ||
// TODO: some ability to control 'returning' would be especially useful here. | ||
const query = `INSERT INTO ${this.table}(${colsSql}) VALUES ${placeholderSql} RETURNING *`; | ||
const result = await db.query(query, vals); | ||
return result.rows; | ||
}; | ||
} | ||
} | ||
class Update { | ||
constructor(table, whereCols, whereAnyCols, setCols, isSingular) { | ||
this.table = table; | ||
@@ -332,69 +238,51 @@ this.whereCols = whereCols; | ||
} | ||
Update.prototype.clone = function () { | ||
return new Update(this.table, this.whereCols, this.whereAnyCols, this.setCols, this.isSingular); | ||
}; | ||
Update.prototype.build = function () { | ||
var _this = this; | ||
var placeholder = 1; | ||
var setKeys = []; | ||
var setClauses = []; | ||
var setCols = this.setCols; | ||
build() { | ||
let placeholder = 1; | ||
const setKeys = []; | ||
const setClauses = []; | ||
const setCols = this.setCols; | ||
if (setCols) { | ||
for (var _i = 0, setCols_1 = setCols; _i < setCols_1.length; _i++) { | ||
var col = setCols_1[_i]; | ||
for (const col of setCols) { | ||
setKeys.push(col); | ||
var n = placeholder++; | ||
setClauses.push("".concat(col, " = $").concat(n)); | ||
const n = placeholder++; | ||
setClauses.push(`${col} = $${n}`); | ||
} | ||
} | ||
var whereKeys = []; | ||
var whereClauses = []; | ||
const whereKeys = []; | ||
const whereClauses = []; | ||
if (this.whereCols) { | ||
for (var _a = 0, _b = this.whereCols; _a < _b.length; _a++) { | ||
var col = _b[_a]; | ||
for (const col of this.whereCols) { | ||
whereKeys.push(col); | ||
var n = placeholder++; | ||
whereClauses.push("".concat(col, " = $").concat(n)); | ||
const n = placeholder++; | ||
whereClauses.push(`${col} = $${n}`); | ||
} | ||
} | ||
if (this.whereAnyCols) { | ||
for (var _c = 0, _d = this.whereAnyCols; _c < _d.length; _c++) { | ||
var anyCol = _d[_c]; | ||
var col = anyCol.__any; | ||
for (const anyCol of this.whereAnyCols) { | ||
const col = anyCol.__any; | ||
whereKeys.push(col); | ||
var n = placeholder++; | ||
whereClauses.push("".concat(col, " = ANY($").concat(n, ")")); | ||
const n = placeholder++; | ||
whereClauses.push(`${col} = ANY($${n})`); | ||
} | ||
} | ||
var whereClause = whereClauses.length | ||
? " WHERE ".concat(whereClauses.join(' AND ')) | ||
const whereClause = whereClauses.length | ||
? ` WHERE ${whereClauses.join(' AND ')}` | ||
: ''; | ||
var limitClause = ''; // this.isSingular ? ' LIMIT 1' : ''; | ||
const limitClause = ''; // this.isSingular ? ' LIMIT 1' : ''; | ||
if (setCols) { | ||
// In this case the query can be determined in advance | ||
var query_1 = setCols | ||
? "UPDATE ".concat(this.table, " SET ").concat(setClauses.join(', ')).concat(whereClause).concat(limitClause, " RETURNING *") | ||
: null; | ||
return function (db, whereObj, updateObj) { return __awaiter(_this, void 0, void 0, function () { | ||
var vals, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
vals = setCols | ||
.map(function (col) { return updateObj[col]; }) | ||
.concat(whereKeys.map(function (col) { | ||
return whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]; | ||
})); | ||
return [4 /*yield*/, db.query(query_1, vals)]; | ||
case 1: | ||
result = _a.sent(); | ||
if (this.isSingular) { | ||
return [2 /*return*/, result.rowCount === 0 ? null : result.rows[0]]; | ||
} | ||
return [2 /*return*/, result.rows]; | ||
} | ||
}); | ||
}); }; | ||
const setSql = setClauses.join(', '); | ||
const query = `UPDATE ${this.table} SET ${setSql}${whereClause}${limitClause} RETURNING *`; | ||
return async (db, whereObj, updateObj) => { | ||
const vals = setCols | ||
.map(col => updateObj[col]) | ||
.concat(whereKeys.map(col => whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col])); | ||
const result = await db.query(query, vals); | ||
if (this.isSingular) { | ||
return result.rowCount === 0 ? null : result.rows[0]; | ||
} | ||
return result.rows; | ||
}; | ||
} | ||
@@ -404,42 +292,30 @@ // In this case the query is dynamic. | ||
// TODO: major shadowing bugs here | ||
return function (db, whereObj, updateObj) { return __awaiter(_this, void 0, void 0, function () { | ||
var dynamicSetCols, vals, dynamicPlaceholder, dynamicSetKeys, dynamicSetClauses, _i, dynamicSetCols_1, col, n, query, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
dynamicSetCols = Object.keys(updateObj); | ||
vals = whereKeys | ||
.map(function (col) { | ||
return whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]; | ||
}) | ||
.concat(dynamicSetCols.map(function (col) { return updateObj[col]; })); | ||
dynamicPlaceholder = placeholder; | ||
dynamicSetKeys = []; | ||
dynamicSetClauses = []; | ||
for (_i = 0, dynamicSetCols_1 = dynamicSetCols; _i < dynamicSetCols_1.length; _i++) { | ||
col = dynamicSetCols_1[_i]; | ||
dynamicSetKeys.push(col); | ||
n = dynamicPlaceholder++; | ||
dynamicSetClauses.push("".concat(col, " = $").concat(n)); | ||
} | ||
query = dynamicSetCols | ||
? "UPDATE ".concat(this.table, " SET ").concat(dynamicSetClauses.join(', ')).concat(whereClause).concat(limitClause, " RETURNING *") | ||
: null; | ||
return [4 /*yield*/, db.query(query, vals)]; | ||
case 1: | ||
result = _a.sent(); | ||
if (this.isSingular) { | ||
return [2 /*return*/, result.rowCount === 0 ? null : result.rows[0]]; | ||
} | ||
return [2 /*return*/, result.rows]; | ||
} | ||
}); | ||
}); }; | ||
}; | ||
return Update; | ||
}()); | ||
var Delete = /** @class */ (function () { | ||
function Delete(table, whereCols, whereAnyCols, isSingular) { | ||
return async (db, whereObj, updateObj) => { | ||
// TODO: maybe better to get this from the schema? | ||
const dynamicSetCols = Object.keys(updateObj); | ||
const vals = whereKeys | ||
.map(col => whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]) | ||
.concat(dynamicSetCols.map(col => updateObj[col])); | ||
let dynamicPlaceholder = placeholder; | ||
const dynamicSetKeys = []; | ||
const dynamicSetClauses = []; | ||
for (const col of dynamicSetCols) { | ||
dynamicSetKeys.push(col); | ||
const n = dynamicPlaceholder++; | ||
dynamicSetClauses.push(`${col} = $${n}`); | ||
} | ||
const setSql = dynamicSetClauses.join(', '); | ||
const query = `UPDATE ${this.table} SET ${setSql}${whereClause}${limitClause} RETURNING *`; | ||
const result = await db.query(query, vals); | ||
if (this.isSingular) { | ||
return result.rowCount === 0 ? null : result.rows[0]; | ||
} | ||
return result.rows; | ||
}; | ||
} | ||
} | ||
class Delete { | ||
constructor(table, whereCols, whereAnyCols, isSingular) { | ||
this.table = table; | ||
@@ -450,55 +326,38 @@ this.whereCols = whereCols; | ||
} | ||
Delete.prototype.clone = function () { | ||
return new Delete(this.table, this.whereCols, this.whereAnyCols, this.isSingular); | ||
}; | ||
Delete.prototype.build = function () { | ||
var _this = this; | ||
var placeholder = 1; | ||
var whereKeys = []; | ||
var whereClauses = []; | ||
build() { | ||
let placeholder = 1; | ||
const whereKeys = []; | ||
const whereClauses = []; | ||
if (this.whereCols) { | ||
for (var _i = 0, _a = this.whereCols; _i < _a.length; _i++) { | ||
var col = _a[_i]; | ||
for (const col of this.whereCols) { | ||
whereKeys.push(col); | ||
var n = placeholder++; | ||
whereClauses.push("".concat(col, " = $").concat(n)); | ||
const n = placeholder++; | ||
whereClauses.push(`${col} = $${n}`); | ||
} | ||
} | ||
if (this.whereAnyCols) { | ||
for (var _b = 0, _c = this.whereAnyCols; _b < _c.length; _b++) { | ||
var anyCol = _c[_b]; | ||
var col = anyCol.__any; | ||
for (const anyCol of this.whereAnyCols) { | ||
const col = anyCol.__any; | ||
whereKeys.push(col); | ||
var n = placeholder++; | ||
whereClauses.push("".concat(col, " = ANY($").concat(n, ")")); | ||
const n = placeholder++; | ||
whereClauses.push(`${col} = ANY($${n})`); | ||
} | ||
} | ||
var whereClause = whereClauses.length | ||
? " WHERE ".concat(whereClauses.join(' AND ')) | ||
const whereClause = whereClauses.length | ||
? ` WHERE ${whereClauses.join(' AND ')}` | ||
: ''; | ||
var limitClause = ''; // this.isSingular ? ' LIMIT 1' : ''; | ||
var query = "DELETE FROM ".concat(this.table).concat(whereClause).concat(limitClause, " RETURNING *"); | ||
return function (db, whereObj) { return __awaiter(_this, void 0, void 0, function () { | ||
var vals, result; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
vals = whereKeys.map(function (col) { | ||
return whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]; | ||
}); | ||
return [4 /*yield*/, db.query(query, vals)]; | ||
case 1: | ||
result = _a.sent(); | ||
if (this.isSingular) { | ||
return [2 /*return*/, result.rowCount === 0 ? null : result.rows[0]]; | ||
} | ||
return [2 /*return*/, result.rows]; | ||
} | ||
}); | ||
}); }; | ||
}; | ||
return Delete; | ||
}()); | ||
const limitClause = ''; // this.isSingular ? ' LIMIT 1' : ''; | ||
const query = `DELETE FROM ${this.table}${whereClause}${limitClause} RETURNING *`; | ||
return async (db, whereObj) => { | ||
const vals = whereKeys.map(col => whereObj[col] instanceof Set | ||
? Array.from(whereObj[col]) | ||
: whereObj[col]); | ||
const result = await db.query(query, vals); | ||
if (this.isSingular) { | ||
return result.rowCount === 0 ? null : result.rows[0]; | ||
} | ||
return result.rows; | ||
}; | ||
} | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "crudely-typed", | ||
"version": "0.1.0", | ||
"version": "0.1.1", | ||
"description": "Simple \"everyday CRUD\" Postgres queries with perfect TypeScript types", | ||
@@ -16,3 +16,4 @@ "main": "dist/src/index.js", | ||
"clean": "rm -rf dist", | ||
"coverage": "jest --coverage" | ||
"coverage": "jest --coverage", | ||
"prepublishOnly": "tsc" | ||
}, | ||
@@ -29,8 +30,11 @@ "files": [ | ||
"eslint": "^8.12.0", | ||
"eslint-plugin-expect-type": "^0.2.0", | ||
"jest": "^27.5.1", | ||
"pg": "^8.7.3", | ||
"pg-to-ts": "^4.1.0", | ||
"prettier": "^2.6.2", | ||
"ts-jest": "^27.1.4", | ||
"typescript": "^4.6.3" | ||
} | ||
}, | ||
"dependencies": {} | ||
} |
419
README.md
# Crudely Typed | ||
[![codecov](https://codecov.io/gh/danvk/crudely-typed/branch/main/graph/badge.svg?token=2C0SU9X0EM)](https://codecov.io/gh/danvk/crudely-typed) | ||
Simple "everyday CRUD" Postgres queries with perfect TypeScript types. | ||
Zero dependencies. Designed to work with [pg-to-ts][] and [node-postgres][]. | ||
## Quickstart | ||
Install [pg-to-ts][] and this library, and generate a schema file: | ||
npm install -D pg-to-ts | ||
npm install crudely-typed | ||
pg-to-ts generate -c $POSTGRES_URL --output src/dbschema.ts | ||
Then generate your queries using a `TypedSQL` instance: | ||
```ts | ||
// src/demo.ts | ||
import {TypedSQL} from 'crudely-typed'; | ||
import {tables} from './dbschema'; | ||
const typedSql = new TypedSQL(tables); | ||
const getDocById = typedSql.table('docs').selectByPrimaryKey(); | ||
// ^? const getDocById: (db: Queryable, where: { id: string }) => Promise<Doc | null> | ||
``` | ||
Crudely Typed supports the basic create / read / update / delete queries. See | ||
[API](#api) for details. Crudely Typed is _not_ a full-fledged query builder, | ||
nor does it aspire to be. See [FAQ](#faq) for more on this. | ||
## API | ||
### TypedSQL | ||
Everything starts with a `TypedSQL` instance, which you construct from the | ||
`tables` export of a [pg-to-ts][] DB schema. There are many schema generators | ||
derived from the old SchemaTS project, but crudely-typed specifically requires | ||
pg-to-ts schemas because they have just the type references it needs. | ||
```ts | ||
import {TypedSQL} from 'crudely-typed'; | ||
import {tables} from './dbschema'; // <-- output of pg-to-ts | ||
const typedSql = new TypedSQL(tables); | ||
``` | ||
### table | ||
From a `TypedSQL` instance, you can produce a `TableBuilder` object for any of | ||
your tables: | ||
```ts | ||
const usersTable = typedSql.table('users'); | ||
``` | ||
The remaining functions in crudely-typed are all defined on this table object. | ||
Each of the functions comes in regular and `ByPrimaryKey` variants, e.g. | ||
`table.select()` and `table.selectByPrimaryKey()`. | ||
### table.select | ||
```ts | ||
table.select(): (db: Queryable) => Promise<Row[]> | ||
``` | ||
with no parameters, this is select all in the order returned by the database. | ||
```ts | ||
table.select(options: { | ||
where?: (Column | SQLAny<Column>)[], | ||
columns?: Column[], | ||
orderBy?: [col: Column, order: 'ASC' | 'DESC'][]; | ||
limitOne?: boolean; | ||
join?: { | ||
[resultingColumnName: string]: Column | ||
}; | ||
}): (db: Queryable, where: ...) => Promise<...> | ||
``` | ||
Looking at each option individually: | ||
- `where` adds a `WHERE` clause to the query: | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getDocsByAuthor = docsTable.select({where: ['author']}); | ||
// ^? const getDocsByAuthor: (db: Queryable, where: {author: string}) => Promise<Doc[]> | ||
``` | ||
If you specify multiple where clauses, they'll be combined with `AND`. | ||
You may also specify an `ANY` clause to match one of many values. | ||
See [Where clasues](#where-clauses), below. | ||
- `columns` restricts the set of columns that are retrieved (by default all | ||
columns are retrieved, i.e. `SELECT *`). You can use this to avoid fetching | ||
large, unneeded columns. | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getTitles = docsTable.select({columns: ['title']}); | ||
// ^? const getTitles: (db: Queryable) => Promise<{title: string}[]> | ||
``` | ||
- `orderBy` sorts the output, i.e. it adds an `ORDER BY` clause to the query. | ||
Adding an `orderBy` clause does not affect the type of the `select`. | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getDocs = docsTable.select({orderBy: [['author', 'ASC']]}); | ||
// ^? const getTitles: (db: Queryable) => Promise<Doc[]> | ||
``` | ||
- `limitOne` adds a `LIMIT 1` clause to the query, so that it always returns | ||
either zero or one row. This changes the return type from `T[]` to `T | null`. | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getTitle = docsTable.select({where: ['title'], limitOne: true}); | ||
// ^? const getTitle: ( | ||
// db: Queryable, | ||
// where: {title: string} | ||
// ) => Promise<Doc | null> | ||
``` | ||
- `join` adds 1-1 joins to the query for columns that are foreign keys into | ||
other tables. The row from the joined table comes back as an object under | ||
the property name that you specify. You may specify multiple joins, though | ||
they cannot be nested and they must all be 1-1. | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getDocs = docsTable.select({ | ||
join: { | ||
author: 'author_id', | ||
publisher: 'publisher_id', | ||
} | ||
}); | ||
// ^? const getDocs: ( | ||
// db: Queryable | ||
// ) => Promise<(Doc & {author: Author; publisher: Publisher })[]> | ||
``` | ||
You don't need to specify the joined table or its type; crudely-typed has all | ||
the information it needs from the dbschema. If you specify a set of columns to | ||
select with `columns`, the foreign key need not be one of those columns. | ||
### table.selectByPrimaryKey | ||
There's a helper for the common case of selecting by primary key: | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const getDocById = docsTable.selectByPrimaryKey(); | ||
// ^? const getDocById: ( | ||
// db: Queryable, | ||
// where: { id: string } | ||
// ) => Promise<Doc | null> | ||
``` | ||
This is exactly equivalent to `docsTable.select({where: ['id'], limitOne: true})` | ||
but saves you some typing. | ||
You may use the `columns` and `join` and with `selectByPrimaryKey`: | ||
```ts | ||
const getDocById = docsTable.selectByPrimaryKey({ | ||
columns: ['title'], | ||
join: { author: 'author_id' } | ||
}); | ||
const doc = await getDocById(db, {id: 'doc-id'}); | ||
// ^? const doc: {title: string; author: Author} | null | ||
``` | ||
### table.insert | ||
```ts | ||
table.insert(): (db: Queryable, row: RowInput) => Promise<Row> | ||
``` | ||
This generates a dynamic `INSERT` query based on the properties of `row`. | ||
The `RowInput` type models the required and optional columns in the table. | ||
If an optional property is omitted from `row`, then it will be set to its | ||
default value and observable in the returned `Row`. If a required property | ||
is omitted, you'll get a type error. | ||
```ts | ||
const insertDoc = docsTable.insert(); | ||
const doc = await insertDoc({author: 'Mark Twain', title: 'Huckleberry Finn'}); | ||
// ^? const doc: Doc | ||
``` | ||
It's sometimes desirable to prevent certain columns from being set, e.g. the | ||
primary key. This can be enforced with the `disallowColumns` option: | ||
```ts | ||
const insertDoc = docsTable.insert({ disallowColumns: ['id'] }); | ||
// ^? const insertDoc: (db: Queryable, row: Omit<DocInput, 'id'>) => Promise<Doc> | ||
insertDoc({id: 'some id'}); | ||
// ~~ type error! | ||
const doc = await insertDoc({author: 'Mark Twain', title: 'Huckleberry Finn'}); | ||
// ^? const doc: Doc | ||
``` | ||
### table.insertMultiple | ||
This is indentical to `insert` but allows multiple rows to be inserted with a | ||
single query. | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const insertDocs = docsTable.insertMultiple(); | ||
// ^? const insertDocs: ( | ||
// db: Queryable, | ||
// rows: readonly DocInput[] | ||
// ) => Promise<Row[]> | ||
const docs = await insertDocs([ | ||
{title: 'Huckleberry Finn', author: 'Mark Twain'}, | ||
{title: 'Poor Richard', author: 'Ben Franklin'} | ||
]); | ||
``` | ||
`insertMultiple` also supports `disallowColumns`, just like `insert`. | ||
### table.update | ||
```ts | ||
table.update({ | ||
where?: (Column | SQLAny<Column>)[], | ||
set?: Column[], | ||
limitOne?: boolean, | ||
}): (db: Queryable, where: ..., set: ...) => Promise<...> | ||
``` | ||
With a `where` and a `set` clause, this updates specific columns on specific rows. All affected rows are returned: | ||
```ts | ||
const docsTable = typedSql.table('docs'); | ||
const setYear = docsTable.update({where: ['title'], set: ['year']}); | ||
// ^? const setYear: ( | ||
// db: Queryable, | ||
// where: {title: string}, | ||
// set: {year: number} | ||
// ) => Promise<Doc[]> | ||
const newDocs = setYear(db, {title: 'Huck Finn'}, {year: 1872}); | ||
// ^? const newDocs: Promise<Doc[]> | ||
``` | ||
Without a `set` clause, this performs a dynamic update based on the param: | ||
```ts | ||
const update = docsTable.update({where: ['title']}); | ||
// ^? const update: ( | ||
// db: Queryable, | ||
// where: {title: string}, | ||
// set: Partial<Doc> | ||
// ) => Promise<Doc[]> | ||
const newDocs = setYear(db, {title: 'Huck Finn'}, {year: 1872}); | ||
// ^? const newDocs: Promise<Doc[]> | ||
``` | ||
The `where` clause can include multiple columns, in which case it operates as | ||
an `AND`, and can support `ANY` clauses. | ||
See [Where clasues](#where-clauses), below. | ||
Without a `where` clause, this updates all rows in the table. | ||
If you pass `limitOne: true`, at most one row will be updated and the function | ||
will return `T | null` instead of `T[]`: | ||
```ts | ||
const update = docsTable.update({where: ['title'], limitOne: true}); | ||
// ^? const update: ( | ||
// db: Queryable, | ||
// where: {title: string}, | ||
// set: Partial<Doc> | ||
// ) => Promise<Doc | null> | ||
const newDoc = setYear(db, {title: 'Huck Finn'}, {year: 1872}); | ||
// ^? const newDocs: Promise<Doc> | ||
``` | ||
### table.updateByPrimaryKey | ||
This is a shortcut for updating a row by its table's primary key: | ||
```ts | ||
const update = docsTable.updateByPrimaryKey(set: ['year']); | ||
// ^? const update: ( | ||
// db: Queryable, | ||
// where: {id: string}, | ||
// set: {year: number} | ||
// ) => Promise<Doc | null> | ||
const newDoc = setYear(db, {id: 'isbn-123'}, {year: 1872}); | ||
// ^? const newDoc: Promise<Doc> | ||
``` | ||
If you pass a `set` option, then this updates a fixed set of columns. | ||
If you don't, it's dynamic based on its parameter, just like `update`. | ||
### table.delete | ||
```ts | ||
table.delete(options: { | ||
where?: (Column | SQLAny<Column>)[]; | ||
limitOne?: boolean; | ||
}): (db: Queryable, where: ...) => Promise<...> | ||
``` | ||
The `where` clause for `delete` works exactly as it does for `select`. It may | ||
be set to an array of columns or `ANY` clauses. See | ||
[Where clauses](#where-clauses), below. | ||
```ts | ||
const docsTable = typedDb.table('docs'); | ||
const deleteByTitle = docsTable.delete({ where: ['title'] }); | ||
// ^? const deleteByTitle: (db: Queryable, where: {title: string}) => Promise<Doc[]> | ||
``` | ||
The `delete` function returns the rows that it deletes (if any). As with | ||
`select`, if you pass `limitOne: true` then it will return `T | null` instead | ||
of `T[]`: | ||
```ts | ||
const docsTable = typedDb.table('docs'); | ||
const deleteByTitle = docsTable.delete({ where: ['title'], limitOne: true }); | ||
// ^? const deleteByTitle: (db: Queryable, where: {title: string}) => Promise<Doc | null> | ||
``` | ||
If you don't specify a `where` clause or `limitOne`, this acts as "delete all". | ||
### table.deleteByPrimaryKey | ||
This is a helper for the common case where you want to delete rows by their | ||
primary key: | ||
```ts | ||
const docsTable = typedDb.table('docs'); | ||
const deleteDoc = docsTable.deleteByPrimaryKey(); | ||
// ^? const deleteDoc: (db: Queryable, where: {id: string}) => Promise<Doc | null> | ||
``` | ||
This is exactly equivalent to `docsTable.delete({ where: ['id'], limitOne: true })`. | ||
## Queryable | ||
All generated functions take a `Queryable` object as their first parameter: | ||
```ts | ||
interface Queryable { | ||
query(sql: string, ...args: any[]): Promise<Result>; | ||
} | ||
``` | ||
The `Client` and `Pool` classes from `pg` conform to this and can be used with | ||
crudely-typed. | ||
## Where clauses | ||
TODO: | ||
- [ ] Publish to npm | ||
- [ ] Write docs | ||
- [ ] Set up CI | ||
- [ ] Split select unit/type tests | ||
- [ ] Check types in type tests | ||
- [ ] Write type tests for: | ||
- [ ] Update | ||
- [ ] Delete | ||
- [ ] Insert | ||
- [ ] Add unit tests to check that all queries are idempotent | ||
- [x] Settle on name (crudely-typed it is!) | ||
- [x] Test in cityci (requires publishing new pg-to-ts with `$type`) | ||
- Multiple columns are `AND`ed | ||
- You can generate `ANY` matchers | ||
- You can generate a mix | ||
For issues: | ||
## Joins | ||
- [ ] Limit joins to join-able columns | ||
- [ ] Support both node-postgres _and_ pg-promise | ||
- [ ] Add upsert | ||
- [ ] Prevent omitting non-optional columns in insert / insertMultiple | ||
TODO | ||
From cityci testing: | ||
## FAQ | ||
- [ ] Should insertMultiple with zero rows throw? | ||
- **Isn't this just a query builder?** | ||
- **Why does crudely-typed generate functions instead of running them?** | ||
- **Can you add support for X?** | ||
Probably not! The goal of this library is to handle the simplest queries for | ||
you with perfect types and a minimum of fuss. Supporting every SQL query is | ||
absolutely not a goal. At some point you should just write SQL (see below). | ||
- **What should I do for complex queries?** | ||
- **Why not use PgTyped for all my queries?** | ||
- **Why not use an ORM?** | ||
- **What's with the name?** | ||
CRUD is short for Create, Read, Update, Delete. I wanted something that had | ||
"crud" in the name but didn't sound like "crud". "crudely" fit the bill. It's | ||
also a play on [DefinitelyTyped][dt] and is a bit tongue in cheek since the | ||
types in this library are anything but crude. | ||
## How this works | ||
The `tables` object that `pg-to-ts` outputs includes all the TypeScript types | ||
and runtime values needed to generate well-typed queries. From there it's just | ||
a bunch of TypeScript generics that should be entirely invisible to you, the | ||
user. See [`index.ts`](/src/index.ts) for all the details. The following blog | ||
posts may be helpful for understanding some of the techniques being used: | ||
- intersect what you have | ||
- unionize/objectify | ||
- display of type | ||
- currying and classes | ||
[pg-to-ts]: https://github.com/danvk/pg-to-ts | ||
[node-postgres]: https://github.com/brianc/node-postgres | ||
[dt]: https://github.com/DefinitelyTyped/DefinitelyTyped |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
51345
411
13
441
1