-
-
Notifications
You must be signed in to change notification settings - Fork 19
Description
The Stricter TypeScript section of the README is a bit unhelpful:
Simply creating a new sql
function, as prescribed, results in Sql
instances where the values
property is still the built-in Value
type, which is just an alias for unknown
- which doesn't actually work, e.g. when you try to pass this to a (properly typed) SQL client library.
Similarly, this doesn't give you join
, bulk
, raw
(etc.) functions with the correct types.
To address this, you'd need some sort of factory function for the whole API, I think?
I tried this:
export function setup<Value>() {
/**
* Supported value or SQL instance.
*/
type RawValue = Value | Sql;
/**
* A SQL instance can be nested within each other to build SQL strings.
*/
class Sql {
readonly values: Value[];
readonly strings: string[];
constructor(rawStrings: readonly string[], rawValues: readonly RawValue[]) {
if (rawStrings.length - 1 !== rawValues.length) {
if (rawStrings.length === 0) {
throw new TypeError("Expected at least 1 string");
}
throw new TypeError(
`Expected ${rawStrings.length} strings to have ${
rawStrings.length - 1
} values`,
);
}
const valuesLength = rawValues.reduce<number>(
(len, value) => len + (value instanceof Sql ? value.values.length : 1),
0,
);
this.values = new Array(valuesLength);
this.strings = new Array(valuesLength + 1);
this.strings[0] = rawStrings[0];
// Iterate over raw values, strings, and children. The value is always
// positioned between two strings, e.g. `index + 1`.
let i = 0,
pos = 0;
while (i < rawValues.length) {
const child = rawValues[i++];
const rawString = rawStrings[i];
// Check for nested `sql` queries.
if (child instanceof Sql) {
// Append child prefix text to current string.
this.strings[pos] += child.strings[0];
let childIndex = 0;
while (childIndex < child.values.length) {
this.values[pos++] = child.values[childIndex++];
this.strings[pos] = child.strings[childIndex];
}
// Append raw string to current string.
this.strings[pos] += rawString;
} else {
this.values[pos++] = child;
this.strings[pos] = rawString;
}
}
}
get sql() {
const len = this.strings.length;
let i = 1;
let value = this.strings[0];
while (i < len) value += `?${this.strings[i++]}`;
return value;
}
get statement() {
const len = this.strings.length;
let i = 1;
let value = this.strings[0];
while (i < len) value += `:${i}${this.strings[i++]}`;
return value;
}
get text() {
const len = this.strings.length;
let i = 1;
let value = this.strings[0];
while (i < len) value += `$${i}${this.strings[i++]}`;
return value;
}
inspect() {
return {
sql: this.sql,
statement: this.statement,
text: this.text,
values: this.values,
};
}
}
/**
* Create a SQL query for a list of values.
*/
function join(
values: readonly RawValue[],
separator = ",",
prefix = "",
suffix = "",
) {
if (values.length === 0) {
throw new TypeError(
"Expected `join([])` to be called with an array of multiple elements, but got an empty array",
);
}
return new Sql(
[prefix, ...Array(values.length - 1).fill(separator), suffix],
values,
);
}
/**
* Create a SQL query for a list of structured values.
*/
function bulk(
data: ReadonlyArray<ReadonlyArray<RawValue>>,
separator = ",",
prefix = "",
suffix = "",
) {
const length = data.length && data[0].length;
if (length === 0) {
throw new TypeError(
"Expected `bulk([][])` to be called with a nested array of multiple elements, but got an empty array",
);
}
const values = data.map((item, index) => {
if (item.length !== length) {
throw new TypeError(
`Expected \`bulk([${index}][])\` to have a length of ${length}, but got ${item.length}`,
);
}
return new Sql(["(", ...Array(item.length - 1).fill(separator), ")"], item);
});
return new Sql(
[prefix, ...Array(values.length - 1).fill(separator), suffix],
values,
);
}
/**
* Create raw SQL statement.
*/
function raw(value: string) {
return new Sql([value], []);
}
/**
* Placeholder value for "no text".
*/
const empty = raw("");
/**
* Create a SQL object from a template string.
*/
function sql(
strings: readonly string[],
...values: readonly RawValue[]
) {
return new Sql(strings, values);
}
return {
sql, join, bulk, raw, empty
}
}
export const { sql, join, bulk, raw, empty } = setup<unknown>();
export default sql;
It's a simple change, but it's a breaking change, in that the exported types Value
and RawValue
are lost - not that these were useful (probably) since, again, they don't represent a strict value type, and they don't work with a custom sql
wrapper function.
I could of course write my own wrapper module using unsafe typecasts, correcting all the types by force - but then I'm not really using the types provided by the package, and instead just throwing them all away and replacing them, which definitely feels wrong.
I don't know, what do you think?
As I recall, you don't use TS yourself, so maybe you don't care?