├── .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 |
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 |
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 | }
--------------------------------------------------------------------------------