Skip to content

Commit

Permalink
Support fully qualified table names
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Pantic committed Apr 3, 2023
1 parent 5b0e4c5 commit 29412b9
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 30 deletions.
60 changes: 32 additions & 28 deletions src/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export type Int8String = `${number}`;
export type RangeString<Bound extends string | number> = `${'[' | '('}${Bound},${Bound}${']' | ')'}`;

/**
* `tsrange`, `tstzrange` or `daterange` value represented as a string. The
* `tsrange`, `tstzrange` or `daterange` value represented as a string. The
* format of the upper and lower bound `date`, `timestamp` or `timestamptz`
* values depends on pg's `DateStyle` setting.
*/
Expand All @@ -76,22 +76,22 @@ export type ByteArrayString = `\\x${string}`;
/**
* Make a function `STRICT` in the Postgres sense — where it's an alias for
* `RETURNS NULL ON NULL INPUT` — with appropriate typing.
*
*
* For example, Zapatos' `toBuffer()` function is defined as:
*
*
* ```
* export const toBuffer = strict((ba: ByteArrayString) => Buffer.from(ba.slice(2), 'hex'));
* ```
*
*
* The generic input and output types `FnIn` and `FnOut` can be inferred from
* `fn`, as seen above, but can also be explicitly narrowed. For example, to
* convert specifically from `TimestampTzString` to Luxon's `DateTime`, but
* convert specifically from `TimestampTzString` to Luxon's `DateTime`, but
* pass through `null`s unchanged:
*
*
* ```
* const toDateTime = db.strict<db.TimestampTzString, DateTime>(DateTime.fromISO);
* ```
*
*
* @param fn The single-argument transformation function to be made strict.
*/
export function strict<FnIn, FnOut>(fn: (x: FnIn) => FnOut):
Expand All @@ -103,54 +103,54 @@ export function strict<FnIn, FnOut>(fn: (x: FnIn) => FnOut):

/**
* Convert a `bytea` hex representation to a JavaScript `Buffer`. Note: for
* large objects, use something like
* large objects, use something like
* [pg-large-object](https://www.npmjs.com/package/pg-large-object) instead.
*
*
* @param ba The `ByteArrayString` hex representation (or `null`)
*/
export const toBuffer = strict((ba: ByteArrayString) => Buffer.from(ba.slice(2), 'hex'));

/**
* Compiles to a numbered query parameter (`$1`, `$2`, etc) and adds the wrapped value
* Compiles to a numbered query parameter (`$1`, `$2`, etc) and adds the wrapped value
* at the appropriate position of the values array passed to `pg`.
* @param x The value to be wrapped
* @param cast Optional cast type. If a string, the parameter will be cast to
* this type within the query e.g. `CAST($1 AS type)` instead of plain `$1`. If
* `true`, the value will be JSON stringified and cast to `json` (irrespective
* of the configuration parameters `castArrayParamsToJson` and
* `true`, the value will be JSON stringified and cast to `json` (irrespective
* of the configuration parameters `castArrayParamsToJson` and
* `castObjectParamsToJson`). If `false`, the value will **not** be JSON-
* stringified or cast to `json` (again irrespective of the configuration
* stringified or cast to `json` (again irrespective of the configuration
* parameters `castArrayParamsToJson` and `castObjectParamsToJson`).
*/
export class Parameter<T = any> { constructor(public value: T, public cast?: boolean | string) { } }

/**
* Returns a `Parameter` instance, which compiles to a numbered query parameter
* Returns a `Parameter` instance, which compiles to a numbered query parameter
* (`$1`, `$2`, etc) and adds its wrapped value at the appropriate position of
* the values array passed to `pg`.
* @param x The value to be wrapped
* @param cast Optional cast type. If a string, the parameter will be cast to
* @param cast Optional cast type. If a string, the parameter will be cast to
* this type within the query e.g. `CAST($1 AS type)` instead of plain `$1`. If
* `true`, the value will be JSON stringified and cast to `json` (irrespective
* of the configuration parameters `castArrayParamsToJson` and
* `castObjectParamsToJson`). If `false`, the value will **not** be JSON
* stringified or cast to `json` (again irrespective of the configuration
* of the configuration parameters `castArrayParamsToJson` and
* `castObjectParamsToJson`). If `false`, the value will **not** be JSON
* stringified or cast to `json` (again irrespective of the configuration
* parameters `castArrayParamsToJson` and `castObjectParamsToJson`).
*/
export function param<T = any>(x: T, cast?: boolean | string) { return new Parameter(x, cast); }

