Slonik:具有严格类型,详细日志记录和断言的PostgreSQL客户端

2021-03-11 06:09:12

(上面的GIF显示Slonik生成查询日志。Slonik使用Roarr产生日志。日志包括堆栈的实际查询调用位置和用于执行查询的值。)

如果您重视我的工作并希望看到Slonik和许多我的许多开源项目被持续改进,那么请考虑成为赞助人:

注意:使用此项目不需要打字标注。它是一个常规的ES6模块。如果您不使用类型系统,请忽略文档中使用的类型定义。

Slonik是一系列用于使用Node-Postgres的公用事业集合。我们继续使用Node-Postgres,因为它为与PostgreSQL交互提供了强大的基础。但是,曾经是一个公司集合的集成,因为它已成长为摘要重复代码模式的框架,可防止不安全的连接处理和值插值,并提供丰富的调试体验。

Slonik已经与大型数据卷和查询进行了战斗,从简单的CRUD操作到数据仓储需求。

官方PostgreSQL标志中描绘的大象的名称是Slonik。这个名字本身来自俄罗斯词"小大象"

在开发Slonik的主要原因中,是减少重复代码模式的动机,并增加型式安全水平。这主要通过诸如一个,许多等的方法来实现,但问题是什么?它最好用一个例子说明。

假设要求是编写一个方法,该方法检索给定值的资源ID定义(我们认为是什么)唯一的约束。如果我们没有上述方便方法可用,那么需要写入:

从&#39导入{sql}; slonik' ;导入类型{databaseConnectiontype}从' slonik' ;键入databaseRecorddype = Number; const getfooidbybar = async(连接:databaseConnectiontype,bar:string):promise< databaseCordidype> => {const fooresult =等待连接。查询(SQL`从foo中选择ID,其中栏= $ {bar}`); if(fooresult。rowcount === 0){抛出新错误('找不到资源。'); }如果(fooresult。rowcount> 1){抛出新的错误('数据完整性约束违规。');返回fooresult [0]。 ID ; };

const getfooidbybar =(连接:databaseConnectiontype,bar:string):promise< databaseCordidype> => {返回连接。 onefirst(sql`从foo中选择ID,其中bar = $ {bar}`); };

在编写多个查询取决于上一个结果的日常内容时,这变得尤为重要。使用具有内置断言的方法可确保在错误的情况下,错误指向原始问题的原始来源。相反,除非在前面示例中键入所有可能结果的断言,否则查询的意外结果将被馈送到下一个操作。如果你很幸运,下一个手术将只是休息;如果您不幸的是,您冒险数据损坏且难以找到错误。

此外,使用保证结果的形状的方法,允许我们利用静态类型检查并捕获一些错误,即使在执行代码之前,也可以捕获一些错误。

const fooid =等待连接。许多(SQL`从foo中选择ID,其中栏= $ {bar}`);等待联系。查询(SQL`从baz中删除,其中foo_id = $ {fooid}`);

上面示例的静态类型检查将产生警告,因为粪便保证是阵列和最后一个查询的绑定是预期原始值。

Slonik仅允许在提供给Pool#Connect()方法的Provine常规的持续时间内签出连接。

仅实现这种连接汇集方法的主要原因是因为替代方案本质上是不安全的,例如,

//注意:此示例是使用不支持的API。 const main = async()=> {const connection =等待池。连接 ( ) ;等待联系。查询(SQL`选择foo()`);等待联系。释放 ( ) ; };

在此示例中,如果选择foo()会产生错误,则不会释放连接,即连接仍有挂起。

//注意:此示例是使用不支持的API。 const main = async()=> {const connection =等待池。连接 ( ) ;让LastExecutionResult;尝试{lastexecutionsresult = await连接。查询(SQL`选择foo()`);最后{等待连接。释放 ( ) ; }返回LastExecutionResult; };

在提供给Connect()方法的函数的承诺后,连接始终释放回池()方法是解决或拒绝的。

就像在上面描述的不安全连接处理中一样,Slonik仅允许在提供给Connection#Transaction()方法的ProSument例程的持续时间内创建事务。

