├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── README.md ├── admin.js ├── index.js ├── package-lock.json ├── package.json ├── test └── util.js ├── util.js └── wireprotocol.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 2017, 9 | }, 10 | "rules": { 11 | "no-console": "off", 12 | "indent": [ 13 | "error", 14 | 2, 15 | { "SwitchCase": 1 } 16 | ], 17 | "linebreak-style": [ 18 | "error", 19 | "unix" 20 | ], 21 | "quotes": [ 22 | "error", 23 | "single" 24 | ], 25 | "semi": [ 26 | "error", 27 | "never" 28 | ] 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .idea -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.4.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | pgmongo logo 3 |

Replace MongoDB with PosgreSQL

4 |

5 | 6 | ## What is pgmongo? 7 | - **Drop-in replacement** Applications do not need code changes. pgmongo appears to your app as a MongoDB server. 8 | - **Stateless proxy** pgmongo rewrites queries and proxies them to a Postgres database. 9 | - **JSON** Primarily supports regular JSON data. Advanced BSON types like binary data, JavaSrcipt, ints and timestamps are not well supported. 10 | 11 | This implements the [MongoDB wire protocol](https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/) and adapts queries to work with a PostgreSQL database using jsonb fields. 12 | I've tested it with [Keystone.js](http://keystonejs.com/) and it seemed to work reasonably well. 13 | 14 |

15 | data flow diagram converting bson to jsonb 16 |

17 | 18 | 19 | ## Getting Started 20 | pgmongo requires node 8 or newer and Postgres 9.4+. Then run the following. 21 | ```bash 22 | npm install -g pgmongo 23 | pgmongo 24 | ``` 25 | This will start a mongo-like server on port 27017. If you already have mongo running on your machine you can start it on a different port with the following. 26 | ```bash 27 | pgmongo localhost 27018 28 | ``` 29 | 30 | ## Supported Features 31 | * listing/creating/dropping collections 32 | * find (including sorting, skip and offset) 33 | * count, distinct 34 | * update (including support for upserting) 35 | * insert (including batch insertion) 36 | * deletion 37 | * creating and listing basic indexes 38 | * most custom parameters like $gt, $exists, $regex, $push, $set, $unset, etc. 39 | See [this repo](https://github.com/thomas4019/mongo-query-to-postgres-jsonb) for the full list 40 | * admin commands (returns mostly stubbed/fake data) 41 | 42 | # Current status 43 | It's not production ready yet, but definitely working enough to play around with or use in basic apps. 44 | Currently passes 190 of the 916 core mongo [jstests](https://github.com/mongodb/mongo/tree/master/jstests/core). 45 | 46 | ## Example Query Conversions 47 | ``` 48 | db.createCollection('users') -> CREATE TABLE IF NOT EXISTS "users" (data jsonb) 49 | db.users.find({ lastLogin: { $lte: '2016' } }) -> SELECT data FROM "users" WHERE data->>'lastLogin'<='2016' 50 | db.users.update({}, { $set: { active: true } }) -> UPDATE "users" SET data = jsonb_set(data,'{active}','true'::jsonb) 51 | db.users.find({}, { firstName: 1 } ) -> SELECT jsonb_build_object('firstName', data->'firstName', '_id', data->'_id') as data FROM "users" 52 | db.blogs.insert({ title: 'first post', comments: [] }) -> INSERT INTO "blogs" VALUES ('{"_id":"5b45b641eb4bd93896d57888","title":"first post","comments":[]}'::jsonb) 53 | db.blogs.remove({ 'state.trash': true }) -> DELETE FROM "blogs" WHERE data->'state'->'trash'='true'::jsonb 54 | ``` 55 | 56 | ## Missing Features / Roadmap (ordered by priority) 57 | Note: contributions/PRs are very much welcome. 58 | * Support for findandmodify 59 | * Better for queries matching array elements 60 | * Preserve BSON (Dates, ObjectIDs, other than _id) 61 | * Cursors (currently all data is returned in first result) 62 | * Better Indexes support (not sure if compound indexes are possible) 63 | * [min](https://docs.mongodb.com/manual/reference/method/cursor.min/) and max 64 | * Support numeric object keys (currently numbers are assumed to be an array index) 65 | * Capped collections 66 | * geo support 67 | * explain queries 68 | * aggregation framework/map reduce queries 69 | 70 | ### Likely Cannot support 71 | * NaN and Infinity 72 | * Preserve the initial order of object keys 73 | * $eval and $where 74 | 75 | ## Resources 76 | * [mongo-query-to-postgres-jsonb](https://github.com/thomas4019/mongo-query-to-postgres-jsonb) (heavily used as a dependency) 77 | * [Mongo Admin Commands](https://docs.mongodb.com/manual/reference/command/nav-administration/) 78 | * [JSONB Index Performance](http://bitnine.net/blog-postgresql/postgresql-internals-jsonb-type-and-its-indexes/) 79 | 80 | ## Want to contribute? 81 | Anyone can help make this project better. Feel free to open an issue to discuss what you want to work on. 82 | 83 | ## Related Projects 84 | [Mongres](https://github.com/umitanuki/mongres), 85 | [mongolike](https://github.com/JerrySievert/mongolike), 86 | [plv8](https://github.com/plv8/plv8), 87 | [ToroDB](https://news.ycombinator.com/item?id=8527013) 88 | -------------------------------------------------------------------------------- /admin.js: -------------------------------------------------------------------------------- 1 | function getIsMasterReply() { 2 | return { 3 | 'ismaster': true, 4 | 'maxBsonObjectSize': 16777216, 5 | 'maxMessageSizeBytes': 48000000, 6 | 'maxWriteBatchSize': 1000, 7 | 'localTime': new Date('2018-06-01T07:08:25.204Z'), 8 | 'maxWireVersion': 5, 9 | 'minWireVersion': 0, 10 | 'readOnly': false, 11 | 'ok': 1 12 | } 13 | } 14 | 15 | function getBuildInfo() { 16 | return { 17 | 'version': '3.4.9', 18 | 'modules': [], 19 | 'allocator': 'system', 20 | 'javascriptEngine': 'mozjs', 21 | 'sysInfo': 'deprecated', 22 | 'versionArray': [3, 4, 9, 0], 23 | 'openssl': { 'running': 'OpenSSL 1.0.2n 7 Dec 2017', 'compiled': 'OpenSSL 1.0.2l 25 May 2017' }, 24 | 'buildEnvironment': { 25 | 'distmod': '', 26 | 'distarch': 'x86_64', 27 | 'cc': '/usr/bin/clang: Apple LLVM version 8.1.0 (clang-802.0.42)', 28 | 'ccflags': '-I/usr/local/opt/openssl/include -fno-omit-frame-pointer -fno-strict-aliasing -ggdb -pthread -Wall -Wsign-compare -Wno-unknown-pragmas -Winvalid-pch -O2 -Wno-unused-local-typedefs -Wno-unused-function -Wno-unused-private-field -Wno-deprecated-declarations -Wno-tautological-constant-out-of-range-compare -Wno-unused-const-variable -Wno-missing-braces -Wno-inconsistent-missing-override -Wno-potentially-evaluated-expression -fstack-protector-strong -Wno-null-conversion -mmacosx-version-min=10.12 -fno-builtin-memcmp', 29 | 'cxx': '/usr/bin/clang++: Apple LLVM version 8.1.0 (clang-802.0.42)', 30 | 'cxxflags': '-Woverloaded-virtual -Wpessimizing-move -Wredundant-move -Wno-undefined-var-template -std=c++11', 31 | 'linkflags': '-L/usr/local/opt/openssl/lib -pthread -Wl,-bind_at_load -fstack-protector-strong -mmacosx-version-min=10.12', 32 | 'target_arch': 'x86_64', 33 | 'target_os': 'osx' 34 | }, 35 | 'bits': 64, 36 | 'debug': false, 37 | 'maxBsonObjectSize': 16777216, 38 | 'storageEngines': ['devnull', 'ephemeralForTest', 'mmapv1', 'wiredTiger'], 39 | 'ok': 1 40 | } 41 | } 42 | 43 | function getServerStatus() { 44 | return { 45 | 'host' : 'Thomass-MBP.hsd1.ca.comcast.net', 46 | 'version' : '3.4.9', 47 | 'process' : 'mongod', 48 | 'pid' : 70211, 49 | 'uptime' : 1835, 50 | 'uptimeMillis' : 1834724, 51 | 'uptimeEstimate' : 1834, 52 | 'localTime' : new Date('2018-06-05T07:44:43.503Z') 53 | } 54 | } 55 | 56 | function replSetGetStatus() { 57 | return { 58 | 'ok': 0, 59 | 'errmsg': 'not running with --replSet', 60 | 'code': 76, 61 | 'codeName': 'NoReplicationEnabled' 62 | } 63 | } 64 | 65 | exports = module.exports = { 66 | getIsMasterReply, 67 | getBuildInfo, 68 | getServerStatus, 69 | replSetGetStatus 70 | } 71 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const net = require('net') 3 | const BSON = require('bson-ext') 4 | const mongoToPostgres = require('mongo-query-to-postgres-jsonb') 5 | const debug = require('debug') 6 | const _ = require('lodash') 7 | 8 | const util = require('./util') 9 | const wire = require('./wireprotocol') 10 | const adminReplies = require('./admin') 11 | 12 | const args = process.argv.slice(2) 13 | if (args.length < 1) { 14 | console.error('node index.js [] []') 15 | return 16 | } 17 | 18 | const createMissingDatabases = true 19 | const pgHost = args[0] || 'localhost' 20 | const mongoPort = parseInt(args[1] || '27018') 21 | const { Client } = require('pg') 22 | 23 | // database -> client 24 | const clients = {} 25 | 26 | let lastResult = { 27 | n: 0, 28 | connectionId : 1789, 29 | updatedExisting: true, 30 | errMsg: '', 31 | syncMillis: 0, 32 | writtenTo: null, 33 | ok: 1 34 | } 35 | 36 | async function createDatabase(databaseName) { 37 | await doQuery('postgres', 'CREATE DATABASE ' + databaseName) 38 | } 39 | 40 | async function doQuery(database, pgQuery) { 41 | if (!clients[database]) { 42 | try { 43 | const client = new Client({ database, host: pgHost }) 44 | clients[database] = client 45 | await client.connect() 46 | } catch (e2) { 47 | if (database !== 'postgres' && createMissingDatabases) { 48 | await createDatabase(database); 49 | const client = new Client({ database, host: pgHost }) 50 | clients[database] = client 51 | await client.connect() 52 | } 53 | } 54 | } 55 | 56 | debug('pgmongo:pgquery')(pgQuery) 57 | return await clients[database].query(pgQuery) 58 | } 59 | 60 | async function createTable(databaseName, collectionName) { 61 | await doQuery(databaseName, `CREATE TABLE IF NOT EXISTS ${collectionName} (data jsonb)`) 62 | } 63 | 64 | async function tryOrCreateTable(action, databaseName, collectionName) { 65 | try { 66 | return await action() 67 | } catch (e) { 68 | if (e.message.includes('does not exist')) { 69 | await createTable(collectionName) 70 | return await action() 71 | } else { 72 | throw e 73 | } 74 | } 75 | } 76 | 77 | var server = net.createServer(function (socket) { 78 | socket.on('data', (data) => processRecord(socket, data)) 79 | }) 80 | 81 | const convertToBSON = function(doc) { 82 | if (doc && doc._id && doc._id.length === 24) { 83 | doc._id = BSON.ObjectID(doc._id) 84 | } 85 | return doc 86 | } 87 | 88 | let commands = ['find', 'count', 'update', 'insert', 'create', 'delete', 'drop', 'validate', 89 | 'listIndexes', 'createIndexes', 'deleteIndexes', 'renameCollection'] 90 | 91 | const arrayPathsMap = {} 92 | 93 | function getArrayPaths(collectionName) { 94 | debug('pgmongo:arraypaths')(collectionName + ' ' + arrayPathsMap[collectionName]) 95 | return arrayPathsMap[collectionName] || [] 96 | } 97 | 98 | function safeWhereConversion(filter, collectionName) { 99 | try { 100 | return mongoToPostgres('data', filter, getArrayPaths(collectionName)) 101 | } catch (err) { 102 | const data = { 103 | errmsg: 'in must be an array', 104 | code: 2, 105 | codeName: 'BadValue', 106 | ok: 0 107 | } 108 | console.error('query error') 109 | console.error(err) 110 | throw data 111 | } 112 | } 113 | 114 | async function crud(socket, reqId, databaseName, commandName, doc, build) { 115 | const normalizedCommandName = commandName.toLowerCase() 116 | let rawCollectionName = doc[commandName] || doc[normalizedCommandName] 117 | let collectionName = '"' + rawCollectionName + '"' 118 | if (commandName === 'distinct') { 119 | if ((doc.query && typeof doc.query !== 'object') || typeof doc.key !== 'string') { 120 | socket.write(build(reqId, { ok: 0, errmsg: '"query" had the wrong type. Expected object or null,', code: 14 }, {})) 121 | //throw new Error('\\"query\\" had the wrong type. Expected object or null, found ' + typeof doc.query) 122 | return true 123 | } 124 | const filter = doc.query || {} 125 | let where = safeWhereConversion(filter, rawCollectionName) 126 | const distinctField = mongoToPostgres.pathToText(['data'].concat(doc.key.split('.')), false) 127 | const arrayCondition = `jsonb_typeof(${distinctField})='array'` 128 | const query1 = `SELECT DISTINCT ${distinctField} AS data FROM ${collectionName} WHERE ${where} AND NOT ${arrayCondition}` 129 | const query2 = `SELECT DISTINCT jsonb_array_elements(${distinctField}) AS data FROM ${collectionName} WHERE ${where} AND ${arrayCondition}` 130 | const query = `${query1} UNION ${query2}` 131 | const res = await tryOrCreateTable(async () => await doQuery(databaseName, query), databaseName, collectionName) 132 | const rows = res.rows.map((row) => convertToBSON(row.data)) 133 | debug('pgmongo:rows')(rows) 134 | const data = { values: rows, ok: 1 } 135 | socket.write(build(reqId, data, {})) 136 | return true 137 | } 138 | if (commandName === 'find') { 139 | const filter = doc.filter 140 | const where = safeWhereConversion(filter, rawCollectionName) 141 | let select = mongoToPostgres.convertSelect('data', doc.projection) 142 | 143 | let query = `SELECT ${select} FROM ${collectionName}` 144 | if (where !== 'TRUE') { 145 | query += ` WHERE ${where}` 146 | } 147 | if (doc.sort) { 148 | query += ' ORDER BY ' + mongoToPostgres.convertSort('data', doc.sort, doc.collation && doc.collation.numericOrdering) 149 | } 150 | if (doc.limit) { 151 | query += ' LIMIT ' + Math.abs(doc.limit) 152 | } 153 | if (doc.skip) { 154 | if (doc.skip < 0) { 155 | socket.write(build(reqId, { ok: 0, errmsg: 'negative skip not allowed' }, {})) 156 | return true 157 | } 158 | query += ' OFFSET ' + doc.skip 159 | } 160 | let res 161 | try { 162 | res = await tryOrCreateTable(async () => await doQuery(databaseName, query), databaseName, collectionName) 163 | } catch (e) { 164 | console.error(e) 165 | res = { rows: [] } 166 | } 167 | const rows = res.rows.map((row) => convertToBSON(row.data)) 168 | debug('pgmongo:rows')(rows) 169 | const data = util.createCursor(databaseName + '.' + collectionName, rows) 170 | socket.write(build(reqId, data, {})) 171 | return true 172 | } 173 | if (commandName === 'count') { 174 | if (doc.skip && doc.skip < 0) { 175 | socket.write(build(reqId, { ok: 0, errmsg: 'negative skip not allowed' }, {})) 176 | return true 177 | } 178 | const where = mongoToPostgres('data', doc.query, getArrayPaths(rawCollectionName)) 179 | const data = { n: 0, ok: 1 } 180 | try { 181 | const res = await doQuery(databaseName, `SELECT COUNT(*) FROM ${collectionName} WHERE ${where}`) 182 | data.n = res.rows[0].count 183 | if (doc.skip) { 184 | data.n = Math.max(0, data.n - doc.skip) 185 | } 186 | if (doc.limit) { 187 | data.n = Math.min(Math.abs(doc.limit), data.n) 188 | } 189 | } catch (e) { 190 | console.error(e) 191 | } 192 | socket.write(build(reqId, data, data)) 193 | return true 194 | } 195 | if (commandName === 'update') { 196 | const update = doc.updates[0] 197 | const where = mongoToPostgres('data', update.q, getArrayPaths(rawCollectionName)) 198 | let res 199 | try { 200 | const newValue = mongoToPostgres.convertUpdate('data', update.u) 201 | // TODO (handle multi) 202 | await createTable(collectionName) 203 | res = await doQuery(databaseName, `UPDATE ${collectionName} SET data = ${newValue} WHERE ${where}`) 204 | if (res.rowCount === 0 && update.upsert) { 205 | const changes = update.u || {} 206 | if (mongoToPostgres.countUpdateSpecialKeys(changes) === 0) { 207 | // TODO: expand dot notation 208 | _.assign(changes, update.q) 209 | } else { 210 | changes['$set'] = changes['$set'] || {} 211 | _.assign(changes['$set'], update.q) 212 | } 213 | const newValue = mongoToPostgres.convertUpdate('data', changes, true) 214 | async function insert() { 215 | const res = await doQuery(databaseName, `INSERT INTO ${collectionName} VALUES (${newValue})`) 216 | const data = { n: res.rowCount, nInserted: res.rowCount, updatedExisting: false, ok: 1 } 217 | socket.write(build(reqId, data, {})) 218 | } 219 | 220 | await tryOrCreateTable(insert, databaseName, collectionName) 221 | return true 222 | } else { 223 | lastResult.n = res.rowCount 224 | socket.write(build(reqId, lastResult, lastResult)) 225 | return true 226 | } 227 | } catch (e) { 228 | console.error(e) 229 | if (e.message.includes('_id')) { 230 | const data = { 231 | errmsg: e.message, 232 | ok: 0 233 | } 234 | socket.write(build(reqId, data, data)) 235 | return true 236 | } 237 | // Can also upsert, fallback 238 | const isChangingId = typeof update.u._id !== 'undefined' 239 | if (isChangingId) { 240 | console.error('failed to update, falling back to insert') 241 | commandName = 'insert' 242 | doc.documents = [update.u] 243 | } 244 | } 245 | } 246 | if (commandName === 'insert') { 247 | arrayPathsMap[rawCollectionName] = _.union(arrayPathsMap[rawCollectionName], util.getArrayPaths(doc.documents[0])) 248 | const newValues = doc.documents.map((values) => '(' + mongoToPostgres.convertUpdate('data', values) + ')') 249 | async function insert() { 250 | const res = await doQuery(databaseName, `INSERT INTO ${collectionName} VALUES ${newValues.join(',')}`) 251 | const data = { n: res.rowCount, nInserted: res.rowCount, updatedExisting: false, ok: 1 } 252 | socket.write(build(reqId, data, {})) 253 | } 254 | 255 | await tryOrCreateTable(insert, databaseName, collectionName) 256 | return true 257 | } 258 | if (commandName === 'create') { 259 | lastResult.ok = 1 260 | await createTable(collectionName) 261 | } 262 | if (commandName === 'delete') { 263 | async function del() { 264 | const where = mongoToPostgres('data', doc.deletes[0].q, getArrayPaths(rawCollectionName)) 265 | // TODO (handle multi) 266 | let query = `DELETE FROM ${collectionName} WHERE ${where}` 267 | if (doc.deletes[0].limit) { 268 | // TODO: handle limits on deletion 269 | //query += ' LIMIT ' + doc.deletes[0].limit 270 | } 271 | const res = await doQuery(databaseName, query) 272 | lastResult.n = res.rowCount 273 | return socket.write(build(reqId, lastResult, lastResult)) 274 | } 275 | await tryOrCreateTable(del, databaseName, collectionName) 276 | return true 277 | } 278 | if (commandName === 'drop') { 279 | try { 280 | await doQuery(databaseName, `DROP TABLE ${collectionName}`) 281 | } catch (e) { 282 | // often not an error if already doesn't exist 283 | } 284 | socket.write(build(reqId, lastResult, lastResult)) 285 | return true 286 | } 287 | if (commandName === 'validate') { 288 | const countRes = await doQuery(`SELECT COUNT(*) FROM ${collectionName}`) 289 | const data = { 290 | 'ns' : databaseName + '.' + collectionName, 291 | 'nrecords' : countRes.rows[0].count, 292 | 'nIndexes' : 0, 293 | 'keysPerIndex' : {}, 294 | 'valid' : true, 295 | 'warnings' : ['Some checks omitted for speed. use {full:true} option to do more thorough scan.'], 296 | 'errors' : [ ], 297 | 'ok' : 1 298 | } 299 | return socket.write(build(reqId, data, data)) 300 | } 301 | if (normalizedCommandName === 'listindexes') { 302 | const listIQuery = util.listIndicesQuery('data', rawCollectionName) 303 | const indexRes = await doQuery(databaseName, listIQuery) 304 | const indices = indexRes.rows.map((row) => ({ 305 | v: 2, 306 | key: { _id: 1 }, 307 | name: row.index_name, 308 | ns: databaseName + '.' + collectionName 309 | })) 310 | const data = util.createCursor(databaseName + '.' + collectionName, indices) 311 | socket.write(build(reqId, data, data)) 312 | return true 313 | } 314 | if (normalizedCommandName === 'createindexes') { 315 | rawCollectionName = rawCollectionName || doc['createIndexes'] 316 | collectionName = '"' + rawCollectionName + '"' 317 | const data = { 318 | createIndexes: { 319 | numIndexesBefore: 0, 320 | numIndexesAfter: 0, 321 | ok: 1 322 | }, 323 | ok: 1 324 | } 325 | const listIQuery = util.listIndicesQuery('data', rawCollectionName) 326 | const indexRes = await doQuery(databaseName, listIQuery) 327 | const indices = indexRes.rows.map((row) => row.index_name) 328 | data.createIndexes.numIndexesBefore = indices.length 329 | for (const index of doc.indexes) { 330 | if (indices.includes(index.name)) { 331 | continue 332 | } 333 | const keys = Object.keys(index.key) 334 | /*if (keys.length > 1) { 335 | console.error(keys) 336 | throw new Error('compound indices not supported'); 337 | }*/ 338 | const pgPath = keys.map(function(key) { 339 | const path = ['data'].concat(key.split('.')) 340 | return mongoToPostgres.pathToText(path, false) 341 | }).join(', ') 342 | let indexQuery = `CREATE INDEX "${index.name}" ON ${collectionName} USING gin ((${pgPath}));` 343 | try { 344 | await doQuery(databaseName, indexQuery) 345 | } catch (e) { 346 | console.error('failed to create index') 347 | //console.error(e) 348 | } 349 | data.createIndexes.numIndexesAfter++ 350 | } 351 | if (normalizedCommandName === 'deleteindexes') { 352 | // Todo 353 | // Also handle case where index is "*" and all need to be dropped. 354 | } 355 | socket.write(build(reqId, data, data)) 356 | return true 357 | } 358 | if (normalizedCommandName === 'renameCollection') { 359 | const data = { ok: 1 } 360 | collectionName = doc[commandName].split('.', 2)[1] 361 | const newName = doc.to.split('.', 2)[1] 362 | let query = `ALTER TABLE ${collectionName} RENAME TO "${newName}"` 363 | query = doc.dropTarget ? `DROP TABLE IF EXISTS ${newName}; ${query}` : query 364 | try { 365 | await doQuery(databaseName, query) 366 | } catch (e) { 367 | data.ok = 0 368 | console.error(e) 369 | } 370 | socket.write(build(reqId, data, data)) 371 | return true 372 | } 373 | } 374 | 375 | async function processAdmin(databaseName, socket, reqId, commandName, doc) { 376 | let res 377 | let reply 378 | switch (commandName) { 379 | case 'listdatabases': 380 | try { 381 | res = await doQuery('postgres', 'SELECT datname AS name FROM pg_database WHERE datistemplate = false') 382 | } catch (e) { 383 | console.error(e) 384 | res = { rows: [] } 385 | } 386 | reply = { 'databases': res.rows, 'ok': 1 } 387 | socket.write(wire.createCommandReply(reqId, reply, reply)) 388 | break 389 | case 'whatsmyuri': 390 | socket.write(wire.createCommandReply(reqId, {}, { 'you': '127.0.0.1:56709', 'ok': 1 })) 391 | break 392 | case 'getlog': 393 | if (doc.getLog === 'startupWarnings') { 394 | const log = { 395 | 'totalLinesWritten': 1, 396 | 'log': ['This is a Postgres database using the Mongo wrapper.'], 397 | 'ok': 1 398 | } 399 | socket.write(wire.createCommandReply(reqId, log, log)) 400 | } else { 401 | return false 402 | } 403 | break 404 | case 'replsetgetstatus': 405 | socket.write(wire.createCommandReply(reqId, {}, adminReplies.replSetGetStatus())) 406 | break 407 | case 'serverstatus': 408 | socket.write(wire.createCommandReply(reqId, adminReplies.getServerStatus(), {})) 409 | break 410 | case 'currentop': 411 | reply = { 412 | inprog: [], 413 | ok: 1 414 | } 415 | socket.write(wire.createCommandReply(reqId, reply, reply)) 416 | break 417 | default: 418 | return false 419 | } 420 | return true 421 | } 422 | 423 | let previousData 424 | 425 | async function processRecord(socket, data) { 426 | if (previousData) { 427 | data = Buffer.concat([previousData, data]) 428 | previousData = null 429 | } 430 | const { msgLength, reqId, opCode } = wire.parseHeader(data) 431 | 432 | if (data.length < msgLength - 1) { 433 | previousData = data 434 | return console.log('partial data received') 435 | } 436 | 437 | try { 438 | await handleRecord(socket, data, opCode, reqId) 439 | } catch (e) { 440 | console.error(e) 441 | if (e.code) { 442 | return socket.write(wire.createCommandReply(reqId, e, {})) 443 | } 444 | return socket.write(wire.createCommandReply(reqId, { errmsg: e.message, ok: 0 }, { ok: 0 })) 445 | } 446 | } 447 | async function handleRecord(socket, data, opCode, reqId) { 448 | if (opCode === wire.OP_QUERY) { 449 | const { doc, collectionName } = wire.parseQuery(data) 450 | if (collectionName === 'admin.$cmd' && (doc.isMaster || doc.ismaster)) { 451 | return socket.write(wire.createResponse(reqId, adminReplies.getIsMasterReply())) 452 | } 453 | const parts = collectionName.split('.') 454 | const databaseName = parts[0] 455 | for (const command of commands) { 456 | if (doc[command] || doc[command.toLowerCase()]) { 457 | if (await crud(socket, reqId, databaseName, command, doc, wire.createResponse)) 458 | return 459 | } 460 | } 461 | console.dir({ err: 'UNHANDLED REQUEST OP_QUERY', collectionName, doc }) 462 | return socket.write(wire.createCommandReply(reqId, { ok: 0 }, { ok: 0 })) 463 | } else if (opCode === wire.OP_COMMAND) { 464 | const { databaseName, commandName, doc } = wire.parseCommand(data) 465 | if (await crud(socket, reqId, databaseName, commandName, doc, wire.createCommandReply)) 466 | return 467 | 468 | if (databaseName === 'admin' || databaseName === 'db') { 469 | if (await processAdmin(databaseName, socket, reqId, commandName, doc)) 470 | return 471 | } 472 | 473 | let res 474 | switch (commandName) { 475 | case 'ismaster': 476 | return socket.write(wire.createCommandReply(reqId, adminReplies.getIsMasterReply(), adminReplies.getIsMasterReply())) 477 | case 'buildinfo': 478 | return socket.write(wire.createCommandReply(reqId, adminReplies.getBuildInfo(), {})) 479 | case 'listcollections': 480 | res = await doQuery(databaseName, 'SELECT table_name FROM information_schema.tables WHERE table_schema=\'public\' AND table_type=\'BASE TABLE\';') 481 | const tables = res.rows.map((row) => ({ name: row.table_name, type: 'collection', options: {}, info: { readOnly: false } })) 482 | const data = util.createCursor(`${databaseName}.$cmd.listCollections`, tables) 483 | return socket.write(wire.createCommandReply(reqId, data, data)) 484 | case 'ping': 485 | return socket.write(wire.createCommandReply(reqId, {}, { 'ok': 1 })) 486 | case 'getlasterror': 487 | return socket.write(wire.createCommandReply(reqId, lastResult, lastResult)) 488 | case 'dropdatabase': 489 | res = await doQuery(databaseName, 'select string_agg(\'drop table "\' || tablename || \'" cascade\', \'; \') from pg_tables where schemaname = \'public\'') 490 | await doQuery(databaseName, res.rows[0].string_agg) 491 | return socket.write(wire.createCommandReply(reqId, {}, { 'ok': 1 })) 492 | } 493 | 494 | console.dir({ err: 'UNHANDLED REQUEST', commandName, databaseName, doc }) 495 | return socket.write(wire.createCommandReply(reqId, { ok: 0 }, { ok: 0 })) 496 | } 497 | } 498 | 499 | server.listen(mongoPort, '127.0.0.1') 500 | console.log(`Connecting to postgres at ${pgHost}:5432`) 501 | console.log(`Serving Mongo on port ${mongoPort}`) 502 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pgmongo", 3 | "version": "0.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "assertion-error": { 8 | "version": "1.1.0", 9 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", 10 | "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", 11 | "dev": true 12 | }, 13 | "balanced-match": { 14 | "version": "1.0.0", 15 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 16 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 17 | "dev": true 18 | }, 19 | "bindings": { 20 | "version": "1.3.0", 21 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", 22 | "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" 23 | }, 24 | "brace-expansion": { 25 | "version": "1.1.11", 26 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 27 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 28 | "dev": true, 29 | "requires": { 30 | "balanced-match": "^1.0.0", 31 | "concat-map": "0.0.1" 32 | } 33 | }, 34 | "browser-stdout": { 35 | "version": "1.3.1", 36 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 37 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 38 | "dev": true 39 | }, 40 | "bson": { 41 | "version": "2.0.8", 42 | "resolved": "https://registry.npmjs.org/bson/-/bson-2.0.8.tgz", 43 | "integrity": "sha512-0F0T3gHeOwJzHWcN60BZomqj5hCBDRk4b3fANuruvDTnyJJ8sggABKSaePM2F34THNZZSIlB2P1mk2nQWgBr9w==" 44 | }, 45 | "bson-ext": { 46 | "version": "2.0.0", 47 | "resolved": "https://registry.npmjs.org/bson-ext/-/bson-ext-2.0.0.tgz", 48 | "integrity": "sha512-iP6gYML/9BjYHVKnvxMzfGcnOxDeFOMJwFW5bbNnSD1iOhM9McdCsvMacPFmrX6ZeOKXui0cqXDTmB+EpLg5xQ==", 49 | "requires": { 50 | "bindings": "^1.3.0", 51 | "bson": "^2.0.2", 52 | "nan": "^2.9.2" 53 | } 54 | }, 55 | "buffer-writer": { 56 | "version": "1.0.1", 57 | "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", 58 | "integrity": "sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=" 59 | }, 60 | "builtin-modules": { 61 | "version": "1.1.1", 62 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 63 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 64 | "dev": true 65 | }, 66 | "chai": { 67 | "version": "4.1.2", 68 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 69 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 70 | "dev": true, 71 | "requires": { 72 | "assertion-error": "^1.0.1", 73 | "check-error": "^1.0.1", 74 | "deep-eql": "^3.0.0", 75 | "get-func-name": "^2.0.0", 76 | "pathval": "^1.0.0", 77 | "type-detect": "^4.0.0" 78 | } 79 | }, 80 | "check-error": { 81 | "version": "1.0.2", 82 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 83 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 84 | "dev": true 85 | }, 86 | "commander": { 87 | "version": "2.15.1", 88 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 89 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 90 | "dev": true 91 | }, 92 | "concat-map": { 93 | "version": "0.0.1", 94 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 95 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 96 | "dev": true 97 | }, 98 | "contains-path": { 99 | "version": "0.1.0", 100 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 101 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 102 | "dev": true 103 | }, 104 | "debug": { 105 | "version": "3.1.0", 106 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 107 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 108 | "requires": { 109 | "ms": "2.0.0" 110 | } 111 | }, 112 | "deep-eql": { 113 | "version": "3.0.1", 114 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 115 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 116 | "dev": true, 117 | "requires": { 118 | "type-detect": "^4.0.0" 119 | } 120 | }, 121 | "diff": { 122 | "version": "3.5.0", 123 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 124 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 125 | "dev": true 126 | }, 127 | "doctrine": { 128 | "version": "1.5.0", 129 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 130 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 131 | "dev": true, 132 | "requires": { 133 | "esutils": "^2.0.2", 134 | "isarray": "^1.0.0" 135 | } 136 | }, 137 | "error-ex": { 138 | "version": "1.3.1", 139 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", 140 | "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", 141 | "dev": true, 142 | "requires": { 143 | "is-arrayish": "^0.2.1" 144 | } 145 | }, 146 | "escape-string-regexp": { 147 | "version": "1.0.5", 148 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 149 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 150 | "dev": true 151 | }, 152 | "eslint-config-airbnb-base": { 153 | "version": "12.1.0", 154 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-12.1.0.tgz", 155 | "integrity": "sha512-/vjm0Px5ZCpmJqnjIzcFb9TKZrKWz0gnuG/7Gfkt0Db1ELJR51xkZth+t14rYdqWgX836XbuxtArbIHlVhbLBA==", 156 | "dev": true, 157 | "requires": { 158 | "eslint-restricted-globals": "^0.1.1" 159 | } 160 | }, 161 | "eslint-import-resolver-node": { 162 | "version": "0.3.2", 163 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", 164 | "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", 165 | "dev": true, 166 | "requires": { 167 | "debug": "^2.6.9", 168 | "resolve": "^1.5.0" 169 | }, 170 | "dependencies": { 171 | "debug": { 172 | "version": "2.6.9", 173 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 174 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 175 | "dev": true, 176 | "requires": { 177 | "ms": "2.0.0" 178 | } 179 | } 180 | } 181 | }, 182 | "eslint-module-utils": { 183 | "version": "2.2.0", 184 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz", 185 | "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=", 186 | "dev": true, 187 | "requires": { 188 | "debug": "^2.6.8", 189 | "pkg-dir": "^1.0.0" 190 | }, 191 | "dependencies": { 192 | "debug": { 193 | "version": "2.6.9", 194 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 195 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 196 | "dev": true, 197 | "requires": { 198 | "ms": "2.0.0" 199 | } 200 | } 201 | } 202 | }, 203 | "eslint-plugin-import": { 204 | "version": "2.12.0", 205 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.12.0.tgz", 206 | "integrity": "sha1-2tMXgSktZmSyUxf9BJ0uKy8CIF0=", 207 | "dev": true, 208 | "requires": { 209 | "contains-path": "^0.1.0", 210 | "debug": "^2.6.8", 211 | "doctrine": "1.5.0", 212 | "eslint-import-resolver-node": "^0.3.1", 213 | "eslint-module-utils": "^2.2.0", 214 | "has": "^1.0.1", 215 | "lodash": "^4.17.4", 216 | "minimatch": "^3.0.3", 217 | "read-pkg-up": "^2.0.0", 218 | "resolve": "^1.6.0" 219 | }, 220 | "dependencies": { 221 | "debug": { 222 | "version": "2.6.9", 223 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 224 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 225 | "dev": true, 226 | "requires": { 227 | "ms": "2.0.0" 228 | } 229 | } 230 | } 231 | }, 232 | "eslint-restricted-globals": { 233 | "version": "0.1.1", 234 | "resolved": "https://registry.npmjs.org/eslint-restricted-globals/-/eslint-restricted-globals-0.1.1.tgz", 235 | "integrity": "sha1-NfDVy8ZMLj7WLpO0saevBbp+1Nc=", 236 | "dev": true 237 | }, 238 | "esutils": { 239 | "version": "2.0.2", 240 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 241 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 242 | "dev": true 243 | }, 244 | "find-up": { 245 | "version": "1.1.2", 246 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 247 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 248 | "dev": true, 249 | "requires": { 250 | "path-exists": "^2.0.0", 251 | "pinkie-promise": "^2.0.0" 252 | } 253 | }, 254 | "flatten-obj": { 255 | "version": "3.1.1", 256 | "resolved": "https://registry.npmjs.org/flatten-obj/-/flatten-obj-3.1.1.tgz", 257 | "integrity": "sha1-pU7+tFryMEYs2IHtWtodHNryRjE=", 258 | "requires": { 259 | "isobj": "^1.0.0" 260 | } 261 | }, 262 | "fs.realpath": { 263 | "version": "1.0.0", 264 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 265 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 266 | "dev": true 267 | }, 268 | "function-bind": { 269 | "version": "1.1.1", 270 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 271 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 272 | "dev": true 273 | }, 274 | "get-func-name": { 275 | "version": "2.0.0", 276 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 277 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 278 | "dev": true 279 | }, 280 | "glob": { 281 | "version": "7.1.2", 282 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 283 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 284 | "dev": true, 285 | "requires": { 286 | "fs.realpath": "^1.0.0", 287 | "inflight": "^1.0.4", 288 | "inherits": "2", 289 | "minimatch": "^3.0.4", 290 | "once": "^1.3.0", 291 | "path-is-absolute": "^1.0.0" 292 | } 293 | }, 294 | "graceful-fs": { 295 | "version": "4.1.11", 296 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 297 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 298 | "dev": true 299 | }, 300 | "growl": { 301 | "version": "1.10.5", 302 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 303 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 304 | "dev": true 305 | }, 306 | "has": { 307 | "version": "1.0.3", 308 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 309 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 310 | "dev": true, 311 | "requires": { 312 | "function-bind": "^1.1.1" 313 | } 314 | }, 315 | "has-flag": { 316 | "version": "3.0.0", 317 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 318 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 319 | "dev": true 320 | }, 321 | "he": { 322 | "version": "1.1.1", 323 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 324 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 325 | "dev": true 326 | }, 327 | "hosted-git-info": { 328 | "version": "2.6.0", 329 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", 330 | "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", 331 | "dev": true 332 | }, 333 | "inflight": { 334 | "version": "1.0.6", 335 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 336 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 337 | "dev": true, 338 | "requires": { 339 | "once": "^1.3.0", 340 | "wrappy": "1" 341 | } 342 | }, 343 | "inherits": { 344 | "version": "2.0.3", 345 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 346 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 347 | "dev": true 348 | }, 349 | "is-arrayish": { 350 | "version": "0.2.1", 351 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 352 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 353 | "dev": true 354 | }, 355 | "is-builtin-module": { 356 | "version": "1.0.0", 357 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", 358 | "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", 359 | "dev": true, 360 | "requires": { 361 | "builtin-modules": "^1.0.0" 362 | } 363 | }, 364 | "isarray": { 365 | "version": "1.0.0", 366 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 367 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 368 | "dev": true 369 | }, 370 | "isobj": { 371 | "version": "1.0.0", 372 | "resolved": "https://registry.npmjs.org/isobj/-/isobj-1.0.0.tgz", 373 | "integrity": "sha1-Z7oo+cbcmBMOuho3m9y9HZOcc/Q=" 374 | }, 375 | "load-json-file": { 376 | "version": "2.0.0", 377 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 378 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 379 | "dev": true, 380 | "requires": { 381 | "graceful-fs": "^4.1.2", 382 | "parse-json": "^2.2.0", 383 | "pify": "^2.0.0", 384 | "strip-bom": "^3.0.0" 385 | } 386 | }, 387 | "locate-path": { 388 | "version": "2.0.0", 389 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 390 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 391 | "dev": true, 392 | "requires": { 393 | "p-locate": "^2.0.0", 394 | "path-exists": "^3.0.0" 395 | }, 396 | "dependencies": { 397 | "path-exists": { 398 | "version": "3.0.0", 399 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 400 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 401 | "dev": true 402 | } 403 | } 404 | }, 405 | "lodash": { 406 | "version": "4.17.10", 407 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 408 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 409 | }, 410 | "minimatch": { 411 | "version": "3.0.4", 412 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 413 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 414 | "dev": true, 415 | "requires": { 416 | "brace-expansion": "^1.1.7" 417 | } 418 | }, 419 | "minimist": { 420 | "version": "0.0.8", 421 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 422 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 423 | "dev": true 424 | }, 425 | "mkdirp": { 426 | "version": "0.5.1", 427 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 428 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 429 | "dev": true, 430 | "requires": { 431 | "minimist": "0.0.8" 432 | } 433 | }, 434 | "mocha": { 435 | "version": "5.2.0", 436 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 437 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 438 | "dev": true, 439 | "requires": { 440 | "browser-stdout": "1.3.1", 441 | "commander": "2.15.1", 442 | "debug": "3.1.0", 443 | "diff": "3.5.0", 444 | "escape-string-regexp": "1.0.5", 445 | "glob": "7.1.2", 446 | "growl": "1.10.5", 447 | "he": "1.1.1", 448 | "minimatch": "3.0.4", 449 | "mkdirp": "0.5.1", 450 | "supports-color": "5.4.0" 451 | } 452 | }, 453 | "mongo-query-to-postgres-jsonb": { 454 | "version": "0.2.0", 455 | "resolved": "https://registry.npmjs.org/mongo-query-to-postgres-jsonb/-/mongo-query-to-postgres-jsonb-0.2.0.tgz", 456 | "integrity": "sha1-lb8eKMxZjxVt3babTzIeOeSVK7c=" 457 | }, 458 | "ms": { 459 | "version": "2.0.0", 460 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 461 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 462 | }, 463 | "nan": { 464 | "version": "2.10.0", 465 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", 466 | "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" 467 | }, 468 | "normalize-package-data": { 469 | "version": "2.4.0", 470 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", 471 | "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", 472 | "dev": true, 473 | "requires": { 474 | "hosted-git-info": "^2.1.4", 475 | "is-builtin-module": "^1.0.0", 476 | "semver": "2 || 3 || 4 || 5", 477 | "validate-npm-package-license": "^3.0.1" 478 | } 479 | }, 480 | "once": { 481 | "version": "1.4.0", 482 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 483 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 484 | "dev": true, 485 | "requires": { 486 | "wrappy": "1" 487 | } 488 | }, 489 | "p-limit": { 490 | "version": "1.3.0", 491 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", 492 | "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", 493 | "dev": true, 494 | "requires": { 495 | "p-try": "^1.0.0" 496 | } 497 | }, 498 | "p-locate": { 499 | "version": "2.0.0", 500 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 501 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 502 | "dev": true, 503 | "requires": { 504 | "p-limit": "^1.1.0" 505 | } 506 | }, 507 | "p-try": { 508 | "version": "1.0.0", 509 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", 510 | "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", 511 | "dev": true 512 | }, 513 | "packet-reader": { 514 | "version": "0.3.1", 515 | "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-0.3.1.tgz", 516 | "integrity": "sha1-zWLmCvjX/qinBexP+ZCHHEaHHyc=" 517 | }, 518 | "parse-json": { 519 | "version": "2.2.0", 520 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 521 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 522 | "dev": true, 523 | "requires": { 524 | "error-ex": "^1.2.0" 525 | } 526 | }, 527 | "path-exists": { 528 | "version": "2.1.0", 529 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 530 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 531 | "dev": true, 532 | "requires": { 533 | "pinkie-promise": "^2.0.0" 534 | } 535 | }, 536 | "path-is-absolute": { 537 | "version": "1.0.1", 538 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 539 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 540 | "dev": true 541 | }, 542 | "path-parse": { 543 | "version": "1.0.5", 544 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 545 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 546 | "dev": true 547 | }, 548 | "path-type": { 549 | "version": "2.0.0", 550 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 551 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 552 | "dev": true, 553 | "requires": { 554 | "pify": "^2.0.0" 555 | } 556 | }, 557 | "pathval": { 558 | "version": "1.1.0", 559 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 560 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 561 | "dev": true 562 | }, 563 | "pg": { 564 | "version": "7.4.3", 565 | "resolved": "https://registry.npmjs.org/pg/-/pg-7.4.3.tgz", 566 | "integrity": "sha1-97b5P1NA7MJZavu5ShPj1rYJg0s=", 567 | "requires": { 568 | "buffer-writer": "1.0.1", 569 | "packet-reader": "0.3.1", 570 | "pg-connection-string": "0.1.3", 571 | "pg-pool": "~2.0.3", 572 | "pg-types": "~1.12.1", 573 | "pgpass": "1.x", 574 | "semver": "4.3.2" 575 | } 576 | }, 577 | "pg-connection-string": { 578 | "version": "0.1.3", 579 | "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz", 580 | "integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc=" 581 | }, 582 | "pg-pool": { 583 | "version": "2.0.3", 584 | "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.3.tgz", 585 | "integrity": "sha1-wCIDLIlJ8xKk+R+2QJzgQHa+Mlc=" 586 | }, 587 | "pg-types": { 588 | "version": "1.12.1", 589 | "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.12.1.tgz", 590 | "integrity": "sha1-1kCH45A7WP+q0nnnWVxSIIoUw9I=", 591 | "requires": { 592 | "postgres-array": "~1.0.0", 593 | "postgres-bytea": "~1.0.0", 594 | "postgres-date": "~1.0.0", 595 | "postgres-interval": "^1.1.0" 596 | } 597 | }, 598 | "pgpass": { 599 | "version": "1.0.2", 600 | "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz", 601 | "integrity": "sha1-Knu0G2BltnkH6R2hsHwYR8h3swY=", 602 | "requires": { 603 | "split": "^1.0.0" 604 | } 605 | }, 606 | "pify": { 607 | "version": "2.3.0", 608 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 609 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 610 | "dev": true 611 | }, 612 | "pinkie": { 613 | "version": "2.0.4", 614 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 615 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 616 | "dev": true 617 | }, 618 | "pinkie-promise": { 619 | "version": "2.0.1", 620 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 621 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 622 | "dev": true, 623 | "requires": { 624 | "pinkie": "^2.0.0" 625 | } 626 | }, 627 | "pkg-dir": { 628 | "version": "1.0.0", 629 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", 630 | "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", 631 | "dev": true, 632 | "requires": { 633 | "find-up": "^1.0.0" 634 | } 635 | }, 636 | "postgres-array": { 637 | "version": "1.0.2", 638 | "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.2.tgz", 639 | "integrity": "sha1-jgsy6wO/d6XAp4UeBEHBaaJWojg=" 640 | }, 641 | "postgres-bytea": { 642 | "version": "1.0.0", 643 | "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", 644 | "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" 645 | }, 646 | "postgres-date": { 647 | "version": "1.0.3", 648 | "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.3.tgz", 649 | "integrity": "sha1-4tiXAu/bJY/52c7g/pG9BpdSV6g=" 650 | }, 651 | "postgres-interval": { 652 | "version": "1.1.1", 653 | "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.1.1.tgz", 654 | "integrity": "sha512-OkuCi9t/3CZmeQreutGgx/OVNv9MKHGIT5jH8KldQ4NLYXkvmT9nDVxEuCENlNwhlGPE374oA/xMqn05G49pHA==", 655 | "requires": { 656 | "xtend": "^4.0.0" 657 | } 658 | }, 659 | "read-pkg": { 660 | "version": "2.0.0", 661 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 662 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 663 | "dev": true, 664 | "requires": { 665 | "load-json-file": "^2.0.0", 666 | "normalize-package-data": "^2.3.2", 667 | "path-type": "^2.0.0" 668 | } 669 | }, 670 | "read-pkg-up": { 671 | "version": "2.0.0", 672 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 673 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 674 | "dev": true, 675 | "requires": { 676 | "find-up": "^2.0.0", 677 | "read-pkg": "^2.0.0" 678 | }, 679 | "dependencies": { 680 | "find-up": { 681 | "version": "2.1.0", 682 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 683 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 684 | "dev": true, 685 | "requires": { 686 | "locate-path": "^2.0.0" 687 | } 688 | } 689 | } 690 | }, 691 | "resolve": { 692 | "version": "1.7.1", 693 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", 694 | "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", 695 | "dev": true, 696 | "requires": { 697 | "path-parse": "^1.0.5" 698 | } 699 | }, 700 | "semver": { 701 | "version": "4.3.2", 702 | "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.2.tgz", 703 | "integrity": "sha1-x6BxWKgL7dBSNVt3DYLWZA+AO+c=" 704 | }, 705 | "spdx-correct": { 706 | "version": "3.0.0", 707 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", 708 | "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", 709 | "dev": true, 710 | "requires": { 711 | "spdx-expression-parse": "^3.0.0", 712 | "spdx-license-ids": "^3.0.0" 713 | } 714 | }, 715 | "spdx-exceptions": { 716 | "version": "2.1.0", 717 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", 718 | "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", 719 | "dev": true 720 | }, 721 | "spdx-expression-parse": { 722 | "version": "3.0.0", 723 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", 724 | "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", 725 | "dev": true, 726 | "requires": { 727 | "spdx-exceptions": "^2.1.0", 728 | "spdx-license-ids": "^3.0.0" 729 | } 730 | }, 731 | "spdx-license-ids": { 732 | "version": "3.0.0", 733 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", 734 | "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", 735 | "dev": true 736 | }, 737 | "split": { 738 | "version": "1.0.1", 739 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 740 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 741 | "requires": { 742 | "through": "2" 743 | } 744 | }, 745 | "strip-bom": { 746 | "version": "3.0.0", 747 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 748 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 749 | "dev": true 750 | }, 751 | "supports-color": { 752 | "version": "5.4.0", 753 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 754 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 755 | "dev": true, 756 | "requires": { 757 | "has-flag": "^3.0.0" 758 | } 759 | }, 760 | "through": { 761 | "version": "2.3.8", 762 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 763 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 764 | }, 765 | "type-detect": { 766 | "version": "4.0.8", 767 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 768 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 769 | "dev": true 770 | }, 771 | "validate-npm-package-license": { 772 | "version": "3.0.3", 773 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", 774 | "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", 775 | "dev": true, 776 | "requires": { 777 | "spdx-correct": "^3.0.0", 778 | "spdx-expression-parse": "^3.0.0" 779 | } 780 | }, 781 | "wrappy": { 782 | "version": "1.0.2", 783 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 784 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 785 | "dev": true 786 | }, 787 | "xtend": { 788 | "version": "4.0.1", 789 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 790 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" 791 | } 792 | } 793 | } 794 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pgmongo", 3 | "version": "0.1.0", 4 | "description": "implements the Mongo wire protocol and adapts queries to use Postgres jsonb.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js mongotest", 8 | "test": "mocha" 9 | }, 10 | "bin": { 11 | "pgmongo": "./index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/thomas4019/postres-mongo-emulator.git" 16 | }, 17 | "author": "Thomas Hansen", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/thomas4019/postres-mongo-emulator/issues" 21 | }, 22 | "homepage": "https://github.com/thomas4019/postres-mongo-emulator#readme", 23 | "dependencies": { 24 | "bson-ext": "^2.0.0", 25 | "debug": "^3.1.0", 26 | "flatten-obj": "^3.1.1", 27 | "lodash": "^4.17.10", 28 | "mongo-query-to-postgres-jsonb": "^0.2.0", 29 | "pg": "^7.4.3" 30 | }, 31 | "devDependencies": { 32 | "chai": "^4.1.2", 33 | "eslint-config-airbnb-base": "^12.1.0", 34 | "eslint-plugin-import": "^2.12.0", 35 | "mocha": "^5.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert 2 | const BSON = require('bson-ext') 3 | const util = require('../util') 4 | const describeTypes = util.describeTypes 5 | const getArrayPaths = util.getArrayPaths 6 | 7 | describe('util: describeTypes', function() { 8 | it('basic type', function () { 9 | assert.equal(describeTypes(BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c30')), 'ObjectID') 10 | }) 11 | it('returns undefined for strings', function () { 12 | assert.equal(describeTypes('123'), undefined) 13 | }) 14 | it('object with no special values', function () { 15 | assert.deepEqual(describeTypes({ a: '123', b: 12 }), {}) 16 | }) 17 | it('arrays', function () { 18 | assert.deepEqual(describeTypes({ arr: [1, 2] }), { arr: [] }) 19 | }) 20 | it('array of ids', function () { 21 | assert.deepEqual(describeTypes({ arr: [BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c30'), BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c32')] }), { arr: ['ObjectID'] }) 22 | }) 23 | it('object with mixed', function () { 24 | assert.deepEqual(describeTypes({ a: '123', id2: BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c30') }), { id2: 'ObjectID'}) 25 | }) 26 | }) 27 | 28 | describe('util: getArrayPaths', function() { 29 | it('arrays', function () { 30 | assert.deepEqual(getArrayPaths({ arr: [1, 2] }), ['arr'] ) 31 | }) 32 | it('multiple arrays', function () { 33 | assert.deepEqual(getArrayPaths({ arr: [1, 2], arr2: ['1'] }), ['arr', 'arr2'] ) 34 | }) 35 | it('deep array', function () { 36 | assert.deepEqual(getArrayPaths({ deep: { arr: [1, 2] } }), ['deep.arr'] ) 37 | }) 38 | it('array of ids', function () { 39 | assert.deepEqual(getArrayPaths({ arr: [BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c30'), BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c32')] }), ['arr']) 40 | }) 41 | it('object with mixed', function () { 42 | assert.deepEqual(getArrayPaths({ a: '123', id2: BSON.ObjectID('5b1e1a6fa00fd75c4e6c4c30') }), [] ) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | const BSON = require('bson-ext') 2 | const Long = BSON.Long 3 | const _ = require('lodash') 4 | 5 | exports.describeTypes = function (doc) { 6 | if (doc._bsontype) { 7 | return doc._bsontype 8 | } else if (Array.isArray(doc)) { 9 | const types = doc.map(exports.describeTypes) 10 | const uniqueTypes = _.uniqWith(types, _.isEqual) 11 | if (uniqueTypes.length > 1) { 12 | //throw new Error('arrays containing multiple data types are not allowed') 13 | } 14 | if (_.isUndefined(uniqueTypes[0])) { 15 | return [] 16 | } 17 | return uniqueTypes 18 | } else if (typeof doc === 'object') { 19 | const out = {} 20 | for (const key of Object.keys(doc)) { 21 | const v = exports.describeTypes(doc[key]) 22 | if (typeof v !== 'undefined') { 23 | out[key] = v 24 | } 25 | } 26 | return out 27 | } 28 | } 29 | 30 | exports.getArrayPaths = function (doc) { 31 | const arrayPaths = [] 32 | 33 | function recursiveHepler(path, doc) { 34 | if (Array.isArray(doc)) { 35 | arrayPaths.push(path) 36 | } else if (doc && typeof doc === 'object') { 37 | for (const key of Object.keys(doc)) { 38 | recursiveHepler(path ? (path + '.' + key) : key, doc[key]) 39 | } 40 | } 41 | } 42 | 43 | recursiveHepler('', doc) 44 | return arrayPaths 45 | } 46 | 47 | exports.createCursor = function (ns, firstBatch, id = Long.fromNumber(0)) { 48 | return { 49 | cursor: { 50 | id, 51 | ns, 52 | firstBatch 53 | }, 54 | ok: 1 55 | } 56 | } 57 | 58 | exports.listIndicesQuery = function (fieldName, collectionName) { 59 | return `select 60 | t.relname as table_name, 61 | i.relname as index_name, 62 | a.attname as column_name 63 | from 64 | pg_class t, 65 | pg_class i, 66 | pg_index ix, 67 | pg_attribute a 68 | where 69 | t.oid = ix.indrelid 70 | and i.oid = ix.indexrelid 71 | and a.attrelid = t.oid 72 | and t.relkind = 'r' 73 | and t.relname = '${collectionName}' 74 | and a.attname = '${fieldName}' 75 | order by 76 | t.relname, 77 | i.relname;` 78 | } 79 | -------------------------------------------------------------------------------- /wireprotocol.js: -------------------------------------------------------------------------------- 1 | const BSON = require('bson-ext') 2 | const bson = new BSON([BSON.Binary, BSON.Code, BSON.DBRef, BSON.Decimal128, BSON.Double, BSON.Int32, BSON.Long, BSON.Map, BSON.MaxKey, BSON.MinKey, BSON.ObjectId, BSON.BSONRegExp, BSON.Symbol, BSON.Timestamp]) 3 | const debug = require('debug') 4 | 5 | const OP_REPLY = 1 6 | const OP_COMMANDREPLY = 2011 7 | 8 | function getRandomInt(max) { 9 | max = max || Math.pow(2, 31) - 1 10 | return Math.floor(Math.random() * Math.floor(max)) 11 | } 12 | 13 | exports.OP_QUERY = 2004 14 | exports.OP_COMMAND = 2010 15 | 16 | exports.parseHeader = function(buffer) { 17 | const opCode = buffer.readInt32LE(12) 18 | debug('pgmongo:opcode')(opCode) 19 | return { 20 | msgLength: buffer.readInt32LE(0), 21 | reqId: buffer.readInt32LE(4), 22 | opCode 23 | } 24 | } 25 | 26 | exports.parseQuery = function(buffer) { 27 | const flags = buffer.readInt32LE(16) 28 | const nameEnd = buffer.indexOf('\0', 20) + 1 29 | const collectionName = buffer.toString('utf8', 20, nameEnd - 1) 30 | const documents = [] 31 | try { 32 | bson.deserializeStream(buffer, nameEnd + 8, 1, documents, 0) 33 | } catch (e) { 34 | console.error('error - missing or invalid body') 35 | } 36 | const doc = documents[0] 37 | return { 38 | flags, 39 | collectionName, 40 | doc 41 | } 42 | } 43 | 44 | exports.parseCommand = function(buffer) { 45 | const databaseEnd = buffer.indexOf('\0', 16) + 1 46 | const databaseName = buffer.toString('utf8', 16, databaseEnd - 1) 47 | const commandEnd = buffer.indexOf('\0', databaseEnd) + 1 48 | const commandName = buffer.toString('utf8', databaseEnd, commandEnd - 1).toLowerCase() 49 | const documents = [] 50 | bson.deserializeStream(buffer, commandEnd, 2, documents, 0) 51 | const doc = documents[0] 52 | 53 | debug('pgmongo:indoc')(doc) 54 | return { 55 | databaseName, 56 | commandName, 57 | doc 58 | } 59 | } 60 | 61 | exports.createCommandReply = function(reqId, metadata, commandReply) { 62 | debug('pgmongo:reply')(metadata) 63 | if (metadata !== commandReply) { 64 | debug('pgmongo:reply2')(commandReply) 65 | } 66 | const metadataBuffer = bson.serialize(metadata) 67 | const replyBuffer = bson.serialize(commandReply) 68 | const length = 16 + metadataBuffer.length + replyBuffer.length 69 | 70 | const buf = Buffer.alloc(length) 71 | buf.writeInt32LE(length, 0) 72 | buf.writeInt32LE(getRandomInt(), 4) 73 | buf.writeInt32LE(reqId, 8) 74 | buf.writeInt32LE(OP_COMMANDREPLY, 12) 75 | 76 | metadataBuffer.copy(buf, 16) 77 | replyBuffer.copy(buf, 16 + metadataBuffer.length) 78 | 79 | return buf 80 | } 81 | 82 | exports.createResponse = function(reqId, doc) { 83 | debug('pgmongo:replyr')(doc) 84 | const docBuffer = bson.serialize(doc) 85 | const length = 36 + docBuffer.length 86 | 87 | const buf = Buffer.alloc(length) 88 | buf.writeInt32LE(length, 0) 89 | buf.writeInt32LE(0, 4) 90 | buf.writeInt32LE(reqId, 8) 91 | buf.writeInt32LE(OP_REPLY, 12) 92 | 93 | buf.writeInt32LE(0, 16) 94 | buf.writeInt32LE(0, 20) 95 | buf.writeInt32LE(0, 24) 96 | buf.writeInt32LE(0, 28) 97 | buf.writeInt32LE(1, 32) 98 | docBuffer.copy(buf, 36) 99 | 100 | return buf 101 | } 102 | --------------------------------------------------------------------------------