/**
* 💥💥💣 **DANGEROUS** 💣💥💥
*
*
* Compiles to the wrapped string value, as is, which may enable SQL injection
* attacks.
*/
export class DangerousRawString { constructor(public value: string) { } }

/**
* 💥💥💣 **DANGEROUS** 💣💥💥
*
* Remember [Little Bobby Tables](https://xkcd.com/327/).
*
* Remember [Little Bobby Tables](https://xkcd.com/327/).
* Did you want `db.param` instead?
* ---
* Returns a `DangerousRawString` instance, wrapping a string.
Expand Down Expand Up @@ -244,7 +244,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>

/**
* When calling `run`, this function is applied to the object returned by `pg`
* to produce the result that is returned. By default, the `rows` array is
* to produce the result that is returned. By default, the `rows` array is
* returned — i.e. `(qr) => qr.rows` — but some shortcut functions alter this
* in order to match their declared `RunResult` type.
*/
Expand Down Expand Up @@ -330,8 +330,12 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
// Re-escape identifier even if it "seems" to be already escaped. A malicious user
// could pass a string that starts with " but injects SQL code. Only trust escaped
// identifiers that we escape ourselves.
result.text += escapeIdentifier(unescapeIdentifier(expression));

const [table, column] = String(expression).split('.');
if (table && column) {
result.text += escapeIdentifier(unescapeIdentifier(table)) + '.' + escapeIdentifier(unescapeIdentifier(column));
} else {
result.text += escapeIdentifier(unescapeIdentifier(expression));
}
} else if (expression instanceof DangerousRawString) {
// Little Bobby Tables passes straight through ...
result.text += expression.value;
Expand Down Expand Up @@ -384,12 +388,12 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
const columnNames = Array.isArray(expression.value) ? expression.value :
Object.keys(expression.value).sort();
result.text += columnNames.map(k => {
const prefix = expression.tableName ? `${escapeIdentifier(expression.tableName)}.` : '';
const prefix = expression.tableName ? `${escapeIdentifier(expression.tableName)}.` : '';
return prefix + escapeIdentifier(k);
}).join(', ');

} else if (expression instanceof ColumnValues) {
// a ColumnValues-wrapped object OR array
// a ColumnValues-wrapped object OR array
// -> values (in ColumnNames-matching order, if applicable) punted as SQLFragments or Parameters

if (Array.isArray(expression.value)) {
Expand All @@ -405,7 +409,7 @@ export class SQLFragment<RunResult = pg.QueryResult['rows'], Constraint = never>
const
columnNames = <Column[]>Object.keys(expression.value).sort(),
columnValues = columnNames.map(k => (<any>expression.value)[k]);

for (let i = 0, len = columnValues.length; i < len; i++) {
const
columnName = columnNames[i],
Expand Down
5 changes: 3 additions & 2 deletions src/generate/tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const definitionForRelationInSchema = async (
orNull = isNullable ? ' | null' : '',
orDefault = isNullable || hasDefault ? ' | db.DefaultType' : '';

// Now, 4 cases:
// Now, 4 cases:
// 1. null domain, known udt <-- standard case
// 2. null domain, unknown udt <-- custom type: create type file, with placeholder 'any'
// 3. non-null domain, known udt <-- alias type: create type file, with udt-based placeholder
Expand Down Expand Up @@ -203,8 +203,9 @@ export namespace ${rel.name} {
uniqueIndexes.map(ui => "'" + ui.indexname + "'").join(' | ') :
'never'};
export type Column = keyof Selectable;
export type FQColumn = \`\${Table}.\${keyof Selectable}\`;
export type OnlyCols<T extends readonly Column[]> = Pick<Selectable, T[number]>;
export type SQLExpression = db.GenericSQLExpression | db.ColumnNames<Updatable | (keyof Updatable)[], Table> | db.ColumnValues<Updatable> | Table | Whereable | Column;
export type SQLExpression = db.GenericSQLExpression | db.ColumnNames<Updatable | (keyof Updatable)[], Table> | db.ColumnValues<Updatable> | Table | Whereable | Column | FQColumn;
export type SQL = SQLExpression | SQLExpression[];
}`;
return tableDef;
Expand Down

0 comments on commit 29412b9

Please sign in to comment.