联系 。事务(异步(TransactionConnection)=> {等待TransactionConnection。查询(SQL`插入Foo(栏)值(' baz')`);等待TransactionConnection。查询(SQL`插入到qux(qux)值(' quuz')`);});

此模式可确保该事务承诺或中止该问题解析或拒绝的那一刻。

SQL注入是最着名的攻击向量之一。一些最大的数据泄漏是用户输入处理不当的结果。通常,通过使用参数化和通过限制数据库权限,例如通过限制数据库权限,可以轻松防范SQL喷射。

在此示例中,查询文本(选择$ 1)和参数(UserInput的值)传递给PostgreSQL服务器,其中参数被安全地替换为查询。这是使用用户输入执行查询的安全方法。

当开发人员削减角落或者当他们不了解参数化时,漏洞出现,即有人会写入的风险:

由于数据泄漏的历史明显,这比任何人都想承认这一点。这尤其是Node.js社区的大风险,其中主要的开发人员来自前端,并且没有与RDBMSES一起使用的培训。因此,Slonik的一个关键销售点之一是它增加了多层保护,以防止不安全处理用户输入。

这意味着运行查询的唯一方法是使用SQL标记的模板文字构造它,例如,

Slonik从这里接管并构建具有值绑定的查询,并将生成的查询文本和参数发送到PostgreSQL。随着SQL标记的模板文字是执行查询的唯一方法,它由于SQL客户端API的有限知识而导致的偶然的Unsaws用户输入处理中的强保护层。

由于Slonik限制了用户' s生成和执行动态SQL的能力,它提供了用于生成查询的片段和相应的值绑定的辅助函数,例如, sql.identifier,sql.join和sql.unnest。这些方法生成令牌查询executor解释以构建安全查询,例如,

联系 。查询(SQL`选择$ {SQL。标识符([' foo'' a'])} from(值($ {sql。加入([sql。加入([&# 39; A1',' b1',' c1'],sql`,`),sql。加入([' a2'和#39; B2',' c2'],sql`,`),sql`),(`)}))foo(a,b,c)其中foo.b in($ {sql。加入([' c1'' a2'],sql`,`)})`);

选择" foo" " A"来自(价值观(1美元,2美元,3美元),(4美元,5美元,6美元))Foo(A,B,C)哪里有Foo。 B IN(7美元,8美元)

总而言之,Slonik旨在防止易受SQL注射群体的盲目创建查询。

然后可以使用Slonik连接池的实例来创建新连接,例如,

连接将保持活力,直到承诺解析(提供给Connect()提供的方法的结果)。

如果您不需要持久连接到相同的后端,那么您可以直接使用池来运行查询,例如,

要注意在后一个例子中,所选要执行查询的连接是从连接池的随机连接,即,使用后一个方法(没有显式连接())不保证多个查询将引用相同的后端。

pool.end()的结果是当所有连接结束时解决的承诺。

导入{createpool,sql,sql,}从' slonik' ; const pool = createpool(' postgres://'); const main = async()=> {等待池。查询(SQL`选择1`);等待游泳池。结尾 ( ) ; };主要的 ( ) ;

使用pool.getPoolState()来查找池是否存在,以及有多少连接处于活动状态和空闲状态,以及有多少客户端正在等待连接。

