Interfaz PostgreSQL para Node.js
Construida sobre node-postgres, esta biblioteca agrega lo siguiente:
En sus inicios en 2015, esta biblioteca solo agregaba promesas al controlador base, de ahí el nombre pg-promise
. Y aunque se mantuvo el nombre original, la funcionalidad de la biblioteca se amplió enormemente, y las promesas ahora son solo una pequeña parte.
Brindo soporte gratuito aquí y en StackOverflow.
Y si quieres ayudar en este proyecto, puedo aceptar Bitcoin: 1yki7MXMkuDw8qqe5icVdh1GJZSQSzKZp
El capítulo Uso a continuación explica los conceptos básicos que necesita saber, mientras que la Documentación oficial lo ayuda a comenzar y proporciona enlaces a todos los demás recursos.
Lea las Notas de contribución antes de abrir cualquier número nuevo o relaciones públicas.
Una vez que haya creado un objeto de base de datos, de acuerdo con los pasos de la Documentación oficial, obtendrá acceso a los métodos documentados a continuación.
Todos los métodos de consulta de la biblioteca se basan en consultas de métodos genéricos.
Normalmente debería utilizar sólo los métodos derivados, específicos de resultados, para ejecutar consultas, todos los cuales se nombran según la cantidad de filas de datos que se espera que devuelva la consulta, por lo que para cada consulta debe elegir el método correcto: ninguno, uno, unoONinguno, muchos, muchosONinguno = cualquiera. No confunda el nombre del método con el número de filas que se verán afectadas por la consulta, lo cual es completamente irrelevante.
Al confiar en los métodos específicos de resultados, protege su código de una cantidad inesperada de filas de datos, que serán rechazadas automáticamente (tratadas como errores).
También existen algunos métodos específicos que necesitará con frecuencia:
El protocolo es totalmente personalizable/ampliable mediante extensión de evento.
IMPORTANTE:
Los métodos más importantes que hay que entender desde el principio son task y tx/txIf (consulte Tareas y transacciones). Como se documenta para la consulta de método, adquiere y libera la conexión, lo que lo convierte en una mala opción para ejecutar múltiples consultas a la vez. Por esta razón, Encadenamiento de consultas es una lectura obligada para evitar escribir código que haga un mal uso de las conexiones.
Aprender con el ejemplo es un tutorial para principiantes basado en ejemplos.
Esta biblioteca viene con un motor de formato de consultas integrado que ofrece escape de valor de alto rendimiento, flexibilidad y extensibilidad. Se utiliza de forma predeterminada con todos los métodos de consulta, a menos que opte por excluirlo por completo mediante la opción pgFormatting
dentro de Opciones de inicialización.
Todos los métodos de formato utilizados internamente están disponibles en el espacio de nombres de formato, por lo que también se pueden utilizar directamente cuando sea necesario. El método principal es el formato, utilizado por todos los métodos de consulta para formatear la consulta.
La sintaxis de formato para las variables se decide a partir del tipo de values
pasados:
values
son una matriz o un único tipo básico;values
son un objeto (que no sea Array
o null
).ATENCIÓN: ¡Nunca utilice cadenas de plantilla de ES6 o concatenación manual para generar consultas, ya que ambas pueden resultar fácilmente en consultas rotas! Sólo el motor de formato de esta biblioteca sabe cómo escapar correctamente los valores de las variables para PostgreSQL.
El formato más simple (clásico) usa la sintaxis $1, $2, ...
para inyectar valores en la cadena de consulta, según su índice (de $1
a $100000
) de la matriz de valores:
await db . any ( 'SELECT * FROM product WHERE price BETWEEN $1 AND $2' , [ 1 , 10 ] )
El motor de formato también admite la parametrización de un solo valor para consultas que usan solo la variable $1
:
await db . any ( 'SELECT * FROM users WHERE name = $1' , 'John' )
Sin embargo, esto solo funciona para los tipos number
, bigint
, string
, boolean
, Date
y null
, porque tipos como Array
y Object
cambian la forma en que se interpretan los parámetros. Es por eso que se recomienda pasar variables de índice dentro de una matriz como algo más seguro, para evitar ambigüedades.
Cuando un método de consulta se parametriza con values
como un objeto, el motor de formato espera que la consulta utilice la sintaxis de parámetro con nombre $*propName*
, siendo *
cualquiera de los siguientes pares de apertura y cierre: {}
, ()
, <>
, []
, //
.
// We can use every supported variable syntax at the same time, if needed:
await db . none ( 'INSERT INTO users(first_name, last_name, age) VALUES(${name.first}, $, $/age/)' , {
name : { first : 'John' , last : 'Dow' } ,
age : 30
} ) ;
IMPORTANTE: Nunca utilice la sintaxis reservada ${}
dentro de las cadenas de plantillas de ES6, ya que no tienen conocimiento de cómo formatear valores para PostgreSQL. Dentro de las cadenas de plantilla de ES6, solo debe usar una de las 4 alternativas: $()
, $<>
, $[]
o $//
. En general, debe utilizar las cadenas estándar para SQL o colocar SQL en archivos externos; consulte Consulta de archivos.
Los nombres de variables válidos se limitan a la sintaxis de las variables JavaScript de nombre abierto. Y el this
tiene un significado especial: se refiere al objeto de formato en sí (ver más abajo).
Tenga en cuenta que, si bien los valores de propiedad null
y undefined
tienen el formato null
, se genera un error cuando la propiedad no existe.
this
referencia
Propiedad this
se refiere al objeto de formato en sí, que se insertará como una cadena con formato JSON.
await db . none ( 'INSERT INTO documents(id, doc) VALUES(${id}, ${this})' , {
id : 123 ,
body : 'some text'
} )
//=> INSERT INTO documents(id, doc) VALUES(123, '{"id":123,"body":"some text"}')
Los parámetros con nombre admiten el anidamiento de nombres de propiedades de cualquier profundidad.
const obj = {
one : {
two : {
three : {
value1 : 123 ,
value2 : a => {
// a = obj.one.two.three
return 'hello' ;
} ,
value3 : function ( a ) {
// a = this = obj.one.two.three
return 'world' ;
} ,
value4 : {
toPostgres : a => {
// Custom Type Formatting
// a = obj.one.two.three.value4
return a . text ;
} ,
text : 'custom'
}
}
}
}
} ;
await db . one ( 'SELECT ${one.two.three.value1}' , obj ) ; //=> SELECT 123
await db . one ( 'SELECT ${one.two.three.value2}' , obj ) ; //=> SELECT 'hello'
await db . one ( 'SELECT ${one.two.three.value3}' , obj ) ; //=> SELECT 'world'
await db . one ( 'SELECT ${one.two.three.value4}' , obj ) ; //=> SELECT 'custom'
El apellido en la resolución puede ser cualquier cosa, incluido:
es decir, la cadena de resolución es infinitamente flexible y admite recursividad sin límites.
Sin embargo, tenga en cuenta que los parámetros anidados no se admiten dentro del espacio de nombres de los asistentes.
De forma predeterminada, todos los valores tienen el formato según su tipo de JavaScript. Filtros de formato (o modificadores), cámbielos para que el valor tenga un formato diferente.
Tenga en cuenta que los filtros de formato funcionan solo para consultas normales y no están disponibles en PreparedStatement o ParameterizedQuery porque, por definición, están formateados en el lado del servidor.
Los filtros utilizan la misma sintaxis para las variables de índice y los parámetros con nombre, inmediatamente después del nombre de la variable:
await db . any ( 'SELECT $1:name FROM $2:name' , [ 'price' , 'products' ] )
//=> SELECT "price" FROM "products"
await db . any ( 'SELECT ${column:name} FROM ${table:name}' , {
column : 'price' ,
table : 'products'
} ) ;
//=> SELECT "price" FROM "products"
Se admiten los siguientes filtros:
:name
/ ~
- Nombres SQL:alias
- Filtro de alias:raw
/ ^
- Texto sin formato:value
/ #
- Valores abiertos:csv
/ :list
- Filtro CSV:json
- Filtro JSON Cuando el nombre de una variable termina con :name
, o una sintaxis más corta ~
(tilde), representa un nombre o identificador SQL, que se debe utilizar como escape en consecuencia:
await db . query ( 'INSERT INTO $1~($2~) VALUES(...)' , [ 'Table Name' , 'Column Name' ] ) ;
//=> INSERT INTO "Table Name"("Column Name") VALUES(...)
await db . query ( 'INSERT INTO $1:name($2:name) VALUES(...)' , [ 'Table Name' , 'Column Name' ] ) ;
//=> INSERT INTO "Table Name"("Column Name") VALUES(...)
Normalmente, una variable de nombre SQL es una cadena de texto, que debe tener al menos 1 carácter. Sin embargo, pg-promise
admite una variedad de formas en las que se pueden proporcionar nombres SQL:
*
(asteriscos) se reconoce automáticamente como todas las columnas : await db . query ( 'SELECT $1:name FROM $2:name' , [ '*' , 'table' ] ) ;
//=> SELECT * FROM "table"
await db . query ( 'SELECT ${columns:name} FROM ${table:name}' , {
columns : [ 'column1' , 'column2' ] ,
table : 'table'
} ) ;
//=> SELECT "column1","column2" FROM "table"
const obj = {
one : 1 ,
two : 2
} ;
await db . query ( 'SELECT $1:name FROM $2:name' , [ obj , 'table' ] ) ;
//=> SELECT "one","two" FROM "table"
Además, la sintaxis admite this
para enumerar nombres de columnas del objeto de formato:
const obj = {
one : 1 ,
two : 2
} ;
await db . query ( 'INSERT INTO table(${this:name}) VALUES(${this:csv})' , obj ) ;
//=> INSERT INTO table("one","two") VALUES(1, 2)
Confiar en este tipo de formato para nombres e identificadores de SQL, junto con el formato de variables normal, protege su aplicación de la inyección de SQL.
El método as.name implementa el formato.
Un alias es una versión más simple y menos estricta de :name
filter, que sólo admite una cadena de texto, es decir, no admite *
, this
, matriz u objeto como entradas, como lo hace :name
. Sin embargo, admite otros casos populares que son menos estrictos, pero que cubren al menos el 99 % de todos los casos de uso, como se muestra a continuación.
await db . any ( 'SELECT full_name as $1:alias FROM $2:name' , [ 'name' , 'table' ] ) ;
//=> SELECT full_name as name FROM "table"
.
y luego escape cada parte por separado, admitiendo así nombres SQL autocompuestos: await db . any ( 'SELECT * FROM $1:alias' , [ 'schemaName.table' ] ) ;
//=> SELECT * FROM "schemaName".table
Para obtener más detalles, consulte el método as.alias que implementa el formato.
Cuando el nombre de una variable termina con :raw
, o una sintaxis más corta ^
, el valor se inyectará como texto sin formato, sin escape.
Dichas variables no pueden ser null
o undefined
, debido al significado ambiguo en este caso, y esos valores arrojarán un error Values null/undefined cannot be used as raw text.
const where = pgp . as . format ( 'WHERE price BETWEEN $1 AND $2' , [ 5 , 10 ] ) ; // pre-format WHERE condition
await db . any ( 'SELECT * FROM products $1:raw' , where ) ;
//=> SELECT * FROM products WHERE price BETWEEN 5 AND 10
Se admite la sintaxis especial this:raw
/ this^
para inyectar el objeto de formato como una cadena JSON sin formato.
ADVERTENCIA:
Este filtro no es seguro y no debe usarse para valores que provienen del lado del cliente, ya que puede resultar en una inyección de SQL.
Cuando el nombre de una variable termina con :value
, o sintaxis más corta #
, se escapa como de costumbre, excepto cuando su tipo es una cadena, las comillas finales no se agregan.
Los valores abiertos son principalmente para poder componer declaraciones dinámicas LIKE
/ ILIKE
completas en archivos SQL externos, sin tener que generarlas en el código.
es decir, puedes generar un filtro como este en tu código:
const name = 'John' ;
const filter = '%' + name + '%' ;
y luego páselo como una variable de cadena normal, o puede pasar solo name
y hacer que su consulta use la sintaxis de valor abierto para agregar la lógica de búsqueda adicional:
SELECT * FROM table WHERE name LIKE ' %$1:value% ' )
ADVERTENCIA:
Este filtro no es seguro y no debe usarse para valores que provienen del lado del cliente, ya que puede resultar en una inyección de SQL.
El método as.value implementa el formato.
Cuando el nombre de una variable termina en :json
, se aplica el formato JSON explícito al valor.
De forma predeterminada, cualquier objeto que no sea Date
, Array
, Buffer
, null
o Custom-Type (consulte Formato de tipo personalizado) se formatea automáticamente como JSON.
El método as.json implementa el formato.
Cuando el nombre de una variable termina en :csv
o :list
, se formatea como una lista de valores separados por comas, y cada valor se formatea según su tipo de JavaScript.
Normalmente, usarías esto para un valor que sea una matriz, aunque también funciona para valores únicos. Vea los ejemplos a continuación.
const ids = [ 1 , 2 , 3 ] ;
await db . any ( 'SELECT * FROM table WHERE id IN ($1:csv)' , [ ids ] )
//=> SELECT * FROM table WHERE id IN (1,2,3)
const ids = [ 1 , 2 , 3 ] ;
await db . any ( 'SELECT * FROM table WHERE id IN ($1:list)' , [ ids ] )
//=> SELECT * FROM table WHERE id IN (1,2,3)
Usando la enumeración automática de propiedades:
const obj = { first : 123 , second : 'text' } ;
await db . none ( 'INSERT INTO table($1:name) VALUES($1:csv)' , [ obj ] )
//=> INSERT INTO table("first","second") VALUES(123,'text')
await db . none ( 'INSERT INTO table(${this:name}) VALUES(${this:csv})' , obj )
//=> INSERT INTO table("first","second") VALUES(123,'text')
const obj = { first : 123 , second : 'text' } ;
await db . none ( 'INSERT INTO table($1:name) VALUES($1:list)' , [ obj ] )
//=> INSERT INTO table("first","second") VALUES(123,'text')
await db . none ( 'INSERT INTO table(${this:name}) VALUES(${this:list})' , obj )
//=> INSERT INTO table("first","second") VALUES(123,'text')
El método as.csv implementa el formato.
La biblioteca admite sintaxis dual para CTF (formato de tipo personalizado):
La biblioteca siempre comprueba primero el CTF simbólico y, si no se utiliza dicha sintaxis, sólo entonces comprueba el CTF explícito.
Cualquier valor/objeto que implemente la función toPostgres
se trata como un tipo de formato personalizado. Luego se llama a la función para obtener el valor real, pasándole el objeto a través de this
contexto y además como un único parámetro (en caso de que toPostgres
sea una función de flecha de ES6):
const obj = {
toPostgres ( self ) {
// self = this = obj
// return a value that needs proper escaping
}
}
La función toPostgres
puede devolver cualquier cosa, incluido otro objeto con su propia función toPostgres
, es decir, se admiten tipos personalizados anidados.
El valor devuelto por toPostgres
se escapa según su tipo de JavaScript, a menos que el objeto también contenga la propiedad rawType
establecida en un valor verdadero, en cuyo caso el valor devuelto se considera preformateado y, por lo tanto, se inyecta directamente, como texto sin formato:
const obj = {
toPostgres ( self ) {
// self = this = obj
// return a pre-formatted value that does not need escaping
} ,
rawType : true // use result from toPostgres directly, as Raw Text
}
El siguiente ejemplo implementa una clase que formatea automáticamente ST_MakePoint
a partir de coordenadas:
class STPoint {
constructor ( x , y ) {
this . x = x ;
this . y = y ;
this . rawType = true ; // no escaping, because we return pre-formatted SQL
}
toPostgres ( self ) {
return pgp . as . format ( 'ST_MakePoint($1, $2)' , [ this . x , this . y ] ) ;
}
}
Y una sintaxis clásica para dicha clase es aún más simple:
function STPoint ( x , y ) {
this . rawType = true ; // no escaping, because we return pre-formatted SQL
this . toPostgres = ( ) => pgp . as . format ( 'ST_MakePoint($1, $2)' , [ x , y ] ) ;
}
Con esta clase puedes usar new STPoint(12, 34)
como valor de formato que se inyectará correctamente.
También puedes usar CTF para anular cualquier tipo estándar:
Date . prototype . toPostgres = a => a . getTime ( ) ;
La única diferencia con CTF explícito es que configuramos toPostgres
y rawType
como propiedades de símbolo ES6, definidas en el espacio de nombres ctf:
const { toPostgres , rawType } = pgp . as . ctf ; // Global CTF symbols
const obj = {
[ toPostgres ] ( self ) {
// self = this = obj
// return a pre-formatted value that does not need escaping
} ,
[ rawType ] : true // use result from toPostgres directly, as Raw Text
} ;
Como los símbolos CTF son globales, también puedes configurar objetos independientemente de esta biblioteca:
const ctf = {
toPostgres : Symbol . for ( 'ctf.toPostgres' ) ,
rawType : Symbol . for ( 'ctf.rawType' )
} ;
Aparte de eso, funciona exactamente como el CTF explícito, pero sin cambiar la firma del objeto.
Si no sabe lo que significa, lea la API de símbolos de ES6 y su uso para nombres de propiedades únicos. Pero en resumen, las propiedades de los símbolos no se enumeran mediante for(name in obj)
, es decir, generalmente no son visibles en JavaScript, solo a través de una API específica Object.getOwnPropertySymbols
.
El uso de archivos SQL externos (a través de QueryFile) ofrece muchas ventajas:
debug
), sin reiniciar la aplicación;params
), automatizando el formateo de SQL en dos pasos;minify
+ compress
), para detección temprana de errores y consultas compactas. const { join : joinPath } = require ( 'path' ) ;
// Helper for linking to external query files:
function sql ( file ) {
const fullPath = joinPath ( __dirname , file ) ;
return new pgp . QueryFile ( fullPath , { minify : true } ) ;
}
// Create a QueryFile globally, once per file:
const sqlFindUser = sql ( './sql/findUser.sql' ) ;
db . one ( sqlFindUser , { id : 123 } )
. then ( user => {
console . log ( user ) ;
} )
. catch ( error => {
if ( error instanceof pgp . errors . QueryFileError ) {
// => the error is related to our QueryFile
}
} ) ;
Archivo findUser.sql
:
/*
multi-line comments are supported
*/
SELECT name, dob -- single-line comments are supported
FROM Users
WHERE id = ${id}
Cada método de consulta de la biblioteca puede aceptar el tipo QueryFile como parámetro query
. El tipo QueryFile nunca arroja ningún error, lo que deja que los métodos de consulta lo rechacen correctamente con QueryFileError.
Se recomienda el uso de parámetros con nombre dentro de archivos SQL externos en lugar de variables de índice, porque hace que SQL sea mucho más fácil de leer y comprender, y porque también permite parámetros con nombre anidados, por lo que las variables en un archivo SQL grande y complejo se pueden agrupar en espacios de nombres. para una separación visual aún más fácil.
Una tarea representa una conexión compartida para ejecutar múltiples consultas:
db . task ( t => {
// execute a chain of queries against the task context, and return the result:
return t . one ( 'SELECT count(*) FROM events WHERE id = $1' , 123 , a => + a . count )
. then ( count => {
if ( count > 0 ) {
return t . any ( 'SELECT * FROM log WHERE event_id = $1' , 123 )
. then ( logs => {
return { count , logs } ;
} )
}
return { count } ;
} ) ;
} )
. then ( data => {
// success, data = either {count} or {count, logs}
} )
. catch ( error => {
// failed
} ) ;
Las tareas proporcionan un contexto de conexión compartido para su función de devolución de llamada, que se liberará cuando finalice y deben usarse siempre que se ejecute más de una consulta a la vez. Consulte también Encadenamiento de consultas para comprender la importancia del uso de tareas.
Opcionalmente, puede etiquetar tareas (consulte Etiquetas) y utilizar la sintaxis asíncrona de ES7:
db . task ( async t => {
const count = await t . one ( 'SELECT count(*) FROM events WHERE id = $1' , 123 , a => + a . count ) ;
if ( count > 0 ) {
const logs = await t . any ( 'SELECT * FROM log WHERE event_id = $1' , 123 ) ;
return { count