├── .eslintrc.json ├── .github └── workflows │ └── test.yml ├── CHANGELOG.md ├── README.md ├── UNLICENSE ├── cf ├── polyfills.js ├── src │ ├── bytes.js │ ├── connection.js │ ├── errors.js │ ├── index.js │ ├── large.js │ ├── query.js │ ├── queue.js │ ├── result.js │ ├── subscribe.js │ └── types.js └── test.js ├── cjs ├── package.json ├── src │ ├── bytes.js │ ├── connection.js │ ├── errors.js │ ├── index.js │ ├── large.js │ ├── query.js │ ├── queue.js │ ├── result.js │ ├── subscribe.js │ └── types.js └── tests │ ├── bootstrap.js │ ├── copy.csv │ ├── index.js │ ├── pg_hba.conf │ ├── select-param.sql │ ├── select.sql │ └── test.js ├── demo.gif ├── deno ├── README.md ├── mod.js ├── package.json ├── polyfills.js ├── src │ ├── bytes.js │ ├── connection.js │ ├── errors.js │ ├── index.js │ ├── large.js │ ├── query.js │ ├── queue.js │ ├── result.js │ ├── subscribe.js │ └── types.js ├── tests │ ├── bootstrap.js │ ├── copy.csv │ ├── index.js │ ├── pg_hba.conf │ ├── select-param.sql │ ├── select.sql │ └── test.js └── types │ └── index.d.ts ├── package.json ├── postgresjs.svg ├── src ├── bytes.js ├── connection.js ├── errors.js ├── index.js ├── large.js ├── query.js ├── queue.js ├── result.js ├── subscribe.js └── types.js ├── tests ├── bootstrap.js ├── copy.csv ├── index.js ├── pg_hba.conf ├── select-param.sql ├── select.sql └── test.js ├── transpile.cf.js ├── transpile.cjs ├── transpile.deno.js └── types ├── index.d.ts ├── package.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "es2020": true, 5 | "node": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2020, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "comma-dangle": 2, 13 | "no-cond-assign": 2, 14 | "no-console": 1, 15 | "no-constant-condition": 2, 16 | "no-control-regex": 2, 17 | "no-debugger": 2, 18 | "no-dupe-args": 2, 19 | "no-dupe-keys": 2, 20 | "no-duplicate-case": 2, 21 | "no-empty": 2, 22 | "no-empty-character-class": 2, 23 | "no-ex-assign": 2, 24 | "no-extra-boolean-cast": 2, 25 | "no-extra-semi": 2, 26 | "no-func-assign": 2, 27 | "no-inner-declarations": 2, 28 | "no-invalid-regexp": 2, 29 | "no-irregular-whitespace": 2, 30 | "no-negated-in-lhs": 0, 31 | "no-obj-calls": 2, 32 | "no-regex-spaces": 2, 33 | "no-sparse-arrays": 2, 34 | "no-unexpected-multiline": 2, 35 | "no-unreachable": 2, 36 | "use-isnan": 2, 37 | "valid-typeof": 2, 38 | "accessor-pairs": 2, 39 | "array-callback-return": 0, 40 | "consistent-return": 0, 41 | "curly": [ 42 | 2, 43 | "multi-or-nest", 44 | "consistent" 45 | ], 46 | "default-case": 2, 47 | "dot-location": [ 48 | 2, 49 | "property" 50 | ], 51 | "dot-notation": [ 52 | 2, 53 | { 54 | "allowPattern": "^[a-z]+(_[a-z]+)+$" 55 | } 56 | ], 57 | "eqeqeq": [ 58 | "error", 59 | "always", 60 | { 61 | "null": "ignore" 62 | } 63 | ], 64 | "no-alert": 2, 65 | "no-caller": 2, 66 | "no-case-declarations": 2, 67 | "no-div-regex": 2, 68 | "no-else-return": 2, 69 | "no-empty-function": 2, 70 | "no-empty-pattern": 2, 71 | "no-eq-null": 0, 72 | "no-eval": 2, 73 | "no-extend-native": 2, 74 | "no-extra-bind": 2, 75 | "no-extra-label": 2, 76 | "no-fallthrough": 2, 77 | "no-floating-decimal": 2, 78 | "no-implicit-coercion": 0, 79 | "no-implicit-globals": 2, 80 | "no-implied-eval": 2, 81 | "no-invalid-this": 2, 82 | "no-iterator": 2, 83 | "no-labels": 2, 84 | "no-lone-blocks": 2, 85 | "no-loop-func": 2, 86 | "no-magic-numbers": 0, 87 | "no-multi-spaces": [ 88 | 2, 89 | { 90 | "ignoreEOLComments": true, 91 | "exceptions": { 92 | "Array": true, 93 | "Property": true, 94 | "VariableDeclarator": true, 95 | "ImportDeclaration": true, 96 | "TernaryExpressions": true, 97 | "Comments": true 98 | } 99 | } 100 | ], 101 | "no-multi-str": 2, 102 | "no-native-reassign": 2, 103 | "no-new": 2, 104 | "no-new-func": 2, 105 | "no-new-wrappers": 2, 106 | "no-octal": 2, 107 | "no-octal-escape": 2, 108 | "no-param-reassign": 0, 109 | "no-proto": 2, 110 | "no-redeclare": 2, 111 | "no-return-assign": 0, 112 | "no-script-url": 2, 113 | "no-self-assign": 2, 114 | "no-self-compare": 2, 115 | "no-sequences": 0, 116 | "no-throw-literal": 2, 117 | "no-unmodified-loop-condition": 2, 118 | "no-unused-expressions": 0, 119 | "no-unused-labels": 2, 120 | "no-useless-call": 2, 121 | "no-useless-concat": 2, 122 | "no-useless-escape": 2, 123 | "no-void": 2, 124 | "no-with": 2, 125 | "wrap-iife": 2, 126 | "no-delete-var": 2, 127 | "no-label-var": 2, 128 | "no-restricted-globals": 2, 129 | "no-shadow": 0, 130 | "no-shadow-restricted-names": 2, 131 | "no-undef": 2, 132 | "no-undef-init": 2, 133 | "no-unused-vars": 2, 134 | "no-use-before-define": [ 135 | 2, 136 | { 137 | "functions": false, 138 | "variables": false 139 | } 140 | ], 141 | "callback-return": 0, 142 | "global-require": 2, 143 | "handle-callback-err": 2, 144 | "no-mixed-requires": 2, 145 | "no-new-require": 2, 146 | "no-path-concat": 2, 147 | "no-process-env": 2, 148 | "no-process-exit": 2, 149 | "array-bracket-spacing": [ 150 | 2, 151 | "never" 152 | ], 153 | "block-spacing": [ 154 | 2, 155 | "always" 156 | ], 157 | "brace-style": [ 158 | 2, 159 | "1tbs", 160 | { 161 | "allowSingleLine": true 162 | } 163 | ], 164 | "camelcase": 0, 165 | "comma-spacing": 2, 166 | "comma-style": [ 167 | 2, 168 | "first", 169 | { 170 | "exceptions": { 171 | "ArrayExpression": true, 172 | "ObjectExpression": true 173 | } 174 | } 175 | ], 176 | "consistent-this": [ 177 | 2, 178 | "self" 179 | ], 180 | "eol-last": 2, 181 | "indent": [ 182 | 2, 183 | 2, 184 | { 185 | "MemberExpression": "off", 186 | "flatTernaryExpressions": true, 187 | "VariableDeclarator": { 188 | "const": 2 189 | }, 190 | "FunctionExpression": { 191 | "parameters": "first" 192 | }, 193 | "CallExpression": { 194 | "arguments": "off" 195 | }, 196 | "ArrayExpression": "first", 197 | "ObjectExpression": "first" 198 | } 199 | ], 200 | "key-spacing": [ 201 | 0, 202 | { 203 | "beforeColon": false, 204 | "afterColon": true, 205 | "mode": "minimum" 206 | } 207 | ], 208 | "keyword-spacing": 2, 209 | "linebreak-style": 2, 210 | "lines-around-comment": 0, 211 | "max-depth": [ 212 | 2, 213 | 5 214 | ], 215 | "max-len": [ 216 | 2, 217 | 150 218 | ], 219 | "max-nested-callbacks": [ 220 | 2, 221 | 5 222 | ], 223 | "max-params": [ 224 | 2, 225 | 5 226 | ], 227 | "max-statements-per-line": 0, 228 | "new-cap": [ 229 | 2, 230 | { 231 | "capIsNew": false 232 | } 233 | ], 234 | "new-parens": 2, 235 | "newline-after-var": 0, 236 | "newline-before-return": 0, 237 | "no-array-constructor": 2, 238 | "no-bitwise": 0, 239 | "no-continue": 2, 240 | "no-lonely-if": 2, 241 | "no-mixed-spaces-and-tabs": 2, 242 | "no-negated-condition": 0, 243 | "no-new-object": 2, 244 | "no-spaced-func": 2, 245 | "no-trailing-spaces": 1, 246 | "no-unneeded-ternary": 2, 247 | "no-whitespace-before-property": 2, 248 | "object-curly-spacing": [ 249 | 2, 250 | "always" 251 | ], 252 | "one-var-declaration-per-line": [ 253 | 2, 254 | "always" 255 | ], 256 | "quote-props": [ 257 | 2, 258 | "as-needed" 259 | ], 260 | "quotes": [ 261 | 2, 262 | "single" 263 | ], 264 | "semi": [ 265 | 2, 266 | "never" 267 | ], 268 | "space-before-blocks": 2, 269 | "space-before-function-paren": [ 270 | 2, 271 | "never" 272 | ], 273 | "space-infix-ops": 2, 274 | "space-unary-ops": 2, 275 | "spaced-comment": 2, 276 | "arrow-spacing": 2, 277 | "constructor-super": 2, 278 | "no-class-assign": 2, 279 | "no-confusing-arrow": 0, 280 | "no-const-assign": 2, 281 | "no-dupe-class-members": 2, 282 | "no-duplicate-imports": 2, 283 | "no-new-symbol": 2, 284 | "no-this-before-super": 2, 285 | "no-useless-constructor": 2, 286 | "no-var": 2, 287 | "object-shorthand": 0, 288 | "prefer-arrow-callback": 0, 289 | "prefer-const": 2, 290 | "prefer-rest-params": 0, 291 | "prefer-spread": 0 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Node v${{ matrix.node }} on PostgreSQL v${{ matrix.postgres }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | node: ['12', '14', '16', '18', '20', '21', '22', '23', '24'] 12 | postgres: ['12', '13', '14', '15', '16', '17'] 13 | runs-on: ubuntu-latest 14 | services: 15 | postgres: 16 | image: postgres:${{ matrix.postgres }} 17 | env: 18 | POSTGRES_USER: postgres 19 | POSTGRES_HOST_AUTH_METHOD: trust 20 | ports: 21 | - 5433:5432 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | steps: 28 | - uses: actions/checkout@v4 29 | - run: | 30 | date 31 | sudo apt purge postgresql-16 32 | sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' 33 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 34 | sudo apt-get update 35 | sudo apt-get -y install "postgresql-${{ matrix.postgres }}" 36 | sudo cp ./tests/pg_hba.conf /etc/postgresql/${{ matrix.postgres }}/main/pg_hba.conf 37 | sudo sed -i 's/.*wal_level.*/wal_level = logical/' /etc/postgresql/${{ matrix.postgres }}/main/postgresql.conf 38 | sudo sed -i 's/.*max_prepared_transactions.*/max_prepared_transactions = 100/' /etc/postgresql/${{ matrix.postgres }}/main/postgresql.conf 39 | sudo sed -i 's/.*ssl = .*/ssl = on/' /etc/postgresql/${{ matrix.postgres }}/main/postgresql.conf 40 | openssl req -new -x509 -nodes -days 365 -text -subj "/CN=localhost" -extensions v3_req -config <(cat /etc/ssl/openssl.cnf <(printf "\n[v3_req]\nbasicConstraints=critical,CA:TRUE\nkeyUsage=nonRepudiation,digitalSignature,keyEncipherment\nsubjectAltName=DNS:localhost")) -keyout server.key -out server.crt 41 | sudo cp server.key /etc/postgresql/${{ matrix.postgres }}/main/server.key 42 | sudo cp server.crt /etc/postgresql/${{ matrix.postgres }}/main/server.crt 43 | sudo chmod og-rwx /etc/postgresql/${{ matrix.postgres }}/main/server.key 44 | sudo systemctl start postgresql.service 45 | sudo systemctl status postgresql.service 46 | pg_isready 47 | sudo -u postgres psql -c "SHOW hba_file;" 48 | - uses: denoland/setup-deno@v1 49 | with: 50 | deno-version: v1.x 51 | - uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node }} 54 | - run: npm test 55 | env: 56 | PGUSER: postgres 57 | PGSOCKET: /var/run/postgresql 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.2.4 - 25 May 2022 4 | - Allow setting keep_alive: false bee62f3 5 | - Fix support for null in arrays - fixes #371 b04c853 6 | 7 | ## v3.2.3 - 23 May 2022 8 | - Fix Only use setKeepAlive in Deno if available 28fbbaf 9 | - Fix wrong helper match on multiple occurances 02f3854 10 | 11 | #### Typescript related 12 | - Fix Deno assertRejects compatibility (#365) 0f0af92 13 | - Fix include missing boolean type in JSONValue union (#373) 1817387 14 | 15 | ## v3.2.2 - 15 May 2022 16 | - Properly handle errors thrown on commit 99ddae4 17 | 18 | ## v3.2.1 - 15 May 2022 19 | - Exclude target_session_attrs from connection obj 43f1442 20 | 21 | ## v3.2.0 - 15 May 2022 22 | - Add `sslmode=verify-full` support e67da29 23 | - Add support for array of fragments 342bf55 24 | - Add uri decode of host in url - fixes #346 1adc113 25 | - Add passing of rest url params to connection (ootb support cockroach urls) 41ed84f 26 | - Fix Deno partial writes 452a30d 27 | - Fix `as` dynamic helper 3300c40 28 | - Fix some nested fragments usage 9bfa902 29 | - Fix missing columns on `Result` when using simple protocol - fixes #350 1e2e298 30 | - Fix fragments in transactions - fixes #333 75914c7 31 | 32 | #### Typescript related 33 | - Upgrade/fix types (#357) 1e6d312 34 | - Add optional `onlisten` callback to `listen()` on TypeScript (#360) 6b749b2 35 | - Add implicit custom type inference (#361) 28512bf 36 | - Fix and improve sql() helper types (#338) c1de3d8 37 | - Fix update query type def for `.writable()` and `.readable()` to return promises (#347) 51269ce 38 | - Add bigint to typescript Serializable - fixes #330 f1e41c3 39 | 40 | ## v3.1.0 - 22 Apr 2022 41 | - Add close method to close but not end connections forever 94fea8f 42 | - Add .values() method to return rows as arrays of values 56873c2 43 | - Support transform.undefined - fixes #314 eab71e5 44 | - Support nested fragments values and dynamics - fixes #326 86445ca 45 | - Fix deno close sequence f76af24 46 | - Fix subscribe reconnect and add onsubscribe method - fixes #315 5097345 47 | - Deno ts fix - fixes #327 50403a1 48 | 49 | ## v3.0.6 - 19 Apr 2022 50 | - Properly close connections in Deno cbc6a75 51 | - Only write end message if socket is open 13950af 52 | - Improve query cancellation 01c2c68 53 | - Use monotonically increasing time for timeout - fixes #316 9d7a21d 54 | - Add support for dynamic columns with `returning` - fixes #317 04644c0 55 | - Fix type errors in TypeScript deno projects (#313) 822fb21 56 | - Execute forEach instantly 44e9fbe 57 | 58 | ## v3.0.5 - 6 Apr 2022 59 | - Fix transaction execution timing 28bb0b3 60 | - Add optional onlisten function to listen 1dc2fd2 61 | - Fix dynamic in helper after insert #305 4d63a59 62 | 63 | ## v3.0.4 - 5 Apr 2022 64 | - Ensure drain only dequeues if ready - fixes #303 2e5f017 65 | 66 | ## v3.0.3 - 4 Apr 2022 67 | - Run tests with github actions b536d0d 68 | - Add custom socket option - fixes #284 5413f0c 69 | - Fix sql function overload type inference (#294) 3c4e90a 70 | - Update deno std to 0.132 and enable last tests 50762d4 71 | - Send proper client-encoding - Fixes #288 e5b8554 72 | 73 | ## v3.0.2 - 31 Mar 2022 74 | - Fix BigInt handling 36a70df 75 | - Fix unsubscribing (#300) b6c597f 76 | - Parse update properly with identity full - Fixes #296 3ed11e7 77 | 78 | ## v3.0.1 - 30 Mar 2022 79 | - Improve connection queue handling + fix leak cee1a57 80 | - Use publications option - fixes #295 b5ceecc 81 | - Throw proper query error if destroyed e148a0a 82 | - Transaction rejects with rethrown error - fixes #289 f7c8ae6 83 | - Only create origin stacktrace for tagged and debug - fixes #290 a782edf 84 | - Include types and readme in deno release - fixes #287 9068820 85 | - Disable fetch_types for Subscribe options 72e0cdb 86 | - Update TypeScript types with v3 changes (#293) db05836 87 | 88 | ## v3.0.0 - 24 Mar 2022 89 | This is a complete rewrite to better support all the features that I was trying to get into v2. There are a few breaking changes from v2 beta, which some (myself included) was using in production, so I'm skipping a stable v2 release and going straight to v3. 90 | 91 | Here are some of the new things available, but check the updated docs. 92 | - Dynamic query builder based on raw sql 93 | - Realtime subscribe to db changes through logical replication 94 | - Multi-host support for High Availability setups 95 | - Postgres input parameter types from `ParameterDescription` 96 | - Deno support 97 | - Cursors as async iterators 98 | - `.describe()` to only get query input types and column definitions 99 | - Support for Large Objects 100 | - `max_lifetime` for connections 101 | - Cancellation of requests 102 | - Converted to ESM (with CJS support) 103 | - Typescript support (Credit @minigugus) 104 | 105 | ### Breaking changes from v2 -> v3 106 | - Cursors are always called with `Result` arrays (previously cursor 1 would return a row object, where > 1 would return an array of rows) 107 | - `.writable()` and `.readable()` is now async (returns a Promise that resolves to the stream) 108 | - Queries now returns a lazy promise instead of being executed immediately. This means the query won't be sent until awaited (.then, .catch, .finally is called) or until `.execute()` is manually called. 109 | - `.stream()` is renamed to `.forEach` 110 | - Returned results are now it's own `Result` class extending `Array` instead of an Array with extra properties (actually shouldn't be breaking unless you're doing something funny) 111 | - Parameters are now cast using the types returned from Postgres ParameterDescription with a fallback to the previously inferred types 112 | - Only tested with node v12 and up 113 | - Implicit array value to multiple parameter expansion removed (use sql([...]) instead) 114 | 115 | ### Breaking changes from v1 -> v2 (v2 never moved on from beta) 116 | - All identifiers from `sql()` in queries are now always quoted 117 | - Undefined parameters are no longer allowed 118 | - Rename timeout option to `idle_timeout` 119 | - Default to 10 connections instead of number of CPUs 120 | - Numbers that cannot be safely cast to JS Number are returned as string. This happens for eg, `select count(*)` because `count()` returns a 64 bit integer (int8), so if you know your `count()` won't be too big for a js number just cast in your query to int4 like `select count(*)::int` 121 | 122 | ## v1.0.2 - 21 Jan 2020 123 | 124 | - Fix standard postgres user env var (#20) cce5ad7 125 | - Ensure url or options is not falsy bc549b0 126 | - Add support for dynamic password b2ab9fb 127 | - Fix hiding pass from options 3f76b98 128 | 129 | 130 | ## v1.0.1 - 3 Jan 2020 131 | 132 | - Fix #3 url without db and trailing slash 45d4233 133 | - Fix stream promise - resolve with correct result 730df2c 134 | - Fix return value of unsafe query with multiple statements 748f198 135 | - Fix destroy before connected f682ca1 136 | - Fix params usage for file() call without options e4f12a4 137 | - Various Performance improvements 138 | 139 | ## v1.0.0 - 22 Dec 2019 140 | 141 | - Initial release 142 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /cf/polyfills.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events' 2 | import { Buffer } from 'node:buffer' 3 | 4 | const Crypto = globalThis.crypto 5 | 6 | let ids = 1 7 | const tasks = new Set() 8 | 9 | const v4Seg = '(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])' 10 | const v4Str = `(${v4Seg}[.]){3}${v4Seg}` 11 | const IPv4Reg = new RegExp(`^${v4Str}$`) 12 | 13 | const v6Seg = '(?:[0-9a-fA-F]{1,4})' 14 | const IPv6Reg = new RegExp( 15 | '^(' + 16 | `(?:${v6Seg}:){7}(?:${v6Seg}|:)|` + 17 | `(?:${v6Seg}:){6}(?:${v4Str}|:${v6Seg}|:)|` + 18 | `(?:${v6Seg}:){5}(?::${v4Str}|(:${v6Seg}){1,2}|:)|` + 19 | `(?:${v6Seg}:){4}(?:(:${v6Seg}){0,1}:${v4Str}|(:${v6Seg}){1,3}|:)|` + 20 | `(?:${v6Seg}:){3}(?:(:${v6Seg}){0,2}:${v4Str}|(:${v6Seg}){1,4}|:)|` + 21 | `(?:${v6Seg}:){2}(?:(:${v6Seg}){0,3}:${v4Str}|(:${v6Seg}){1,5}|:)|` + 22 | `(?:${v6Seg}:){1}(?:(:${v6Seg}){0,4}:${v4Str}|(:${v6Seg}){1,6}|:)|` + 23 | `(?::((?::${v6Seg}){0,5}:${v4Str}|(?::${v6Seg}){1,7}|:))` + 24 | ')(%[0-9a-zA-Z-.:]{1,})?$' 25 | ) 26 | 27 | const textEncoder = new TextEncoder() 28 | export const crypto = { 29 | randomBytes: l => Crypto.getRandomValues(Buffer.alloc(l)), 30 | pbkdf2Sync: async(password, salt, iterations, keylen) => 31 | Crypto.subtle.deriveBits( 32 | { 33 | name: 'PBKDF2', 34 | hash: 'SHA-256', 35 | salt, 36 | iterations 37 | }, 38 | await Crypto.subtle.importKey( 39 | 'raw', 40 | textEncoder.encode(password), 41 | 'PBKDF2', 42 | false, 43 | ['deriveBits'] 44 | ), 45 | keylen * 8, 46 | ['deriveBits'] 47 | ), 48 | createHash: type => ({ 49 | update: x => ({ 50 | digest: encoding => { 51 | if (!(x instanceof Uint8Array)) { 52 | x = textEncoder.encode(x) 53 | } 54 | let prom 55 | if (type === 'sha256') { 56 | prom = Crypto.subtle.digest('SHA-256', x) 57 | } else if (type === 'md5') { 58 | prom = Crypto.subtle.digest('md5', x) 59 | } else { 60 | throw Error('createHash only supports sha256 or md5 in this environment, not ${type}.') 61 | } 62 | if (encoding === 'hex') { 63 | return prom.then((arrayBuf) => Buffer.from(arrayBuf).toString('hex')) 64 | } else if (encoding) { 65 | throw Error(`createHash only supports hex encoding or unencoded in this environment, not ${encoding}`) 66 | } else { 67 | return prom 68 | } 69 | } 70 | }) 71 | }), 72 | createHmac: (type, key) => ({ 73 | update: x => ({ 74 | digest: async() => 75 | Buffer.from( 76 | await Crypto.subtle.sign( 77 | 'HMAC', 78 | await Crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']), 79 | textEncoder.encode(x) 80 | ) 81 | ) 82 | }) 83 | }) 84 | } 85 | 86 | export const performance = globalThis.performance 87 | 88 | export const process = { 89 | env: {} 90 | } 91 | 92 | export const os = { 93 | userInfo() { 94 | return { username: 'postgres' } 95 | } 96 | } 97 | 98 | export const fs = { 99 | readFile() { 100 | throw new Error('Reading files not supported on CloudFlare') 101 | } 102 | } 103 | 104 | export const net = { 105 | isIP: (x) => IPv4Reg.test(x) ? 4 : IPv6Reg.test(x) ? 6 : 0, 106 | Socket 107 | } 108 | 109 | export { setImmediate, clearImmediate } 110 | 111 | export const tls = { 112 | connect({ socket: tcp, servername }) { 113 | tcp.writer.releaseLock() 114 | tcp.reader.releaseLock() 115 | tcp.readyState = 'upgrading' 116 | tcp.raw = tcp.raw.startTls({ servername }) 117 | tcp.raw.closed.then( 118 | () => tcp.emit('close'), 119 | (e) => tcp.emit('error', e) 120 | ) 121 | tcp.writer = tcp.raw.writable.getWriter() 122 | tcp.reader = tcp.raw.readable.getReader() 123 | 124 | tcp.writer.ready.then(() => { 125 | tcp.read() 126 | tcp.readyState = 'upgrade' 127 | }) 128 | return tcp 129 | } 130 | } 131 | 132 | function Socket() { 133 | const tcp = Object.assign(new EventEmitter(), { 134 | readyState: 'open', 135 | raw: null, 136 | writer: null, 137 | reader: null, 138 | connect, 139 | write, 140 | end, 141 | destroy, 142 | read 143 | }) 144 | 145 | return tcp 146 | 147 | async function connect(port, host) { 148 | try { 149 | tcp.readyState = 'opening' 150 | const { connect } = await import('cloudflare:sockets') 151 | tcp.raw = connect(host + ':' + port, tcp.ssl ? { secureTransport: 'starttls' } : {}) 152 | tcp.raw.closed.then( 153 | () => { 154 | tcp.readyState !== 'upgrade' 155 | ? close() 156 | : ((tcp.readyState = 'open'), tcp.emit('secureConnect')) 157 | }, 158 | (e) => tcp.emit('error', e) 159 | ) 160 | tcp.writer = tcp.raw.writable.getWriter() 161 | tcp.reader = tcp.raw.readable.getReader() 162 | 163 | tcp.ssl ? readFirst() : read() 164 | tcp.writer.ready.then(() => { 165 | tcp.readyState = 'open' 166 | tcp.emit('connect') 167 | }) 168 | } catch (err) { 169 | error(err) 170 | } 171 | } 172 | 173 | function close() { 174 | if (tcp.readyState === 'closed') 175 | return 176 | 177 | tcp.readyState = 'closed' 178 | tcp.emit('close') 179 | } 180 | 181 | function write(data, cb) { 182 | tcp.writer.write(data).then(cb, error) 183 | return true 184 | } 185 | 186 | function end(data) { 187 | return data 188 | ? tcp.write(data, () => tcp.raw.close()) 189 | : tcp.raw.close() 190 | } 191 | 192 | function destroy() { 193 | tcp.destroyed = true 194 | tcp.end() 195 | } 196 | 197 | async function read() { 198 | try { 199 | let done 200 | , value 201 | while (({ done, value } = await tcp.reader.read(), !done)) 202 | tcp.emit('data', Buffer.from(value)) 203 | } catch (err) { 204 | error(err) 205 | } 206 | } 207 | 208 | async function readFirst() { 209 | const { value } = await tcp.reader.read() 210 | tcp.emit('data', Buffer.from(value)) 211 | } 212 | 213 | function error(err) { 214 | tcp.emit('error', err) 215 | tcp.emit('close') 216 | } 217 | } 218 | 219 | function setImmediate(fn) { 220 | const id = ids++ 221 | tasks.add(id) 222 | queueMicrotask(() => { 223 | if (tasks.has(id)) { 224 | fn() 225 | tasks.delete(id) 226 | } 227 | }) 228 | return id 229 | } 230 | 231 | function clearImmediate(id) { 232 | tasks.delete(id) 233 | } 234 | -------------------------------------------------------------------------------- /cf/src/bytes.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | const size = 256 3 | let buffer = Buffer.allocUnsafe(size) 4 | 5 | const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { 6 | const v = x.charCodeAt(0) 7 | acc[x] = () => { 8 | buffer[0] = v 9 | b.i = 5 10 | return b 11 | } 12 | return acc 13 | }, {}) 14 | 15 | const b = Object.assign(reset, messages, { 16 | N: String.fromCharCode(0), 17 | i: 0, 18 | inc(x) { 19 | b.i += x 20 | return b 21 | }, 22 | str(x) { 23 | const length = Buffer.byteLength(x) 24 | fit(length) 25 | b.i += buffer.write(x, b.i, length, 'utf8') 26 | return b 27 | }, 28 | i16(x) { 29 | fit(2) 30 | buffer.writeUInt16BE(x, b.i) 31 | b.i += 2 32 | return b 33 | }, 34 | i32(x, i) { 35 | if (i || i === 0) { 36 | buffer.writeUInt32BE(x, i) 37 | return b 38 | } 39 | fit(4) 40 | buffer.writeUInt32BE(x, b.i) 41 | b.i += 4 42 | return b 43 | }, 44 | z(x) { 45 | fit(x) 46 | buffer.fill(0, b.i, b.i + x) 47 | b.i += x 48 | return b 49 | }, 50 | raw(x) { 51 | buffer = Buffer.concat([buffer.subarray(0, b.i), x]) 52 | b.i = buffer.length 53 | return b 54 | }, 55 | end(at = 1) { 56 | buffer.writeUInt32BE(b.i - at, at) 57 | const out = buffer.subarray(0, b.i) 58 | b.i = 0 59 | buffer = Buffer.allocUnsafe(size) 60 | return out 61 | } 62 | }) 63 | 64 | export default b 65 | 66 | function fit(x) { 67 | if (buffer.length - b.i < x) { 68 | const prev = buffer 69 | , length = prev.length 70 | 71 | buffer = Buffer.allocUnsafe(length + (length >> 1) + x) 72 | prev.copy(buffer) 73 | } 74 | } 75 | 76 | function reset() { 77 | b.i = 0 78 | return b 79 | } 80 | -------------------------------------------------------------------------------- /cf/src/errors.js: -------------------------------------------------------------------------------- 1 | export class PostgresError extends Error { 2 | constructor(x) { 3 | super(x.message) 4 | this.name = this.constructor.name 5 | Object.assign(this, x) 6 | } 7 | } 8 | 9 | export const Errors = { 10 | connection, 11 | postgres, 12 | generic, 13 | notSupported 14 | } 15 | 16 | function connection(x, options, socket) { 17 | const { host, port } = socket || options 18 | const error = Object.assign( 19 | new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), 20 | { 21 | code: x, 22 | errno: x, 23 | address: options.path || host 24 | }, options.path ? {} : { port: port } 25 | ) 26 | Error.captureStackTrace(error, connection) 27 | return error 28 | } 29 | 30 | function postgres(x) { 31 | const error = new PostgresError(x) 32 | Error.captureStackTrace(error, postgres) 33 | return error 34 | } 35 | 36 | function generic(code, message) { 37 | const error = Object.assign(new Error(code + ': ' + message), { code }) 38 | Error.captureStackTrace(error, generic) 39 | return error 40 | } 41 | 42 | /* c8 ignore next 10 */ 43 | function notSupported(x) { 44 | const error = Object.assign( 45 | new Error(x + ' (B) is not supported'), 46 | { 47 | code: 'MESSAGE_NOT_SUPPORTED', 48 | name: x 49 | } 50 | ) 51 | Error.captureStackTrace(error, notSupported) 52 | return error 53 | } 54 | -------------------------------------------------------------------------------- /cf/src/large.js: -------------------------------------------------------------------------------- 1 | import Stream from 'node:stream' 2 | 3 | export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { 4 | return new Promise(async(resolve, reject) => { 5 | await sql.begin(async sql => { 6 | let finish 7 | !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) 8 | const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` 9 | 10 | const lo = { 11 | writable, 12 | readable, 13 | close : () => sql`select lo_close(${ fd })`.then(finish), 14 | tell : () => sql`select lo_tell64(${ fd })`, 15 | read : (x) => sql`select loread(${ fd }, ${ x }) as data`, 16 | write : (x) => sql`select lowrite(${ fd }, ${ x })`, 17 | truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, 18 | seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, 19 | size : () => sql` 20 | select 21 | lo_lseek64(${ fd }, location, 0) as position, 22 | seek.size 23 | from ( 24 | select 25 | lo_lseek64($1, 0, 2) as size, 26 | tell.location 27 | from (select lo_tell64($1) as location) tell 28 | ) seek 29 | ` 30 | } 31 | 32 | resolve(lo) 33 | 34 | return new Promise(async r => finish = r) 35 | 36 | async function readable({ 37 | highWaterMark = 2048 * 8, 38 | start = 0, 39 | end = Infinity 40 | } = {}) { 41 | let max = end - start 42 | start && await lo.seek(start) 43 | return new Stream.Readable({ 44 | highWaterMark, 45 | async read(size) { 46 | const l = size > max ? size - max : size 47 | max -= size 48 | const [{ data }] = await lo.read(l) 49 | this.push(data) 50 | if (data.length < size) 51 | this.push(null) 52 | } 53 | }) 54 | } 55 | 56 | async function writable({ 57 | highWaterMark = 2048 * 8, 58 | start = 0 59 | } = {}) { 60 | start && await lo.seek(start) 61 | return new Stream.Writable({ 62 | highWaterMark, 63 | write(chunk, encoding, callback) { 64 | lo.write(chunk).then(() => callback(), callback) 65 | } 66 | }) 67 | } 68 | }).catch(reject) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /cf/src/query.js: -------------------------------------------------------------------------------- 1 | const originCache = new Map() 2 | , originStackCache = new Map() 3 | , originError = Symbol('OriginError') 4 | 5 | export const CLOSE = {} 6 | export class Query extends Promise { 7 | constructor(strings, args, handler, canceller, options = {}) { 8 | let resolve 9 | , reject 10 | 11 | super((a, b) => { 12 | resolve = a 13 | reject = b 14 | }) 15 | 16 | this.tagged = Array.isArray(strings.raw) 17 | this.strings = strings 18 | this.args = args 19 | this.handler = handler 20 | this.canceller = canceller 21 | this.options = options 22 | 23 | this.state = null 24 | this.statement = null 25 | 26 | this.resolve = x => (this.active = false, resolve(x)) 27 | this.reject = x => (this.active = false, reject(x)) 28 | 29 | this.active = false 30 | this.cancelled = null 31 | this.executed = false 32 | this.signature = '' 33 | 34 | this[originError] = this.handler.debug 35 | ? new Error() 36 | : this.tagged && cachedError(this.strings) 37 | } 38 | 39 | get origin() { 40 | return (this.handler.debug 41 | ? this[originError].stack 42 | : this.tagged && originStackCache.has(this.strings) 43 | ? originStackCache.get(this.strings) 44 | : originStackCache.set(this.strings, this[originError].stack).get(this.strings) 45 | ) || '' 46 | } 47 | 48 | static get [Symbol.species]() { 49 | return Promise 50 | } 51 | 52 | cancel() { 53 | return this.canceller && (this.canceller(this), this.canceller = null) 54 | } 55 | 56 | simple() { 57 | this.options.simple = true 58 | this.options.prepare = false 59 | return this 60 | } 61 | 62 | async readable() { 63 | this.simple() 64 | this.streaming = true 65 | return this 66 | } 67 | 68 | async writable() { 69 | this.simple() 70 | this.streaming = true 71 | return this 72 | } 73 | 74 | cursor(rows = 1, fn) { 75 | this.options.simple = false 76 | if (typeof rows === 'function') { 77 | fn = rows 78 | rows = 1 79 | } 80 | 81 | this.cursorRows = rows 82 | 83 | if (typeof fn === 'function') 84 | return (this.cursorFn = fn, this) 85 | 86 | let prev 87 | return { 88 | [Symbol.asyncIterator]: () => ({ 89 | next: () => { 90 | if (this.executed && !this.active) 91 | return { done: true } 92 | 93 | prev && prev() 94 | const promise = new Promise((resolve, reject) => { 95 | this.cursorFn = value => { 96 | resolve({ value, done: false }) 97 | return new Promise(r => prev = r) 98 | } 99 | this.resolve = () => (this.active = false, resolve({ done: true })) 100 | this.reject = x => (this.active = false, reject(x)) 101 | }) 102 | this.execute() 103 | return promise 104 | }, 105 | return() { 106 | prev && prev(CLOSE) 107 | return { done: true } 108 | } 109 | }) 110 | } 111 | } 112 | 113 | describe() { 114 | this.options.simple = false 115 | this.onlyDescribe = this.options.prepare = true 116 | return this 117 | } 118 | 119 | stream() { 120 | throw new Error('.stream has been renamed to .forEach') 121 | } 122 | 123 | forEach(fn) { 124 | this.forEachFn = fn 125 | this.handle() 126 | return this 127 | } 128 | 129 | raw() { 130 | this.isRaw = true 131 | return this 132 | } 133 | 134 | values() { 135 | this.isRaw = 'values' 136 | return this 137 | } 138 | 139 | async handle() { 140 | !this.executed && (this.executed = true) && await 1 && this.handler(this) 141 | } 142 | 143 | execute() { 144 | this.handle() 145 | return this 146 | } 147 | 148 | then() { 149 | this.handle() 150 | return super.then.apply(this, arguments) 151 | } 152 | 153 | catch() { 154 | this.handle() 155 | return super.catch.apply(this, arguments) 156 | } 157 | 158 | finally() { 159 | this.handle() 160 | return super.finally.apply(this, arguments) 161 | } 162 | } 163 | 164 | function cachedError(xs) { 165 | if (originCache.has(xs)) 166 | return originCache.get(xs) 167 | 168 | const x = Error.stackTraceLimit 169 | Error.stackTraceLimit = 4 170 | originCache.set(xs, new Error()) 171 | Error.stackTraceLimit = x 172 | return originCache.get(xs) 173 | } 174 | -------------------------------------------------------------------------------- /cf/src/queue.js: -------------------------------------------------------------------------------- 1 | export default Queue 2 | 3 | function Queue(initial = []) { 4 | let xs = initial.slice() 5 | let index = 0 6 | 7 | return { 8 | get length() { 9 | return xs.length - index 10 | }, 11 | remove: (x) => { 12 | const index = xs.indexOf(x) 13 | return index === -1 14 | ? null 15 | : (xs.splice(index, 1), x) 16 | }, 17 | push: (x) => (xs.push(x), x), 18 | shift: () => { 19 | const out = xs[index++] 20 | 21 | if (index === xs.length) { 22 | index = 0 23 | xs = [] 24 | } else { 25 | xs[index - 1] = undefined 26 | } 27 | 28 | return out 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cf/src/result.js: -------------------------------------------------------------------------------- 1 | export default class Result extends Array { 2 | constructor() { 3 | super() 4 | Object.defineProperties(this, { 5 | count: { value: null, writable: true }, 6 | state: { value: null, writable: true }, 7 | command: { value: null, writable: true }, 8 | columns: { value: null, writable: true }, 9 | statement: { value: null, writable: true } 10 | }) 11 | } 12 | 13 | static get [Symbol.species]() { 14 | return Array 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cf/src/subscribe.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | const noop = () => { /* noop */ } 3 | 4 | export default function Subscribe(postgres, options) { 5 | const subscribers = new Map() 6 | , slot = 'postgresjs_' + Math.random().toString(36).slice(2) 7 | , state = {} 8 | 9 | let connection 10 | , stream 11 | , ended = false 12 | 13 | const sql = subscribe.sql = postgres({ 14 | ...options, 15 | transform: { column: {}, value: {}, row: {} }, 16 | max: 1, 17 | fetch_types: false, 18 | idle_timeout: null, 19 | max_lifetime: null, 20 | connection: { 21 | ...options.connection, 22 | replication: 'database' 23 | }, 24 | onclose: async function() { 25 | if (ended) 26 | return 27 | stream = null 28 | state.pid = state.secret = undefined 29 | connected(await init(sql, slot, options.publications)) 30 | subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) 31 | }, 32 | no_subscribe: true 33 | }) 34 | 35 | const end = sql.end 36 | , close = sql.close 37 | 38 | sql.end = async() => { 39 | ended = true 40 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 41 | return end() 42 | } 43 | 44 | sql.close = async() => { 45 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 46 | return close() 47 | } 48 | 49 | return subscribe 50 | 51 | async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { 52 | event = parseEvent(event) 53 | 54 | if (!connection) 55 | connection = init(sql, slot, options.publications) 56 | 57 | const subscriber = { fn, onsubscribe } 58 | const fns = subscribers.has(event) 59 | ? subscribers.get(event).add(subscriber) 60 | : subscribers.set(event, new Set([subscriber])).get(event) 61 | 62 | const unsubscribe = () => { 63 | fns.delete(subscriber) 64 | fns.size === 0 && subscribers.delete(event) 65 | } 66 | 67 | return connection.then(x => { 68 | connected(x) 69 | onsubscribe() 70 | stream && stream.on('error', onerror) 71 | return { unsubscribe, state, sql } 72 | }) 73 | } 74 | 75 | function connected(x) { 76 | stream = x.stream 77 | state.pid = x.state.pid 78 | state.secret = x.state.secret 79 | } 80 | 81 | async function init(sql, slot, publications) { 82 | if (!publications) 83 | throw new Error('Missing publication names') 84 | 85 | const xs = await sql.unsafe( 86 | `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` 87 | ) 88 | 89 | const [x] = xs 90 | 91 | const stream = await sql.unsafe( 92 | `START_REPLICATION SLOT ${ slot } LOGICAL ${ 93 | x.consistent_point 94 | } (proto_version '1', publication_names '${ publications }')` 95 | ).writable() 96 | 97 | const state = { 98 | lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) 99 | } 100 | 101 | stream.on('data', data) 102 | stream.on('error', error) 103 | stream.on('close', sql.close) 104 | 105 | return { stream, state: xs.state } 106 | 107 | function error(e) { 108 | console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line 109 | } 110 | 111 | function data(x) { 112 | if (x[0] === 0x77) { 113 | parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) 114 | } else if (x[0] === 0x6b && x[17]) { 115 | state.lsn = x.subarray(1, 9) 116 | pong() 117 | } 118 | } 119 | 120 | function handle(a, b) { 121 | const path = b.relation.schema + '.' + b.relation.table 122 | call('*', a, b) 123 | call('*:' + path, a, b) 124 | b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 125 | call(b.command, a, b) 126 | call(b.command + ':' + path, a, b) 127 | b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 128 | } 129 | 130 | function pong() { 131 | const x = Buffer.alloc(34) 132 | x[0] = 'r'.charCodeAt(0) 133 | x.fill(state.lsn, 1) 134 | x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) 135 | stream.write(x) 136 | } 137 | } 138 | 139 | function call(x, a, b) { 140 | subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) 141 | } 142 | } 143 | 144 | function Time(x) { 145 | return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) 146 | } 147 | 148 | function parse(x, state, parsers, handle, transform) { 149 | const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) 150 | 151 | Object.entries({ 152 | R: x => { // Relation 153 | let i = 1 154 | const r = state[x.readUInt32BE(i)] = { 155 | schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', 156 | table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), 157 | columns: Array(x.readUInt16BE(i += 2)), 158 | keys: [] 159 | } 160 | i += 2 161 | 162 | let columnIndex = 0 163 | , column 164 | 165 | while (i < x.length) { 166 | column = r.columns[columnIndex++] = { 167 | key: x[i++], 168 | name: transform.column.from 169 | ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) 170 | : x.toString('utf8', i, i = x.indexOf(0, i)), 171 | type: x.readUInt32BE(i += 1), 172 | parser: parsers[x.readUInt32BE(i)], 173 | atttypmod: x.readUInt32BE(i += 4) 174 | } 175 | 176 | column.key && r.keys.push(column) 177 | i += 4 178 | } 179 | }, 180 | Y: () => { /* noop */ }, // Type 181 | O: () => { /* noop */ }, // Origin 182 | B: x => { // Begin 183 | state.date = Time(x.readBigInt64BE(9)) 184 | state.lsn = x.subarray(1, 9) 185 | }, 186 | I: x => { // Insert 187 | let i = 1 188 | const relation = state[x.readUInt32BE(i)] 189 | const { row } = tuples(x, relation.columns, i += 7, transform) 190 | 191 | handle(row, { 192 | command: 'insert', 193 | relation 194 | }) 195 | }, 196 | D: x => { // Delete 197 | let i = 1 198 | const relation = state[x.readUInt32BE(i)] 199 | i += 4 200 | const key = x[i] === 75 201 | handle(key || x[i] === 79 202 | ? tuples(x, relation.columns, i += 3, transform).row 203 | : null 204 | , { 205 | command: 'delete', 206 | relation, 207 | key 208 | }) 209 | }, 210 | U: x => { // Update 211 | let i = 1 212 | const relation = state[x.readUInt32BE(i)] 213 | i += 4 214 | const key = x[i] === 75 215 | const xs = key || x[i] === 79 216 | ? tuples(x, relation.columns, i += 3, transform) 217 | : null 218 | 219 | xs && (i = xs.i) 220 | 221 | const { row } = tuples(x, relation.columns, i + 3, transform) 222 | 223 | handle(row, { 224 | command: 'update', 225 | relation, 226 | key, 227 | old: xs && xs.row 228 | }) 229 | }, 230 | T: () => { /* noop */ }, // Truncate, 231 | C: () => { /* noop */ } // Commit 232 | }).reduce(char, {})[x[0]](x) 233 | } 234 | 235 | function tuples(x, columns, xi, transform) { 236 | let type 237 | , column 238 | , value 239 | 240 | const row = transform.raw ? new Array(columns.length) : {} 241 | for (let i = 0; i < columns.length; i++) { 242 | type = x[xi++] 243 | column = columns[i] 244 | value = type === 110 // n 245 | ? null 246 | : type === 117 // u 247 | ? undefined 248 | : column.parser === undefined 249 | ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) 250 | : column.parser.array === true 251 | ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) 252 | : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) 253 | 254 | transform.raw 255 | ? (row[i] = transform.raw === true 256 | ? value 257 | : transform.value.from ? transform.value.from(value, column) : value) 258 | : (row[column.name] = transform.value.from 259 | ? transform.value.from(value, column) 260 | : value 261 | ) 262 | } 263 | 264 | return { i: xi, row: transform.row.from ? transform.row.from(row) : row } 265 | } 266 | 267 | function parseEvent(x) { 268 | const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] 269 | 270 | if (!xs) 271 | throw new Error('Malformed subscribe pattern: ' + x) 272 | 273 | const [, command, path, key] = xs 274 | 275 | return (command || '*') 276 | + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') 277 | + (key ? '=' + key : '') 278 | } 279 | -------------------------------------------------------------------------------- /cf/src/types.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'node:buffer' 2 | import { Query } from './query.js' 3 | import { Errors } from './errors.js' 4 | 5 | export const types = { 6 | string: { 7 | to: 25, 8 | from: null, // defaults to string 9 | serialize: x => '' + x 10 | }, 11 | number: { 12 | to: 0, 13 | from: [21, 23, 26, 700, 701], 14 | serialize: x => '' + x, 15 | parse: x => +x 16 | }, 17 | json: { 18 | to: 114, 19 | from: [114, 3802], 20 | serialize: x => JSON.stringify(x), 21 | parse: x => JSON.parse(x) 22 | }, 23 | boolean: { 24 | to: 16, 25 | from: 16, 26 | serialize: x => x === true ? 't' : 'f', 27 | parse: x => x === 't' 28 | }, 29 | date: { 30 | to: 1184, 31 | from: [1082, 1114, 1184], 32 | serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), 33 | parse: x => new Date(x) 34 | }, 35 | bytea: { 36 | to: 17, 37 | from: 17, 38 | serialize: x => '\\x' + Buffer.from(x).toString('hex'), 39 | parse: x => Buffer.from(x.slice(2), 'hex') 40 | } 41 | } 42 | 43 | class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} 44 | 45 | export class Identifier extends NotTagged { 46 | constructor(value) { 47 | super() 48 | this.value = escapeIdentifier(value) 49 | } 50 | } 51 | 52 | export class Parameter extends NotTagged { 53 | constructor(value, type, array) { 54 | super() 55 | this.value = value 56 | this.type = type 57 | this.array = array 58 | } 59 | } 60 | 61 | export class Builder extends NotTagged { 62 | constructor(first, rest) { 63 | super() 64 | this.first = first 65 | this.rest = rest 66 | } 67 | 68 | build(before, parameters, types, options) { 69 | const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() 70 | return keyword.i === -1 71 | ? escapeIdentifiers(this.first, options) 72 | : keyword.fn(this.first, this.rest, parameters, types, options) 73 | } 74 | } 75 | 76 | export function handleValue(x, parameters, types, options) { 77 | let value = x instanceof Parameter ? x.value : x 78 | if (value === undefined) { 79 | x instanceof Parameter 80 | ? x.value = options.transform.undefined 81 | : value = x = options.transform.undefined 82 | 83 | if (value === undefined) 84 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 85 | } 86 | 87 | return '$' + (types.push( 88 | x instanceof Parameter 89 | ? (parameters.push(x.value), x.array 90 | ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) 91 | : x.type 92 | ) 93 | : (parameters.push(x), inferType(x)) 94 | )) 95 | } 96 | 97 | const defaultHandlers = typeHandlers(types) 98 | 99 | export function stringify(q, string, value, parameters, types, options) { // eslint-disable-line 100 | for (let i = 1; i < q.strings.length; i++) { 101 | string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] 102 | value = q.args[i] 103 | } 104 | 105 | return string 106 | } 107 | 108 | function stringifyValue(string, value, parameters, types, o) { 109 | return ( 110 | value instanceof Builder ? value.build(string, parameters, types, o) : 111 | value instanceof Query ? fragment(value, parameters, types, o) : 112 | value instanceof Identifier ? value.value : 113 | value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : 114 | handleValue(value, parameters, types, o) 115 | ) 116 | } 117 | 118 | function fragment(q, parameters, types, options) { 119 | q.fragment = true 120 | return stringify(q, q.strings[0], q.args[0], parameters, types, options) 121 | } 122 | 123 | function valuesBuilder(first, parameters, types, columns, options) { 124 | return first.map(row => 125 | '(' + columns.map(column => 126 | stringifyValue('values', row[column], parameters, types, options) 127 | ).join(',') + ')' 128 | ).join(',') 129 | } 130 | 131 | function values(first, rest, parameters, types, options) { 132 | const multi = Array.isArray(first[0]) 133 | const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) 134 | return valuesBuilder(multi ? first : [first], parameters, types, columns, options) 135 | } 136 | 137 | function select(first, rest, parameters, types, options) { 138 | typeof first === 'string' && (first = [first].concat(rest)) 139 | if (Array.isArray(first)) 140 | return escapeIdentifiers(first, options) 141 | 142 | let value 143 | const columns = rest.length ? rest.flat() : Object.keys(first) 144 | return columns.map(x => { 145 | value = first[x] 146 | return ( 147 | value instanceof Query ? fragment(value, parameters, types, options) : 148 | value instanceof Identifier ? value.value : 149 | handleValue(value, parameters, types, options) 150 | ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) 151 | }).join(',') 152 | } 153 | 154 | const builders = Object.entries({ 155 | values, 156 | in: (...xs) => { 157 | const x = values(...xs) 158 | return x === '()' ? '(null)' : x 159 | }, 160 | select, 161 | as: select, 162 | returning: select, 163 | '\\(': select, 164 | 165 | update(first, rest, parameters, types, options) { 166 | return (rest.length ? rest.flat() : Object.keys(first)).map(x => 167 | escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + 168 | '=' + stringifyValue('values', first[x], parameters, types, options) 169 | ) 170 | }, 171 | 172 | insert(first, rest, parameters, types, options) { 173 | const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) 174 | return '(' + escapeIdentifiers(columns, options) + ')values' + 175 | valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) 176 | } 177 | }).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) 178 | 179 | function notTagged() { 180 | throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') 181 | } 182 | 183 | export const serializers = defaultHandlers.serializers 184 | export const parsers = defaultHandlers.parsers 185 | 186 | export const END = {} 187 | 188 | function firstIsString(x) { 189 | if (Array.isArray(x)) 190 | return firstIsString(x[0]) 191 | return typeof x === 'string' ? 1009 : 0 192 | } 193 | 194 | export const mergeUserTypes = function(types) { 195 | const user = typeHandlers(types || {}) 196 | return { 197 | serializers: Object.assign({}, serializers, user.serializers), 198 | parsers: Object.assign({}, parsers, user.parsers) 199 | } 200 | } 201 | 202 | function typeHandlers(types) { 203 | return Object.keys(types).reduce((acc, k) => { 204 | types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) 205 | if (types[k].serialize) { 206 | acc.serializers[types[k].to] = types[k].serialize 207 | types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) 208 | } 209 | return acc 210 | }, { parsers: {}, serializers: {} }) 211 | } 212 | 213 | function escapeIdentifiers(xs, { transform: { column } }) { 214 | return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') 215 | } 216 | 217 | export const escapeIdentifier = function escape(str) { 218 | return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' 219 | } 220 | 221 | export const inferType = function inferType(x) { 222 | return ( 223 | x instanceof Parameter ? x.type : 224 | x instanceof Date ? 1184 : 225 | x instanceof Uint8Array ? 17 : 226 | (x === true || x === false) ? 16 : 227 | typeof x === 'bigint' ? 20 : 228 | Array.isArray(x) ? inferType(x[0]) : 229 | 0 230 | ) 231 | } 232 | 233 | const escapeBackslash = /\\/g 234 | const escapeQuote = /"/g 235 | 236 | function arrayEscape(x) { 237 | return x 238 | .replace(escapeBackslash, '\\\\') 239 | .replace(escapeQuote, '\\"') 240 | } 241 | 242 | export const arraySerializer = function arraySerializer(xs, serializer, options, typarray) { 243 | if (Array.isArray(xs) === false) 244 | return xs 245 | 246 | if (!xs.length) 247 | return '{}' 248 | 249 | const first = xs[0] 250 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 251 | const delimiter = typarray === 1020 ? ';' : ',' 252 | 253 | if (Array.isArray(first) && !first.type) 254 | return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' 255 | 256 | return '{' + xs.map(x => { 257 | if (x === undefined) { 258 | x = options.transform.undefined 259 | if (x === undefined) 260 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 261 | } 262 | 263 | return x === null 264 | ? 'null' 265 | : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' 266 | }).join(delimiter) + '}' 267 | } 268 | 269 | const arrayParserState = { 270 | i: 0, 271 | char: null, 272 | str: '', 273 | quoted: false, 274 | last: 0 275 | } 276 | 277 | export const arrayParser = function arrayParser(x, parser, typarray) { 278 | arrayParserState.i = arrayParserState.last = 0 279 | return arrayParserLoop(arrayParserState, x, parser, typarray) 280 | } 281 | 282 | function arrayParserLoop(s, x, parser, typarray) { 283 | const xs = [] 284 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 285 | const delimiter = typarray === 1020 ? ';' : ',' 286 | for (; s.i < x.length; s.i++) { 287 | s.char = x[s.i] 288 | if (s.quoted) { 289 | if (s.char === '\\') { 290 | s.str += x[++s.i] 291 | } else if (s.char === '"') { 292 | xs.push(parser ? parser(s.str) : s.str) 293 | s.str = '' 294 | s.quoted = x[s.i + 1] === '"' 295 | s.last = s.i + 2 296 | } else { 297 | s.str += s.char 298 | } 299 | } else if (s.char === '"') { 300 | s.quoted = true 301 | } else if (s.char === '{') { 302 | s.last = ++s.i 303 | xs.push(arrayParserLoop(s, x, parser, typarray)) 304 | } else if (s.char === '}') { 305 | s.quoted = false 306 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 307 | s.last = s.i + 1 308 | break 309 | } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { 310 | xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 311 | s.last = s.i + 1 312 | } 313 | s.p = s.char 314 | } 315 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) 316 | return xs 317 | } 318 | 319 | export const toCamel = x => { 320 | let str = x[0] 321 | for (let i = 1; i < x.length; i++) 322 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 323 | return str 324 | } 325 | 326 | export const toPascal = x => { 327 | let str = x[0].toUpperCase() 328 | for (let i = 1; i < x.length; i++) 329 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 330 | return str 331 | } 332 | 333 | export const toKebab = x => x.replace(/_/g, '-') 334 | 335 | export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() 336 | export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() 337 | export const fromKebab = x => x.replace(/-/g, '_') 338 | 339 | function createJsonTransform(fn) { 340 | return function jsonTransform(x, column) { 341 | return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) 342 | ? Array.isArray(x) 343 | ? x.map(x => jsonTransform(x, column)) 344 | : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) 345 | : x 346 | } 347 | } 348 | 349 | toCamel.column = { from: toCamel } 350 | toCamel.value = { from: createJsonTransform(toCamel) } 351 | fromCamel.column = { to: fromCamel } 352 | 353 | export const camel = { ...toCamel } 354 | camel.column.to = fromCamel 355 | 356 | toPascal.column = { from: toPascal } 357 | toPascal.value = { from: createJsonTransform(toPascal) } 358 | fromPascal.column = { to: fromPascal } 359 | 360 | export const pascal = { ...toPascal } 361 | pascal.column.to = fromPascal 362 | 363 | toKebab.column = { from: toKebab } 364 | toKebab.value = { from: createJsonTransform(toKebab) } 365 | fromKebab.column = { to: fromKebab } 366 | 367 | export const kebab = { ...toKebab } 368 | kebab.column.to = fromKebab 369 | -------------------------------------------------------------------------------- /cf/test.js: -------------------------------------------------------------------------------- 1 | // Add your database url and run this file with the below two commands to test pages and workers 2 | // npx wrangler@latest pages dev ./cf --script-path test.js --compatibility-date=2023-06-20 --log-level=debug --compatibility-flag=nodejs_compat 3 | // npx wrangler@latest dev ./cf/test.js --compatibility-date=2023-06-20 --log-level=debug --compatibility-flag=nodejs_compat 4 | 5 | import postgres from './src/index.js' 6 | const DATABASE_URL = '' 7 | 8 | export default { 9 | async fetch() { 10 | const sql = postgres(DATABASE_URL) 11 | const rows = await sql`SELECT table_name FROM information_schema.columns` 12 | return new Response(rows.map((e) => e.table_name).join('\n')) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /cjs/src/bytes.js: -------------------------------------------------------------------------------- 1 | const size = 256 2 | let buffer = Buffer.allocUnsafe(size) 3 | 4 | const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { 5 | const v = x.charCodeAt(0) 6 | acc[x] = () => { 7 | buffer[0] = v 8 | b.i = 5 9 | return b 10 | } 11 | return acc 12 | }, {}) 13 | 14 | const b = Object.assign(reset, messages, { 15 | N: String.fromCharCode(0), 16 | i: 0, 17 | inc(x) { 18 | b.i += x 19 | return b 20 | }, 21 | str(x) { 22 | const length = Buffer.byteLength(x) 23 | fit(length) 24 | b.i += buffer.write(x, b.i, length, 'utf8') 25 | return b 26 | }, 27 | i16(x) { 28 | fit(2) 29 | buffer.writeUInt16BE(x, b.i) 30 | b.i += 2 31 | return b 32 | }, 33 | i32(x, i) { 34 | if (i || i === 0) { 35 | buffer.writeUInt32BE(x, i) 36 | return b 37 | } 38 | fit(4) 39 | buffer.writeUInt32BE(x, b.i) 40 | b.i += 4 41 | return b 42 | }, 43 | z(x) { 44 | fit(x) 45 | buffer.fill(0, b.i, b.i + x) 46 | b.i += x 47 | return b 48 | }, 49 | raw(x) { 50 | buffer = Buffer.concat([buffer.subarray(0, b.i), x]) 51 | b.i = buffer.length 52 | return b 53 | }, 54 | end(at = 1) { 55 | buffer.writeUInt32BE(b.i - at, at) 56 | const out = buffer.subarray(0, b.i) 57 | b.i = 0 58 | buffer = Buffer.allocUnsafe(size) 59 | return out 60 | } 61 | }) 62 | 63 | module.exports = b 64 | 65 | function fit(x) { 66 | if (buffer.length - b.i < x) { 67 | const prev = buffer 68 | , length = prev.length 69 | 70 | buffer = Buffer.allocUnsafe(length + (length >> 1) + x) 71 | prev.copy(buffer) 72 | } 73 | } 74 | 75 | function reset() { 76 | b.i = 0 77 | return b 78 | } 79 | -------------------------------------------------------------------------------- /cjs/src/errors.js: -------------------------------------------------------------------------------- 1 | const PostgresError = module.exports.PostgresError = class PostgresError extends Error { 2 | constructor(x) { 3 | super(x.message) 4 | this.name = this.constructor.name 5 | Object.assign(this, x) 6 | } 7 | } 8 | 9 | const Errors = module.exports.Errors = { 10 | connection, 11 | postgres, 12 | generic, 13 | notSupported 14 | } 15 | 16 | function connection(x, options, socket) { 17 | const { host, port } = socket || options 18 | const error = Object.assign( 19 | new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), 20 | { 21 | code: x, 22 | errno: x, 23 | address: options.path || host 24 | }, options.path ? {} : { port: port } 25 | ) 26 | Error.captureStackTrace(error, connection) 27 | return error 28 | } 29 | 30 | function postgres(x) { 31 | const error = new PostgresError(x) 32 | Error.captureStackTrace(error, postgres) 33 | return error 34 | } 35 | 36 | function generic(code, message) { 37 | const error = Object.assign(new Error(code + ': ' + message), { code }) 38 | Error.captureStackTrace(error, generic) 39 | return error 40 | } 41 | 42 | /* c8 ignore next 10 */ 43 | function notSupported(x) { 44 | const error = Object.assign( 45 | new Error(x + ' (B) is not supported'), 46 | { 47 | code: 'MESSAGE_NOT_SUPPORTED', 48 | name: x 49 | } 50 | ) 51 | Error.captureStackTrace(error, notSupported) 52 | return error 53 | } 54 | -------------------------------------------------------------------------------- /cjs/src/large.js: -------------------------------------------------------------------------------- 1 | const Stream = require('stream') 2 | 3 | module.exports = largeObject;function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { 4 | return new Promise(async(resolve, reject) => { 5 | await sql.begin(async sql => { 6 | let finish 7 | !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) 8 | const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` 9 | 10 | const lo = { 11 | writable, 12 | readable, 13 | close : () => sql`select lo_close(${ fd })`.then(finish), 14 | tell : () => sql`select lo_tell64(${ fd })`, 15 | read : (x) => sql`select loread(${ fd }, ${ x }) as data`, 16 | write : (x) => sql`select lowrite(${ fd }, ${ x })`, 17 | truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, 18 | seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, 19 | size : () => sql` 20 | select 21 | lo_lseek64(${ fd }, location, 0) as position, 22 | seek.size 23 | from ( 24 | select 25 | lo_lseek64($1, 0, 2) as size, 26 | tell.location 27 | from (select lo_tell64($1) as location) tell 28 | ) seek 29 | ` 30 | } 31 | 32 | resolve(lo) 33 | 34 | return new Promise(async r => finish = r) 35 | 36 | async function readable({ 37 | highWaterMark = 2048 * 8, 38 | start = 0, 39 | end = Infinity 40 | } = {}) { 41 | let max = end - start 42 | start && await lo.seek(start) 43 | return new Stream.Readable({ 44 | highWaterMark, 45 | async read(size) { 46 | const l = size > max ? size - max : size 47 | max -= size 48 | const [{ data }] = await lo.read(l) 49 | this.push(data) 50 | if (data.length < size) 51 | this.push(null) 52 | } 53 | }) 54 | } 55 | 56 | async function writable({ 57 | highWaterMark = 2048 * 8, 58 | start = 0 59 | } = {}) { 60 | start && await lo.seek(start) 61 | return new Stream.Writable({ 62 | highWaterMark, 63 | write(chunk, encoding, callback) { 64 | lo.write(chunk).then(() => callback(), callback) 65 | } 66 | }) 67 | } 68 | }).catch(reject) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /cjs/src/query.js: -------------------------------------------------------------------------------- 1 | const originCache = new Map() 2 | , originStackCache = new Map() 3 | , originError = Symbol('OriginError') 4 | 5 | const CLOSE = module.exports.CLOSE = {} 6 | const Query = module.exports.Query = class Query extends Promise { 7 | constructor(strings, args, handler, canceller, options = {}) { 8 | let resolve 9 | , reject 10 | 11 | super((a, b) => { 12 | resolve = a 13 | reject = b 14 | }) 15 | 16 | this.tagged = Array.isArray(strings.raw) 17 | this.strings = strings 18 | this.args = args 19 | this.handler = handler 20 | this.canceller = canceller 21 | this.options = options 22 | 23 | this.state = null 24 | this.statement = null 25 | 26 | this.resolve = x => (this.active = false, resolve(x)) 27 | this.reject = x => (this.active = false, reject(x)) 28 | 29 | this.active = false 30 | this.cancelled = null 31 | this.executed = false 32 | this.signature = '' 33 | 34 | this[originError] = this.handler.debug 35 | ? new Error() 36 | : this.tagged && cachedError(this.strings) 37 | } 38 | 39 | get origin() { 40 | return (this.handler.debug 41 | ? this[originError].stack 42 | : this.tagged && originStackCache.has(this.strings) 43 | ? originStackCache.get(this.strings) 44 | : originStackCache.set(this.strings, this[originError].stack).get(this.strings) 45 | ) || '' 46 | } 47 | 48 | static get [Symbol.species]() { 49 | return Promise 50 | } 51 | 52 | cancel() { 53 | return this.canceller && (this.canceller(this), this.canceller = null) 54 | } 55 | 56 | simple() { 57 | this.options.simple = true 58 | this.options.prepare = false 59 | return this 60 | } 61 | 62 | async readable() { 63 | this.simple() 64 | this.streaming = true 65 | return this 66 | } 67 | 68 | async writable() { 69 | this.simple() 70 | this.streaming = true 71 | return this 72 | } 73 | 74 | cursor(rows = 1, fn) { 75 | this.options.simple = false 76 | if (typeof rows === 'function') { 77 | fn = rows 78 | rows = 1 79 | } 80 | 81 | this.cursorRows = rows 82 | 83 | if (typeof fn === 'function') 84 | return (this.cursorFn = fn, this) 85 | 86 | let prev 87 | return { 88 | [Symbol.asyncIterator]: () => ({ 89 | next: () => { 90 | if (this.executed && !this.active) 91 | return { done: true } 92 | 93 | prev && prev() 94 | const promise = new Promise((resolve, reject) => { 95 | this.cursorFn = value => { 96 | resolve({ value, done: false }) 97 | return new Promise(r => prev = r) 98 | } 99 | this.resolve = () => (this.active = false, resolve({ done: true })) 100 | this.reject = x => (this.active = false, reject(x)) 101 | }) 102 | this.execute() 103 | return promise 104 | }, 105 | return() { 106 | prev && prev(CLOSE) 107 | return { done: true } 108 | } 109 | }) 110 | } 111 | } 112 | 113 | describe() { 114 | this.options.simple = false 115 | this.onlyDescribe = this.options.prepare = true 116 | return this 117 | } 118 | 119 | stream() { 120 | throw new Error('.stream has been renamed to .forEach') 121 | } 122 | 123 | forEach(fn) { 124 | this.forEachFn = fn 125 | this.handle() 126 | return this 127 | } 128 | 129 | raw() { 130 | this.isRaw = true 131 | return this 132 | } 133 | 134 | values() { 135 | this.isRaw = 'values' 136 | return this 137 | } 138 | 139 | async handle() { 140 | !this.executed && (this.executed = true) && await 1 && this.handler(this) 141 | } 142 | 143 | execute() { 144 | this.handle() 145 | return this 146 | } 147 | 148 | then() { 149 | this.handle() 150 | return super.then.apply(this, arguments) 151 | } 152 | 153 | catch() { 154 | this.handle() 155 | return super.catch.apply(this, arguments) 156 | } 157 | 158 | finally() { 159 | this.handle() 160 | return super.finally.apply(this, arguments) 161 | } 162 | } 163 | 164 | function cachedError(xs) { 165 | if (originCache.has(xs)) 166 | return originCache.get(xs) 167 | 168 | const x = Error.stackTraceLimit 169 | Error.stackTraceLimit = 4 170 | originCache.set(xs, new Error()) 171 | Error.stackTraceLimit = x 172 | return originCache.get(xs) 173 | } 174 | -------------------------------------------------------------------------------- /cjs/src/queue.js: -------------------------------------------------------------------------------- 1 | module.exports = Queue 2 | 3 | function Queue(initial = []) { 4 | let xs = initial.slice() 5 | let index = 0 6 | 7 | return { 8 | get length() { 9 | return xs.length - index 10 | }, 11 | remove: (x) => { 12 | const index = xs.indexOf(x) 13 | return index === -1 14 | ? null 15 | : (xs.splice(index, 1), x) 16 | }, 17 | push: (x) => (xs.push(x), x), 18 | shift: () => { 19 | const out = xs[index++] 20 | 21 | if (index === xs.length) { 22 | index = 0 23 | xs = [] 24 | } else { 25 | xs[index - 1] = undefined 26 | } 27 | 28 | return out 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cjs/src/result.js: -------------------------------------------------------------------------------- 1 | module.exports = class Result extends Array { 2 | constructor() { 3 | super() 4 | Object.defineProperties(this, { 5 | count: { value: null, writable: true }, 6 | state: { value: null, writable: true }, 7 | command: { value: null, writable: true }, 8 | columns: { value: null, writable: true }, 9 | statement: { value: null, writable: true } 10 | }) 11 | } 12 | 13 | static get [Symbol.species]() { 14 | return Array 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cjs/src/subscribe.js: -------------------------------------------------------------------------------- 1 | const noop = () => { /* noop */ } 2 | 3 | module.exports = Subscribe;function Subscribe(postgres, options) { 4 | const subscribers = new Map() 5 | , slot = 'postgresjs_' + Math.random().toString(36).slice(2) 6 | , state = {} 7 | 8 | let connection 9 | , stream 10 | , ended = false 11 | 12 | const sql = subscribe.sql = postgres({ 13 | ...options, 14 | transform: { column: {}, value: {}, row: {} }, 15 | max: 1, 16 | fetch_types: false, 17 | idle_timeout: null, 18 | max_lifetime: null, 19 | connection: { 20 | ...options.connection, 21 | replication: 'database' 22 | }, 23 | onclose: async function() { 24 | if (ended) 25 | return 26 | stream = null 27 | state.pid = state.secret = undefined 28 | connected(await init(sql, slot, options.publications)) 29 | subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) 30 | }, 31 | no_subscribe: true 32 | }) 33 | 34 | const end = sql.end 35 | , close = sql.close 36 | 37 | sql.end = async() => { 38 | ended = true 39 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 40 | return end() 41 | } 42 | 43 | sql.close = async() => { 44 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 45 | return close() 46 | } 47 | 48 | return subscribe 49 | 50 | async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { 51 | event = parseEvent(event) 52 | 53 | if (!connection) 54 | connection = init(sql, slot, options.publications) 55 | 56 | const subscriber = { fn, onsubscribe } 57 | const fns = subscribers.has(event) 58 | ? subscribers.get(event).add(subscriber) 59 | : subscribers.set(event, new Set([subscriber])).get(event) 60 | 61 | const unsubscribe = () => { 62 | fns.delete(subscriber) 63 | fns.size === 0 && subscribers.delete(event) 64 | } 65 | 66 | return connection.then(x => { 67 | connected(x) 68 | onsubscribe() 69 | stream && stream.on('error', onerror) 70 | return { unsubscribe, state, sql } 71 | }) 72 | } 73 | 74 | function connected(x) { 75 | stream = x.stream 76 | state.pid = x.state.pid 77 | state.secret = x.state.secret 78 | } 79 | 80 | async function init(sql, slot, publications) { 81 | if (!publications) 82 | throw new Error('Missing publication names') 83 | 84 | const xs = await sql.unsafe( 85 | `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` 86 | ) 87 | 88 | const [x] = xs 89 | 90 | const stream = await sql.unsafe( 91 | `START_REPLICATION SLOT ${ slot } LOGICAL ${ 92 | x.consistent_point 93 | } (proto_version '1', publication_names '${ publications }')` 94 | ).writable() 95 | 96 | const state = { 97 | lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) 98 | } 99 | 100 | stream.on('data', data) 101 | stream.on('error', error) 102 | stream.on('close', sql.close) 103 | 104 | return { stream, state: xs.state } 105 | 106 | function error(e) { 107 | console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line 108 | } 109 | 110 | function data(x) { 111 | if (x[0] === 0x77) { 112 | parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) 113 | } else if (x[0] === 0x6b && x[17]) { 114 | state.lsn = x.subarray(1, 9) 115 | pong() 116 | } 117 | } 118 | 119 | function handle(a, b) { 120 | const path = b.relation.schema + '.' + b.relation.table 121 | call('*', a, b) 122 | call('*:' + path, a, b) 123 | b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 124 | call(b.command, a, b) 125 | call(b.command + ':' + path, a, b) 126 | b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 127 | } 128 | 129 | function pong() { 130 | const x = Buffer.alloc(34) 131 | x[0] = 'r'.charCodeAt(0) 132 | x.fill(state.lsn, 1) 133 | x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) 134 | stream.write(x) 135 | } 136 | } 137 | 138 | function call(x, a, b) { 139 | subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) 140 | } 141 | } 142 | 143 | function Time(x) { 144 | return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) 145 | } 146 | 147 | function parse(x, state, parsers, handle, transform) { 148 | const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) 149 | 150 | Object.entries({ 151 | R: x => { // Relation 152 | let i = 1 153 | const r = state[x.readUInt32BE(i)] = { 154 | schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', 155 | table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), 156 | columns: Array(x.readUInt16BE(i += 2)), 157 | keys: [] 158 | } 159 | i += 2 160 | 161 | let columnIndex = 0 162 | , column 163 | 164 | while (i < x.length) { 165 | column = r.columns[columnIndex++] = { 166 | key: x[i++], 167 | name: transform.column.from 168 | ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) 169 | : x.toString('utf8', i, i = x.indexOf(0, i)), 170 | type: x.readUInt32BE(i += 1), 171 | parser: parsers[x.readUInt32BE(i)], 172 | atttypmod: x.readUInt32BE(i += 4) 173 | } 174 | 175 | column.key && r.keys.push(column) 176 | i += 4 177 | } 178 | }, 179 | Y: () => { /* noop */ }, // Type 180 | O: () => { /* noop */ }, // Origin 181 | B: x => { // Begin 182 | state.date = Time(x.readBigInt64BE(9)) 183 | state.lsn = x.subarray(1, 9) 184 | }, 185 | I: x => { // Insert 186 | let i = 1 187 | const relation = state[x.readUInt32BE(i)] 188 | const { row } = tuples(x, relation.columns, i += 7, transform) 189 | 190 | handle(row, { 191 | command: 'insert', 192 | relation 193 | }) 194 | }, 195 | D: x => { // Delete 196 | let i = 1 197 | const relation = state[x.readUInt32BE(i)] 198 | i += 4 199 | const key = x[i] === 75 200 | handle(key || x[i] === 79 201 | ? tuples(x, relation.columns, i += 3, transform).row 202 | : null 203 | , { 204 | command: 'delete', 205 | relation, 206 | key 207 | }) 208 | }, 209 | U: x => { // Update 210 | let i = 1 211 | const relation = state[x.readUInt32BE(i)] 212 | i += 4 213 | const key = x[i] === 75 214 | const xs = key || x[i] === 79 215 | ? tuples(x, relation.columns, i += 3, transform) 216 | : null 217 | 218 | xs && (i = xs.i) 219 | 220 | const { row } = tuples(x, relation.columns, i + 3, transform) 221 | 222 | handle(row, { 223 | command: 'update', 224 | relation, 225 | key, 226 | old: xs && xs.row 227 | }) 228 | }, 229 | T: () => { /* noop */ }, // Truncate, 230 | C: () => { /* noop */ } // Commit 231 | }).reduce(char, {})[x[0]](x) 232 | } 233 | 234 | function tuples(x, columns, xi, transform) { 235 | let type 236 | , column 237 | , value 238 | 239 | const row = transform.raw ? new Array(columns.length) : {} 240 | for (let i = 0; i < columns.length; i++) { 241 | type = x[xi++] 242 | column = columns[i] 243 | value = type === 110 // n 244 | ? null 245 | : type === 117 // u 246 | ? undefined 247 | : column.parser === undefined 248 | ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) 249 | : column.parser.array === true 250 | ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) 251 | : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) 252 | 253 | transform.raw 254 | ? (row[i] = transform.raw === true 255 | ? value 256 | : transform.value.from ? transform.value.from(value, column) : value) 257 | : (row[column.name] = transform.value.from 258 | ? transform.value.from(value, column) 259 | : value 260 | ) 261 | } 262 | 263 | return { i: xi, row: transform.row.from ? transform.row.from(row) : row } 264 | } 265 | 266 | function parseEvent(x) { 267 | const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] 268 | 269 | if (!xs) 270 | throw new Error('Malformed subscribe pattern: ' + x) 271 | 272 | const [, command, path, key] = xs 273 | 274 | return (command || '*') 275 | + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') 276 | + (key ? '=' + key : '') 277 | } 278 | -------------------------------------------------------------------------------- /cjs/src/types.js: -------------------------------------------------------------------------------- 1 | const { Query } = require('./query.js') 2 | const { Errors } = require('./errors.js') 3 | 4 | const types = module.exports.types = { 5 | string: { 6 | to: 25, 7 | from: null, // defaults to string 8 | serialize: x => '' + x 9 | }, 10 | number: { 11 | to: 0, 12 | from: [21, 23, 26, 700, 701], 13 | serialize: x => '' + x, 14 | parse: x => +x 15 | }, 16 | json: { 17 | to: 114, 18 | from: [114, 3802], 19 | serialize: x => JSON.stringify(x), 20 | parse: x => JSON.parse(x) 21 | }, 22 | boolean: { 23 | to: 16, 24 | from: 16, 25 | serialize: x => x === true ? 't' : 'f', 26 | parse: x => x === 't' 27 | }, 28 | date: { 29 | to: 1184, 30 | from: [1082, 1114, 1184], 31 | serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), 32 | parse: x => new Date(x) 33 | }, 34 | bytea: { 35 | to: 17, 36 | from: 17, 37 | serialize: x => '\\x' + Buffer.from(x).toString('hex'), 38 | parse: x => Buffer.from(x.slice(2), 'hex') 39 | } 40 | } 41 | 42 | class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} 43 | 44 | const Identifier = module.exports.Identifier = class Identifier extends NotTagged { 45 | constructor(value) { 46 | super() 47 | this.value = escapeIdentifier(value) 48 | } 49 | } 50 | 51 | const Parameter = module.exports.Parameter = class Parameter extends NotTagged { 52 | constructor(value, type, array) { 53 | super() 54 | this.value = value 55 | this.type = type 56 | this.array = array 57 | } 58 | } 59 | 60 | const Builder = module.exports.Builder = class Builder extends NotTagged { 61 | constructor(first, rest) { 62 | super() 63 | this.first = first 64 | this.rest = rest 65 | } 66 | 67 | build(before, parameters, types, options) { 68 | const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() 69 | return keyword.i === -1 70 | ? escapeIdentifiers(this.first, options) 71 | : keyword.fn(this.first, this.rest, parameters, types, options) 72 | } 73 | } 74 | 75 | module.exports.handleValue = handleValue;function handleValue(x, parameters, types, options) { 76 | let value = x instanceof Parameter ? x.value : x 77 | if (value === undefined) { 78 | x instanceof Parameter 79 | ? x.value = options.transform.undefined 80 | : value = x = options.transform.undefined 81 | 82 | if (value === undefined) 83 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 84 | } 85 | 86 | return '$' + (types.push( 87 | x instanceof Parameter 88 | ? (parameters.push(x.value), x.array 89 | ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) 90 | : x.type 91 | ) 92 | : (parameters.push(x), inferType(x)) 93 | )) 94 | } 95 | 96 | const defaultHandlers = typeHandlers(types) 97 | 98 | module.exports.stringify = stringify;function stringify(q, string, value, parameters, types, options) { // eslint-disable-line 99 | for (let i = 1; i < q.strings.length; i++) { 100 | string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] 101 | value = q.args[i] 102 | } 103 | 104 | return string 105 | } 106 | 107 | function stringifyValue(string, value, parameters, types, o) { 108 | return ( 109 | value instanceof Builder ? value.build(string, parameters, types, o) : 110 | value instanceof Query ? fragment(value, parameters, types, o) : 111 | value instanceof Identifier ? value.value : 112 | value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : 113 | handleValue(value, parameters, types, o) 114 | ) 115 | } 116 | 117 | function fragment(q, parameters, types, options) { 118 | q.fragment = true 119 | return stringify(q, q.strings[0], q.args[0], parameters, types, options) 120 | } 121 | 122 | function valuesBuilder(first, parameters, types, columns, options) { 123 | return first.map(row => 124 | '(' + columns.map(column => 125 | stringifyValue('values', row[column], parameters, types, options) 126 | ).join(',') + ')' 127 | ).join(',') 128 | } 129 | 130 | function values(first, rest, parameters, types, options) { 131 | const multi = Array.isArray(first[0]) 132 | const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) 133 | return valuesBuilder(multi ? first : [first], parameters, types, columns, options) 134 | } 135 | 136 | function select(first, rest, parameters, types, options) { 137 | typeof first === 'string' && (first = [first].concat(rest)) 138 | if (Array.isArray(first)) 139 | return escapeIdentifiers(first, options) 140 | 141 | let value 142 | const columns = rest.length ? rest.flat() : Object.keys(first) 143 | return columns.map(x => { 144 | value = first[x] 145 | return ( 146 | value instanceof Query ? fragment(value, parameters, types, options) : 147 | value instanceof Identifier ? value.value : 148 | handleValue(value, parameters, types, options) 149 | ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) 150 | }).join(',') 151 | } 152 | 153 | const builders = Object.entries({ 154 | values, 155 | in: (...xs) => { 156 | const x = values(...xs) 157 | return x === '()' ? '(null)' : x 158 | }, 159 | select, 160 | as: select, 161 | returning: select, 162 | '\\(': select, 163 | 164 | update(first, rest, parameters, types, options) { 165 | return (rest.length ? rest.flat() : Object.keys(first)).map(x => 166 | escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + 167 | '=' + stringifyValue('values', first[x], parameters, types, options) 168 | ) 169 | }, 170 | 171 | insert(first, rest, parameters, types, options) { 172 | const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) 173 | return '(' + escapeIdentifiers(columns, options) + ')values' + 174 | valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) 175 | } 176 | }).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) 177 | 178 | function notTagged() { 179 | throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') 180 | } 181 | 182 | const serializers = module.exports.serializers = defaultHandlers.serializers 183 | const parsers = module.exports.parsers = defaultHandlers.parsers 184 | 185 | const END = module.exports.END = {} 186 | 187 | function firstIsString(x) { 188 | if (Array.isArray(x)) 189 | return firstIsString(x[0]) 190 | return typeof x === 'string' ? 1009 : 0 191 | } 192 | 193 | const mergeUserTypes = module.exports.mergeUserTypes = function(types) { 194 | const user = typeHandlers(types || {}) 195 | return { 196 | serializers: Object.assign({}, serializers, user.serializers), 197 | parsers: Object.assign({}, parsers, user.parsers) 198 | } 199 | } 200 | 201 | function typeHandlers(types) { 202 | return Object.keys(types).reduce((acc, k) => { 203 | types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) 204 | if (types[k].serialize) { 205 | acc.serializers[types[k].to] = types[k].serialize 206 | types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) 207 | } 208 | return acc 209 | }, { parsers: {}, serializers: {} }) 210 | } 211 | 212 | function escapeIdentifiers(xs, { transform: { column } }) { 213 | return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') 214 | } 215 | 216 | const escapeIdentifier = module.exports.escapeIdentifier = function escape(str) { 217 | return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' 218 | } 219 | 220 | const inferType = module.exports.inferType = function inferType(x) { 221 | return ( 222 | x instanceof Parameter ? x.type : 223 | x instanceof Date ? 1184 : 224 | x instanceof Uint8Array ? 17 : 225 | (x === true || x === false) ? 16 : 226 | typeof x === 'bigint' ? 20 : 227 | Array.isArray(x) ? inferType(x[0]) : 228 | 0 229 | ) 230 | } 231 | 232 | const escapeBackslash = /\\/g 233 | const escapeQuote = /"/g 234 | 235 | function arrayEscape(x) { 236 | return x 237 | .replace(escapeBackslash, '\\\\') 238 | .replace(escapeQuote, '\\"') 239 | } 240 | 241 | const arraySerializer = module.exports.arraySerializer = function arraySerializer(xs, serializer, options, typarray) { 242 | if (Array.isArray(xs) === false) 243 | return xs 244 | 245 | if (!xs.length) 246 | return '{}' 247 | 248 | const first = xs[0] 249 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 250 | const delimiter = typarray === 1020 ? ';' : ',' 251 | 252 | if (Array.isArray(first) && !first.type) 253 | return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' 254 | 255 | return '{' + xs.map(x => { 256 | if (x === undefined) { 257 | x = options.transform.undefined 258 | if (x === undefined) 259 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 260 | } 261 | 262 | return x === null 263 | ? 'null' 264 | : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' 265 | }).join(delimiter) + '}' 266 | } 267 | 268 | const arrayParserState = { 269 | i: 0, 270 | char: null, 271 | str: '', 272 | quoted: false, 273 | last: 0 274 | } 275 | 276 | const arrayParser = module.exports.arrayParser = function arrayParser(x, parser, typarray) { 277 | arrayParserState.i = arrayParserState.last = 0 278 | return arrayParserLoop(arrayParserState, x, parser, typarray) 279 | } 280 | 281 | function arrayParserLoop(s, x, parser, typarray) { 282 | const xs = [] 283 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 284 | const delimiter = typarray === 1020 ? ';' : ',' 285 | for (; s.i < x.length; s.i++) { 286 | s.char = x[s.i] 287 | if (s.quoted) { 288 | if (s.char === '\\') { 289 | s.str += x[++s.i] 290 | } else if (s.char === '"') { 291 | xs.push(parser ? parser(s.str) : s.str) 292 | s.str = '' 293 | s.quoted = x[s.i + 1] === '"' 294 | s.last = s.i + 2 295 | } else { 296 | s.str += s.char 297 | } 298 | } else if (s.char === '"') { 299 | s.quoted = true 300 | } else if (s.char === '{') { 301 | s.last = ++s.i 302 | xs.push(arrayParserLoop(s, x, parser, typarray)) 303 | } else if (s.char === '}') { 304 | s.quoted = false 305 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 306 | s.last = s.i + 1 307 | break 308 | } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { 309 | xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 310 | s.last = s.i + 1 311 | } 312 | s.p = s.char 313 | } 314 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) 315 | return xs 316 | } 317 | 318 | const toCamel = module.exports.toCamel = x => { 319 | let str = x[0] 320 | for (let i = 1; i < x.length; i++) 321 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 322 | return str 323 | } 324 | 325 | const toPascal = module.exports.toPascal = x => { 326 | let str = x[0].toUpperCase() 327 | for (let i = 1; i < x.length; i++) 328 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 329 | return str 330 | } 331 | 332 | const toKebab = module.exports.toKebab = x => x.replace(/_/g, '-') 333 | 334 | const fromCamel = module.exports.fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() 335 | const fromPascal = module.exports.fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() 336 | const fromKebab = module.exports.fromKebab = x => x.replace(/-/g, '_') 337 | 338 | function createJsonTransform(fn) { 339 | return function jsonTransform(x, column) { 340 | return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) 341 | ? Array.isArray(x) 342 | ? x.map(x => jsonTransform(x, column)) 343 | : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) 344 | : x 345 | } 346 | } 347 | 348 | toCamel.column = { from: toCamel } 349 | toCamel.value = { from: createJsonTransform(toCamel) } 350 | fromCamel.column = { to: fromCamel } 351 | 352 | const camel = module.exports.camel = { ...toCamel } 353 | camel.column.to = fromCamel 354 | 355 | toPascal.column = { from: toPascal } 356 | toPascal.value = { from: createJsonTransform(toPascal) } 357 | fromPascal.column = { to: fromPascal } 358 | 359 | const pascal = module.exports.pascal = { ...toPascal } 360 | pascal.column.to = fromPascal 361 | 362 | toKebab.column = { from: toKebab } 363 | toKebab.value = { from: createJsonTransform(toKebab) } 364 | fromKebab.column = { to: fromKebab } 365 | 366 | const kebab = module.exports.kebab = { ...toKebab } 367 | kebab.column.to = fromKebab 368 | -------------------------------------------------------------------------------- /cjs/tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require('child_process') 2 | 3 | exec('dropdb', ['postgres_js_test']) 4 | 5 | exec('psql', ['-c', 'alter system set ssl=on']) 6 | exec('psql', ['-c', 'drop user postgres_js_test']) 7 | exec('psql', ['-c', 'create user postgres_js_test']) 8 | exec('psql', ['-c', 'alter system set password_encryption=md5']) 9 | exec('psql', ['-c', 'select pg_reload_conf()']) 10 | exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) 11 | exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) 12 | exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) 13 | exec('psql', ['-c', 'select pg_reload_conf()']) 14 | exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) 15 | exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) 16 | 17 | exec('createdb', ['postgres_js_test']) 18 | exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) 19 | exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) 20 | 21 | module.exports.exec = exec;function exec(cmd, args) { 22 | const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) 23 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 24 | throw stderr 25 | } 26 | 27 | async function execAsync(cmd, args) { // eslint-disable-line 28 | let stderr = '' 29 | const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line 30 | cp.stderr.on('data', x => stderr += x) 31 | await new Promise(x => cp.on('exit', x)) 32 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 33 | throw new Error(stderr) 34 | } 35 | -------------------------------------------------------------------------------- /cjs/tests/copy.csv: -------------------------------------------------------------------------------- 1 | 1 2 3 2 | 4 5 6 3 | -------------------------------------------------------------------------------- /cjs/tests/pg_hba.conf: -------------------------------------------------------------------------------- 1 | local all all trust 2 | host all postgres samehost trust 3 | host postgres_js_test postgres_js_test samehost trust 4 | host postgres_js_test postgres_js_test_md5 samehost md5 5 | host postgres_js_test postgres_js_test_scram samehost scram-sha-256 6 | -------------------------------------------------------------------------------- /cjs/tests/select-param.sql: -------------------------------------------------------------------------------- 1 | select $1 as x 2 | -------------------------------------------------------------------------------- /cjs/tests/select.sql: -------------------------------------------------------------------------------- 1 | select 1 as x 2 | -------------------------------------------------------------------------------- /cjs/tests/test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | const util = require('util') 4 | 5 | let done = 0 6 | let only = false 7 | let ignored = 0 8 | let failed = false 9 | let promise = Promise.resolve() 10 | const tests = {} 11 | , ignore = {} 12 | 13 | const nt = module.exports.nt = () => ignored++ 14 | const ot = module.exports.ot = (...rest) => (only = true, test(true, ...rest)) 15 | const t = module.exports.t = (...rest) => test(false, ...rest) 16 | t.timeout = 5 17 | 18 | async function test(o, name, options, fn) { 19 | typeof options !== 'object' && (fn = options, options = {}) 20 | const line = new Error().stack.split('\n')[3].match(':([0-9]+):')[1] 21 | 22 | await 1 23 | 24 | if (only && !o) 25 | return 26 | 27 | tests[line] = { fn, line, name } 28 | promise = promise.then(() => Promise.race([ 29 | new Promise((resolve, reject) => 30 | fn.timer = setTimeout(() => reject('Timed out'), (options.timeout || t.timeout) * 1000) 31 | ), 32 | failed 33 | ? (ignored++, ignore) 34 | : fn() 35 | ])) 36 | .then(async x => { 37 | clearTimeout(fn.timer) 38 | if (x === ignore) 39 | return 40 | 41 | if (!Array.isArray(x)) 42 | throw new Error('Test should return result array') 43 | 44 | const [expected, got] = await Promise.all(x) 45 | if (expected !== got) { 46 | failed = true 47 | throw new Error(util.inspect(expected) + ' != ' + util.inspect(got)) 48 | } 49 | 50 | tests[line].succeeded = true 51 | process.stdout.write('✅') 52 | }) 53 | .catch(err => { 54 | tests[line].failed = failed = true 55 | tests[line].error = err instanceof Error ? err : new Error(util.inspect(err)) 56 | }) 57 | .then(() => { 58 | ++done === Object.keys(tests).length && exit() 59 | }) 60 | } 61 | 62 | function exit() { 63 | let success = true 64 | Object.values(tests).every((x) => { 65 | if (x.succeeded) 66 | return true 67 | 68 | success = false 69 | x.cleanup 70 | ? console.error('⛔️', x.name + ' at line', x.line, 'cleanup failed', '\n', util.inspect(x.cleanup)) 71 | : console.error('⛔️', x.name + ' at line', x.line, x.failed 72 | ? 'failed' 73 | : 'never finished', x.error ? '\n' + util.inspect(x.error) : '' 74 | ) 75 | }) 76 | 77 | only 78 | ? console.error('⚠️', 'Not all tests were run') 79 | : ignored 80 | ? console.error('⚠️', ignored, 'ignored test' + (ignored === 1 ? '' : 's', '\n')) 81 | : success 82 | ? console.log('🎉') 83 | : console.error('⚠️', 'Not good') 84 | 85 | !process.exitCode && (!success || only || ignored) && (process.exitCode = 1) 86 | } 87 | 88 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/porsager/postgres/9b92b65da6a5121545581a6dd5de859c2a70177f/demo.gif -------------------------------------------------------------------------------- /deno/mod.js: -------------------------------------------------------------------------------- 1 | // @deno-types="./types/index.d.ts" 2 | export { default } from './src/index.js' 3 | -------------------------------------------------------------------------------- /deno/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /deno/polyfills.js: -------------------------------------------------------------------------------- 1 | /* global Deno */ 2 | 3 | import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' 4 | import { isIP } from 'https://deno.land/std@0.132.0/node/net.ts' 5 | 6 | const events = () => ({ data: [], error: [], drain: [], connect: [], secureConnect: [], close: [] }) 7 | 8 | class Socket { 9 | constructor() { 10 | return createSocket() 11 | } 12 | } 13 | 14 | function createSocket() { 15 | let paused 16 | , resume 17 | , keepAlive 18 | 19 | const socket = { 20 | error, 21 | success, 22 | readyState: 'open', 23 | setKeepAlive: x => { 24 | keepAlive = x 25 | socket.raw && socket.raw.setKeepAlive && socket.raw.setKeepAlive(x) 26 | }, 27 | connect: (port, hostname) => { 28 | socket.raw = null 29 | socket.readyState = 'connecting' 30 | typeof port === 'string' 31 | ? Deno.connect({ transport: 'unix', path: socket.path = port }).then(success, error) 32 | : Deno.connect({ transport: 'tcp', port: socket.port = port, hostname: socket.hostname = hostname || 'localhost' }).then(success, error) // eslint-disable-line 33 | return socket 34 | }, 35 | pause: () => { 36 | paused = new Promise(r => resume = r) 37 | }, 38 | resume: () => { 39 | resume && resume() 40 | paused = null 41 | }, 42 | isPaused: () => !!paused, 43 | removeAllListeners: () => socket.events = events(), 44 | events: events(), 45 | raw: null, 46 | on: (x, fn) => socket.events[x].push(fn), 47 | once: (x, fn) => { 48 | if (x === 'data') 49 | socket.break = true 50 | const e = socket.events[x] 51 | e.push(once) 52 | once.once = fn 53 | function once(...args) { 54 | fn(...args) 55 | e.indexOf(once) > -1 && e.splice(e.indexOf(once), 1) 56 | } 57 | }, 58 | removeListener: (x, fn) => { 59 | socket.events[x] = socket.events[x].filter(x => x !== fn && x.once !== fn) 60 | }, 61 | write: (x, cb) => { 62 | socket.raw.write(x).then(l => { 63 | l < x.length 64 | ? socket.write(x.slice(l), cb) 65 | : (cb && cb(null)) 66 | }).catch(err => { 67 | cb && cb() 68 | call(socket.events.error, err) 69 | }) 70 | return false 71 | }, 72 | destroy: () => close(), 73 | end: (x) => { 74 | x && socket.write(x) 75 | close() 76 | } 77 | } 78 | 79 | return socket 80 | 81 | async function success(raw) { 82 | if (socket.readyState !== 'connecting') 83 | return raw.close() 84 | 85 | const encrypted = socket.encrypted 86 | socket.raw = raw 87 | keepAlive != null && raw.setKeepAlive && raw.setKeepAlive(keepAlive) 88 | socket.readyState = 'open' 89 | socket.encrypted 90 | ? call(socket.events.secureConnect) 91 | : call(socket.events.connect) 92 | 93 | const b = new Uint8Array(1024) 94 | let result 95 | 96 | try { 97 | while ((result = socket.readyState === 'open' && await raw.read(b))) { 98 | call(socket.events.data, Buffer.from(b.subarray(0, result))) 99 | if (!encrypted && socket.break && (socket.break = false, b[0] === 83)) 100 | return socket.break = false 101 | paused && await paused 102 | } 103 | } catch (e) { 104 | if (e instanceof Deno.errors.BadResource === false) 105 | error(e) 106 | } 107 | 108 | if (!socket.encrypted || encrypted) 109 | closed() 110 | } 111 | 112 | function close() { 113 | try { 114 | socket.raw && socket.raw.close() 115 | } catch (e) { 116 | if (e instanceof Deno.errors.BadResource === false) 117 | call(socket.events.error, e) 118 | } 119 | } 120 | 121 | function closed() { 122 | if (socket.readyState === 'closed') 123 | return 124 | 125 | socket.break = socket.encrypted = false 126 | socket.readyState = 'closed' 127 | call(socket.events.close) 128 | } 129 | 130 | function error(err) { 131 | call(socket.events.error, err) 132 | socket.raw 133 | ? close() 134 | : closed() 135 | } 136 | 137 | function call(xs, x) { 138 | xs.slice().forEach(fn => fn(x)) 139 | } 140 | } 141 | 142 | export const net = { 143 | isIP, 144 | createServer() { 145 | const server = { 146 | address() { 147 | return { port: 9876 } 148 | }, 149 | async listen() { 150 | server.raw = Deno.listen({ port: 9876, transport: 'tcp' }) 151 | for await (const conn of server.raw) 152 | setTimeout(() => conn.close(), 500) 153 | }, 154 | close() { 155 | server.raw.close() 156 | } 157 | } 158 | return server 159 | }, 160 | Socket 161 | } 162 | 163 | export const tls = { 164 | connect({ socket, ...options }) { 165 | socket.encrypted = true 166 | socket.readyState = 'connecting' 167 | Deno.startTls(socket.raw, { hostname: socket.hostname, ...options }) 168 | .then(socket.success, socket.error) 169 | socket.raw = null 170 | return socket 171 | } 172 | } 173 | 174 | let ids = 1 175 | const tasks = new Set() 176 | export const setImmediate = fn => { 177 | const id = ids++ 178 | tasks.add(id) 179 | queueMicrotask(() => { 180 | if (tasks.has(id)) { 181 | fn() 182 | tasks.delete(id) 183 | } 184 | }) 185 | return id 186 | } 187 | 188 | export const clearImmediate = id => tasks.delete(id) 189 | 190 | -------------------------------------------------------------------------------- /deno/src/bytes.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' 2 | const size = 256 3 | let buffer = Buffer.allocUnsafe(size) 4 | 5 | const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { 6 | const v = x.charCodeAt(0) 7 | acc[x] = () => { 8 | buffer[0] = v 9 | b.i = 5 10 | return b 11 | } 12 | return acc 13 | }, {}) 14 | 15 | const b = Object.assign(reset, messages, { 16 | N: String.fromCharCode(0), 17 | i: 0, 18 | inc(x) { 19 | b.i += x 20 | return b 21 | }, 22 | str(x) { 23 | const length = Buffer.byteLength(x) 24 | fit(length) 25 | b.i += buffer.write(x, b.i, length, 'utf8') 26 | return b 27 | }, 28 | i16(x) { 29 | fit(2) 30 | buffer.writeUInt16BE(x, b.i) 31 | b.i += 2 32 | return b 33 | }, 34 | i32(x, i) { 35 | if (i || i === 0) { 36 | buffer.writeUInt32BE(x, i) 37 | return b 38 | } 39 | fit(4) 40 | buffer.writeUInt32BE(x, b.i) 41 | b.i += 4 42 | return b 43 | }, 44 | z(x) { 45 | fit(x) 46 | buffer.fill(0, b.i, b.i + x) 47 | b.i += x 48 | return b 49 | }, 50 | raw(x) { 51 | buffer = Buffer.concat([buffer.subarray(0, b.i), x]) 52 | b.i = buffer.length 53 | return b 54 | }, 55 | end(at = 1) { 56 | buffer.writeUInt32BE(b.i - at, at) 57 | const out = buffer.subarray(0, b.i) 58 | b.i = 0 59 | buffer = Buffer.allocUnsafe(size) 60 | return out 61 | } 62 | }) 63 | 64 | export default b 65 | 66 | function fit(x) { 67 | if (buffer.length - b.i < x) { 68 | const prev = buffer 69 | , length = prev.length 70 | 71 | buffer = Buffer.allocUnsafe(length + (length >> 1) + x) 72 | prev.copy(buffer) 73 | } 74 | } 75 | 76 | function reset() { 77 | b.i = 0 78 | return b 79 | } 80 | -------------------------------------------------------------------------------- /deno/src/errors.js: -------------------------------------------------------------------------------- 1 | export class PostgresError extends Error { 2 | constructor(x) { 3 | super(x.message) 4 | this.name = this.constructor.name 5 | Object.assign(this, x) 6 | } 7 | } 8 | 9 | export const Errors = { 10 | connection, 11 | postgres, 12 | generic, 13 | notSupported 14 | } 15 | 16 | function connection(x, options, socket) { 17 | const { host, port } = socket || options 18 | const error = Object.assign( 19 | new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), 20 | { 21 | code: x, 22 | errno: x, 23 | address: options.path || host 24 | }, options.path ? {} : { port: port } 25 | ) 26 | Error.captureStackTrace(error, connection) 27 | return error 28 | } 29 | 30 | function postgres(x) { 31 | const error = new PostgresError(x) 32 | Error.captureStackTrace(error, postgres) 33 | return error 34 | } 35 | 36 | function generic(code, message) { 37 | const error = Object.assign(new Error(code + ': ' + message), { code }) 38 | Error.captureStackTrace(error, generic) 39 | return error 40 | } 41 | 42 | /* c8 ignore next 10 */ 43 | function notSupported(x) { 44 | const error = Object.assign( 45 | new Error(x + ' (B) is not supported'), 46 | { 47 | code: 'MESSAGE_NOT_SUPPORTED', 48 | name: x 49 | } 50 | ) 51 | Error.captureStackTrace(error, notSupported) 52 | return error 53 | } 54 | -------------------------------------------------------------------------------- /deno/src/large.js: -------------------------------------------------------------------------------- 1 | import Stream from 'https://deno.land/std@0.132.0/node/stream.ts' 2 | 3 | export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { 4 | return new Promise(async(resolve, reject) => { 5 | await sql.begin(async sql => { 6 | let finish 7 | !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) 8 | const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` 9 | 10 | const lo = { 11 | writable, 12 | readable, 13 | close : () => sql`select lo_close(${ fd })`.then(finish), 14 | tell : () => sql`select lo_tell64(${ fd })`, 15 | read : (x) => sql`select loread(${ fd }, ${ x }) as data`, 16 | write : (x) => sql`select lowrite(${ fd }, ${ x })`, 17 | truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, 18 | seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, 19 | size : () => sql` 20 | select 21 | lo_lseek64(${ fd }, location, 0) as position, 22 | seek.size 23 | from ( 24 | select 25 | lo_lseek64($1, 0, 2) as size, 26 | tell.location 27 | from (select lo_tell64($1) as location) tell 28 | ) seek 29 | ` 30 | } 31 | 32 | resolve(lo) 33 | 34 | return new Promise(async r => finish = r) 35 | 36 | async function readable({ 37 | highWaterMark = 2048 * 8, 38 | start = 0, 39 | end = Infinity 40 | } = {}) { 41 | let max = end - start 42 | start && await lo.seek(start) 43 | return new Stream.Readable({ 44 | highWaterMark, 45 | async read(size) { 46 | const l = size > max ? size - max : size 47 | max -= size 48 | const [{ data }] = await lo.read(l) 49 | this.push(data) 50 | if (data.length < size) 51 | this.push(null) 52 | } 53 | }) 54 | } 55 | 56 | async function writable({ 57 | highWaterMark = 2048 * 8, 58 | start = 0 59 | } = {}) { 60 | start && await lo.seek(start) 61 | return new Stream.Writable({ 62 | highWaterMark, 63 | write(chunk, encoding, callback) { 64 | lo.write(chunk).then(() => callback(), callback) 65 | } 66 | }) 67 | } 68 | }).catch(reject) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /deno/src/query.js: -------------------------------------------------------------------------------- 1 | const originCache = new Map() 2 | , originStackCache = new Map() 3 | , originError = Symbol('OriginError') 4 | 5 | export const CLOSE = {} 6 | export class Query extends Promise { 7 | constructor(strings, args, handler, canceller, options = {}) { 8 | let resolve 9 | , reject 10 | 11 | super((a, b) => { 12 | resolve = a 13 | reject = b 14 | }) 15 | 16 | this.tagged = Array.isArray(strings.raw) 17 | this.strings = strings 18 | this.args = args 19 | this.handler = handler 20 | this.canceller = canceller 21 | this.options = options 22 | 23 | this.state = null 24 | this.statement = null 25 | 26 | this.resolve = x => (this.active = false, resolve(x)) 27 | this.reject = x => (this.active = false, reject(x)) 28 | 29 | this.active = false 30 | this.cancelled = null 31 | this.executed = false 32 | this.signature = '' 33 | 34 | this[originError] = this.handler.debug 35 | ? new Error() 36 | : this.tagged && cachedError(this.strings) 37 | } 38 | 39 | get origin() { 40 | return (this.handler.debug 41 | ? this[originError].stack 42 | : this.tagged && originStackCache.has(this.strings) 43 | ? originStackCache.get(this.strings) 44 | : originStackCache.set(this.strings, this[originError].stack).get(this.strings) 45 | ) || '' 46 | } 47 | 48 | static get [Symbol.species]() { 49 | return Promise 50 | } 51 | 52 | cancel() { 53 | return this.canceller && (this.canceller(this), this.canceller = null) 54 | } 55 | 56 | simple() { 57 | this.options.simple = true 58 | this.options.prepare = false 59 | return this 60 | } 61 | 62 | async readable() { 63 | this.simple() 64 | this.streaming = true 65 | return this 66 | } 67 | 68 | async writable() { 69 | this.simple() 70 | this.streaming = true 71 | return this 72 | } 73 | 74 | cursor(rows = 1, fn) { 75 | this.options.simple = false 76 | if (typeof rows === 'function') { 77 | fn = rows 78 | rows = 1 79 | } 80 | 81 | this.cursorRows = rows 82 | 83 | if (typeof fn === 'function') 84 | return (this.cursorFn = fn, this) 85 | 86 | let prev 87 | return { 88 | [Symbol.asyncIterator]: () => ({ 89 | next: () => { 90 | if (this.executed && !this.active) 91 | return { done: true } 92 | 93 | prev && prev() 94 | const promise = new Promise((resolve, reject) => { 95 | this.cursorFn = value => { 96 | resolve({ value, done: false }) 97 | return new Promise(r => prev = r) 98 | } 99 | this.resolve = () => (this.active = false, resolve({ done: true })) 100 | this.reject = x => (this.active = false, reject(x)) 101 | }) 102 | this.execute() 103 | return promise 104 | }, 105 | return() { 106 | prev && prev(CLOSE) 107 | return { done: true } 108 | } 109 | }) 110 | } 111 | } 112 | 113 | describe() { 114 | this.options.simple = false 115 | this.onlyDescribe = this.options.prepare = true 116 | return this 117 | } 118 | 119 | stream() { 120 | throw new Error('.stream has been renamed to .forEach') 121 | } 122 | 123 | forEach(fn) { 124 | this.forEachFn = fn 125 | this.handle() 126 | return this 127 | } 128 | 129 | raw() { 130 | this.isRaw = true 131 | return this 132 | } 133 | 134 | values() { 135 | this.isRaw = 'values' 136 | return this 137 | } 138 | 139 | async handle() { 140 | !this.executed && (this.executed = true) && await 1 && this.handler(this) 141 | } 142 | 143 | execute() { 144 | this.handle() 145 | return this 146 | } 147 | 148 | then() { 149 | this.handle() 150 | return super.then.apply(this, arguments) 151 | } 152 | 153 | catch() { 154 | this.handle() 155 | return super.catch.apply(this, arguments) 156 | } 157 | 158 | finally() { 159 | this.handle() 160 | return super.finally.apply(this, arguments) 161 | } 162 | } 163 | 164 | function cachedError(xs) { 165 | if (originCache.has(xs)) 166 | return originCache.get(xs) 167 | 168 | const x = Error.stackTraceLimit 169 | Error.stackTraceLimit = 4 170 | originCache.set(xs, new Error()) 171 | Error.stackTraceLimit = x 172 | return originCache.get(xs) 173 | } 174 | -------------------------------------------------------------------------------- /deno/src/queue.js: -------------------------------------------------------------------------------- 1 | export default Queue 2 | 3 | function Queue(initial = []) { 4 | let xs = initial.slice() 5 | let index = 0 6 | 7 | return { 8 | get length() { 9 | return xs.length - index 10 | }, 11 | remove: (x) => { 12 | const index = xs.indexOf(x) 13 | return index === -1 14 | ? null 15 | : (xs.splice(index, 1), x) 16 | }, 17 | push: (x) => (xs.push(x), x), 18 | shift: () => { 19 | const out = xs[index++] 20 | 21 | if (index === xs.length) { 22 | index = 0 23 | xs = [] 24 | } else { 25 | xs[index - 1] = undefined 26 | } 27 | 28 | return out 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /deno/src/result.js: -------------------------------------------------------------------------------- 1 | export default class Result extends Array { 2 | constructor() { 3 | super() 4 | Object.defineProperties(this, { 5 | count: { value: null, writable: true }, 6 | state: { value: null, writable: true }, 7 | command: { value: null, writable: true }, 8 | columns: { value: null, writable: true }, 9 | statement: { value: null, writable: true } 10 | }) 11 | } 12 | 13 | static get [Symbol.species]() { 14 | return Array 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /deno/src/subscribe.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' 2 | const noop = () => { /* noop */ } 3 | 4 | export default function Subscribe(postgres, options) { 5 | const subscribers = new Map() 6 | , slot = 'postgresjs_' + Math.random().toString(36).slice(2) 7 | , state = {} 8 | 9 | let connection 10 | , stream 11 | , ended = false 12 | 13 | const sql = subscribe.sql = postgres({ 14 | ...options, 15 | transform: { column: {}, value: {}, row: {} }, 16 | max: 1, 17 | fetch_types: false, 18 | idle_timeout: null, 19 | max_lifetime: null, 20 | connection: { 21 | ...options.connection, 22 | replication: 'database' 23 | }, 24 | onclose: async function() { 25 | if (ended) 26 | return 27 | stream = null 28 | state.pid = state.secret = undefined 29 | connected(await init(sql, slot, options.publications)) 30 | subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) 31 | }, 32 | no_subscribe: true 33 | }) 34 | 35 | const end = sql.end 36 | , close = sql.close 37 | 38 | sql.end = async() => { 39 | ended = true 40 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 41 | return end() 42 | } 43 | 44 | sql.close = async() => { 45 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 46 | return close() 47 | } 48 | 49 | return subscribe 50 | 51 | async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { 52 | event = parseEvent(event) 53 | 54 | if (!connection) 55 | connection = init(sql, slot, options.publications) 56 | 57 | const subscriber = { fn, onsubscribe } 58 | const fns = subscribers.has(event) 59 | ? subscribers.get(event).add(subscriber) 60 | : subscribers.set(event, new Set([subscriber])).get(event) 61 | 62 | const unsubscribe = () => { 63 | fns.delete(subscriber) 64 | fns.size === 0 && subscribers.delete(event) 65 | } 66 | 67 | return connection.then(x => { 68 | connected(x) 69 | onsubscribe() 70 | stream && stream.on('error', onerror) 71 | return { unsubscribe, state, sql } 72 | }) 73 | } 74 | 75 | function connected(x) { 76 | stream = x.stream 77 | state.pid = x.state.pid 78 | state.secret = x.state.secret 79 | } 80 | 81 | async function init(sql, slot, publications) { 82 | if (!publications) 83 | throw new Error('Missing publication names') 84 | 85 | const xs = await sql.unsafe( 86 | `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` 87 | ) 88 | 89 | const [x] = xs 90 | 91 | const stream = await sql.unsafe( 92 | `START_REPLICATION SLOT ${ slot } LOGICAL ${ 93 | x.consistent_point 94 | } (proto_version '1', publication_names '${ publications }')` 95 | ).writable() 96 | 97 | const state = { 98 | lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) 99 | } 100 | 101 | stream.on('data', data) 102 | stream.on('error', error) 103 | stream.on('close', sql.close) 104 | 105 | return { stream, state: xs.state } 106 | 107 | function error(e) { 108 | console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line 109 | } 110 | 111 | function data(x) { 112 | if (x[0] === 0x77) { 113 | parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) 114 | } else if (x[0] === 0x6b && x[17]) { 115 | state.lsn = x.subarray(1, 9) 116 | pong() 117 | } 118 | } 119 | 120 | function handle(a, b) { 121 | const path = b.relation.schema + '.' + b.relation.table 122 | call('*', a, b) 123 | call('*:' + path, a, b) 124 | b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 125 | call(b.command, a, b) 126 | call(b.command + ':' + path, a, b) 127 | b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 128 | } 129 | 130 | function pong() { 131 | const x = Buffer.alloc(34) 132 | x[0] = 'r'.charCodeAt(0) 133 | x.fill(state.lsn, 1) 134 | x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) 135 | stream.write(x) 136 | } 137 | } 138 | 139 | function call(x, a, b) { 140 | subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) 141 | } 142 | } 143 | 144 | function Time(x) { 145 | return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) 146 | } 147 | 148 | function parse(x, state, parsers, handle, transform) { 149 | const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) 150 | 151 | Object.entries({ 152 | R: x => { // Relation 153 | let i = 1 154 | const r = state[x.readUInt32BE(i)] = { 155 | schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', 156 | table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), 157 | columns: Array(x.readUInt16BE(i += 2)), 158 | keys: [] 159 | } 160 | i += 2 161 | 162 | let columnIndex = 0 163 | , column 164 | 165 | while (i < x.length) { 166 | column = r.columns[columnIndex++] = { 167 | key: x[i++], 168 | name: transform.column.from 169 | ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) 170 | : x.toString('utf8', i, i = x.indexOf(0, i)), 171 | type: x.readUInt32BE(i += 1), 172 | parser: parsers[x.readUInt32BE(i)], 173 | atttypmod: x.readUInt32BE(i += 4) 174 | } 175 | 176 | column.key && r.keys.push(column) 177 | i += 4 178 | } 179 | }, 180 | Y: () => { /* noop */ }, // Type 181 | O: () => { /* noop */ }, // Origin 182 | B: x => { // Begin 183 | state.date = Time(x.readBigInt64BE(9)) 184 | state.lsn = x.subarray(1, 9) 185 | }, 186 | I: x => { // Insert 187 | let i = 1 188 | const relation = state[x.readUInt32BE(i)] 189 | const { row } = tuples(x, relation.columns, i += 7, transform) 190 | 191 | handle(row, { 192 | command: 'insert', 193 | relation 194 | }) 195 | }, 196 | D: x => { // Delete 197 | let i = 1 198 | const relation = state[x.readUInt32BE(i)] 199 | i += 4 200 | const key = x[i] === 75 201 | handle(key || x[i] === 79 202 | ? tuples(x, relation.columns, i += 3, transform).row 203 | : null 204 | , { 205 | command: 'delete', 206 | relation, 207 | key 208 | }) 209 | }, 210 | U: x => { // Update 211 | let i = 1 212 | const relation = state[x.readUInt32BE(i)] 213 | i += 4 214 | const key = x[i] === 75 215 | const xs = key || x[i] === 79 216 | ? tuples(x, relation.columns, i += 3, transform) 217 | : null 218 | 219 | xs && (i = xs.i) 220 | 221 | const { row } = tuples(x, relation.columns, i + 3, transform) 222 | 223 | handle(row, { 224 | command: 'update', 225 | relation, 226 | key, 227 | old: xs && xs.row 228 | }) 229 | }, 230 | T: () => { /* noop */ }, // Truncate, 231 | C: () => { /* noop */ } // Commit 232 | }).reduce(char, {})[x[0]](x) 233 | } 234 | 235 | function tuples(x, columns, xi, transform) { 236 | let type 237 | , column 238 | , value 239 | 240 | const row = transform.raw ? new Array(columns.length) : {} 241 | for (let i = 0; i < columns.length; i++) { 242 | type = x[xi++] 243 | column = columns[i] 244 | value = type === 110 // n 245 | ? null 246 | : type === 117 // u 247 | ? undefined 248 | : column.parser === undefined 249 | ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) 250 | : column.parser.array === true 251 | ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) 252 | : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) 253 | 254 | transform.raw 255 | ? (row[i] = transform.raw === true 256 | ? value 257 | : transform.value.from ? transform.value.from(value, column) : value) 258 | : (row[column.name] = transform.value.from 259 | ? transform.value.from(value, column) 260 | : value 261 | ) 262 | } 263 | 264 | return { i: xi, row: transform.row.from ? transform.row.from(row) : row } 265 | } 266 | 267 | function parseEvent(x) { 268 | const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] 269 | 270 | if (!xs) 271 | throw new Error('Malformed subscribe pattern: ' + x) 272 | 273 | const [, command, path, key] = xs 274 | 275 | return (command || '*') 276 | + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') 277 | + (key ? '=' + key : '') 278 | } 279 | -------------------------------------------------------------------------------- /deno/src/types.js: -------------------------------------------------------------------------------- 1 | import { Buffer } from 'https://deno.land/std@0.132.0/node/buffer.ts' 2 | import { Query } from './query.js' 3 | import { Errors } from './errors.js' 4 | 5 | export const types = { 6 | string: { 7 | to: 25, 8 | from: null, // defaults to string 9 | serialize: x => '' + x 10 | }, 11 | number: { 12 | to: 0, 13 | from: [21, 23, 26, 700, 701], 14 | serialize: x => '' + x, 15 | parse: x => +x 16 | }, 17 | json: { 18 | to: 114, 19 | from: [114, 3802], 20 | serialize: x => JSON.stringify(x), 21 | parse: x => JSON.parse(x) 22 | }, 23 | boolean: { 24 | to: 16, 25 | from: 16, 26 | serialize: x => x === true ? 't' : 'f', 27 | parse: x => x === 't' 28 | }, 29 | date: { 30 | to: 1184, 31 | from: [1082, 1114, 1184], 32 | serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), 33 | parse: x => new Date(x) 34 | }, 35 | bytea: { 36 | to: 17, 37 | from: 17, 38 | serialize: x => '\\x' + Buffer.from(x).toString('hex'), 39 | parse: x => Buffer.from(x.slice(2), 'hex') 40 | } 41 | } 42 | 43 | class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} 44 | 45 | export class Identifier extends NotTagged { 46 | constructor(value) { 47 | super() 48 | this.value = escapeIdentifier(value) 49 | } 50 | } 51 | 52 | export class Parameter extends NotTagged { 53 | constructor(value, type, array) { 54 | super() 55 | this.value = value 56 | this.type = type 57 | this.array = array 58 | } 59 | } 60 | 61 | export class Builder extends NotTagged { 62 | constructor(first, rest) { 63 | super() 64 | this.first = first 65 | this.rest = rest 66 | } 67 | 68 | build(before, parameters, types, options) { 69 | const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() 70 | return keyword.i === -1 71 | ? escapeIdentifiers(this.first, options) 72 | : keyword.fn(this.first, this.rest, parameters, types, options) 73 | } 74 | } 75 | 76 | export function handleValue(x, parameters, types, options) { 77 | let value = x instanceof Parameter ? x.value : x 78 | if (value === undefined) { 79 | x instanceof Parameter 80 | ? x.value = options.transform.undefined 81 | : value = x = options.transform.undefined 82 | 83 | if (value === undefined) 84 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 85 | } 86 | 87 | return '$' + (types.push( 88 | x instanceof Parameter 89 | ? (parameters.push(x.value), x.array 90 | ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) 91 | : x.type 92 | ) 93 | : (parameters.push(x), inferType(x)) 94 | )) 95 | } 96 | 97 | const defaultHandlers = typeHandlers(types) 98 | 99 | export function stringify(q, string, value, parameters, types, options) { // eslint-disable-line 100 | for (let i = 1; i < q.strings.length; i++) { 101 | string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] 102 | value = q.args[i] 103 | } 104 | 105 | return string 106 | } 107 | 108 | function stringifyValue(string, value, parameters, types, o) { 109 | return ( 110 | value instanceof Builder ? value.build(string, parameters, types, o) : 111 | value instanceof Query ? fragment(value, parameters, types, o) : 112 | value instanceof Identifier ? value.value : 113 | value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : 114 | handleValue(value, parameters, types, o) 115 | ) 116 | } 117 | 118 | function fragment(q, parameters, types, options) { 119 | q.fragment = true 120 | return stringify(q, q.strings[0], q.args[0], parameters, types, options) 121 | } 122 | 123 | function valuesBuilder(first, parameters, types, columns, options) { 124 | return first.map(row => 125 | '(' + columns.map(column => 126 | stringifyValue('values', row[column], parameters, types, options) 127 | ).join(',') + ')' 128 | ).join(',') 129 | } 130 | 131 | function values(first, rest, parameters, types, options) { 132 | const multi = Array.isArray(first[0]) 133 | const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) 134 | return valuesBuilder(multi ? first : [first], parameters, types, columns, options) 135 | } 136 | 137 | function select(first, rest, parameters, types, options) { 138 | typeof first === 'string' && (first = [first].concat(rest)) 139 | if (Array.isArray(first)) 140 | return escapeIdentifiers(first, options) 141 | 142 | let value 143 | const columns = rest.length ? rest.flat() : Object.keys(first) 144 | return columns.map(x => { 145 | value = first[x] 146 | return ( 147 | value instanceof Query ? fragment(value, parameters, types, options) : 148 | value instanceof Identifier ? value.value : 149 | handleValue(value, parameters, types, options) 150 | ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) 151 | }).join(',') 152 | } 153 | 154 | const builders = Object.entries({ 155 | values, 156 | in: (...xs) => { 157 | const x = values(...xs) 158 | return x === '()' ? '(null)' : x 159 | }, 160 | select, 161 | as: select, 162 | returning: select, 163 | '\\(': select, 164 | 165 | update(first, rest, parameters, types, options) { 166 | return (rest.length ? rest.flat() : Object.keys(first)).map(x => 167 | escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + 168 | '=' + stringifyValue('values', first[x], parameters, types, options) 169 | ) 170 | }, 171 | 172 | insert(first, rest, parameters, types, options) { 173 | const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) 174 | return '(' + escapeIdentifiers(columns, options) + ')values' + 175 | valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) 176 | } 177 | }).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) 178 | 179 | function notTagged() { 180 | throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') 181 | } 182 | 183 | export const serializers = defaultHandlers.serializers 184 | export const parsers = defaultHandlers.parsers 185 | 186 | export const END = {} 187 | 188 | function firstIsString(x) { 189 | if (Array.isArray(x)) 190 | return firstIsString(x[0]) 191 | return typeof x === 'string' ? 1009 : 0 192 | } 193 | 194 | export const mergeUserTypes = function(types) { 195 | const user = typeHandlers(types || {}) 196 | return { 197 | serializers: Object.assign({}, serializers, user.serializers), 198 | parsers: Object.assign({}, parsers, user.parsers) 199 | } 200 | } 201 | 202 | function typeHandlers(types) { 203 | return Object.keys(types).reduce((acc, k) => { 204 | types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) 205 | if (types[k].serialize) { 206 | acc.serializers[types[k].to] = types[k].serialize 207 | types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) 208 | } 209 | return acc 210 | }, { parsers: {}, serializers: {} }) 211 | } 212 | 213 | function escapeIdentifiers(xs, { transform: { column } }) { 214 | return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') 215 | } 216 | 217 | export const escapeIdentifier = function escape(str) { 218 | return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' 219 | } 220 | 221 | export const inferType = function inferType(x) { 222 | return ( 223 | x instanceof Parameter ? x.type : 224 | x instanceof Date ? 1184 : 225 | x instanceof Uint8Array ? 17 : 226 | (x === true || x === false) ? 16 : 227 | typeof x === 'bigint' ? 20 : 228 | Array.isArray(x) ? inferType(x[0]) : 229 | 0 230 | ) 231 | } 232 | 233 | const escapeBackslash = /\\/g 234 | const escapeQuote = /"/g 235 | 236 | function arrayEscape(x) { 237 | return x 238 | .replace(escapeBackslash, '\\\\') 239 | .replace(escapeQuote, '\\"') 240 | } 241 | 242 | export const arraySerializer = function arraySerializer(xs, serializer, options, typarray) { 243 | if (Array.isArray(xs) === false) 244 | return xs 245 | 246 | if (!xs.length) 247 | return '{}' 248 | 249 | const first = xs[0] 250 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 251 | const delimiter = typarray === 1020 ? ';' : ',' 252 | 253 | if (Array.isArray(first) && !first.type) 254 | return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' 255 | 256 | return '{' + xs.map(x => { 257 | if (x === undefined) { 258 | x = options.transform.undefined 259 | if (x === undefined) 260 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 261 | } 262 | 263 | return x === null 264 | ? 'null' 265 | : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' 266 | }).join(delimiter) + '}' 267 | } 268 | 269 | const arrayParserState = { 270 | i: 0, 271 | char: null, 272 | str: '', 273 | quoted: false, 274 | last: 0 275 | } 276 | 277 | export const arrayParser = function arrayParser(x, parser, typarray) { 278 | arrayParserState.i = arrayParserState.last = 0 279 | return arrayParserLoop(arrayParserState, x, parser, typarray) 280 | } 281 | 282 | function arrayParserLoop(s, x, parser, typarray) { 283 | const xs = [] 284 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 285 | const delimiter = typarray === 1020 ? ';' : ',' 286 | for (; s.i < x.length; s.i++) { 287 | s.char = x[s.i] 288 | if (s.quoted) { 289 | if (s.char === '\\') { 290 | s.str += x[++s.i] 291 | } else if (s.char === '"') { 292 | xs.push(parser ? parser(s.str) : s.str) 293 | s.str = '' 294 | s.quoted = x[s.i + 1] === '"' 295 | s.last = s.i + 2 296 | } else { 297 | s.str += s.char 298 | } 299 | } else if (s.char === '"') { 300 | s.quoted = true 301 | } else if (s.char === '{') { 302 | s.last = ++s.i 303 | xs.push(arrayParserLoop(s, x, parser, typarray)) 304 | } else if (s.char === '}') { 305 | s.quoted = false 306 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 307 | s.last = s.i + 1 308 | break 309 | } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { 310 | xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 311 | s.last = s.i + 1 312 | } 313 | s.p = s.char 314 | } 315 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) 316 | return xs 317 | } 318 | 319 | export const toCamel = x => { 320 | let str = x[0] 321 | for (let i = 1; i < x.length; i++) 322 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 323 | return str 324 | } 325 | 326 | export const toPascal = x => { 327 | let str = x[0].toUpperCase() 328 | for (let i = 1; i < x.length; i++) 329 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 330 | return str 331 | } 332 | 333 | export const toKebab = x => x.replace(/_/g, '-') 334 | 335 | export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() 336 | export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() 337 | export const fromKebab = x => x.replace(/-/g, '_') 338 | 339 | function createJsonTransform(fn) { 340 | return function jsonTransform(x, column) { 341 | return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) 342 | ? Array.isArray(x) 343 | ? x.map(x => jsonTransform(x, column)) 344 | : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) 345 | : x 346 | } 347 | } 348 | 349 | toCamel.column = { from: toCamel } 350 | toCamel.value = { from: createJsonTransform(toCamel) } 351 | fromCamel.column = { to: fromCamel } 352 | 353 | export const camel = { ...toCamel } 354 | camel.column.to = fromCamel 355 | 356 | toPascal.column = { from: toPascal } 357 | toPascal.value = { from: createJsonTransform(toPascal) } 358 | fromPascal.column = { to: fromPascal } 359 | 360 | export const pascal = { ...toPascal } 361 | pascal.column.to = fromPascal 362 | 363 | toKebab.column = { from: toKebab } 364 | toKebab.value = { from: createJsonTransform(toKebab) } 365 | fromKebab.column = { to: fromKebab } 366 | 367 | export const kebab = { ...toKebab } 368 | kebab.column.to = fromKebab 369 | -------------------------------------------------------------------------------- /deno/tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { spawn } from 'https://deno.land/std@0.132.0/node/child_process.ts' 2 | 3 | await exec('dropdb', ['postgres_js_test']) 4 | 5 | await exec('psql', ['-c', 'alter system set ssl=on']) 6 | await exec('psql', ['-c', 'drop user postgres_js_test']) 7 | await exec('psql', ['-c', 'create user postgres_js_test']) 8 | await exec('psql', ['-c', 'alter system set password_encryption=md5']) 9 | await exec('psql', ['-c', 'select pg_reload_conf()']) 10 | await exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) 11 | await exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) 12 | await exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) 13 | await exec('psql', ['-c', 'select pg_reload_conf()']) 14 | await exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) 15 | await exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) 16 | 17 | await exec('createdb', ['postgres_js_test']) 18 | await exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) 19 | await exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) 20 | 21 | function ignore(cmd, args) { 22 | const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) 23 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 24 | throw stderr 25 | } 26 | 27 | export async function exec(cmd, args) { // eslint-disable-line 28 | let stderr = '' 29 | const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line 30 | cp.stderr.on('data', x => stderr += x) 31 | await new Promise(x => cp.on('exit', x)) 32 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 33 | throw new Error(stderr) 34 | } 35 | -------------------------------------------------------------------------------- /deno/tests/copy.csv: -------------------------------------------------------------------------------- 1 | 1 2 3 2 | 4 5 6 3 | -------------------------------------------------------------------------------- /deno/tests/pg_hba.conf: -------------------------------------------------------------------------------- 1 | local all all trust 2 | host all postgres samehost trust 3 | host postgres_js_test postgres_js_test samehost trust 4 | host postgres_js_test postgres_js_test_md5 samehost md5 5 | host postgres_js_test postgres_js_test_scram samehost scram-sha-256 6 | -------------------------------------------------------------------------------- /deno/tests/select-param.sql: -------------------------------------------------------------------------------- 1 | select $1 as x 2 | -------------------------------------------------------------------------------- /deno/tests/select.sql: -------------------------------------------------------------------------------- 1 | select 1 as x 2 | -------------------------------------------------------------------------------- /deno/tests/test.js: -------------------------------------------------------------------------------- 1 | import process from 'https://deno.land/std@0.132.0/node/process.ts' 2 | /* eslint no-console: 0 */ 3 | 4 | import util from 'https://deno.land/std@0.132.0/node/util.ts' 5 | 6 | let done = 0 7 | let only = false 8 | let ignored = 0 9 | let failed = false 10 | let promise = Promise.resolve() 11 | const tests = {} 12 | , ignore = {} 13 | 14 | export const nt = () => ignored++ 15 | export const ot = (...rest) => (only = true, test(true, ...rest)) 16 | export const t = (...rest) => test(false, ...rest) 17 | t.timeout = 5 18 | 19 | async function test(o, name, options, fn) { 20 | typeof options !== 'object' && (fn = options, options = {}) 21 | const line = new Error().stack.split('\n')[3].match(':([0-9]+):')[1] 22 | 23 | await 1 24 | 25 | if (only && !o) 26 | return 27 | 28 | tests[line] = { fn, line, name } 29 | promise = promise.then(() => Promise.race([ 30 | new Promise((resolve, reject) => 31 | fn.timer = setTimeout(() => reject('Timed out'), (options.timeout || t.timeout) * 1000) 32 | ), 33 | failed 34 | ? (ignored++, ignore) 35 | : fn() 36 | ])) 37 | .then(async x => { 38 | clearTimeout(fn.timer) 39 | if (x === ignore) 40 | return 41 | 42 | if (!Array.isArray(x)) 43 | throw new Error('Test should return result array') 44 | 45 | const [expected, got] = await Promise.all(x) 46 | if (expected !== got) { 47 | failed = true 48 | throw new Error(util.inspect(expected) + ' != ' + util.inspect(got)) 49 | } 50 | 51 | tests[line].succeeded = true 52 | process.stdout.write('✅') 53 | }) 54 | .catch(err => { 55 | tests[line].failed = failed = true 56 | tests[line].error = err instanceof Error ? err : new Error(util.inspect(err)) 57 | }) 58 | .then(() => { 59 | ++done === Object.keys(tests).length && exit() 60 | }) 61 | } 62 | 63 | function exit() { 64 | let success = true 65 | Object.values(tests).every((x) => { 66 | if (x.succeeded) 67 | return true 68 | 69 | success = false 70 | x.cleanup 71 | ? console.error('⛔️', x.name + ' at line', x.line, 'cleanup failed', '\n', util.inspect(x.cleanup)) 72 | : console.error('⛔️', x.name + ' at line', x.line, x.failed 73 | ? 'failed' 74 | : 'never finished', x.error ? '\n' + util.inspect(x.error) : '' 75 | ) 76 | }) 77 | 78 | only 79 | ? console.error('⚠️', 'Not all tests were run') 80 | : ignored 81 | ? console.error('⚠️', ignored, 'ignored test' + (ignored === 1 ? '' : 's', '\n')) 82 | : success 83 | ? console.log('🎉') 84 | : console.error('⚠️', 'Not good') 85 | 86 | !process.exitCode && (!success || only || ignored) && (process.exitCode = 1) 87 | } 88 | 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postgres", 3 | "version": "3.4.7", 4 | "description": "Fastest full featured PostgreSQL client for Node.js", 5 | "type": "module", 6 | "module": "src/index.js", 7 | "main": "cjs/src/index.js", 8 | "exports": { 9 | "types": "./types/index.d.ts", 10 | "bun": "./src/index.js", 11 | "workerd": "./cf/src/index.js", 12 | "import": "./src/index.js", 13 | "default": "./cjs/src/index.js" 14 | }, 15 | "types": "types/index.d.ts", 16 | "typings": "types/index.d.ts", 17 | "engines": { 18 | "node": ">=12" 19 | }, 20 | "scripts": { 21 | "build": "npm run build:cjs && npm run build:deno && npm run build:cf", 22 | "build:cjs": "node transpile.cjs", 23 | "build:deno": "node transpile.deno.js", 24 | "build:cf": "node transpile.cf.js", 25 | "test": "npm run test:esm && npm run test:cjs && npm run test:deno", 26 | "test:esm": "node tests/index.js", 27 | "test:cjs": "npm run build:cjs && cd cjs/tests && node index.js && cd ../../", 28 | "test:deno": "npm run build:deno && cd deno/tests && deno run --no-lock --allow-all --unsafely-ignore-certificate-errors index.js && cd ../../", 29 | "lint": "eslint src && eslint tests", 30 | "prepare": "npm run build", 31 | "prepublishOnly": "npm run lint" 32 | }, 33 | "files": [ 34 | "/cf/src", 35 | "/cf/polyfills.js", 36 | "/cjs/src", 37 | "/cjs/package.json", 38 | "/src", 39 | "/types" 40 | ], 41 | "author": "Rasmus Porsager (https://www.porsager.com)", 42 | "funding": { 43 | "type": "individual", 44 | "url": "https://github.com/sponsors/porsager" 45 | }, 46 | "license": "Unlicense", 47 | "repository": "porsager/postgres", 48 | "homepage": "https://github.com/porsager/postgres", 49 | "bugs": "https://github.com/porsager/postgres/issues", 50 | "keywords": [ 51 | "driver", 52 | "postgresql", 53 | "postgres.js", 54 | "postgres", 55 | "postrges", 56 | "postgre", 57 | "client", 58 | "sql", 59 | "db", 60 | "pg", 61 | "database" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /postgresjs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/bytes.js: -------------------------------------------------------------------------------- 1 | const size = 256 2 | let buffer = Buffer.allocUnsafe(size) 3 | 4 | const messages = 'BCcDdEFfHPpQSX'.split('').reduce((acc, x) => { 5 | const v = x.charCodeAt(0) 6 | acc[x] = () => { 7 | buffer[0] = v 8 | b.i = 5 9 | return b 10 | } 11 | return acc 12 | }, {}) 13 | 14 | const b = Object.assign(reset, messages, { 15 | N: String.fromCharCode(0), 16 | i: 0, 17 | inc(x) { 18 | b.i += x 19 | return b 20 | }, 21 | str(x) { 22 | const length = Buffer.byteLength(x) 23 | fit(length) 24 | b.i += buffer.write(x, b.i, length, 'utf8') 25 | return b 26 | }, 27 | i16(x) { 28 | fit(2) 29 | buffer.writeUInt16BE(x, b.i) 30 | b.i += 2 31 | return b 32 | }, 33 | i32(x, i) { 34 | if (i || i === 0) { 35 | buffer.writeUInt32BE(x, i) 36 | return b 37 | } 38 | fit(4) 39 | buffer.writeUInt32BE(x, b.i) 40 | b.i += 4 41 | return b 42 | }, 43 | z(x) { 44 | fit(x) 45 | buffer.fill(0, b.i, b.i + x) 46 | b.i += x 47 | return b 48 | }, 49 | raw(x) { 50 | buffer = Buffer.concat([buffer.subarray(0, b.i), x]) 51 | b.i = buffer.length 52 | return b 53 | }, 54 | end(at = 1) { 55 | buffer.writeUInt32BE(b.i - at, at) 56 | const out = buffer.subarray(0, b.i) 57 | b.i = 0 58 | buffer = Buffer.allocUnsafe(size) 59 | return out 60 | } 61 | }) 62 | 63 | export default b 64 | 65 | function fit(x) { 66 | if (buffer.length - b.i < x) { 67 | const prev = buffer 68 | , length = prev.length 69 | 70 | buffer = Buffer.allocUnsafe(length + (length >> 1) + x) 71 | prev.copy(buffer) 72 | } 73 | } 74 | 75 | function reset() { 76 | b.i = 0 77 | return b 78 | } 79 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export class PostgresError extends Error { 2 | constructor(x) { 3 | super(x.message) 4 | this.name = this.constructor.name 5 | Object.assign(this, x) 6 | } 7 | } 8 | 9 | export const Errors = { 10 | connection, 11 | postgres, 12 | generic, 13 | notSupported 14 | } 15 | 16 | function connection(x, options, socket) { 17 | const { host, port } = socket || options 18 | const error = Object.assign( 19 | new Error(('write ' + x + ' ' + (options.path || (host + ':' + port)))), 20 | { 21 | code: x, 22 | errno: x, 23 | address: options.path || host 24 | }, options.path ? {} : { port: port } 25 | ) 26 | Error.captureStackTrace(error, connection) 27 | return error 28 | } 29 | 30 | function postgres(x) { 31 | const error = new PostgresError(x) 32 | Error.captureStackTrace(error, postgres) 33 | return error 34 | } 35 | 36 | function generic(code, message) { 37 | const error = Object.assign(new Error(code + ': ' + message), { code }) 38 | Error.captureStackTrace(error, generic) 39 | return error 40 | } 41 | 42 | /* c8 ignore next 10 */ 43 | function notSupported(x) { 44 | const error = Object.assign( 45 | new Error(x + ' (B) is not supported'), 46 | { 47 | code: 'MESSAGE_NOT_SUPPORTED', 48 | name: x 49 | } 50 | ) 51 | Error.captureStackTrace(error, notSupported) 52 | return error 53 | } 54 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import fs from 'fs' 3 | 4 | import { 5 | mergeUserTypes, 6 | inferType, 7 | Parameter, 8 | Identifier, 9 | Builder, 10 | toPascal, 11 | pascal, 12 | toCamel, 13 | camel, 14 | toKebab, 15 | kebab, 16 | fromPascal, 17 | fromCamel, 18 | fromKebab 19 | } from './types.js' 20 | 21 | import Connection from './connection.js' 22 | import { Query, CLOSE } from './query.js' 23 | import Queue from './queue.js' 24 | import { Errors, PostgresError } from './errors.js' 25 | import Subscribe from './subscribe.js' 26 | import largeObject from './large.js' 27 | 28 | Object.assign(Postgres, { 29 | PostgresError, 30 | toPascal, 31 | pascal, 32 | toCamel, 33 | camel, 34 | toKebab, 35 | kebab, 36 | fromPascal, 37 | fromCamel, 38 | fromKebab, 39 | BigInt: { 40 | to: 20, 41 | from: [20], 42 | parse: x => BigInt(x), // eslint-disable-line 43 | serialize: x => x.toString() 44 | } 45 | }) 46 | 47 | export default Postgres 48 | 49 | function Postgres(a, b) { 50 | const options = parseOptions(a, b) 51 | , subscribe = options.no_subscribe || Subscribe(Postgres, { ...options }) 52 | 53 | let ending = false 54 | 55 | const queries = Queue() 56 | , connecting = Queue() 57 | , reserved = Queue() 58 | , closed = Queue() 59 | , ended = Queue() 60 | , open = Queue() 61 | , busy = Queue() 62 | , full = Queue() 63 | , queues = { connecting, reserved, closed, ended, open, busy, full } 64 | 65 | const connections = [...Array(options.max)].map(() => Connection(options, queues, { onopen, onend, onclose })) 66 | 67 | const sql = Sql(handler) 68 | 69 | Object.assign(sql, { 70 | get parameters() { return options.parameters }, 71 | largeObject: largeObject.bind(null, sql), 72 | subscribe, 73 | CLOSE, 74 | END: CLOSE, 75 | PostgresError, 76 | options, 77 | reserve, 78 | listen, 79 | begin, 80 | close, 81 | end 82 | }) 83 | 84 | return sql 85 | 86 | function Sql(handler) { 87 | handler.debug = options.debug 88 | 89 | Object.entries(options.types).reduce((acc, [name, type]) => { 90 | acc[name] = (x) => new Parameter(x, type.to) 91 | return acc 92 | }, typed) 93 | 94 | Object.assign(sql, { 95 | types: typed, 96 | typed, 97 | unsafe, 98 | notify, 99 | array, 100 | json, 101 | file 102 | }) 103 | 104 | return sql 105 | 106 | function typed(value, type) { 107 | return new Parameter(value, type) 108 | } 109 | 110 | function sql(strings, ...args) { 111 | const query = strings && Array.isArray(strings.raw) 112 | ? new Query(strings, args, handler, cancel) 113 | : typeof strings === 'string' && !args.length 114 | ? new Identifier(options.transform.column.to ? options.transform.column.to(strings) : strings) 115 | : new Builder(strings, args) 116 | return query 117 | } 118 | 119 | function unsafe(string, args = [], options = {}) { 120 | arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) 121 | const query = new Query([string], args, handler, cancel, { 122 | prepare: false, 123 | ...options, 124 | simple: 'simple' in options ? options.simple : args.length === 0 125 | }) 126 | return query 127 | } 128 | 129 | function file(path, args = [], options = {}) { 130 | arguments.length === 2 && !Array.isArray(args) && (options = args, args = []) 131 | const query = new Query([], args, (query) => { 132 | fs.readFile(path, 'utf8', (err, string) => { 133 | if (err) 134 | return query.reject(err) 135 | 136 | query.strings = [string] 137 | handler(query) 138 | }) 139 | }, cancel, { 140 | ...options, 141 | simple: 'simple' in options ? options.simple : args.length === 0 142 | }) 143 | return query 144 | } 145 | } 146 | 147 | async function listen(name, fn, onlisten) { 148 | const listener = { fn, onlisten } 149 | 150 | const sql = listen.sql || (listen.sql = Postgres({ 151 | ...options, 152 | max: 1, 153 | idle_timeout: null, 154 | max_lifetime: null, 155 | fetch_types: false, 156 | onclose() { 157 | Object.entries(listen.channels).forEach(([name, { listeners }]) => { 158 | delete listen.channels[name] 159 | Promise.all(listeners.map(l => listen(name, l.fn, l.onlisten).catch(() => { /* noop */ }))) 160 | }) 161 | }, 162 | onnotify(c, x) { 163 | c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x)) 164 | } 165 | })) 166 | 167 | const channels = listen.channels || (listen.channels = {}) 168 | , exists = name in channels 169 | 170 | if (exists) { 171 | channels[name].listeners.push(listener) 172 | const result = await channels[name].result 173 | listener.onlisten && listener.onlisten() 174 | return { state: result.state, unlisten } 175 | } 176 | 177 | channels[name] = { result: sql`listen ${ 178 | sql.unsafe('"' + name.replace(/"/g, '""') + '"') 179 | }`, listeners: [listener] } 180 | const result = await channels[name].result 181 | listener.onlisten && listener.onlisten() 182 | return { state: result.state, unlisten } 183 | 184 | async function unlisten() { 185 | if (name in channels === false) 186 | return 187 | 188 | channels[name].listeners = channels[name].listeners.filter(x => x !== listener) 189 | if (channels[name].listeners.length) 190 | return 191 | 192 | delete channels[name] 193 | return sql`unlisten ${ 194 | sql.unsafe('"' + name.replace(/"/g, '""') + '"') 195 | }` 196 | } 197 | } 198 | 199 | async function notify(channel, payload) { 200 | return await sql`select pg_notify(${ channel }, ${ '' + payload })` 201 | } 202 | 203 | async function reserve() { 204 | const queue = Queue() 205 | const c = open.length 206 | ? open.shift() 207 | : await new Promise((resolve, reject) => { 208 | const query = { reserve: resolve, reject } 209 | queries.push(query) 210 | closed.length && connect(closed.shift(), query) 211 | }) 212 | 213 | move(c, reserved) 214 | c.reserved = () => queue.length 215 | ? c.execute(queue.shift()) 216 | : move(c, reserved) 217 | c.reserved.release = true 218 | 219 | const sql = Sql(handler) 220 | sql.release = () => { 221 | c.reserved = null 222 | onopen(c) 223 | } 224 | 225 | return sql 226 | 227 | function handler(q) { 228 | c.queue === full 229 | ? queue.push(q) 230 | : c.execute(q) || move(c, full) 231 | } 232 | } 233 | 234 | async function begin(options, fn) { 235 | !fn && (fn = options, options = '') 236 | const queries = Queue() 237 | let savepoints = 0 238 | , connection 239 | , prepare = null 240 | 241 | try { 242 | await sql.unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), [], { onexecute }).execute() 243 | return await Promise.race([ 244 | scope(connection, fn), 245 | new Promise((_, reject) => connection.onclose = reject) 246 | ]) 247 | } catch (error) { 248 | throw error 249 | } 250 | 251 | async function scope(c, fn, name) { 252 | const sql = Sql(handler) 253 | sql.savepoint = savepoint 254 | sql.prepare = x => prepare = x.replace(/[^a-z0-9$-_. ]/gi) 255 | let uncaughtError 256 | , result 257 | 258 | name && await sql`savepoint ${ sql(name) }` 259 | try { 260 | result = await new Promise((resolve, reject) => { 261 | const x = fn(sql) 262 | Promise.resolve(Array.isArray(x) ? Promise.all(x) : x).then(resolve, reject) 263 | }) 264 | 265 | if (uncaughtError) 266 | throw uncaughtError 267 | } catch (e) { 268 | await (name 269 | ? sql`rollback to ${ sql(name) }` 270 | : sql`rollback` 271 | ) 272 | throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e 273 | } 274 | 275 | if (!name) { 276 | prepare 277 | ? await sql`prepare transaction '${ sql.unsafe(prepare) }'` 278 | : await sql`commit` 279 | } 280 | 281 | return result 282 | 283 | function savepoint(name, fn) { 284 | if (name && Array.isArray(name.raw)) 285 | return savepoint(sql => sql.apply(sql, arguments)) 286 | 287 | arguments.length === 1 && (fn = name, name = null) 288 | return scope(c, fn, 's' + savepoints++ + (name ? '_' + name : '')) 289 | } 290 | 291 | function handler(q) { 292 | q.catch(e => uncaughtError || (uncaughtError = e)) 293 | c.queue === full 294 | ? queries.push(q) 295 | : c.execute(q) || move(c, full) 296 | } 297 | } 298 | 299 | function onexecute(c) { 300 | connection = c 301 | move(c, reserved) 302 | c.reserved = () => queries.length 303 | ? c.execute(queries.shift()) 304 | : move(c, reserved) 305 | } 306 | } 307 | 308 | function move(c, queue) { 309 | c.queue.remove(c) 310 | queue.push(c) 311 | c.queue = queue 312 | queue === open 313 | ? c.idleTimer.start() 314 | : c.idleTimer.cancel() 315 | return c 316 | } 317 | 318 | function json(x) { 319 | return new Parameter(x, 3802) 320 | } 321 | 322 | function array(x, type) { 323 | if (!Array.isArray(x)) 324 | return array(Array.from(arguments)) 325 | 326 | return new Parameter(x, type || (x.length ? inferType(x) || 25 : 0), options.shared.typeArrayMap) 327 | } 328 | 329 | function handler(query) { 330 | if (ending) 331 | return query.reject(Errors.connection('CONNECTION_ENDED', options, options)) 332 | 333 | if (open.length) 334 | return go(open.shift(), query) 335 | 336 | if (closed.length) 337 | return connect(closed.shift(), query) 338 | 339 | busy.length 340 | ? go(busy.shift(), query) 341 | : queries.push(query) 342 | } 343 | 344 | function go(c, query) { 345 | return c.execute(query) 346 | ? move(c, busy) 347 | : move(c, full) 348 | } 349 | 350 | function cancel(query) { 351 | return new Promise((resolve, reject) => { 352 | query.state 353 | ? query.active 354 | ? Connection(options).cancel(query.state, resolve, reject) 355 | : query.cancelled = { resolve, reject } 356 | : ( 357 | queries.remove(query), 358 | query.cancelled = true, 359 | query.reject(Errors.generic('57014', 'canceling statement due to user request')), 360 | resolve() 361 | ) 362 | }) 363 | } 364 | 365 | async function end({ timeout = null } = {}) { 366 | if (ending) 367 | return ending 368 | 369 | await 1 370 | let timer 371 | return ending = Promise.race([ 372 | new Promise(r => timeout !== null && (timer = setTimeout(destroy, timeout * 1000, r))), 373 | Promise.all(connections.map(c => c.end()).concat( 374 | listen.sql ? listen.sql.end({ timeout: 0 }) : [], 375 | subscribe.sql ? subscribe.sql.end({ timeout: 0 }) : [] 376 | )) 377 | ]).then(() => clearTimeout(timer)) 378 | } 379 | 380 | async function close() { 381 | await Promise.all(connections.map(c => c.end())) 382 | } 383 | 384 | async function destroy(resolve) { 385 | await Promise.all(connections.map(c => c.terminate())) 386 | while (queries.length) 387 | queries.shift().reject(Errors.connection('CONNECTION_DESTROYED', options)) 388 | resolve() 389 | } 390 | 391 | function connect(c, query) { 392 | move(c, connecting) 393 | c.connect(query) 394 | return c 395 | } 396 | 397 | function onend(c) { 398 | move(c, ended) 399 | } 400 | 401 | function onopen(c) { 402 | if (queries.length === 0) 403 | return move(c, open) 404 | 405 | let max = Math.ceil(queries.length / (connecting.length + 1)) 406 | , ready = true 407 | 408 | while (ready && queries.length && max-- > 0) { 409 | const query = queries.shift() 410 | if (query.reserve) 411 | return query.reserve(c) 412 | 413 | ready = c.execute(query) 414 | } 415 | 416 | ready 417 | ? move(c, busy) 418 | : move(c, full) 419 | } 420 | 421 | function onclose(c, e) { 422 | move(c, closed) 423 | c.reserved = null 424 | c.onclose && (c.onclose(e), c.onclose = null) 425 | options.onclose && options.onclose(c.id) 426 | queries.length && connect(c, queries.shift()) 427 | } 428 | } 429 | 430 | function parseOptions(a, b) { 431 | if (a && a.shared) 432 | return a 433 | 434 | const env = process.env // eslint-disable-line 435 | , o = (!a || typeof a === 'string' ? b : a) || {} 436 | , { url, multihost } = parseUrl(a) 437 | , query = [...url.searchParams].reduce((a, [b, c]) => (a[b] = c, a), {}) 438 | , host = o.hostname || o.host || multihost || url.hostname || env.PGHOST || 'localhost' 439 | , port = o.port || url.port || env.PGPORT || 5432 440 | , user = o.user || o.username || url.username || env.PGUSERNAME || env.PGUSER || osUsername() 441 | 442 | o.no_prepare && (o.prepare = false) 443 | query.sslmode && (query.ssl = query.sslmode, delete query.sslmode) 444 | 'timeout' in o && (console.log('The timeout option is deprecated, use idle_timeout instead'), o.idle_timeout = o.timeout) // eslint-disable-line 445 | query.sslrootcert === 'system' && (query.ssl = 'verify-full') 446 | 447 | const ints = ['idle_timeout', 'connect_timeout', 'max_lifetime', 'max_pipeline', 'backoff', 'keep_alive'] 448 | const defaults = { 449 | max : 10, 450 | ssl : false, 451 | idle_timeout : null, 452 | connect_timeout : 30, 453 | max_lifetime : max_lifetime, 454 | max_pipeline : 100, 455 | backoff : backoff, 456 | keep_alive : 60, 457 | prepare : true, 458 | debug : false, 459 | fetch_types : true, 460 | publications : 'alltables', 461 | target_session_attrs: null 462 | } 463 | 464 | return { 465 | host : Array.isArray(host) ? host : host.split(',').map(x => x.split(':')[0]), 466 | port : Array.isArray(port) ? port : host.split(',').map(x => parseInt(x.split(':')[1] || port)), 467 | path : o.path || host.indexOf('/') > -1 && host + '/.s.PGSQL.' + port, 468 | database : o.database || o.db || (url.pathname || '').slice(1) || env.PGDATABASE || user, 469 | user : user, 470 | pass : o.pass || o.password || url.password || env.PGPASSWORD || '', 471 | ...Object.entries(defaults).reduce( 472 | (acc, [k, d]) => { 473 | const value = k in o ? o[k] : k in query 474 | ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k]) 475 | : env['PG' + k.toUpperCase()] || d 476 | acc[k] = typeof value === 'string' && ints.includes(k) 477 | ? +value 478 | : value 479 | return acc 480 | }, 481 | {} 482 | ), 483 | connection : { 484 | application_name: env.PGAPPNAME || 'postgres.js', 485 | ...o.connection, 486 | ...Object.entries(query).reduce((acc, [k, v]) => (k in defaults || (acc[k] = v), acc), {}) 487 | }, 488 | types : o.types || {}, 489 | target_session_attrs: tsa(o, url, env), 490 | onnotice : o.onnotice, 491 | onnotify : o.onnotify, 492 | onclose : o.onclose, 493 | onparameter : o.onparameter, 494 | socket : o.socket, 495 | transform : parseTransform(o.transform || { undefined: undefined }), 496 | parameters : {}, 497 | shared : { retries: 0, typeArrayMap: {} }, 498 | ...mergeUserTypes(o.types) 499 | } 500 | } 501 | 502 | function tsa(o, url, env) { 503 | const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS 504 | if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x)) 505 | return x 506 | 507 | throw new Error('target_session_attrs ' + x + ' is not supported') 508 | } 509 | 510 | function backoff(retries) { 511 | return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20) 512 | } 513 | 514 | function max_lifetime() { 515 | return 60 * (30 + Math.random() * 30) 516 | } 517 | 518 | function parseTransform(x) { 519 | return { 520 | undefined: x.undefined, 521 | column: { 522 | from: typeof x.column === 'function' ? x.column : x.column && x.column.from, 523 | to: x.column && x.column.to 524 | }, 525 | value: { 526 | from: typeof x.value === 'function' ? x.value : x.value && x.value.from, 527 | to: x.value && x.value.to 528 | }, 529 | row: { 530 | from: typeof x.row === 'function' ? x.row : x.row && x.row.from, 531 | to: x.row && x.row.to 532 | } 533 | } 534 | } 535 | 536 | function parseUrl(url) { 537 | if (!url || typeof url !== 'string') 538 | return { url: { searchParams: new Map() } } 539 | 540 | let host = url 541 | host = host.slice(host.indexOf('://') + 3).split(/[?/]/)[0] 542 | host = decodeURIComponent(host.slice(host.indexOf('@') + 1)) 543 | 544 | const urlObj = new URL(url.replace(host, host.split(',')[0])) 545 | 546 | return { 547 | url: { 548 | username: decodeURIComponent(urlObj.username), 549 | password: decodeURIComponent(urlObj.password), 550 | host: urlObj.host, 551 | hostname: urlObj.hostname, 552 | port: urlObj.port, 553 | pathname: urlObj.pathname, 554 | searchParams: urlObj.searchParams 555 | }, 556 | multihost: host.indexOf(',') > -1 && host 557 | } 558 | } 559 | 560 | function osUsername() { 561 | try { 562 | return os.userInfo().username // eslint-disable-line 563 | } catch (_) { 564 | return process.env.USERNAME || process.env.USER || process.env.LOGNAME // eslint-disable-line 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /src/large.js: -------------------------------------------------------------------------------- 1 | import Stream from 'stream' 2 | 3 | export default function largeObject(sql, oid, mode = 0x00020000 | 0x00040000) { 4 | return new Promise(async(resolve, reject) => { 5 | await sql.begin(async sql => { 6 | let finish 7 | !oid && ([{ oid }] = await sql`select lo_creat(-1) as oid`) 8 | const [{ fd }] = await sql`select lo_open(${ oid }, ${ mode }) as fd` 9 | 10 | const lo = { 11 | writable, 12 | readable, 13 | close : () => sql`select lo_close(${ fd })`.then(finish), 14 | tell : () => sql`select lo_tell64(${ fd })`, 15 | read : (x) => sql`select loread(${ fd }, ${ x }) as data`, 16 | write : (x) => sql`select lowrite(${ fd }, ${ x })`, 17 | truncate : (x) => sql`select lo_truncate64(${ fd }, ${ x })`, 18 | seek : (x, whence = 0) => sql`select lo_lseek64(${ fd }, ${ x }, ${ whence })`, 19 | size : () => sql` 20 | select 21 | lo_lseek64(${ fd }, location, 0) as position, 22 | seek.size 23 | from ( 24 | select 25 | lo_lseek64($1, 0, 2) as size, 26 | tell.location 27 | from (select lo_tell64($1) as location) tell 28 | ) seek 29 | ` 30 | } 31 | 32 | resolve(lo) 33 | 34 | return new Promise(async r => finish = r) 35 | 36 | async function readable({ 37 | highWaterMark = 2048 * 8, 38 | start = 0, 39 | end = Infinity 40 | } = {}) { 41 | let max = end - start 42 | start && await lo.seek(start) 43 | return new Stream.Readable({ 44 | highWaterMark, 45 | async read(size) { 46 | const l = size > max ? size - max : size 47 | max -= size 48 | const [{ data }] = await lo.read(l) 49 | this.push(data) 50 | if (data.length < size) 51 | this.push(null) 52 | } 53 | }) 54 | } 55 | 56 | async function writable({ 57 | highWaterMark = 2048 * 8, 58 | start = 0 59 | } = {}) { 60 | start && await lo.seek(start) 61 | return new Stream.Writable({ 62 | highWaterMark, 63 | write(chunk, encoding, callback) { 64 | lo.write(chunk).then(() => callback(), callback) 65 | } 66 | }) 67 | } 68 | }).catch(reject) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /src/query.js: -------------------------------------------------------------------------------- 1 | const originCache = new Map() 2 | , originStackCache = new Map() 3 | , originError = Symbol('OriginError') 4 | 5 | export const CLOSE = {} 6 | export class Query extends Promise { 7 | constructor(strings, args, handler, canceller, options = {}) { 8 | let resolve 9 | , reject 10 | 11 | super((a, b) => { 12 | resolve = a 13 | reject = b 14 | }) 15 | 16 | this.tagged = Array.isArray(strings.raw) 17 | this.strings = strings 18 | this.args = args 19 | this.handler = handler 20 | this.canceller = canceller 21 | this.options = options 22 | 23 | this.state = null 24 | this.statement = null 25 | 26 | this.resolve = x => (this.active = false, resolve(x)) 27 | this.reject = x => (this.active = false, reject(x)) 28 | 29 | this.active = false 30 | this.cancelled = null 31 | this.executed = false 32 | this.signature = '' 33 | 34 | this[originError] = this.handler.debug 35 | ? new Error() 36 | : this.tagged && cachedError(this.strings) 37 | } 38 | 39 | get origin() { 40 | return (this.handler.debug 41 | ? this[originError].stack 42 | : this.tagged && originStackCache.has(this.strings) 43 | ? originStackCache.get(this.strings) 44 | : originStackCache.set(this.strings, this[originError].stack).get(this.strings) 45 | ) || '' 46 | } 47 | 48 | static get [Symbol.species]() { 49 | return Promise 50 | } 51 | 52 | cancel() { 53 | return this.canceller && (this.canceller(this), this.canceller = null) 54 | } 55 | 56 | simple() { 57 | this.options.simple = true 58 | this.options.prepare = false 59 | return this 60 | } 61 | 62 | async readable() { 63 | this.simple() 64 | this.streaming = true 65 | return this 66 | } 67 | 68 | async writable() { 69 | this.simple() 70 | this.streaming = true 71 | return this 72 | } 73 | 74 | cursor(rows = 1, fn) { 75 | this.options.simple = false 76 | if (typeof rows === 'function') { 77 | fn = rows 78 | rows = 1 79 | } 80 | 81 | this.cursorRows = rows 82 | 83 | if (typeof fn === 'function') 84 | return (this.cursorFn = fn, this) 85 | 86 | let prev 87 | return { 88 | [Symbol.asyncIterator]: () => ({ 89 | next: () => { 90 | if (this.executed && !this.active) 91 | return { done: true } 92 | 93 | prev && prev() 94 | const promise = new Promise((resolve, reject) => { 95 | this.cursorFn = value => { 96 | resolve({ value, done: false }) 97 | return new Promise(r => prev = r) 98 | } 99 | this.resolve = () => (this.active = false, resolve({ done: true })) 100 | this.reject = x => (this.active = false, reject(x)) 101 | }) 102 | this.execute() 103 | return promise 104 | }, 105 | return() { 106 | prev && prev(CLOSE) 107 | return { done: true } 108 | } 109 | }) 110 | } 111 | } 112 | 113 | describe() { 114 | this.options.simple = false 115 | this.onlyDescribe = this.options.prepare = true 116 | return this 117 | } 118 | 119 | stream() { 120 | throw new Error('.stream has been renamed to .forEach') 121 | } 122 | 123 | forEach(fn) { 124 | this.forEachFn = fn 125 | this.handle() 126 | return this 127 | } 128 | 129 | raw() { 130 | this.isRaw = true 131 | return this 132 | } 133 | 134 | values() { 135 | this.isRaw = 'values' 136 | return this 137 | } 138 | 139 | async handle() { 140 | !this.executed && (this.executed = true) && await 1 && this.handler(this) 141 | } 142 | 143 | execute() { 144 | this.handle() 145 | return this 146 | } 147 | 148 | then() { 149 | this.handle() 150 | return super.then.apply(this, arguments) 151 | } 152 | 153 | catch() { 154 | this.handle() 155 | return super.catch.apply(this, arguments) 156 | } 157 | 158 | finally() { 159 | this.handle() 160 | return super.finally.apply(this, arguments) 161 | } 162 | } 163 | 164 | function cachedError(xs) { 165 | if (originCache.has(xs)) 166 | return originCache.get(xs) 167 | 168 | const x = Error.stackTraceLimit 169 | Error.stackTraceLimit = 4 170 | originCache.set(xs, new Error()) 171 | Error.stackTraceLimit = x 172 | return originCache.get(xs) 173 | } 174 | -------------------------------------------------------------------------------- /src/queue.js: -------------------------------------------------------------------------------- 1 | export default Queue 2 | 3 | function Queue(initial = []) { 4 | let xs = initial.slice() 5 | let index = 0 6 | 7 | return { 8 | get length() { 9 | return xs.length - index 10 | }, 11 | remove: (x) => { 12 | const index = xs.indexOf(x) 13 | return index === -1 14 | ? null 15 | : (xs.splice(index, 1), x) 16 | }, 17 | push: (x) => (xs.push(x), x), 18 | shift: () => { 19 | const out = xs[index++] 20 | 21 | if (index === xs.length) { 22 | index = 0 23 | xs = [] 24 | } else { 25 | xs[index - 1] = undefined 26 | } 27 | 28 | return out 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/result.js: -------------------------------------------------------------------------------- 1 | export default class Result extends Array { 2 | constructor() { 3 | super() 4 | Object.defineProperties(this, { 5 | count: { value: null, writable: true }, 6 | state: { value: null, writable: true }, 7 | command: { value: null, writable: true }, 8 | columns: { value: null, writable: true }, 9 | statement: { value: null, writable: true } 10 | }) 11 | } 12 | 13 | static get [Symbol.species]() { 14 | return Array 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/subscribe.js: -------------------------------------------------------------------------------- 1 | const noop = () => { /* noop */ } 2 | 3 | export default function Subscribe(postgres, options) { 4 | const subscribers = new Map() 5 | , slot = 'postgresjs_' + Math.random().toString(36).slice(2) 6 | , state = {} 7 | 8 | let connection 9 | , stream 10 | , ended = false 11 | 12 | const sql = subscribe.sql = postgres({ 13 | ...options, 14 | transform: { column: {}, value: {}, row: {} }, 15 | max: 1, 16 | fetch_types: false, 17 | idle_timeout: null, 18 | max_lifetime: null, 19 | connection: { 20 | ...options.connection, 21 | replication: 'database' 22 | }, 23 | onclose: async function() { 24 | if (ended) 25 | return 26 | stream = null 27 | state.pid = state.secret = undefined 28 | connected(await init(sql, slot, options.publications)) 29 | subscribers.forEach(event => event.forEach(({ onsubscribe }) => onsubscribe())) 30 | }, 31 | no_subscribe: true 32 | }) 33 | 34 | const end = sql.end 35 | , close = sql.close 36 | 37 | sql.end = async() => { 38 | ended = true 39 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 40 | return end() 41 | } 42 | 43 | sql.close = async() => { 44 | stream && (await new Promise(r => (stream.once('close', r), stream.end()))) 45 | return close() 46 | } 47 | 48 | return subscribe 49 | 50 | async function subscribe(event, fn, onsubscribe = noop, onerror = noop) { 51 | event = parseEvent(event) 52 | 53 | if (!connection) 54 | connection = init(sql, slot, options.publications) 55 | 56 | const subscriber = { fn, onsubscribe } 57 | const fns = subscribers.has(event) 58 | ? subscribers.get(event).add(subscriber) 59 | : subscribers.set(event, new Set([subscriber])).get(event) 60 | 61 | const unsubscribe = () => { 62 | fns.delete(subscriber) 63 | fns.size === 0 && subscribers.delete(event) 64 | } 65 | 66 | return connection.then(x => { 67 | connected(x) 68 | onsubscribe() 69 | stream && stream.on('error', onerror) 70 | return { unsubscribe, state, sql } 71 | }) 72 | } 73 | 74 | function connected(x) { 75 | stream = x.stream 76 | state.pid = x.state.pid 77 | state.secret = x.state.secret 78 | } 79 | 80 | async function init(sql, slot, publications) { 81 | if (!publications) 82 | throw new Error('Missing publication names') 83 | 84 | const xs = await sql.unsafe( 85 | `CREATE_REPLICATION_SLOT ${ slot } TEMPORARY LOGICAL pgoutput NOEXPORT_SNAPSHOT` 86 | ) 87 | 88 | const [x] = xs 89 | 90 | const stream = await sql.unsafe( 91 | `START_REPLICATION SLOT ${ slot } LOGICAL ${ 92 | x.consistent_point 93 | } (proto_version '1', publication_names '${ publications }')` 94 | ).writable() 95 | 96 | const state = { 97 | lsn: Buffer.concat(x.consistent_point.split('/').map(x => Buffer.from(('00000000' + x).slice(-8), 'hex'))) 98 | } 99 | 100 | stream.on('data', data) 101 | stream.on('error', error) 102 | stream.on('close', sql.close) 103 | 104 | return { stream, state: xs.state } 105 | 106 | function error(e) { 107 | console.error('Unexpected error during logical streaming - reconnecting', e) // eslint-disable-line 108 | } 109 | 110 | function data(x) { 111 | if (x[0] === 0x77) { 112 | parse(x.subarray(25), state, sql.options.parsers, handle, options.transform) 113 | } else if (x[0] === 0x6b && x[17]) { 114 | state.lsn = x.subarray(1, 9) 115 | pong() 116 | } 117 | } 118 | 119 | function handle(a, b) { 120 | const path = b.relation.schema + '.' + b.relation.table 121 | call('*', a, b) 122 | call('*:' + path, a, b) 123 | b.relation.keys.length && call('*:' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 124 | call(b.command, a, b) 125 | call(b.command + ':' + path, a, b) 126 | b.relation.keys.length && call(b.command + ':' + path + '=' + b.relation.keys.map(x => a[x.name]), a, b) 127 | } 128 | 129 | function pong() { 130 | const x = Buffer.alloc(34) 131 | x[0] = 'r'.charCodeAt(0) 132 | x.fill(state.lsn, 1) 133 | x.writeBigInt64BE(BigInt(Date.now() - Date.UTC(2000, 0, 1)) * BigInt(1000), 25) 134 | stream.write(x) 135 | } 136 | } 137 | 138 | function call(x, a, b) { 139 | subscribers.has(x) && subscribers.get(x).forEach(({ fn }) => fn(a, b, x)) 140 | } 141 | } 142 | 143 | function Time(x) { 144 | return new Date(Date.UTC(2000, 0, 1) + Number(x / BigInt(1000))) 145 | } 146 | 147 | function parse(x, state, parsers, handle, transform) { 148 | const char = (acc, [k, v]) => (acc[k.charCodeAt(0)] = v, acc) 149 | 150 | Object.entries({ 151 | R: x => { // Relation 152 | let i = 1 153 | const r = state[x.readUInt32BE(i)] = { 154 | schema: x.toString('utf8', i += 4, i = x.indexOf(0, i)) || 'pg_catalog', 155 | table: x.toString('utf8', i + 1, i = x.indexOf(0, i + 1)), 156 | columns: Array(x.readUInt16BE(i += 2)), 157 | keys: [] 158 | } 159 | i += 2 160 | 161 | let columnIndex = 0 162 | , column 163 | 164 | while (i < x.length) { 165 | column = r.columns[columnIndex++] = { 166 | key: x[i++], 167 | name: transform.column.from 168 | ? transform.column.from(x.toString('utf8', i, i = x.indexOf(0, i))) 169 | : x.toString('utf8', i, i = x.indexOf(0, i)), 170 | type: x.readUInt32BE(i += 1), 171 | parser: parsers[x.readUInt32BE(i)], 172 | atttypmod: x.readUInt32BE(i += 4) 173 | } 174 | 175 | column.key && r.keys.push(column) 176 | i += 4 177 | } 178 | }, 179 | Y: () => { /* noop */ }, // Type 180 | O: () => { /* noop */ }, // Origin 181 | B: x => { // Begin 182 | state.date = Time(x.readBigInt64BE(9)) 183 | state.lsn = x.subarray(1, 9) 184 | }, 185 | I: x => { // Insert 186 | let i = 1 187 | const relation = state[x.readUInt32BE(i)] 188 | const { row } = tuples(x, relation.columns, i += 7, transform) 189 | 190 | handle(row, { 191 | command: 'insert', 192 | relation 193 | }) 194 | }, 195 | D: x => { // Delete 196 | let i = 1 197 | const relation = state[x.readUInt32BE(i)] 198 | i += 4 199 | const key = x[i] === 75 200 | handle(key || x[i] === 79 201 | ? tuples(x, relation.columns, i += 3, transform).row 202 | : null 203 | , { 204 | command: 'delete', 205 | relation, 206 | key 207 | }) 208 | }, 209 | U: x => { // Update 210 | let i = 1 211 | const relation = state[x.readUInt32BE(i)] 212 | i += 4 213 | const key = x[i] === 75 214 | const xs = key || x[i] === 79 215 | ? tuples(x, relation.columns, i += 3, transform) 216 | : null 217 | 218 | xs && (i = xs.i) 219 | 220 | const { row } = tuples(x, relation.columns, i + 3, transform) 221 | 222 | handle(row, { 223 | command: 'update', 224 | relation, 225 | key, 226 | old: xs && xs.row 227 | }) 228 | }, 229 | T: () => { /* noop */ }, // Truncate, 230 | C: () => { /* noop */ } // Commit 231 | }).reduce(char, {})[x[0]](x) 232 | } 233 | 234 | function tuples(x, columns, xi, transform) { 235 | let type 236 | , column 237 | , value 238 | 239 | const row = transform.raw ? new Array(columns.length) : {} 240 | for (let i = 0; i < columns.length; i++) { 241 | type = x[xi++] 242 | column = columns[i] 243 | value = type === 110 // n 244 | ? null 245 | : type === 117 // u 246 | ? undefined 247 | : column.parser === undefined 248 | ? x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi)) 249 | : column.parser.array === true 250 | ? column.parser(x.toString('utf8', xi + 5, xi += 4 + x.readUInt32BE(xi))) 251 | : column.parser(x.toString('utf8', xi + 4, xi += 4 + x.readUInt32BE(xi))) 252 | 253 | transform.raw 254 | ? (row[i] = transform.raw === true 255 | ? value 256 | : transform.value.from ? transform.value.from(value, column) : value) 257 | : (row[column.name] = transform.value.from 258 | ? transform.value.from(value, column) 259 | : value 260 | ) 261 | } 262 | 263 | return { i: xi, row: transform.row.from ? transform.row.from(row) : row } 264 | } 265 | 266 | function parseEvent(x) { 267 | const xs = x.match(/^(\*|insert|update|delete)?:?([^.]+?\.?[^=]+)?=?(.+)?/i) || [] 268 | 269 | if (!xs) 270 | throw new Error('Malformed subscribe pattern: ' + x) 271 | 272 | const [, command, path, key] = xs 273 | 274 | return (command || '*') 275 | + (path ? ':' + (path.indexOf('.') === -1 ? 'public.' + path : path) : '') 276 | + (key ? '=' + key : '') 277 | } 278 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | import { Query } from './query.js' 2 | import { Errors } from './errors.js' 3 | 4 | export const types = { 5 | string: { 6 | to: 25, 7 | from: null, // defaults to string 8 | serialize: x => '' + x 9 | }, 10 | number: { 11 | to: 0, 12 | from: [21, 23, 26, 700, 701], 13 | serialize: x => '' + x, 14 | parse: x => +x 15 | }, 16 | json: { 17 | to: 114, 18 | from: [114, 3802], 19 | serialize: x => JSON.stringify(x), 20 | parse: x => JSON.parse(x) 21 | }, 22 | boolean: { 23 | to: 16, 24 | from: 16, 25 | serialize: x => x === true ? 't' : 'f', 26 | parse: x => x === 't' 27 | }, 28 | date: { 29 | to: 1184, 30 | from: [1082, 1114, 1184], 31 | serialize: x => (x instanceof Date ? x : new Date(x)).toISOString(), 32 | parse: x => new Date(x) 33 | }, 34 | bytea: { 35 | to: 17, 36 | from: 17, 37 | serialize: x => '\\x' + Buffer.from(x).toString('hex'), 38 | parse: x => Buffer.from(x.slice(2), 'hex') 39 | } 40 | } 41 | 42 | class NotTagged { then() { notTagged() } catch() { notTagged() } finally() { notTagged() }} 43 | 44 | export class Identifier extends NotTagged { 45 | constructor(value) { 46 | super() 47 | this.value = escapeIdentifier(value) 48 | } 49 | } 50 | 51 | export class Parameter extends NotTagged { 52 | constructor(value, type, array) { 53 | super() 54 | this.value = value 55 | this.type = type 56 | this.array = array 57 | } 58 | } 59 | 60 | export class Builder extends NotTagged { 61 | constructor(first, rest) { 62 | super() 63 | this.first = first 64 | this.rest = rest 65 | } 66 | 67 | build(before, parameters, types, options) { 68 | const keyword = builders.map(([x, fn]) => ({ fn, i: before.search(x) })).sort((a, b) => a.i - b.i).pop() 69 | return keyword.i === -1 70 | ? escapeIdentifiers(this.first, options) 71 | : keyword.fn(this.first, this.rest, parameters, types, options) 72 | } 73 | } 74 | 75 | export function handleValue(x, parameters, types, options) { 76 | let value = x instanceof Parameter ? x.value : x 77 | if (value === undefined) { 78 | x instanceof Parameter 79 | ? x.value = options.transform.undefined 80 | : value = x = options.transform.undefined 81 | 82 | if (value === undefined) 83 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 84 | } 85 | 86 | return '$' + (types.push( 87 | x instanceof Parameter 88 | ? (parameters.push(x.value), x.array 89 | ? x.array[x.type || inferType(x.value)] || x.type || firstIsString(x.value) 90 | : x.type 91 | ) 92 | : (parameters.push(x), inferType(x)) 93 | )) 94 | } 95 | 96 | const defaultHandlers = typeHandlers(types) 97 | 98 | export function stringify(q, string, value, parameters, types, options) { // eslint-disable-line 99 | for (let i = 1; i < q.strings.length; i++) { 100 | string += (stringifyValue(string, value, parameters, types, options)) + q.strings[i] 101 | value = q.args[i] 102 | } 103 | 104 | return string 105 | } 106 | 107 | function stringifyValue(string, value, parameters, types, o) { 108 | return ( 109 | value instanceof Builder ? value.build(string, parameters, types, o) : 110 | value instanceof Query ? fragment(value, parameters, types, o) : 111 | value instanceof Identifier ? value.value : 112 | value && value[0] instanceof Query ? value.reduce((acc, x) => acc + ' ' + fragment(x, parameters, types, o), '') : 113 | handleValue(value, parameters, types, o) 114 | ) 115 | } 116 | 117 | function fragment(q, parameters, types, options) { 118 | q.fragment = true 119 | return stringify(q, q.strings[0], q.args[0], parameters, types, options) 120 | } 121 | 122 | function valuesBuilder(first, parameters, types, columns, options) { 123 | return first.map(row => 124 | '(' + columns.map(column => 125 | stringifyValue('values', row[column], parameters, types, options) 126 | ).join(',') + ')' 127 | ).join(',') 128 | } 129 | 130 | function values(first, rest, parameters, types, options) { 131 | const multi = Array.isArray(first[0]) 132 | const columns = rest.length ? rest.flat() : Object.keys(multi ? first[0] : first) 133 | return valuesBuilder(multi ? first : [first], parameters, types, columns, options) 134 | } 135 | 136 | function select(first, rest, parameters, types, options) { 137 | typeof first === 'string' && (first = [first].concat(rest)) 138 | if (Array.isArray(first)) 139 | return escapeIdentifiers(first, options) 140 | 141 | let value 142 | const columns = rest.length ? rest.flat() : Object.keys(first) 143 | return columns.map(x => { 144 | value = first[x] 145 | return ( 146 | value instanceof Query ? fragment(value, parameters, types, options) : 147 | value instanceof Identifier ? value.value : 148 | handleValue(value, parameters, types, options) 149 | ) + ' as ' + escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) 150 | }).join(',') 151 | } 152 | 153 | const builders = Object.entries({ 154 | values, 155 | in: (...xs) => { 156 | const x = values(...xs) 157 | return x === '()' ? '(null)' : x 158 | }, 159 | select, 160 | as: select, 161 | returning: select, 162 | '\\(': select, 163 | 164 | update(first, rest, parameters, types, options) { 165 | return (rest.length ? rest.flat() : Object.keys(first)).map(x => 166 | escapeIdentifier(options.transform.column.to ? options.transform.column.to(x) : x) + 167 | '=' + stringifyValue('values', first[x], parameters, types, options) 168 | ) 169 | }, 170 | 171 | insert(first, rest, parameters, types, options) { 172 | const columns = rest.length ? rest.flat() : Object.keys(Array.isArray(first) ? first[0] : first) 173 | return '(' + escapeIdentifiers(columns, options) + ')values' + 174 | valuesBuilder(Array.isArray(first) ? first : [first], parameters, types, columns, options) 175 | } 176 | }).map(([x, fn]) => ([new RegExp('((?:^|[\\s(])' + x + '(?:$|[\\s(]))(?![\\s\\S]*\\1)', 'i'), fn])) 177 | 178 | function notTagged() { 179 | throw Errors.generic('NOT_TAGGED_CALL', 'Query not called as a tagged template literal') 180 | } 181 | 182 | export const serializers = defaultHandlers.serializers 183 | export const parsers = defaultHandlers.parsers 184 | 185 | export const END = {} 186 | 187 | function firstIsString(x) { 188 | if (Array.isArray(x)) 189 | return firstIsString(x[0]) 190 | return typeof x === 'string' ? 1009 : 0 191 | } 192 | 193 | export const mergeUserTypes = function(types) { 194 | const user = typeHandlers(types || {}) 195 | return { 196 | serializers: Object.assign({}, serializers, user.serializers), 197 | parsers: Object.assign({}, parsers, user.parsers) 198 | } 199 | } 200 | 201 | function typeHandlers(types) { 202 | return Object.keys(types).reduce((acc, k) => { 203 | types[k].from && [].concat(types[k].from).forEach(x => acc.parsers[x] = types[k].parse) 204 | if (types[k].serialize) { 205 | acc.serializers[types[k].to] = types[k].serialize 206 | types[k].from && [].concat(types[k].from).forEach(x => acc.serializers[x] = types[k].serialize) 207 | } 208 | return acc 209 | }, { parsers: {}, serializers: {} }) 210 | } 211 | 212 | function escapeIdentifiers(xs, { transform: { column } }) { 213 | return xs.map(x => escapeIdentifier(column.to ? column.to(x) : x)).join(',') 214 | } 215 | 216 | export const escapeIdentifier = function escape(str) { 217 | return '"' + str.replace(/"/g, '""').replace(/\./g, '"."') + '"' 218 | } 219 | 220 | export const inferType = function inferType(x) { 221 | return ( 222 | x instanceof Parameter ? x.type : 223 | x instanceof Date ? 1184 : 224 | x instanceof Uint8Array ? 17 : 225 | (x === true || x === false) ? 16 : 226 | typeof x === 'bigint' ? 20 : 227 | Array.isArray(x) ? inferType(x[0]) : 228 | 0 229 | ) 230 | } 231 | 232 | const escapeBackslash = /\\/g 233 | const escapeQuote = /"/g 234 | 235 | function arrayEscape(x) { 236 | return x 237 | .replace(escapeBackslash, '\\\\') 238 | .replace(escapeQuote, '\\"') 239 | } 240 | 241 | export const arraySerializer = function arraySerializer(xs, serializer, options, typarray) { 242 | if (Array.isArray(xs) === false) 243 | return xs 244 | 245 | if (!xs.length) 246 | return '{}' 247 | 248 | const first = xs[0] 249 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 250 | const delimiter = typarray === 1020 ? ';' : ',' 251 | 252 | if (Array.isArray(first) && !first.type) 253 | return '{' + xs.map(x => arraySerializer(x, serializer, options, typarray)).join(delimiter) + '}' 254 | 255 | return '{' + xs.map(x => { 256 | if (x === undefined) { 257 | x = options.transform.undefined 258 | if (x === undefined) 259 | throw Errors.generic('UNDEFINED_VALUE', 'Undefined values are not allowed') 260 | } 261 | 262 | return x === null 263 | ? 'null' 264 | : '"' + arrayEscape(serializer ? serializer(x.type ? x.value : x) : '' + x) + '"' 265 | }).join(delimiter) + '}' 266 | } 267 | 268 | const arrayParserState = { 269 | i: 0, 270 | char: null, 271 | str: '', 272 | quoted: false, 273 | last: 0 274 | } 275 | 276 | export const arrayParser = function arrayParser(x, parser, typarray) { 277 | arrayParserState.i = arrayParserState.last = 0 278 | return arrayParserLoop(arrayParserState, x, parser, typarray) 279 | } 280 | 281 | function arrayParserLoop(s, x, parser, typarray) { 282 | const xs = [] 283 | // Only _box (1020) has the ';' delimiter for arrays, all other types use the ',' delimiter 284 | const delimiter = typarray === 1020 ? ';' : ',' 285 | for (; s.i < x.length; s.i++) { 286 | s.char = x[s.i] 287 | if (s.quoted) { 288 | if (s.char === '\\') { 289 | s.str += x[++s.i] 290 | } else if (s.char === '"') { 291 | xs.push(parser ? parser(s.str) : s.str) 292 | s.str = '' 293 | s.quoted = x[s.i + 1] === '"' 294 | s.last = s.i + 2 295 | } else { 296 | s.str += s.char 297 | } 298 | } else if (s.char === '"') { 299 | s.quoted = true 300 | } else if (s.char === '{') { 301 | s.last = ++s.i 302 | xs.push(arrayParserLoop(s, x, parser, typarray)) 303 | } else if (s.char === '}') { 304 | s.quoted = false 305 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 306 | s.last = s.i + 1 307 | break 308 | } else if (s.char === delimiter && s.p !== '}' && s.p !== '"') { 309 | xs.push(parser ? parser(x.slice(s.last, s.i)) : x.slice(s.last, s.i)) 310 | s.last = s.i + 1 311 | } 312 | s.p = s.char 313 | } 314 | s.last < s.i && xs.push(parser ? parser(x.slice(s.last, s.i + 1)) : x.slice(s.last, s.i + 1)) 315 | return xs 316 | } 317 | 318 | export const toCamel = x => { 319 | let str = x[0] 320 | for (let i = 1; i < x.length; i++) 321 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 322 | return str 323 | } 324 | 325 | export const toPascal = x => { 326 | let str = x[0].toUpperCase() 327 | for (let i = 1; i < x.length; i++) 328 | str += x[i] === '_' ? x[++i].toUpperCase() : x[i] 329 | return str 330 | } 331 | 332 | export const toKebab = x => x.replace(/_/g, '-') 333 | 334 | export const fromCamel = x => x.replace(/([A-Z])/g, '_$1').toLowerCase() 335 | export const fromPascal = x => (x.slice(0, 1) + x.slice(1).replace(/([A-Z])/g, '_$1')).toLowerCase() 336 | export const fromKebab = x => x.replace(/-/g, '_') 337 | 338 | function createJsonTransform(fn) { 339 | return function jsonTransform(x, column) { 340 | return typeof x === 'object' && x !== null && (column.type === 114 || column.type === 3802) 341 | ? Array.isArray(x) 342 | ? x.map(x => jsonTransform(x, column)) 343 | : Object.entries(x).reduce((acc, [k, v]) => Object.assign(acc, { [fn(k)]: jsonTransform(v, column) }), {}) 344 | : x 345 | } 346 | } 347 | 348 | toCamel.column = { from: toCamel } 349 | toCamel.value = { from: createJsonTransform(toCamel) } 350 | fromCamel.column = { to: fromCamel } 351 | 352 | export const camel = { ...toCamel } 353 | camel.column.to = fromCamel 354 | 355 | toPascal.column = { from: toPascal } 356 | toPascal.value = { from: createJsonTransform(toPascal) } 357 | fromPascal.column = { to: fromPascal } 358 | 359 | export const pascal = { ...toPascal } 360 | pascal.column.to = fromPascal 361 | 362 | toKebab.column = { from: toKebab } 363 | toKebab.value = { from: createJsonTransform(toKebab) } 364 | fromKebab.column = { to: fromKebab } 365 | 366 | export const kebab = { ...toKebab } 367 | kebab.column.to = fromKebab 368 | -------------------------------------------------------------------------------- /tests/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { spawnSync } from 'child_process' 2 | 3 | exec('dropdb', ['postgres_js_test']) 4 | 5 | exec('psql', ['-c', 'alter system set ssl=on']) 6 | exec('psql', ['-c', 'drop user postgres_js_test']) 7 | exec('psql', ['-c', 'create user postgres_js_test']) 8 | exec('psql', ['-c', 'alter system set password_encryption=md5']) 9 | exec('psql', ['-c', 'select pg_reload_conf()']) 10 | exec('psql', ['-c', 'drop user if exists postgres_js_test_md5']) 11 | exec('psql', ['-c', 'create user postgres_js_test_md5 with password \'postgres_js_test_md5\'']) 12 | exec('psql', ['-c', 'alter system set password_encryption=\'scram-sha-256\'']) 13 | exec('psql', ['-c', 'select pg_reload_conf()']) 14 | exec('psql', ['-c', 'drop user if exists postgres_js_test_scram']) 15 | exec('psql', ['-c', 'create user postgres_js_test_scram with password \'postgres_js_test_scram\'']) 16 | 17 | exec('createdb', ['postgres_js_test']) 18 | exec('psql', ['-c', 'grant all on database postgres_js_test to postgres_js_test']) 19 | exec('psql', ['-c', 'alter database postgres_js_test owner to postgres_js_test']) 20 | 21 | export function exec(cmd, args) { 22 | const { stderr } = spawnSync(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) 23 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 24 | throw stderr 25 | } 26 | 27 | async function execAsync(cmd, args) { // eslint-disable-line 28 | let stderr = '' 29 | const cp = await spawn(cmd, args, { stdio: 'pipe', encoding: 'utf8' }) // eslint-disable-line 30 | cp.stderr.on('data', x => stderr += x) 31 | await new Promise(x => cp.on('exit', x)) 32 | if (stderr && !stderr.includes('already exists') && !stderr.includes('does not exist')) 33 | throw new Error(stderr) 34 | } 35 | -------------------------------------------------------------------------------- /tests/copy.csv: -------------------------------------------------------------------------------- 1 | 1 2 3 2 | 4 5 6 3 | -------------------------------------------------------------------------------- /tests/pg_hba.conf: -------------------------------------------------------------------------------- 1 | local all all trust 2 | host all postgres samehost trust 3 | host postgres_js_test postgres_js_test samehost trust 4 | host postgres_js_test postgres_js_test_md5 samehost md5 5 | host postgres_js_test postgres_js_test_scram samehost scram-sha-256 6 | -------------------------------------------------------------------------------- /tests/select-param.sql: -------------------------------------------------------------------------------- 1 | select $1 as x 2 | -------------------------------------------------------------------------------- /tests/select.sql: -------------------------------------------------------------------------------- 1 | select 1 as x 2 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | import util from 'util' 4 | 5 | let done = 0 6 | let only = false 7 | let ignored = 0 8 | let failed = false 9 | let promise = Promise.resolve() 10 | const tests = {} 11 | , ignore = {} 12 | 13 | export const nt = () => ignored++ 14 | export const ot = (...rest) => (only = true, test(true, ...rest)) 15 | export const t = (...rest) => test(false, ...rest) 16 | t.timeout = 5 17 | 18 | async function test(o, name, options, fn) { 19 | typeof options !== 'object' && (fn = options, options = {}) 20 | const line = new Error().stack.split('\n')[3].match(':([0-9]+):')[1] 21 | 22 | await 1 23 | 24 | if (only && !o) 25 | return 26 | 27 | tests[line] = { fn, line, name } 28 | promise = promise.then(() => Promise.race([ 29 | new Promise((resolve, reject) => 30 | fn.timer = setTimeout(() => reject('Timed out'), (options.timeout || t.timeout) * 1000) 31 | ), 32 | failed 33 | ? (ignored++, ignore) 34 | : fn() 35 | ])) 36 | .then(async x => { 37 | clearTimeout(fn.timer) 38 | if (x === ignore) 39 | return 40 | 41 | if (!Array.isArray(x)) 42 | throw new Error('Test should return result array') 43 | 44 | const [expected, got] = await Promise.all(x) 45 | if (expected !== got) { 46 | failed = true 47 | throw new Error(util.inspect(expected) + ' != ' + util.inspect(got)) 48 | } 49 | 50 | tests[line].succeeded = true 51 | process.stdout.write('✅') 52 | }) 53 | .catch(err => { 54 | tests[line].failed = failed = true 55 | tests[line].error = err instanceof Error ? err : new Error(util.inspect(err)) 56 | }) 57 | .then(() => { 58 | ++done === Object.keys(tests).length && exit() 59 | }) 60 | } 61 | 62 | function exit() { 63 | let success = true 64 | Object.values(tests).every((x) => { 65 | if (x.succeeded) 66 | return true 67 | 68 | success = false 69 | x.cleanup 70 | ? console.error('⛔️', x.name + ' at line', x.line, 'cleanup failed', '\n', util.inspect(x.cleanup)) 71 | : console.error('⛔️', x.name + ' at line', x.line, x.failed 72 | ? 'failed' 73 | : 'never finished', x.error ? '\n' + util.inspect(x.error) : '' 74 | ) 75 | }) 76 | 77 | only 78 | ? console.error('⚠️', 'Not all tests were run') 79 | : ignored 80 | ? console.error('⚠️', ignored, 'ignored test' + (ignored === 1 ? '' : 's', '\n')) 81 | : success 82 | ? console.log('🎉') 83 | : console.error('⚠️', 'Not good') 84 | 85 | !process.exitCode && (!success || only || ignored) && (process.exitCode = 1) 86 | } 87 | 88 | -------------------------------------------------------------------------------- /transpile.cf.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const empty = x => fs.readdirSync(x).forEach(f => fs.unlinkSync(path.join(x, f))) 5 | , ensureEmpty = x => !fs.existsSync(x) ? fs.mkdirSync(x) : empty(x) 6 | , root = 'cf' 7 | , src = path.join(root, 'src') 8 | 9 | ensureEmpty(src) 10 | 11 | fs.readdirSync('src').forEach(name => 12 | fs.writeFileSync( 13 | path.join(src, name), 14 | transpile(fs.readFileSync(path.join('src', name), 'utf8'), name, 'src') 15 | ) 16 | ) 17 | 18 | function transpile(x) { 19 | const timers = x.includes('setImmediate') 20 | ? 'import { setImmediate, clearImmediate } from \'../polyfills.js\'\n' 21 | : '' 22 | 23 | const process = x.includes('process.') 24 | ? 'import { process } from \'../polyfills.js\'\n' 25 | : '' 26 | 27 | const buffer = x.includes('Buffer') 28 | ? 'import { Buffer } from \'node:buffer\'\n' 29 | : '' 30 | 31 | return process + buffer + timers + x 32 | .replace('import net from \'net\'', 'import { net } from \'../polyfills.js\'') 33 | .replace('import tls from \'tls\'', 'import { tls } from \'../polyfills.js\'') 34 | .replace('import crypto from \'crypto\'', 'import { crypto } from \'../polyfills.js\'') 35 | .replace('import os from \'os\'', 'import { os } from \'../polyfills.js\'') 36 | .replace('import fs from \'fs\'', 'import { fs } from \'../polyfills.js\'') 37 | .replace('import { performance } from \'perf_hooks\'', 'import { performance } from \'../polyfills.js\'') 38 | .replace(/ from '([a-z_]+)'/g, ' from \'node:$1\'') 39 | } 40 | -------------------------------------------------------------------------------- /transpile.cjs: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | , path = require('path') 3 | 4 | const empty = x => fs.readdirSync(x).forEach(f => fs.unlinkSync(path.join(x, f))) 5 | , ensureEmpty = x => !fs.existsSync(x) ? fs.mkdirSync(x) : empty(x) 6 | , root = 'cjs' 7 | , src = path.join(root, 'src') 8 | , tests = path.join(root, 'tests') 9 | 10 | !fs.existsSync(root) && fs.mkdirSync(root) 11 | ensureEmpty(src) 12 | ensureEmpty(tests) 13 | 14 | fs.readdirSync('src').forEach(name => 15 | fs.writeFileSync( 16 | path.join(src, name), 17 | transpile(fs.readFileSync(path.join('src', name), 'utf8')) 18 | ) 19 | ) 20 | 21 | fs.readdirSync('tests').forEach(name => 22 | fs.writeFileSync( 23 | path.join(tests, name), 24 | name.endsWith('.js') 25 | ? transpile(fs.readFileSync(path.join('tests', name), 'utf8')) 26 | : fs.readFileSync(path.join('tests', name), 'utf8') 27 | ) 28 | ) 29 | 30 | fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ type: 'commonjs' })) 31 | 32 | function transpile(x) { 33 | return x.replace(/export default function ([^(]+)/, 'module.exports = $1;function $1') 34 | .replace(/export class ([a-z0-9_$]+)/gi, 'const $1 = module.exports.$1 = class $1') 35 | .replace(/export default /, 'module.exports = ') 36 | .replace(/export {/g, 'module.exports = {') 37 | .replace(/export const ([a-z0-9_$]+)/gi, 'const $1 = module.exports.$1') 38 | .replace(/export function ([a-z0-9_$]+)/gi, 'module.exports.$1 = $1;function $1') 39 | .replace(/import {([^{}]*?)} from (['"].*?['"])/gi, 'const {$1} = require($2)') 40 | .replace(/import (.*?) from (['"].*?['"])/gi, 'const $1 = require($2)') 41 | .replace(/import (['"].*?['"])/gi, 'require($1)') 42 | .replace('new URL(x, import.meta.url)', 'require("path").join(__dirname, x)') 43 | } 44 | -------------------------------------------------------------------------------- /transpile.deno.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | const std = 'https://deno.land/std@0.132.0/' 5 | , empty = x => fs.readdirSync(x).forEach(f => fs.unlinkSync(path.join(x, f))) 6 | , ensureEmpty = x => !fs.existsSync(x) ? fs.mkdirSync(x) : empty(x) 7 | , root = 'deno' 8 | , src = path.join(root, 'src') 9 | , types = path.join(root, 'types') 10 | , tests = path.join(root, 'tests') 11 | 12 | ensureEmpty(src) 13 | ensureEmpty(types) 14 | ensureEmpty(tests) 15 | 16 | fs.writeFileSync( 17 | path.join(types, 'index.d.ts'), 18 | transpile(fs.readFileSync(path.join('types', 'index.d.ts'), 'utf8'), 'index.d.ts', 'types') 19 | ) 20 | 21 | fs.writeFileSync( 22 | path.join(root, 'README.md'), 23 | fs.readFileSync('README.md', 'utf8') 24 | .replace(/### Installation(\n.*){4}/, '') 25 | .replace( 26 | 'import postgres from \'postgres\'', 27 | 'import postgres from \'https://deno.land/x/postgresjs/mod.js\'' 28 | ) 29 | ) 30 | 31 | fs.readdirSync('src').forEach(name => 32 | fs.writeFileSync( 33 | path.join(src, name), 34 | transpile(fs.readFileSync(path.join('src', name), 'utf8'), name, 'src') 35 | ) 36 | ) 37 | 38 | fs.readdirSync('tests').forEach(name => 39 | fs.writeFileSync( 40 | path.join(tests, name), 41 | name.endsWith('.js') 42 | ? transpile(fs.readFileSync(path.join('tests', name), 'utf8'), name, 'tests') 43 | : fs.readFileSync(path.join('tests', name), 'utf8') 44 | ) 45 | ) 46 | 47 | fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ type: 'commonjs' })) 48 | 49 | function transpile(x, name, folder) { 50 | if (folder === 'tests') { 51 | if (name === 'bootstrap.js') { 52 | x = x.replace('export function exec(', 'function ignore(') 53 | .replace('async function execAsync(', 'export async function exec(') 54 | .replace(/\nexec\(/g, '\nawait exec(') 55 | .replace('{ spawnSync }', '{ spawn }') 56 | } 57 | if (name === 'index.js') 58 | x += '\n;globalThis.addEventListener("unload", () => Deno.exit(process.exitCode))' 59 | } 60 | 61 | const buffer = x.includes('Buffer') 62 | ? 'import { Buffer } from \'' + std + 'node/buffer.ts\'\n' 63 | : '' 64 | 65 | const process = x.includes('process.') 66 | ? 'import process from \'' + std + 'node/process.ts\'\n' 67 | : '' 68 | 69 | const timers = x.includes('setImmediate') 70 | ? 'import { setImmediate, clearImmediate } from \'../polyfills.js\'\n' 71 | : '' 72 | 73 | const hmac = x.includes('createHmac') 74 | ? 'import { HmacSha256 } from \'' + std + 'hash/sha256.ts\'\n' 75 | : '' 76 | 77 | return hmac + buffer + process + timers + x 78 | .replace( 79 | 'crypto.createHmac(\'sha256\', key).update(x).digest()', 80 | 'Buffer.from(new HmacSha256(key).update(x).digest())' 81 | ) 82 | .replace( 83 | 'query.writable.push({ chunk, callback })', 84 | '(query.writable.push({ chunk }), callback())' 85 | ) 86 | .replace('socket.setKeepAlive(true, 1000 * keep_alive)', 'socket.setKeepAlive(true)') 87 | .replace('node:stream', std + 'node/stream.ts') 88 | .replace('import net from \'net\'', 'import { net } from \'../polyfills.js\'') 89 | .replace('import tls from \'tls\'', 'import { tls } from \'../polyfills.js\'') 90 | .replace('import { performance } from \'perf_hooks\'', '') 91 | .replace(/ from '([a-z_]+)'/g, ' from \'' + std + 'node/$1.ts\'') 92 | } 93 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/node": "^16" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2015" 5 | ], 6 | "types": [ 7 | "node" 8 | ], 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "exactOptionalPropertyTypes": true 13 | } 14 | } --------------------------------------------------------------------------------