导入{createpool,sql,sql,}从' slonik' ; const pool = createpool(' postgres://'); const main = async()=> { 水池 。 getpoolstate(); // {// activeConnectionCount:0,//结束:false,// idleconnectioncount:0,//等候窗口:0,//}等待池。 connect(()=> {pool. getPoolState(); // {// activeConnectionCount:1,// Ended:False,// IDleConnectionCount:0,// SudewLientCount:0,//}});水池 。 getpoolstate(); // {// activeConnectionCount:0,//结束:false,// idleconnectioncount:1,//等候窗口:0,//}等待池。结尾 ( ) ;水池 。 getpoolstate(); // {// activeConnectionCount:0,//结束:true,// idleconnectioncount:0,// sudewlientcount:0,//}};主要的 ( ) ;

/ ** * @param connectionuri postgreSQL [连接URI](https://www.postgresql.org/docs/current/libpq-connect.html#libpq-conntring)。 * / createpool(connectionuri:string,clientconfiguration:clientconfigurationtype):databasepooltype; / ** * @property captureStacktrace指示是否在执行查询之前捕获堆栈跟踪。中间摆乘以查询执行上下文访问堆栈跟踪。 (默认值:true)* @property connectionretrylimit次数重试建立新连接。 (默认值:3)* @Property ConnectionTimeOut超时(以毫秒为单位),之后会在无法建立连接时提出错误。 (默认值:5000)* @Property IdleIntransActionSignterTimeOut超时(以毫秒为单位),之后闲置客户端关闭。使用' disable_timeout'常量禁用超时。 (默认值:60000)* @Property IDLETIMEOUT超时(以毫秒为单位),之后闲置客户端关闭。使用' disable_timeout'常量禁用超时。 (默认值:5000)* @Property拦截器[Slonik拦截器](https://github.com/gajus/slonik#slonik-interceptors)。 * @property maximumpoolsize不允许多个连接。使用' disable_timeout'常量禁用超时。 (默认值:10)* @Property PreplopnativeBindings使用LibpQ绑定时,何时安装了PG-Native`模块时。 (默认值:true)* @property语句timeout(以毫秒为单位),之后指示数据库中止查询。使用' disable_timeout'常量禁用超时。 (默认值:60000)* @Property TransactionRetryLimit RetryLimit Retry乘时失败的事务回滚类错误的事务失败。 (默认值:5)* @property typeparsers [slonik类型解析器](https://github.com/gajus/slonik#slonik-type-parsers)。 * /型ClientConfigurationInputtype = {| + CaptureStacktrace? :Boolean,+ ConnectionRetryLimit? :号码,+ connectionTimeout? :号码| ' disable_timeout' ,+ idleintransactiondionTimeout? :号码| ' disable_timeout' ,+ iDletimeout? :号码| ' disable_timeout' ,+拦截器?:$ readonlyarray< InterceptorType> ,+ maximumpoolsize?:number,+ prefernative inings? :布尔,+ statementTimeout? :号码| ' disable_timeout' ,+ transactionretrylimit? :号码,+ typeparssers?:$ readonlyarray< typeparsertype> ,| };

超时(以毫秒为单位),之后提出错误如果无法建立连接,则会提出错误。

超时(以毫秒为单位)关闭空闲客户端关闭。使用' disable_timeout'常量禁用超时。

超时(以毫秒为单位)关闭空闲客户端关闭。使用' disable_timeout'常量禁用超时。

超时(以毫秒为单位),之后指示数据库中止查询。使用' disable_timeout'常量禁用超时。

Slonik默认设置了积极的超时。这些超时旨在为数据库提供安全接口。这些超时可能不适用于所有程序。如果您的程序有长时间的运行语句,请考虑为这些语句调整超时,而不是更改默认值。

默认情况下,Slonik在安装PG-Native时使用本机绑定。要在安装PG-Native时使用JavaScript绑定,请配置PrefernativeBindings:False。

Slonik仅允许在提供给Pool#Connect()方法的Provine常规的持续时间内签出连接。

导入{createpool,}从' slonik' ; const pool = createpool(' postgres:// localhost'); Const结果=等待池。 Connect(ASYNC(连接)=> {等待连接。查询(SQL`选择1`);等待连接。查询(SQL` SELECT 2`);返回' foo&#39 ;;结果 ; //#39; foo'

通过提供给Connect()方法的函数产生的承诺后,连接将释放回池()方法是解决或拒绝的。

导入{createmockpool,createmockqueryresult,}从' slonik' ; over ridestype = {| +查询:( sql:string,值:$ readonlyarray< incriventvalueexpressiontype>,)=>承诺< queryresulttype< queryresultrowtype>> ,| }; CreateMockPool(覆盖:覆盖型):databasepooltype; createmockqueryResult(行:$ ReadOnlyArray< queryresultrowtype>):queryresulttype< queryresultrowtype> ;

导入{createmockpool,createmockqueryresult,}从' slonik' ; const pool = createmockpool({查询:async()=> {return createmockqueryresult([{foo:'栏和#39;},]);},});等待游泳池。 Connect(异步(连接)=> {const结果=等待连接。查询(SQL`选择$ {' foo'}`);

PG故意构建,以提供未致电,最小的抽象,并鼓励使用其他模块来实现便利方法。

Slonik是基于PG的顶部构建的,为构建查询和查询数据提供了便利方法。

PG工作开始于2010年2月28日星期二28:09:21。它由Brian Carlson撰写。

顾名思义,最初建立了pg-promise,以便使用PG模块与承诺(当时只支持的延续传递样式(CPS),即回调)使用PG模块。从那以来,PG-Promise添加了连接/事务处理的功能,强大的查询格式化引擎和处理查询结果的声明方法。

Slonik不允许执行原始文本查询。 Slonik查询只能使用SQL标记的模板文字构建。该设计可防止不安全值插值。

Slonik实现拦截器API(中间件)。中间摆允许修改连接处理,覆盖查询并修改查询结果。示例SLONIK拦截器包括字段名称转换,查询标准化和查询基准测试。

注意:PG-Promise的作者反对上述索赔。我已经删除了显然错误的差异。我认为上述两种差异仍然有效差异:尽管PG-Praine可能具有可变插值和拦截器的替代功能,但它以不提供Slonik提供的相同优势的方式实现它们,即:保证安全性和支持使用多个插件扩展库功能。

斯洛尼克不提供。当前的提议是创建一个可以访问查询片段构造函数的拦截器。

当加权哪些抽象使用时,不承认pg-promise是一个具有数十种贡献者的成熟项目。与此同时,Slonik是一个年轻的项目(2017年3月开始),直到最近在没有活跃的社区投入的情况下发展。但是,如果您确实支持Slonik增加的独特功能,则本发明的API设计,并不害怕在年轻日期里采用技术,那么我热烈邀请您采用Slonik并成为我打算做出的贡献者Node.js社区中的标准PostgreSQL客户端。

PG-Promise的工作开始于2015年3月4日星期三。它由Vitaly Tomilov撰写撰写型号。

注意:与使用OID识别类型的PG类型不同,Slonik使用其名称标识类型。

从&#39导入{createTimestamptypeparser} slonik' ; createTimestAmptyPeparser(); // {//名称:'时间戳',//解析:(值)=> {//返回值=== null?值:date.parse(价值); //} //}

拦截器是一种实现可以在连接生命周期的不同阶段更改数据库客户端的行为的方法

键入Instsportype = {| + interpoolConnection?:(connectionContext:ConnectionContextType,连接:DatabasePoolConnectionType)=> MantPromiseType< null> ,+ juildqueryexecution?:(querycontext:querycontexttype,查询:querytype,结果:queryresulttype< queryresultrowtype>)=> MantPromiseType< queryresulttype< queryresultrowtype>> ,+ passpoolconnection?:( connectionContext:connectionContextType)=> MantpromiseType&lt ;? databasepooltype> ,+ passpoolconnectionrelease?:(connectionContext:ConnectionContextType,Connection:databasepoolconnectiontype)=> MantPromiseType< null> ,+ pressuedexecution?:(querycontext:querycontexttype,查询:querytype)=> MantPromiseType< queryresulttype< queryresultrowtype>> | MantPromiseType< null> ,+ preasqueredresult?:(querycontext:querycontexttype,查询:querytype,结果:queryresulttype< queryresultrowtype>)=> MantPromiseType< null> ,+ beforeTransformQuery?:(QueryContext:QueryContextType,查询:querytype)=>承诺< null> ,+ queryexecutionerror?:(querycontext:querycontexttype,查询:querytype,错误:slonikerror)=> MantPromiseType< null> ,+变形式?:( querycontext:querycontexttype,查询:querytype)=> querytype,+ transformrow?:( querycontext:querycontexttype,查询:querytype,行:queryresultrowtype,字段:$ readonlyarray< fieldtype>)=> queryresultrowtype | };

从连接池获取连接后执行(或创建新连接),例如,

注意:使用流执行查询时,请使用空结果集调用afterquery。

此函数可以可选地返回查询的直接结果,这将导致从未执行的实际查询。

在变形前执行。 使用此拦截器捕获原始查询(例如,用于日志记录目的)。 const pool = await createpool(' postgres://'); 水池 。 连接(异步()=> {等待1; //拦截器在此处执行。↓}); ......