├── .gitignore ├── Docker ├── db.Dockerfile └── init.sql ├── Dockerfile ├── LICENSE ├── README.md ├── ably-postgres-connector-1.png ├── ably-postgres-connector-2.png ├── ably-postgres-connector.png ├── config ├── .env └── default.json ├── docker-compose.yml ├── examples ├── package-lock.json ├── package.json ├── with-env-config.js ├── with-env-docker.js └── with-json-config.js └── lib ├── .dockerignore ├── .npmignore ├── README.md ├── ably-postgres-connector-1.png ├── ably-postgres-connector-2.png ├── package-lock.json ├── package.json ├── src ├── connector.ts └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | lib/node_modules 2 | lib/dist 3 | examples/node_modules -------------------------------------------------------------------------------- /Docker/db.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:13 2 | COPY init.sql /docker-entrypoint-initdb.d/ -------------------------------------------------------------------------------- /Docker/init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE mydb; 2 | \connect mydb; 3 | CREATE TABLE users ( 4 | id integer, 5 | name text 6 | ); -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | RUN mkdir -p /ably-postgres-connector/lib 3 | WORKDIR /ably-postgres-connector/lib 4 | 5 | COPY lib/package-lock.json lib/package.json lib/tsconfig.json ./ 6 | RUN npm install 7 | RUN npm install -g typescript 8 | 9 | COPY lib/src src/ 10 | RUN npm run build 11 | 12 | WORKDIR /ably-postgres-connector 13 | COPY examples examples/ 14 | COPY config config/ 15 | EXPOSE 3000 16 | CMD ["node", "examples/with-env-docker.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Streaming PostgresDB changes to millions of clients in realtime 2 | 3 | The Ably-Postgres connector publishes a message on a given Ably channel whenever any operations (insert/update/delete) are executed on the tables of your PostgreSQL database. 4 | 5 | You can setup the connector with the configuration details of your database, as well as the Ably app, including your API Key, channel names for various types of updates, etc. 6 | 7 | Check out the [example config](config/default.json) for more info. 8 | 9 | ### Prerequisites 10 | 11 | - [PostgreSQL](https://www.postgresql.org/) (this project was tested on version 13) 12 | - [An Ably account](https://ably.com/) 13 | 14 | ### Installation 15 | 16 | ```sh 17 | npm install ably-postgres-connector --save 18 | ``` 19 | 20 | ### Setup config 21 | 22 | - The first step is to add in your configuration. You can do this via env file or a JSON file. 23 | 24 | #### Option 1 - Adding config via a JSON file 25 | 26 | - Create a JSON config file within your application, let's say `config/default.json` for example. (refer to the [example JSON config](config/default.json)). 27 | - Add your database and Ably account credentials as needed. 28 | 29 | ##### Example usage 30 | 31 | ```javascript 32 | const { Connector } = require("ably-postgres-connector"); 33 | const useWithJSONConfig = () => { 34 | const ablyconnector = new Connector("config/default.json"); 35 | ablyconnector.start(); 36 | }; 37 | useWithJSONConfig(); 38 | ``` 39 | 40 | ##### Running 41 | 42 | ```sh 43 | cd examples 44 | npm i 45 | node with-json-config.js 46 | ``` 47 | 48 | #### Option 2 - Adding config via a env file 49 | 50 | - Create a env config file within your application, let's say `config/.env` for example. (refer to the [example env config](config/.env)). 51 | - Add your database and Ably account credentials as needed. 52 | 53 | ##### Example usage 54 | 55 | ```javascript 56 | const { Connector } = require("ably-postgres-connector"); 57 | const useWithEnvConfig = () => { 58 | const ablyconnector = new Connector("config/.env"); 59 | ablyconnector.start(); 60 | }; 61 | useWithEnvConfig(); 62 | ``` 63 | 64 | ##### Running (Using the example file) 65 | 66 | ```sh 67 | cd examples 68 | npm i 69 | node with-env-config.js 70 | ``` 71 | 72 | #### Option 3 - Adding config via a env file through docker-compose 73 | 74 | - Create a env config file within your application, let's say `config/.env` for example. (refer to the [example env config](config/.env)). 75 | - Add your database and Ably account credentials as needed. 76 | - Add path of `.env` file to your `docker-compose` file (refer to the [example docker-compose](docker-compose.yml)). 77 | 78 | ##### Example usage 79 | 80 | ```javascript 81 | const { Connector } = require("ably-postgres-connector"); 82 | const useWithEnvDockerCompose = () => { 83 | const ablyconnector = new Connector(); 84 | ablyconnector.start(); 85 | }; 86 | useWithEnvDockerCompose(); 87 | ``` 88 | 89 | ```yaml 90 | # connector-block 91 | connector: 92 | build: 93 | context: . 94 | env_file: ./config/.env 95 | depends_on: 96 | - db 97 | ports: 98 | - "3000:3000" 99 | ``` 100 | 101 | ##### Running (Using the example docker-compose file) 102 | 103 | - Uses the `Docker` folder to setup the postgresql image with a dummy DB & users table. 104 | - Uses the `Dockerfile` to create the container with node, build the connector & add the config file. 105 | 106 | ```sh 107 | docker-compose run connector 108 | ``` 109 | 110 | ### Connector in Action! 111 | 112 | Visit your Ably dev console and connect to the channel `ably-users-added` (or whichever channel you specified in your config). Try performing various operations (insert, update, delete) on your table. For every change, you should see a new message in the specific channel(s). 113 | 114 | ## How does the connector work? 115 | 116 | ably-to-db-postgres@2x (3) 117 | 118 | - The config file contains the details related to the tables you want to listen for data changes on and your Ably API key. 119 | - Using that config file, the connector creates an Ably config table `ablycontroltable` to maintain the table to Ably channel mapping in the DB. 120 | - The connector then creates a DB procedure/function which performs the [`pg_notify`](https://www.postgresql.org/docs/current/sql-notify.html) function that publishes data changes on a data channel. 121 | - The connector then creates triggers for the table-operation combination specified in the config. The job of the trigger is to execute the procedure created above. 122 | - The connector is listening for changes on that particular data channel using the [`LISTEN`](https://www.postgresql.org/docs/current/sql-listen.html) feature. When it gets a notification it publishes the data on the appropriate Ably channel. 123 | -------------------------------------------------------------------------------- /ably-postgres-connector-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/ably-postgres-connector/28061b5b2e77151dd77cbbdcc9122dcab14862d5/ably-postgres-connector-1.png -------------------------------------------------------------------------------- /ably-postgres-connector-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/ably-postgres-connector/28061b5b2e77151dd77cbbdcc9122dcab14862d5/ably-postgres-connector-2.png -------------------------------------------------------------------------------- /ably-postgres-connector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/ably-postgres-connector/28061b5b2e77151dd77cbbdcc9122dcab14862d5/ably-postgres-connector.png -------------------------------------------------------------------------------- /config/.env: -------------------------------------------------------------------------------- 1 | DB_HOST=db 2 | DB_PORT=5432 3 | DB_USER=postgres 4 | DB_PASSWORD=postgres 5 | DB_NAME=mydb 6 | ABLY_API_KEY=dummy 7 | ABLY_CONNECTOR=[{"tablename":"users","ablychannelname":"ably-users-added","operation":"INSERT"},{"tablename":"users","ablychannelname":"ably-users-updated","operation":"UPDATE"},{"tablename":"users","ablychannelname":"ably-users-removed","operation":"DELETE"}] 8 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "dbConfig": { 3 | "host": "db", 4 | "port": 5432, 5 | "user": "postgres", 6 | "password": "postgres", 7 | "database": "mydb" 8 | }, 9 | "connector": [ 10 | { 11 | "tablename": "users", 12 | "ablychannelname": "ably-users-added", 13 | "operation": "INSERT" 14 | }, 15 | { 16 | "tablename": "users", 17 | "ablychannelname": "ably-users-updated", 18 | "operation": "UPDATE" 19 | }, 20 | { 21 | "tablename": "users", 22 | "ablychannelname": "ably-users-removed", 23 | "operation": "DELETE" 24 | } 25 | ], 26 | "ably": { 27 | "apiKey": "API_KEY" 28 | } 29 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | db: 4 | build: 5 | context: ./Docker 6 | dockerfile: db.Dockerfile 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: postgres 10 | ports: 11 | - "5432:5432" 12 | volumes: 13 | - connector-db:/var/lib/postgresql/data 14 | 15 | connector: 16 | build: 17 | context: . 18 | env_file: ./config/.env 19 | depends_on: 20 | - db 21 | ports: 22 | - "3000:3000" 23 | 24 | volumes: 25 | connector-db: 26 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ably/msgpack-js": { 8 | "version": "0.3.4", 9 | "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.3.4.tgz", 10 | "integrity": "sha512-gmnsxxcN/8WfoxZxQQF9LvM3ZUbuVH0LCS6oX7EJS+VfkXWBFIgDV+h7a0sntwKSvAEg4uJzNDje7kpH8/LJ3Q==", 11 | "requires": { 12 | "bops": "^1.0.1" 13 | } 14 | }, 15 | "ably": { 16 | "version": "1.2.13", 17 | "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.13.tgz", 18 | "integrity": "sha512-oYTRhjspzTh/LdqYr3JJTofNPyZgyUVUCt1Do653dx7SwK9kxOJe+P6jqcmosR/cQJVRrCxE76yy+PUA80ZudA==", 19 | "requires": { 20 | "@ably/msgpack-js": "^0.3.3", 21 | "request": "^2.87.0", 22 | "ws": "^5.1" 23 | } 24 | }, 25 | "ably-postgres-connector": { 26 | "version": "1.0.1", 27 | "resolved": "https://registry.npmjs.org/ably-postgres-connector/-/ably-postgres-connector-1.0.1.tgz", 28 | "integrity": "sha512-K7idh9pLftQTQ92/YaGGUXI3ploNQz/1xbT9xHV0Uqm8IpPfKwca6OqYtP+GwSJ0apdBJB2s+N4Jrk3JOs0lQw==", 29 | "requires": { 30 | "ably": "^1.2.5", 31 | "dotenv": "^10.0.0", 32 | "pg": "^8.5.1" 33 | } 34 | }, 35 | "ajv": { 36 | "version": "6.12.6", 37 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 38 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 39 | "requires": { 40 | "fast-deep-equal": "^3.1.1", 41 | "fast-json-stable-stringify": "^2.0.0", 42 | "json-schema-traverse": "^0.4.1", 43 | "uri-js": "^4.2.2" 44 | } 45 | }, 46 | "asn1": { 47 | "version": "0.2.4", 48 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 49 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 50 | "requires": { 51 | "safer-buffer": "~2.1.0" 52 | } 53 | }, 54 | "assert-plus": { 55 | "version": "1.0.0", 56 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 57 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 58 | }, 59 | "async-limiter": { 60 | "version": "1.0.1", 61 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 62 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 63 | }, 64 | "asynckit": { 65 | "version": "0.4.0", 66 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 67 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 68 | }, 69 | "aws-sign2": { 70 | "version": "0.7.0", 71 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 72 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 73 | }, 74 | "aws4": { 75 | "version": "1.11.0", 76 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 77 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" 78 | }, 79 | "base64-js": { 80 | "version": "1.0.2", 81 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", 82 | "integrity": "sha1-R0IRyV5s8qVH20YeT2d4tR0I+mU=" 83 | }, 84 | "bcrypt-pbkdf": { 85 | "version": "1.0.2", 86 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 87 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 88 | "requires": { 89 | "tweetnacl": "^0.14.3" 90 | } 91 | }, 92 | "bops": { 93 | "version": "1.0.1", 94 | "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", 95 | "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", 96 | "requires": { 97 | "base64-js": "1.0.2", 98 | "to-utf8": "0.0.1" 99 | } 100 | }, 101 | "buffer-writer": { 102 | "version": "2.0.0", 103 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 104 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 105 | }, 106 | "caseless": { 107 | "version": "0.12.0", 108 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 109 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 110 | }, 111 | "combined-stream": { 112 | "version": "1.0.8", 113 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 114 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 115 | "requires": { 116 | "delayed-stream": "~1.0.0" 117 | } 118 | }, 119 | "core-util-is": { 120 | "version": "1.0.2", 121 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 122 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 123 | }, 124 | "dashdash": { 125 | "version": "1.14.1", 126 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 127 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 128 | "requires": { 129 | "assert-plus": "^1.0.0" 130 | } 131 | }, 132 | "delayed-stream": { 133 | "version": "1.0.0", 134 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 135 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 136 | }, 137 | "dotenv": { 138 | "version": "10.0.0", 139 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 140 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" 141 | }, 142 | "ecc-jsbn": { 143 | "version": "0.1.2", 144 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 145 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 146 | "requires": { 147 | "jsbn": "~0.1.0", 148 | "safer-buffer": "^2.1.0" 149 | } 150 | }, 151 | "extend": { 152 | "version": "3.0.2", 153 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 154 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 155 | }, 156 | "extsprintf": { 157 | "version": "1.3.0", 158 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 159 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 160 | }, 161 | "fast-deep-equal": { 162 | "version": "3.1.3", 163 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 164 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 165 | }, 166 | "fast-json-stable-stringify": { 167 | "version": "2.1.0", 168 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 169 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 170 | }, 171 | "forever-agent": { 172 | "version": "0.6.1", 173 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 174 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 175 | }, 176 | "form-data": { 177 | "version": "2.3.3", 178 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 179 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 180 | "requires": { 181 | "asynckit": "^0.4.0", 182 | "combined-stream": "^1.0.6", 183 | "mime-types": "^2.1.12" 184 | } 185 | }, 186 | "getpass": { 187 | "version": "0.1.7", 188 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 189 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 190 | "requires": { 191 | "assert-plus": "^1.0.0" 192 | } 193 | }, 194 | "har-schema": { 195 | "version": "2.0.0", 196 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 197 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 198 | }, 199 | "har-validator": { 200 | "version": "5.1.5", 201 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 202 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 203 | "requires": { 204 | "ajv": "^6.12.3", 205 | "har-schema": "^2.0.0" 206 | } 207 | }, 208 | "http-signature": { 209 | "version": "1.2.0", 210 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 211 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 212 | "requires": { 213 | "assert-plus": "^1.0.0", 214 | "jsprim": "^1.2.2", 215 | "sshpk": "^1.7.0" 216 | } 217 | }, 218 | "inherits": { 219 | "version": "2.0.4", 220 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 221 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 222 | }, 223 | "is-typedarray": { 224 | "version": "1.0.0", 225 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 226 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 227 | }, 228 | "isstream": { 229 | "version": "0.1.2", 230 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 231 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 232 | }, 233 | "jsbn": { 234 | "version": "0.1.1", 235 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 236 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 237 | }, 238 | "json-schema": { 239 | "version": "0.2.3", 240 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 241 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 242 | }, 243 | "json-schema-traverse": { 244 | "version": "0.4.1", 245 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 246 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 247 | }, 248 | "json-stringify-safe": { 249 | "version": "5.0.1", 250 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 251 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 252 | }, 253 | "jsprim": { 254 | "version": "1.4.1", 255 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 256 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 257 | "requires": { 258 | "assert-plus": "1.0.0", 259 | "extsprintf": "1.3.0", 260 | "json-schema": "0.2.3", 261 | "verror": "1.10.0" 262 | } 263 | }, 264 | "mime-db": { 265 | "version": "1.49.0", 266 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", 267 | "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" 268 | }, 269 | "mime-types": { 270 | "version": "2.1.32", 271 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", 272 | "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", 273 | "requires": { 274 | "mime-db": "1.49.0" 275 | } 276 | }, 277 | "oauth-sign": { 278 | "version": "0.9.0", 279 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 280 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 281 | }, 282 | "packet-reader": { 283 | "version": "1.0.0", 284 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 285 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 286 | }, 287 | "performance-now": { 288 | "version": "2.1.0", 289 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 290 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 291 | }, 292 | "pg": { 293 | "version": "8.7.1", 294 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz", 295 | "integrity": "sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA==", 296 | "requires": { 297 | "buffer-writer": "2.0.0", 298 | "packet-reader": "1.0.0", 299 | "pg-connection-string": "^2.5.0", 300 | "pg-pool": "^3.4.1", 301 | "pg-protocol": "^1.5.0", 302 | "pg-types": "^2.1.0", 303 | "pgpass": "1.x" 304 | } 305 | }, 306 | "pg-connection-string": { 307 | "version": "2.5.0", 308 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz", 309 | "integrity": "sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==" 310 | }, 311 | "pg-int8": { 312 | "version": "1.0.1", 313 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 314 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 315 | }, 316 | "pg-pool": { 317 | "version": "3.4.1", 318 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", 319 | "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" 320 | }, 321 | "pg-protocol": { 322 | "version": "1.5.0", 323 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", 324 | "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" 325 | }, 326 | "pg-types": { 327 | "version": "2.2.0", 328 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 329 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 330 | "requires": { 331 | "pg-int8": "1.0.1", 332 | "postgres-array": "~2.0.0", 333 | "postgres-bytea": "~1.0.0", 334 | "postgres-date": "~1.0.4", 335 | "postgres-interval": "^1.1.0" 336 | } 337 | }, 338 | "pgpass": { 339 | "version": "1.0.4", 340 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", 341 | "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", 342 | "requires": { 343 | "split2": "^3.1.1" 344 | } 345 | }, 346 | "postgres-array": { 347 | "version": "2.0.0", 348 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 349 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 350 | }, 351 | "postgres-bytea": { 352 | "version": "1.0.0", 353 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 354 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 355 | }, 356 | "postgres-date": { 357 | "version": "1.0.7", 358 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 359 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" 360 | }, 361 | "postgres-interval": { 362 | "version": "1.2.0", 363 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 364 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 365 | "requires": { 366 | "xtend": "^4.0.0" 367 | } 368 | }, 369 | "psl": { 370 | "version": "1.8.0", 371 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 372 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 373 | }, 374 | "punycode": { 375 | "version": "2.1.1", 376 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 377 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 378 | }, 379 | "qs": { 380 | "version": "6.5.2", 381 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 382 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 383 | }, 384 | "readable-stream": { 385 | "version": "3.6.0", 386 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 387 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 388 | "requires": { 389 | "inherits": "^2.0.3", 390 | "string_decoder": "^1.1.1", 391 | "util-deprecate": "^1.0.1" 392 | } 393 | }, 394 | "request": { 395 | "version": "2.88.2", 396 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 397 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 398 | "requires": { 399 | "aws-sign2": "~0.7.0", 400 | "aws4": "^1.8.0", 401 | "caseless": "~0.12.0", 402 | "combined-stream": "~1.0.6", 403 | "extend": "~3.0.2", 404 | "forever-agent": "~0.6.1", 405 | "form-data": "~2.3.2", 406 | "har-validator": "~5.1.3", 407 | "http-signature": "~1.2.0", 408 | "is-typedarray": "~1.0.0", 409 | "isstream": "~0.1.2", 410 | "json-stringify-safe": "~5.0.1", 411 | "mime-types": "~2.1.19", 412 | "oauth-sign": "~0.9.0", 413 | "performance-now": "^2.1.0", 414 | "qs": "~6.5.2", 415 | "safe-buffer": "^5.1.2", 416 | "tough-cookie": "~2.5.0", 417 | "tunnel-agent": "^0.6.0", 418 | "uuid": "^3.3.2" 419 | } 420 | }, 421 | "safe-buffer": { 422 | "version": "5.2.1", 423 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 424 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 425 | }, 426 | "safer-buffer": { 427 | "version": "2.1.2", 428 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 429 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 430 | }, 431 | "split2": { 432 | "version": "3.2.2", 433 | "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", 434 | "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", 435 | "requires": { 436 | "readable-stream": "^3.0.0" 437 | } 438 | }, 439 | "sshpk": { 440 | "version": "1.16.1", 441 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 442 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 443 | "requires": { 444 | "asn1": "~0.2.3", 445 | "assert-plus": "^1.0.0", 446 | "bcrypt-pbkdf": "^1.0.0", 447 | "dashdash": "^1.12.0", 448 | "ecc-jsbn": "~0.1.1", 449 | "getpass": "^0.1.1", 450 | "jsbn": "~0.1.0", 451 | "safer-buffer": "^2.0.2", 452 | "tweetnacl": "~0.14.0" 453 | } 454 | }, 455 | "string_decoder": { 456 | "version": "1.3.0", 457 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 458 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 459 | "requires": { 460 | "safe-buffer": "~5.2.0" 461 | } 462 | }, 463 | "to-utf8": { 464 | "version": "0.0.1", 465 | "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", 466 | "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" 467 | }, 468 | "tough-cookie": { 469 | "version": "2.5.0", 470 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 471 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 472 | "requires": { 473 | "psl": "^1.1.28", 474 | "punycode": "^2.1.1" 475 | } 476 | }, 477 | "tunnel-agent": { 478 | "version": "0.6.0", 479 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 480 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 481 | "requires": { 482 | "safe-buffer": "^5.0.1" 483 | } 484 | }, 485 | "tweetnacl": { 486 | "version": "0.14.5", 487 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 488 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 489 | }, 490 | "uri-js": { 491 | "version": "4.4.1", 492 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 493 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 494 | "requires": { 495 | "punycode": "^2.1.0" 496 | } 497 | }, 498 | "util-deprecate": { 499 | "version": "1.0.2", 500 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 501 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 502 | }, 503 | "uuid": { 504 | "version": "3.4.0", 505 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 506 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 507 | }, 508 | "verror": { 509 | "version": "1.10.0", 510 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 511 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 512 | "requires": { 513 | "assert-plus": "^1.0.0", 514 | "core-util-is": "1.0.2", 515 | "extsprintf": "^1.2.0" 516 | } 517 | }, 518 | "ws": { 519 | "version": "5.2.3", 520 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.3.tgz", 521 | "integrity": "sha512-jZArVERrMsKUatIdnLzqvcfydI85dvd/Fp1u/VOpfdDWQ4c9qWXe+VIeAbQ5FrDwciAkr+lzofXLz3Kuf26AOA==", 522 | "requires": { 523 | "async-limiter": "~1.0.0" 524 | } 525 | }, 526 | "xtend": { 527 | "version": "4.0.2", 528 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 529 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 530 | } 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "Examples for ably-postgres-connector", 5 | "main": "", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ably-postgres-connector": "^1.0.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/with-env-config.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require("ably-postgres-connector"); 2 | const useWithEnvConfig = () => { 3 | const ablyconnector = new Connector("../config/.env"); 4 | ablyconnector.start(); 5 | }; 6 | useWithEnvConfig(); 7 | -------------------------------------------------------------------------------- /examples/with-env-docker.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require("../lib/dist"); 2 | const useWithEnvDockerCompose = () => { 3 | const ablyconnector = new Connector(); 4 | ablyconnector.start(); 5 | }; 6 | useWithEnvDockerCompose(); 7 | -------------------------------------------------------------------------------- /examples/with-json-config.js: -------------------------------------------------------------------------------- 1 | const { Connector } = require("ably-postgres-connector"); 2 | const useWithJSONConfig = () => { 3 | const ablyconnector = new Connector("../config/default.json"); 4 | ablyconnector.start(); 5 | }; 6 | useWithJSONConfig(); 7 | -------------------------------------------------------------------------------- /lib/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | lib/node_modules/ -------------------------------------------------------------------------------- /lib/.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | .dockerignore -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | ## Streaming PostgresDB changes to millions of clients in realtime 2 | 3 | The Ably-Postgres connector publishes a message on a given Ably channel whenever any operations (insert/update/delete) are executed on the tables of your PostgreSQL database. 4 | 5 | You can setup the connector with the configuration details of your database, as well as the Ably app, including your API Key, channel names for various types of updates, etc. 6 | 7 | Check out the [example config](../config/default.json) for more info. 8 | 9 | ### Prerequisites 10 | 11 | - [PostgreSQL](https://www.postgresql.org/) (this project was tested on version 13) 12 | - [An Ably account](https://ably.com/) 13 | 14 | ### Installation 15 | 16 | ```sh 17 | npm install ably-postgres-connector --save 18 | ``` 19 | 20 | ### Setup config 21 | 22 | - The first step is to add in your configuration. You can do this via env file or a JSON file. 23 | 24 | #### Option 1 - Adding config via a JSON file 25 | 26 | - Create a JSON config file within your application, let's say `config/default.json` for example. (refer to the [example JSON config](../config/default.json)). 27 | - Add your database and Ably account credentials as needed. 28 | 29 | ##### Example usage 30 | 31 | ```javascript 32 | const { Connector } = require("ably-postgres-connector"); 33 | const useWithJSONConfig = () => { 34 | const ablyconnector = new Connector("config/default.json"); 35 | ablyconnector.start(); 36 | }; 37 | useWithJSONConfig(); 38 | ``` 39 | 40 | ##### Running 41 | 42 | ```sh 43 | node examples/with-json-config.js 44 | ``` 45 | 46 | #### Option 2 - Adding config via a env file 47 | 48 | - Create a env config file within your application, let's say `config/.env` for example. (refer to the [example env config](../config/.env)). 49 | - Add your database and Ably account credentials as needed. 50 | 51 | ##### Example usage 52 | 53 | ```javascript 54 | const { Connector } = require("ably-postgres-connector"); 55 | const useWithEnvConfig = () => { 56 | const ablyconnector = new Connector("config/.env"); 57 | ablyconnector.start(); 58 | }; 59 | useWithEnvConfig(); 60 | ``` 61 | 62 | ##### Running (Using the example file) 63 | 64 | ```sh 65 | node examples/with-env-config.js 66 | ``` 67 | 68 | #### Option 3 - Adding config via a env file through docker-compose 69 | 70 | - Create a env config file within your application, let's say `config/.env` for example. (refer to the [example env config](../config/.env)). 71 | - Add your database and Ably account credentials as needed. 72 | - Add path of `.env` file to your `docker-compose` file (refer to the [example docker-compose](docker-compose.yml)). 73 | 74 | ##### Example usage 75 | 76 | ```javascript 77 | const { Connector } = require("ably-postgres-connector"); 78 | const useWithEnvDockerCompose = () => { 79 | const ablyconnector = new Connector(); 80 | ablyconnector.start(); 81 | }; 82 | useWithEnvDockerCompose(); 83 | ``` 84 | 85 | ```yaml 86 | # connector-block 87 | connector: 88 | build: 89 | context: . 90 | env_file: ./config/.env 91 | depends_on: 92 | - db 93 | ports: 94 | - "3000:3000" 95 | ``` 96 | 97 | ##### Running (Using the example docker-compose file) 98 | 99 | - Uses the `Docker` folder to setup the postgresql image with a dummy DB & users table. 100 | - Uses the `Dockerfile` to create the container with node, build the connector & add the config file. 101 | 102 | ```sh 103 | docker-compose run connector 104 | ``` 105 | 106 | ### Connector in Action! 107 | 108 | Visit your Ably dev console and connect to the channel `ably-users-added` (or whichever channel you specified in your config). Try performing various operations (insert, update, delete) on your table. For every change, you should see a new message in the specific channel(s). 109 | 110 | ## How does the connector work? 111 | 112 | ably-to-db-postgres@2x (3) 113 | 114 | 115 | - The config file contains the details related to the tables you want to listen for data changes on and your Ably API key. 116 | - Using that config file, the connector creates an Ably config table `ablycontroltable` to maintain the table to Ably channel mapping in the DB. 117 | - The connector then creates a DB procedure/function which performs the [`pg_notify`](https://www.postgresql.org/docs/current/sql-notify.html) function that publishes data changes on a data channel. 118 | - The connector then creates triggers for the table-operation combination specified in the config. The job of the trigger is to execute the procedure created above. 119 | - The connector is listening for changes on that particular data channel using the [`LISTEN`](https://www.postgresql.org/docs/current/sql-listen.html) feature. When it gets a notification it publishes the data on the appropriate Ably channel. 120 | -------------------------------------------------------------------------------- /lib/ably-postgres-connector-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/ably-postgres-connector/28061b5b2e77151dd77cbbdcc9122dcab14862d5/lib/ably-postgres-connector-1.png -------------------------------------------------------------------------------- /lib/ably-postgres-connector-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ably-labs/ably-postgres-connector/28061b5b2e77151dd77cbbdcc9122dcab14862d5/lib/ably-postgres-connector-2.png -------------------------------------------------------------------------------- /lib/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ably-postgres-connector", 3 | "version": "1.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ably/msgpack-js": { 8 | "version": "0.3.4", 9 | "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.3.4.tgz", 10 | "integrity": "sha512-gmnsxxcN/8WfoxZxQQF9LvM3ZUbuVH0LCS6oX7EJS+VfkXWBFIgDV+h7a0sntwKSvAEg4uJzNDje7kpH8/LJ3Q==", 11 | "requires": { 12 | "bops": "^1.0.1" 13 | } 14 | }, 15 | "@types/node": { 16 | "version": "14.14.35", 17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", 18 | "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==", 19 | "dev": true 20 | }, 21 | "@types/pg": { 22 | "version": "7.14.11", 23 | "resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.14.11.tgz", 24 | "integrity": "sha512-EnZkZ1OMw9DvNfQkn2MTJrwKmhJYDEs5ujWrPfvseWNoI95N8B4HzU/Ltrq5ZfYxDX/Zg8mTzwr6UAyTjjFvXA==", 25 | "dev": true, 26 | "requires": { 27 | "@types/node": "*", 28 | "pg-protocol": "^1.2.0", 29 | "pg-types": "^2.2.0" 30 | } 31 | }, 32 | "ably": { 33 | "version": "1.2.6", 34 | "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.6.tgz", 35 | "integrity": "sha512-5dBg0iMcJEP/ogY7v9FGTZ1c7y5odf6o2TM+purdfy3g5fCZ8qHyN0Wu+jaLHmNV5auFcxX2Y/W14RX3sSE2hg==", 36 | "requires": { 37 | "@ably/msgpack-js": "^0.3.3", 38 | "request": "^2.87.0", 39 | "ws": "^5.1" 40 | } 41 | }, 42 | "ajv": { 43 | "version": "6.12.6", 44 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 45 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 46 | "requires": { 47 | "fast-deep-equal": "^3.1.1", 48 | "fast-json-stable-stringify": "^2.0.0", 49 | "json-schema-traverse": "^0.4.1", 50 | "uri-js": "^4.2.2" 51 | } 52 | }, 53 | "asn1": { 54 | "version": "0.2.4", 55 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 56 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 57 | "requires": { 58 | "safer-buffer": "~2.1.0" 59 | } 60 | }, 61 | "assert-plus": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 64 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 65 | }, 66 | "async-limiter": { 67 | "version": "1.0.1", 68 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 69 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 70 | }, 71 | "asynckit": { 72 | "version": "0.4.0", 73 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 74 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 75 | }, 76 | "aws-sign2": { 77 | "version": "0.7.0", 78 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 79 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 80 | }, 81 | "aws4": { 82 | "version": "1.11.0", 83 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 84 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" 85 | }, 86 | "base64-js": { 87 | "version": "1.0.2", 88 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.0.2.tgz", 89 | "integrity": "sha1-R0IRyV5s8qVH20YeT2d4tR0I+mU=" 90 | }, 91 | "bcrypt-pbkdf": { 92 | "version": "1.0.2", 93 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 94 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 95 | "requires": { 96 | "tweetnacl": "^0.14.3" 97 | } 98 | }, 99 | "bops": { 100 | "version": "1.0.1", 101 | "resolved": "https://registry.npmjs.org/bops/-/bops-1.0.1.tgz", 102 | "integrity": "sha512-qCMBuZKP36tELrrgXpAfM+gHzqa0nLsWZ+L37ncsb8txYlnAoxOPpVp+g7fK0sGkMXfA0wl8uQkESqw3v4HNag==", 103 | "requires": { 104 | "base64-js": "1.0.2", 105 | "to-utf8": "0.0.1" 106 | } 107 | }, 108 | "buffer-writer": { 109 | "version": "2.0.0", 110 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", 111 | "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" 112 | }, 113 | "caseless": { 114 | "version": "0.12.0", 115 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 116 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 117 | }, 118 | "combined-stream": { 119 | "version": "1.0.8", 120 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 121 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 122 | "requires": { 123 | "delayed-stream": "~1.0.0" 124 | } 125 | }, 126 | "core-util-is": { 127 | "version": "1.0.2", 128 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 129 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 130 | }, 131 | "dashdash": { 132 | "version": "1.14.1", 133 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 134 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 135 | "requires": { 136 | "assert-plus": "^1.0.0" 137 | } 138 | }, 139 | "delayed-stream": { 140 | "version": "1.0.0", 141 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 142 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 143 | }, 144 | "dotenv": { 145 | "version": "10.0.0", 146 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", 147 | "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" 148 | }, 149 | "ecc-jsbn": { 150 | "version": "0.1.2", 151 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 152 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 153 | "requires": { 154 | "jsbn": "~0.1.0", 155 | "safer-buffer": "^2.1.0" 156 | } 157 | }, 158 | "extend": { 159 | "version": "3.0.2", 160 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 161 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 162 | }, 163 | "extsprintf": { 164 | "version": "1.3.0", 165 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 166 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 167 | }, 168 | "fast-deep-equal": { 169 | "version": "3.1.3", 170 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 171 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 172 | }, 173 | "fast-json-stable-stringify": { 174 | "version": "2.1.0", 175 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 176 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 177 | }, 178 | "forever-agent": { 179 | "version": "0.6.1", 180 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 181 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 182 | }, 183 | "form-data": { 184 | "version": "2.3.3", 185 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 186 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 187 | "requires": { 188 | "asynckit": "^0.4.0", 189 | "combined-stream": "^1.0.6", 190 | "mime-types": "^2.1.12" 191 | } 192 | }, 193 | "getpass": { 194 | "version": "0.1.7", 195 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 196 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 197 | "requires": { 198 | "assert-plus": "^1.0.0" 199 | } 200 | }, 201 | "har-schema": { 202 | "version": "2.0.0", 203 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 204 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 205 | }, 206 | "har-validator": { 207 | "version": "5.1.5", 208 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 209 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 210 | "requires": { 211 | "ajv": "^6.12.3", 212 | "har-schema": "^2.0.0" 213 | } 214 | }, 215 | "http-signature": { 216 | "version": "1.2.0", 217 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 218 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 219 | "requires": { 220 | "assert-plus": "^1.0.0", 221 | "jsprim": "^1.2.2", 222 | "sshpk": "^1.7.0" 223 | } 224 | }, 225 | "inherits": { 226 | "version": "2.0.4", 227 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 228 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 229 | }, 230 | "is-typedarray": { 231 | "version": "1.0.0", 232 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 233 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 234 | }, 235 | "isstream": { 236 | "version": "0.1.2", 237 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 238 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 239 | }, 240 | "jsbn": { 241 | "version": "0.1.1", 242 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 243 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 244 | }, 245 | "json-schema": { 246 | "version": "0.2.3", 247 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 248 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 249 | }, 250 | "json-schema-traverse": { 251 | "version": "0.4.1", 252 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 253 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 254 | }, 255 | "json-stringify-safe": { 256 | "version": "5.0.1", 257 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 258 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 259 | }, 260 | "jsprim": { 261 | "version": "1.4.1", 262 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 263 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 264 | "requires": { 265 | "assert-plus": "1.0.0", 266 | "extsprintf": "1.3.0", 267 | "json-schema": "0.2.3", 268 | "verror": "1.10.0" 269 | } 270 | }, 271 | "mime-db": { 272 | "version": "1.46.0", 273 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.46.0.tgz", 274 | "integrity": "sha512-svXaP8UQRZ5K7or+ZmfNhg2xX3yKDMUzqadsSqi4NCH/KomcH75MAMYAGVlvXn4+b/xOPhS3I2uHKRUzvjY7BQ==" 275 | }, 276 | "mime-types": { 277 | "version": "2.1.29", 278 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.29.tgz", 279 | "integrity": "sha512-Y/jMt/S5sR9OaqteJtslsFZKWOIIqMACsJSiHghlCAyhf7jfVYjKBmLiX8OgpWeW+fjJ2b+Az69aPFPkUOY6xQ==", 280 | "requires": { 281 | "mime-db": "1.46.0" 282 | } 283 | }, 284 | "oauth-sign": { 285 | "version": "0.9.0", 286 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 287 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 288 | }, 289 | "packet-reader": { 290 | "version": "1.0.0", 291 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", 292 | "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" 293 | }, 294 | "performance-now": { 295 | "version": "2.1.0", 296 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 297 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 298 | }, 299 | "pg": { 300 | "version": "8.5.1", 301 | "resolved": "https://registry.npmjs.org/pg/-/pg-8.5.1.tgz", 302 | "integrity": "sha512-9wm3yX9lCfjvA98ybCyw2pADUivyNWT/yIP4ZcDVpMN0og70BUWYEGXPCTAQdGTAqnytfRADb7NERrY1qxhIqw==", 303 | "requires": { 304 | "buffer-writer": "2.0.0", 305 | "packet-reader": "1.0.0", 306 | "pg-connection-string": "^2.4.0", 307 | "pg-pool": "^3.2.2", 308 | "pg-protocol": "^1.4.0", 309 | "pg-types": "^2.1.0", 310 | "pgpass": "1.x" 311 | } 312 | }, 313 | "pg-connection-string": { 314 | "version": "2.4.0", 315 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz", 316 | "integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ==" 317 | }, 318 | "pg-int8": { 319 | "version": "1.0.1", 320 | "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", 321 | "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" 322 | }, 323 | "pg-pool": { 324 | "version": "3.2.2", 325 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.2.2.tgz", 326 | "integrity": "sha512-ORJoFxAlmmros8igi608iVEbQNNZlp89diFVx6yV5v+ehmpMY9sK6QgpmgoXbmkNaBAx8cOOZh9g80kJv1ooyA==" 327 | }, 328 | "pg-protocol": { 329 | "version": "1.4.0", 330 | "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.4.0.tgz", 331 | "integrity": "sha512-El+aXWcwG/8wuFICMQjM5ZSAm6OWiJicFdNYo+VY3QP+8vI4SvLIWVe51PppTzMhikUJR+PsyIFKqfdXPz/yxA==" 332 | }, 333 | "pg-types": { 334 | "version": "2.2.0", 335 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", 336 | "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", 337 | "requires": { 338 | "pg-int8": "1.0.1", 339 | "postgres-array": "~2.0.0", 340 | "postgres-bytea": "~1.0.0", 341 | "postgres-date": "~1.0.4", 342 | "postgres-interval": "^1.1.0" 343 | } 344 | }, 345 | "pgpass": { 346 | "version": "1.0.4", 347 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz", 348 | "integrity": "sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==", 349 | "requires": { 350 | "split2": "^3.1.1" 351 | } 352 | }, 353 | "postgres-array": { 354 | "version": "2.0.0", 355 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", 356 | "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" 357 | }, 358 | "postgres-bytea": { 359 | "version": "1.0.0", 360 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 361 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 362 | }, 363 | "postgres-date": { 364 | "version": "1.0.7", 365 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", 366 | "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" 367 | }, 368 | "postgres-interval": { 369 | "version": "1.2.0", 370 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", 371 | "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", 372 | "requires": { 373 | "xtend": "^4.0.0" 374 | } 375 | }, 376 | "psl": { 377 | "version": "1.8.0", 378 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 379 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 380 | }, 381 | "punycode": { 382 | "version": "2.1.1", 383 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 384 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 385 | }, 386 | "qs": { 387 | "version": "6.5.2", 388 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 389 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 390 | }, 391 | "readable-stream": { 392 | "version": "3.6.0", 393 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 394 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 395 | "requires": { 396 | "inherits": "^2.0.3", 397 | "string_decoder": "^1.1.1", 398 | "util-deprecate": "^1.0.1" 399 | } 400 | }, 401 | "request": { 402 | "version": "2.88.2", 403 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 404 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 405 | "requires": { 406 | "aws-sign2": "~0.7.0", 407 | "aws4": "^1.8.0", 408 | "caseless": "~0.12.0", 409 | "combined-stream": "~1.0.6", 410 | "extend": "~3.0.2", 411 | "forever-agent": "~0.6.1", 412 | "form-data": "~2.3.2", 413 | "har-validator": "~5.1.3", 414 | "http-signature": "~1.2.0", 415 | "is-typedarray": "~1.0.0", 416 | "isstream": "~0.1.2", 417 | "json-stringify-safe": "~5.0.1", 418 | "mime-types": "~2.1.19", 419 | "oauth-sign": "~0.9.0", 420 | "performance-now": "^2.1.0", 421 | "qs": "~6.5.2", 422 | "safe-buffer": "^5.1.2", 423 | "tough-cookie": "~2.5.0", 424 | "tunnel-agent": "^0.6.0", 425 | "uuid": "^3.3.2" 426 | } 427 | }, 428 | "safe-buffer": { 429 | "version": "5.2.1", 430 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 431 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 432 | }, 433 | "safer-buffer": { 434 | "version": "2.1.2", 435 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 436 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 437 | }, 438 | "split2": { 439 | "version": "3.2.2", 440 | "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", 441 | "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", 442 | "requires": { 443 | "readable-stream": "^3.0.0" 444 | } 445 | }, 446 | "sshpk": { 447 | "version": "1.16.1", 448 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 449 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 450 | "requires": { 451 | "asn1": "~0.2.3", 452 | "assert-plus": "^1.0.0", 453 | "bcrypt-pbkdf": "^1.0.0", 454 | "dashdash": "^1.12.0", 455 | "ecc-jsbn": "~0.1.1", 456 | "getpass": "^0.1.1", 457 | "jsbn": "~0.1.0", 458 | "safer-buffer": "^2.0.2", 459 | "tweetnacl": "~0.14.0" 460 | } 461 | }, 462 | "string_decoder": { 463 | "version": "1.3.0", 464 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 465 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 466 | "requires": { 467 | "safe-buffer": "~5.2.0" 468 | } 469 | }, 470 | "to-utf8": { 471 | "version": "0.0.1", 472 | "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", 473 | "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" 474 | }, 475 | "tough-cookie": { 476 | "version": "2.5.0", 477 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 478 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 479 | "requires": { 480 | "psl": "^1.1.28", 481 | "punycode": "^2.1.1" 482 | } 483 | }, 484 | "tunnel-agent": { 485 | "version": "0.6.0", 486 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 487 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 488 | "requires": { 489 | "safe-buffer": "^5.0.1" 490 | } 491 | }, 492 | "tweetnacl": { 493 | "version": "0.14.5", 494 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 495 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 496 | }, 497 | "uri-js": { 498 | "version": "4.4.1", 499 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 500 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 501 | "requires": { 502 | "punycode": "^2.1.0" 503 | } 504 | }, 505 | "util-deprecate": { 506 | "version": "1.0.2", 507 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 508 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 509 | }, 510 | "uuid": { 511 | "version": "3.4.0", 512 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 513 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 514 | }, 515 | "verror": { 516 | "version": "1.10.0", 517 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 518 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 519 | "requires": { 520 | "assert-plus": "^1.0.0", 521 | "core-util-is": "1.0.2", 522 | "extsprintf": "^1.2.0" 523 | } 524 | }, 525 | "ws": { 526 | "version": "5.2.2", 527 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 528 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 529 | "requires": { 530 | "async-limiter": "~1.0.0" 531 | } 532 | }, 533 | "xtend": { 534 | "version": "4.0.2", 535 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", 536 | "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" 537 | } 538 | } 539 | } 540 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ably-postgres-connector", 3 | "version": "1.0.1", 4 | "description": "Ably-Postgres connector publishes a message on a given Ably channel whenever any operations (insert/update/delete) are executed on the tables of your PostgreSQL database.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/ably-labs/ably-postgres-connector.git", 14 | "directory": "lib" 15 | }, 16 | "keywords": [], 17 | "author": "Apoorv Vardhan", 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "ably": "^1.2.5", 21 | "dotenv": "^10.0.0", 22 | "pg": "^8.5.1" 23 | }, 24 | "devDependencies": { 25 | "@types/pg": "^7.14.10" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/src/connector.ts: -------------------------------------------------------------------------------- 1 | import * as Ably from "ably"; 2 | import { Client, ClientConfig } from "pg"; 3 | const dotenv = require("dotenv"); 4 | const fs = require("fs"); 5 | 6 | export class Connector { 7 | private readonly ably: Ably.Rest; 8 | private readonly ablyApiKey: string; 9 | private readonly pgClient: Client; 10 | private readonly pgConfig: ClientConfig; 11 | private readonly connector: any; 12 | private readonly fileext: string; 13 | 14 | constructor(filepath: string) { 15 | if (filepath) { 16 | this.fileext = filepath.split(".").pop(); 17 | } else { 18 | this.fileext = ""; 19 | } 20 | 21 | if (this.fileext == "json") { 22 | const rawdata = fs.readFileSync(filepath); 23 | const config = JSON.parse(rawdata); 24 | this.pgConfig = config["dbConfig"]; 25 | this.connector = config["connector"]; 26 | this.ablyApiKey = config["ably"].apiKey; 27 | } else if (this.fileext == "env" || this.fileext == "") { 28 | if (this.fileext == "env") { 29 | dotenv.config({ path: filepath }); 30 | } 31 | const { 32 | DB_HOST, 33 | DB_PORT, 34 | DB_USER, 35 | DB_PASSWORD, 36 | DB_NAME, 37 | ABLY_API_KEY, 38 | ABLY_CONNECTOR, 39 | } = process.env; 40 | this.pgConfig = { 41 | user: DB_USER, 42 | port: +DB_PORT, 43 | password: DB_PASSWORD, 44 | database: DB_NAME, 45 | host: DB_HOST, 46 | }; 47 | this.ablyApiKey = ABLY_API_KEY; 48 | this.connector = JSON.parse(ABLY_CONNECTOR); 49 | } else { 50 | console.error("Invalid config"); 51 | return; 52 | } 53 | // instantiate Ably 54 | this.ably = new Ably.Rest(this.ablyApiKey); 55 | 56 | // instantiate node-postgresconnector client 57 | this.pgClient = new Client(this.pgConfig); 58 | } 59 | 60 | public start = async () => { 61 | // Setup Ably postgresconnector 62 | await this.setup(); 63 | this.pgClient.query("BEGIN", (err) => { 64 | if (this.shouldAbort(err)) return; 65 | 66 | // Create fn to trigger the pg_notify on data change 67 | const queryText = `CREATE OR REPLACE FUNCTION ably_notify() RETURNS trigger AS $$ 68 | DECLARE 69 | rec record; 70 | BEGIN 71 | IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN 72 | rec = NEW; 73 | ELSE 74 | rec = OLD; 75 | END IF; 76 | PERFORM pg_notify('table_update', json_build_object('table', TG_TABLE_NAME, 'type', TG_OP, 'row', rec)::text); 77 | RETURN NEW; 78 | END; 79 | $$ LANGUAGE plpgsql;`; 80 | this.pgClient.query(queryText, (err, res) => { 81 | if (this.shouldAbort(err)) return; 82 | 83 | // Create Ably config table, to maintain table-to-ablychannel mapping 84 | const createCtrlTable = `CREATE TABLE IF NOT EXISTS ablycontroltable(tablename VARCHAR(100) NOT NULL, ablychannelname VARCHAR(100) NOT NULL, operation VARCHAR(50), 85 | PRIMARY KEY(tablename, ablychannelname, operation));`; 86 | this.pgClient.query(createCtrlTable, (err, res) => { 87 | if (this.shouldAbort(err)) return; 88 | 89 | let deleteQuery = `Delete from ablycontroltable where not (`; 90 | let selDropQuery = `Select * from ablycontroltable where not (`; 91 | let commonQueryPart = ``; 92 | 93 | for (let i = 0; i < this.connector.length; i++) { 94 | const tableName = this.connector[i].tablename; 95 | const op = this.connector[i].operation; 96 | const ablyChannel = this.connector[i].ablychannelname; 97 | if (i == 0) { 98 | commonQueryPart += `(tablename='${tableName}' and ablychannelname='${ablyChannel}' and operation='${op}')`; 99 | } else { 100 | commonQueryPart += ` or (tablename='${tableName}' and ablychannelname='${ablyChannel}' and operation='${op}')`; 101 | } 102 | let queryCtrlTable = `SELECT * from ablycontroltable where tablename='${tableName}' and ablychannelname='${ablyChannel}' and operation='${op}'`; 103 | this.pgClient.query(queryCtrlTable, (err, res) => { 104 | if (this.shouldAbort(err)) return; 105 | 106 | if (res.rows.length == 0) { 107 | const insertData = 108 | "INSERT INTO ablycontroltable(tablename, ablychannelname, operation) VALUES($1, $2, $3) RETURNING *"; 109 | const values = [tableName, ablyChannel, op]; 110 | 111 | // Insert mapping into the Ably config table 112 | this.pgClient.query(insertData, values, (err, res) => { 113 | if (err) { 114 | console.log(err.stack); 115 | } 116 | 117 | // Create trigger for the particular table & DB operation combination 118 | const createTrigger = `CREATE TRIGGER ${tableName}_notify_${op} AFTER ${op} ON ${tableName} FOR EACH ROW EXECUTE PROCEDURE ably_notify();`; 119 | this.pgClient.query(createTrigger, (err, res) => { 120 | if (err) { 121 | console.log(err.stack); 122 | } 123 | }); 124 | }); 125 | } 126 | }); 127 | } 128 | commonQueryPart += ");"; 129 | deleteQuery += commonQueryPart; 130 | selDropQuery += commonQueryPart; 131 | 132 | // Manage deletion to config by dropping stale triggers & removing stale data from Ably config table 133 | this.pgClient.query(selDropQuery, (err, res) => { 134 | if (this.shouldAbort(err)) return; 135 | for (let i = 0; i < res.rows.length; i++) { 136 | const tableName = res.rows[i].tablename; 137 | const op = res.rows[i].operation; 138 | const dropTrigger = `DROP TRIGGER IF EXISTS ${tableName}_notify_${op} ON ${tableName};`; 139 | 140 | this.pgClient.query(dropTrigger, (err, res) => { 141 | if (this.shouldAbort(err)) return; 142 | }); 143 | } 144 | this.pgClient.query(deleteQuery, (err, res) => { 145 | if (this.shouldAbort(err)) return; 146 | console.log("Connected!"); 147 | }); 148 | }); 149 | }); 150 | 151 | // Commit the transaction 152 | this.pgClient.query("COMMIT", (err) => { 153 | if (err) { 154 | console.error("Error committing transaction", err.stack); 155 | } 156 | }); 157 | }); 158 | }); 159 | }; 160 | 161 | private setup = async () => { 162 | await this.pgClient.connect(); 163 | 164 | try { 165 | // listen on a particular data channel 166 | await this.pgClient.query('LISTEN "table_update"'); 167 | // on trigger of notification by pg_notify 168 | this.pgClient.on("notification", (data) => { 169 | if (data.channel === "table_update") { 170 | const notifyData = JSON.parse(data.payload); 171 | const operation = notifyData.type; 172 | const tableName = notifyData.table; 173 | const queryGetAblyChannelName = `Select ablychannelname from ablycontroltable where tablename='${tableName}' and operation='${operation}'`; 174 | 175 | // get the ably channel to publish data change on 176 | this.pgClient.query(queryGetAblyChannelName, (err, res) => { 177 | if (err) { 178 | console.log(err.stack); 179 | } else { 180 | if (res.rows.length != 0) { 181 | const channel = this.ably.channels.get( 182 | res.rows[0].ablychannelname 183 | ); 184 | 185 | // Publish message to Ably channel 186 | channel.publish( 187 | "New message from the Ably/ Postgres connector", 188 | data.payload 189 | ); 190 | } else { 191 | console.log("Matching config not found!"); 192 | } 193 | } 194 | }); 195 | } 196 | }); 197 | } catch (err) { 198 | console.log(err.stack); 199 | } 200 | }; 201 | 202 | // Rollback in case of error during transaction 203 | private shouldAbort = (err) => { 204 | if (err) { 205 | console.error("Error in transaction", err.stack); 206 | this.pgClient.query("ROLLBACK", (err) => { 207 | if (err) { 208 | console.error("Error rolling back client", err.stack); 209 | } 210 | }); 211 | } 212 | return !!err; 213 | }; 214 | } 215 | -------------------------------------------------------------------------------- /lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Connector } from "./connector"; 2 | -------------------------------------------------------------------------------- /lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "declaration": true, 6 | "outDir": "./dist" 7 | }, 8 | "include": [ 9 | "src/**/*" 10 | ] 11 | } --------------------------------------------------------------------------------