├── bson
├── bench
│ ├── .gitignore
│ ├── package.json
│ ├── index.mjs
│ ├── deno_bench.ts
│ └── yarn.lock
├── .gitignore
├── src
│ ├── encoder
│ │ ├── mod.rs
│ │ ├── value.rs
│ │ ├── number.rs
│ │ ├── object.rs
│ │ └── extended.rs
│ └── lib.rs
├── Cargo.toml
├── Makefile
├── types.ts
├── scripts
│ └── generate.ts
├── mod.ts
└── Cargo.lock
├── .gitignore
├── assets
└── mango.png
├── tmp
├── mongo-data-dump
│ ├── profiles.bson
│ └── profiles.metadata.json
├── mongodb.env
├── mongo.seeder.dockerfile
├── docker-compose.yml
└── app.ts
├── .editorconfig
├── deps.ts
├── DEV.md
├── mod.ts
├── protocol
├── header.ts
├── mod.ts
└── op_msg.ts
├── LICENSE
└── README.md
/bson/bench/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/bson/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
3 | build
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS files
2 | .DS_Store
3 | .cache
4 |
5 | # IDE
6 | .vscode
7 |
--------------------------------------------------------------------------------
/assets/mango.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denodrivers/mango/HEAD/assets/mango.png
--------------------------------------------------------------------------------
/tmp/mongo-data-dump/profiles.bson:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/denodrivers/mango/HEAD/tmp/mongo-data-dump/profiles.bson
--------------------------------------------------------------------------------
/bson/src/encoder/mod.rs:
--------------------------------------------------------------------------------
1 | mod extended;
2 | mod number;
3 | mod object;
4 | mod value;
5 |
6 | pub use object::create_document;
7 |
--------------------------------------------------------------------------------
/bson/bench/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bench",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "dependencies": {
7 | "bson": "4.1.0"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tmp/mongo-data-dump/profiles.metadata.json:
--------------------------------------------------------------------------------
1 | {"options":{},"indexes":[{"v":{"$numberInt":"2"},"key":{"_id":{"$numberInt":"1"}},"name":"_id_","ns":"juanportal.profiles"}],"uuid":"11fd8fa3efba4eb09623636a7565a738"}
--------------------------------------------------------------------------------
/tmp/mongodb.env:
--------------------------------------------------------------------------------
1 | MONGODB_ROOT_USERNAME=root
2 | MONGODB_ROOT_PASSWD=rootpassword
3 | MONGODB_ROOT_ROLE=root
4 | MONGODB_USERNAME=user
5 | MONGODB_PASSWD=userpassword
6 | MONGODB_DBNAME=juanportal
7 | MONGODB_ROLE=readWrite
--------------------------------------------------------------------------------
/tmp/mongo.seeder.dockerfile:
--------------------------------------------------------------------------------
1 | FROM mongo
2 |
3 | COPY ./mongo-data-dump /mongo_data
4 | # Profiles collection
5 | CMD mongorestore --host mongodb --db juanportal /mongo_data
6 | #CMD mongorestore --host mongodb --db juanportal /mongo_data
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_style = space
7 | indent_size = 2
8 |
9 | [*.rs]
10 | indent_size = 4
11 |
12 | [Makefile]
13 | indent_style = tab
14 | indent_size = 4
15 |
--------------------------------------------------------------------------------
/deps.ts:
--------------------------------------------------------------------------------
1 | export { BufReader } from "https://deno.land/std@0.74.0/io/mod.ts";
2 | export { deferred } from "https://deno.land/std@0.74.0/async/deferred.ts";
3 | export type { Deferred } from "https://deno.land/std@0.74.0/async/deferred.ts";
4 | export {
5 | assert,
6 | assertEquals,
7 | } from "https://deno.land/std@0.74.0/testing/asserts.ts";
8 |
--------------------------------------------------------------------------------
/DEV.md:
--------------------------------------------------------------------------------
1 | # Using the BSON converter
2 |
3 | `make`
4 |
5 | Or to be smart: `make && deno run --allow-read bson/mod.ts`
6 |
7 | # Using Driver Dev Environment
8 |
9 | ```shell script
10 | $ cd tmp
11 | $ docker-compose up -d
12 | $ deno run -A app.ts
13 | ```
14 |
15 | Mongo database is seeded with a `profiles` collection, with 3 documents
16 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | import { MongoProtocol } from "./protocol/mod.ts";
2 |
3 | const socket = await Deno.connect({ port: 27017, transport: "tcp" });
4 | console.log("Connected");
5 |
6 | const protocol = new MongoProtocol(socket);
7 |
8 | const res = await protocol.executeOpMsg([{
9 | kind: 0,
10 | body: { find: "profiles", "$db": "juanportal" },
11 | }]);
12 | console.log(res[0]);
13 |
14 | protocol.close();
15 | console.log("Closed");
16 |
--------------------------------------------------------------------------------
/bson/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "mango_bson"
3 | version = "0.1.0"
4 | authors = ["Filippo Rossi"]
5 | edition = "2018"
6 |
7 | [lib]
8 | crate-type = ["cdylib"]
9 | path = "src/lib.rs"
10 |
11 | [dependencies]
12 | chrono = "0.4"
13 | serde = { version = "1.0", features = ["derive"] }
14 | serde_json = "1.0"
15 | wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
16 | js-sys = "0.3"
17 | bson = "1.1"
18 | hex = "0.4"
19 | base64 = "0.13"
20 |
21 | [profile.release]
22 | lto = true
23 | opt-level = 3
24 |
--------------------------------------------------------------------------------
/bson/Makefile:
--------------------------------------------------------------------------------
1 | BUILD=build
2 | CRATE=mango_bson
3 |
4 | WASM=wasm
5 |
6 | all: generate
7 |
8 | generate: build
9 | wasm-bindgen --target deno \
10 | --out-dir "$(BUILD)" \
11 | "./target/wasm32-unknown-unknown/release/$(CRATE).wasm"
12 | deno run --allow-read --allow-write scripts/generate.ts
13 |
14 | build: prepare
15 | cargo build --release --target wasm32-unknown-unknown
16 |
17 | prepare:
18 | mkdir -p $(BUILD)
19 | mkdir -p $(WASM)
20 |
21 | clean:
22 | rm -rf $(BUILD)
23 | rm -rf $(WASM)
24 |
25 | .PHONY: all generate build prepare
26 |
--------------------------------------------------------------------------------
/tmp/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | mongodb:
6 | container_name: mango_mongo
7 | image: mongo
8 | ports:
9 | - "27017:27017"
10 | env_file:
11 | - ./mongodb.env
12 | networks:
13 | - mango-network
14 |
15 | mongoseeder:
16 | container_name: mango_mongoseeder
17 | build:
18 | context: .
19 | dockerfile: mongo.seeder.dockerfile
20 | depends_on:
21 | - mongodb
22 | networks:
23 | - mango-network
24 |
25 | networks:
26 | mango-network:
27 | driver: bridge
28 |
--------------------------------------------------------------------------------
/bson/bench/index.mjs:
--------------------------------------------------------------------------------
1 | import bson from "bson";
2 | import { performance } from "perf_hooks";
3 |
4 | const date = new Date();
5 | const data = bson.serialize(
6 | {
7 | foo: "bar",
8 | hello: 55,
9 | date,
10 | binary: Buffer.alloc(128),
11 | },
12 | );
13 |
14 | const start = performance.now();
15 |
16 | for (let i = 0; i < 50000; i++) {
17 | const date = new Date();
18 | const data = bson.serialize(
19 | {
20 | foo: "bar",
21 | hello: 55,
22 | date,
23 | binary: Buffer.alloc(128),
24 | },
25 | );
26 | }
27 |
28 | const stop = performance.now();
29 | console.log(stop - start);
30 |
--------------------------------------------------------------------------------
/bson/src/encoder/value.rs:
--------------------------------------------------------------------------------
1 | use bson::Bson;
2 | use wasm_bindgen::JsValue;
3 |
4 | use super::object;
5 | use crate::Result;
6 |
7 | /// Inspect a generic JsValue, taking into account default javascript values
8 | pub fn inspect(target: &JsValue) -> Result {
9 | if let Some(n) = target.as_f64() {
10 | return Ok(Bson::Double(n));
11 | } else if target.is_string() {
12 | return Ok(Bson::String(target.as_string().unwrap()));
13 | } else if let Some(b) = target.as_bool() {
14 | return Ok(Bson::Boolean(b));
15 | } else if target.is_null() {
16 | return Ok(Bson::Null);
17 | } else if target.is_object() {
18 | return Ok(object::inspect(target)?);
19 | }
20 | Err("type not valid in BSON spec".into())
21 | }
22 |
--------------------------------------------------------------------------------
/protocol/header.ts:
--------------------------------------------------------------------------------
1 | export interface MessageHeader {
2 | messageLength: number;
3 | requestID: number;
4 | responseTo: number;
5 | opCode: number;
6 | }
7 |
8 | export function serializeHeader(header: MessageHeader): Uint8Array {
9 | const view = new DataView(new ArrayBuffer(16));
10 | view.setInt32(0, header.messageLength, true);
11 | view.setInt32(4, header.requestID, true);
12 | view.setInt32(8, header.responseTo, true);
13 | view.setInt32(12, header.opCode, true);
14 | return new Uint8Array(view.buffer);
15 | }
16 |
17 | export function parseHeader(buf: Uint8Array): MessageHeader {
18 | const view = new DataView(buf.buffer);
19 | return {
20 | messageLength: view.getInt32(0, true),
21 | requestID: view.getInt32(4, true),
22 | responseTo: view.getInt32(8, true),
23 | opCode: view.getInt32(12, true),
24 | };
25 | }
26 |
--------------------------------------------------------------------------------
/bson/bench/deno_bench.ts:
--------------------------------------------------------------------------------
1 | import { Binary, encode } from "../mod.ts";
2 | import { BinarySubtype } from "../types.ts";
3 | import {
4 | bench,
5 | runBenchmarks,
6 | } from "https://deno.land/std@0.74.0/testing/bench.ts";
7 |
8 | bench({
9 | name: "encode",
10 | runs: 3,
11 | func(b) {
12 | const date = new Date();
13 | const data = encode(
14 | {
15 | foo: "bar",
16 | hello: 55,
17 | date,
18 | binary: Binary(new Uint8Array(128), BinarySubtype.Generic),
19 | },
20 | );
21 | b.start();
22 | for (let i = 0; i < 50000; i++) {
23 | const data = encode(
24 | {
25 | foo: "bar",
26 | hello: 55,
27 | date,
28 | binary: Binary(new Uint8Array(128), BinarySubtype.Generic),
29 | },
30 | );
31 | }
32 | b.stop();
33 | },
34 | });
35 |
36 | await runBenchmarks();
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 the denodrivers team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/bson/src/lib.rs:
--------------------------------------------------------------------------------
1 | use wasm_bindgen::prelude::*;
2 |
3 | type Result = std::result::Result;
4 |
5 | mod encoder;
6 |
7 | #[wasm_bindgen]
8 | pub fn to_bson_document(target: &JsValue) -> Result> {
9 | if !target.is_object() {
10 | return Err(js_sys::Error::new("only object can be serialized to bson documents").into());
11 | }
12 | let document = encoder::create_document(&target)?;
13 | let mut buf: Vec = vec![];
14 | document.to_writer(&mut buf).map_err(|err| {
15 | js_sys::Error::new(&format!("error writing document: {}", err.to_string()))
16 | })?;
17 | Ok(buf)
18 | }
19 |
20 | #[wasm_bindgen]
21 | pub fn from_bson_document(buf: Vec) -> Result {
22 | let mut x: &[u8] = &buf;
23 | let document = bson::Document::from_reader(&mut x).map_err(|err| {
24 | js_sys::Error::new(&format!("error parsing document: {}", err.to_string()))
25 | })?;
26 | Ok(JsValue::from_serde(&document).map_err(|err| {
27 | js_sys::Error::new(&format!(
28 | "error serializing document to json: {}",
29 | err.to_string()
30 | ))
31 | })?)
32 | }
33 |
--------------------------------------------------------------------------------
/bson/types.ts:
--------------------------------------------------------------------------------
1 | export interface ObjectID {
2 | $oid: string;
3 | }
4 | export interface DateTime {
5 | $date: {
6 | $numberLong: string;
7 | };
8 | }
9 | export interface Double {
10 | $numberDouble: string;
11 | }
12 | export interface Int32 {
13 | $numberInt: string;
14 | }
15 | export interface Int64 {
16 | $numberLong: string;
17 | }
18 | export interface RegularExpression {
19 | $regularExpression: {
20 | pattern: string;
21 | options: string;
22 | };
23 | }
24 | export interface Timestamp {
25 | $timestamp: {
26 | t: number;
27 | i: number;
28 | };
29 | }
30 | export enum BinarySubtype {
31 | Generic = "00",
32 | Function = "01",
33 | _Binary = "02",
34 | _UUID = "03",
35 | UUID = "04",
36 | MD5 = "05",
37 | Encrypted = "06",
38 | UserDefined = "80",
39 | }
40 | export interface Binary {
41 | $binary: {
42 | base64: string;
43 | subType: BinarySubtype;
44 | };
45 | }
46 | export interface MaxKey {
47 | $maxKey: 1;
48 | }
49 | export interface MinKey {
50 | $minKey: 1;
51 | }
52 | export type BsonField =
53 | | BsonObject
54 | | ObjectID
55 | | DateTime
56 | | Double
57 | | Int32
58 | | Int64
59 | | RegularExpression
60 | | Timestamp
61 | | Binary
62 | | MaxKey
63 | | MinKey
64 | | number
65 | | Date
66 | | string;
67 | export interface BsonObject {
68 | [key: string]: BsonField;
69 | }
70 | export type Document = Uint8Array;
71 |
--------------------------------------------------------------------------------
/bson/src/encoder/number.rs:
--------------------------------------------------------------------------------
1 | use wasm_bindgen::JsValue;
2 |
3 | use crate::Result;
4 |
5 | /// Safely extract a JsValue into a rust String
6 | fn extract_string(target: &JsValue) -> Result {
7 | target
8 | .as_string()
9 | .ok_or_else(|| "failed to extract string value".into())
10 | }
11 |
12 | /// Parse a string JsValue into an i64
13 | pub(crate) fn long(target: &JsValue) -> Result {
14 | let n = extract_string(target)?;
15 | let n = n
16 | .parse::()
17 | .map_err(|err| format!("error converting $numberLong value: {}", err.to_string()))?;
18 | Ok(n)
19 | }
20 |
21 | /// Parse a string JsValue into an i32
22 | pub(crate) fn int(target: &JsValue) -> Result {
23 | let n = extract_string(target)?;
24 | let n = n
25 | .parse::()
26 | .map_err(|err| format!("error converting $numberInt value: {}", err.to_string()))?;
27 | Ok(n)
28 | }
29 |
30 | /// Parse a string JsValue into an f64
31 | pub(crate) fn double(target: &JsValue) -> Result {
32 | let n = extract_string(target)?;
33 | match n.as_str() {
34 | "Infinity" => Ok(f64::INFINITY),
35 | "-Infinity" => Ok(f64::NEG_INFINITY),
36 | _ => {
37 | let n = n.parse::().map_err(|err| {
38 | format!("error converting $numberDouble value: {}", err.to_string())
39 | })?;
40 | Ok(n)
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/bson/bench/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | base64-js@^1.0.2:
6 | version "1.3.1"
7 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
8 | integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
9 |
10 | bson@4.1.0:
11 | version "4.1.0"
12 | resolved "https://registry.yarnpkg.com/bson/-/bson-4.1.0.tgz#8135dbaaf90d78d0ee4675f31600162d4c12be28"
13 | integrity sha512-xwNzRRsK2xmHvHuPESi0zVfgsm4edO47WdulNaShTriunNUMRDOAlKwpJc8zpkOXczIzbxUD7kFzhUGVYoZLDw==
14 | dependencies:
15 | buffer "^5.1.0"
16 | long "^4.0.0"
17 |
18 | buffer@^5.1.0:
19 | version "5.6.0"
20 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786"
21 | integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==
22 | dependencies:
23 | base64-js "^1.0.2"
24 | ieee754 "^1.1.4"
25 |
26 | ieee754@^1.1.4:
27 | version "1.1.13"
28 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
29 | integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
30 |
31 | long@^4.0.0:
32 | version "4.0.0"
33 | resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
34 | integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
35 |
--------------------------------------------------------------------------------
/bson/scripts/generate.ts:
--------------------------------------------------------------------------------
1 | /// wasm-bindgen -> deno (without --allow-read)
2 | /// Generate a single javascript file that handles wasm loading from a const var
3 |
4 | const configuration = {
5 | in: {
6 | dir: "build",
7 | wasm: "mango_bson_bg.wasm",
8 | glue: "mango_bson.js",
9 | },
10 | out: {
11 | dir: "wasm",
12 | file: "mango_bson.js",
13 | },
14 | };
15 |
16 | // ... do the magic ...
17 |
18 | import { basename, join } from "https://deno.land/std@0.74.0/path/mod.ts";
19 | import { encode } from "https://deno.land/std@0.74.0/encoding/base64.ts";
20 | import { minify } from "https://jspm.dev/terser@5.3.8";
21 |
22 | configuration.in.wasm = join(configuration.in.dir, configuration.in.wasm);
23 | configuration.in.glue = join(configuration.in.dir, configuration.in.glue);
24 | configuration.out.file = join(configuration.out.dir, configuration.out.file);
25 |
26 | const wasmsrc = await Deno.readFile(configuration.in.wasm);
27 | const gluesrc = await Deno.readTextFile(configuration.in.glue);
28 |
29 | const wasmencoded = encode(wasmsrc);
30 |
31 | const target = `const file = new URL(import.meta.url).pathname;
32 | const wasmFile = file.substring(0, file.lastIndexOf(Deno.build.os === 'windows' ? '\\\\' : '/') + 1) + '${
33 | basename(configuration.in.wasm)
34 | }';
35 | const wasmModule = new WebAssembly.Module(Deno.readFileSync(wasmFile));`;
36 |
37 | const replace =
38 | `import { decode } from "https://deno.land/std@0.74.0/encoding/base64.ts";
39 | const encoded = "${wasmencoded}";
40 | const source = decode(encoded);
41 | const wasmModule = new WebAssembly.Module(source);`;
42 |
43 | const data = gluesrc.replace(target, replace);
44 |
45 | const output = await minify(data, {
46 | mangle: { module: true },
47 | output: {
48 | preamble: "//deno-fmt-ignore-file",
49 | },
50 | });
51 |
52 | await Deno.writeTextFile(configuration.out.file, output.code);
53 |
--------------------------------------------------------------------------------
/tmp/app.ts:
--------------------------------------------------------------------------------
1 | const conn = await Deno.connect({
2 | hostname: "0.0.0.0", // or "mango_mongo" if this script is ran inside a container
3 | port: 27017
4 | })
5 |
6 | async function listen () {
7 | (async () => {
8 | try {
9 | for await (const chunk of Deno.iter(conn)) {
10 | console.log('got msg')
11 | const response = new TextDecoder().decode(chunk)
12 | console.log(response)
13 | }
14 | } catch (e) {
15 | console.log('got error')
16 | console.error(e.stack);
17 | }
18 | })();
19 | }
20 |
21 | await listen()
22 |
23 | // see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#standard-message-header
24 | type MsgHeader = {
25 | messageLength: number // total message size, including this
26 | requestID: number // Identifier for this message
27 | opCode: number // Request type, see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#request-opcodes
28 | }
29 | type ResponseHeader = MsgHeader & {
30 | responseTo: number // requestId from the original request (used in response from db)
31 | }
32 | // Base type for OP's, to reuse between all actual OP's. Not official
33 | type OP_BASE = {
34 | header: MsgHeader,
35 | flags: number,
36 | fullCollectionName: string // "."
37 | }
38 | // TODO
39 | type document = {}
40 | // see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/#op-query
41 | type OP_QUERY = OP_BASE & {
42 | numberToSkip: number // Number of documents to skip
43 | numberToReturn: number // Number of documents to return
44 | query: document // query object, see link above
45 | returnFieldsSelector?: document // Optional. Selector indicating the fields to return. See link above for more details
46 | }
47 | // TODO :: add all other `OP_*`
48 | // ...
49 | const query: OP_QUERY = {
50 | header: {
51 | messageLength: 0,
52 | requestID: 1,
53 | opCode: 2004
54 | },
55 | flags: 5,
56 | fullCollectionName: "juanportal.profiles",
57 | numberToSkip: 0,
58 | numberToReturn: 1,
59 | query: "" // TODO
60 | }
61 |
62 | //await conn.write(new TextEncoder().encode(JSON.stringify(query)))
63 |
--------------------------------------------------------------------------------
/protocol/mod.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assert,
3 | assertEquals,
4 | BufReader,
5 | Deferred,
6 | deferred,
7 | } from "../deps.ts";
8 | import { MessageHeader, parseHeader, serializeHeader } from "./header.ts";
9 | import { parseOpMsg, Section, serializeOpMsg } from "./op_msg.ts";
10 |
11 | export class MongoProtocol {
12 | #reader: BufReader;
13 | #socket: Deno.Writer & Deno.Closer;
14 | #requestIDCounter = 0;
15 | #ops: Record> = {};
16 | #polling = false;
17 |
18 | constructor(socket: Deno.Reader & Deno.Writer & Deno.Closer) {
19 | this.#reader = new BufReader(socket);
20 | this.#socket = socket;
21 | }
22 |
23 | private async send(header: MessageHeader, body: Uint8Array) {
24 | const buf = serializeHeader(header);
25 | await Deno.writeAll(this.#socket, buf);
26 | await Deno.writeAll(this.#socket, body);
27 | }
28 |
29 | private async wake() {
30 | if (this.#polling) return;
31 | await this.receiveLoop();
32 | }
33 |
34 | private async receiveLoop() {
35 | this.#polling = true;
36 | while (Object.keys(this.#ops).length > 0) {
37 | const headerBuf = await this.#reader.readFull(new Uint8Array(16));
38 | assert(headerBuf);
39 | const header = parseHeader(headerBuf);
40 |
41 | const buf = await this.#reader.readFull(
42 | new Uint8Array(header.messageLength - 16),
43 | );
44 | assert(buf);
45 |
46 | this.#ops[header.responseTo].resolve([header, buf]);
47 | delete this.#ops[header.responseTo];
48 | }
49 | this.#polling = false;
50 | }
51 |
52 | private async receive(reqID: number): Promise<[MessageHeader, Uint8Array]> {
53 | this.#ops[reqID] = deferred();
54 | this.wake();
55 | const resp = await this.#ops[reqID];
56 | return resp;
57 | }
58 |
59 | private nextRequestID(): number {
60 | this.#requestIDCounter++;
61 | return this.#requestIDCounter;
62 | }
63 |
64 | async executeOpMsg(sections: Section[]): Promise {
65 | const reqID = this.nextRequestID();
66 | const op = serializeOpMsg({
67 | sections,
68 | });
69 | await this.send({
70 | messageLength: 16 + op.byteLength,
71 | opCode: 2013,
72 | requestID: reqID,
73 | responseTo: 0,
74 | }, op);
75 | const [header, buf] = await this.receive(reqID);
76 | assertEquals(header.opCode, 2013);
77 |
78 | const res = parseOpMsg(buf);
79 |
80 | return res.sections;
81 | }
82 |
83 | close() {
84 | this.#socket.close();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/bson/src/encoder/object.rs:
--------------------------------------------------------------------------------
1 | use bson::{Bson, Document};
2 | use chrono::prelude::*;
3 | use wasm_bindgen::{JsCast, JsValue};
4 |
5 | use super::{extended, value};
6 | use crate::Result;
7 |
8 | /// Inspect an object JsValue, taking into account default javascript values
9 | pub fn inspect(target: &JsValue) -> Result {
10 | if let Some(date) = target.dyn_ref::() {
11 | // Date
12 | let ms: f64 = date.get_time(); // [ms]
13 | let secs = (ms / 1e3).floor() as i64; // [s]
14 | let nsecs = ((ms % 1e3) * 1e6).ceil() as u32; // [ns]
15 | let date = chrono::Utc.timestamp(secs, nsecs);
16 | return Ok(Bson::DateTime(date));
17 | } else if let Some(iterable) = target.dyn_ref::() {
18 | // Array
19 | let mut array = vec![];
20 | for x in iterable.iter() {
21 | array.push(value::inspect(&x)?)
22 | }
23 | return Ok(Bson::Array(array));
24 | } else if let Some(iterable) = target.dyn_ref::() {
25 | // Set
26 | let mut array = vec![];
27 | for x in iterable.keys() {
28 | let x = x?;
29 | array.push(value::inspect(&x)?)
30 | }
31 | return Ok(Bson::Array(array));
32 | } else if let Some(map) = target.dyn_ref::() {
33 | // Map
34 | let mut document = Document::default();
35 | for key in map.keys() {
36 | let key = key?;
37 | let val = map.get(&key);
38 | let key = key
39 | .as_string()
40 | .ok_or_else(|| "only object can be serialized to bson documents")?;
41 | document.insert(key, value::inspect(&val)?);
42 | }
43 | return Ok(Bson::Document(document));
44 | } else if let Some(ext) = extended::inspect(target)? {
45 | // { $type: ... } objects
46 | return Ok(ext);
47 | }
48 |
49 | // Plain JS object
50 | return Ok(Bson::Document(create_document(target)?));
51 | }
52 |
53 | // Create a BSON decument from a pure javascript object
54 | pub fn create_document(target: &JsValue) -> Result {
55 | let mut document = Document::default();
56 | let keys = js_sys::Reflect::own_keys(target)?;
57 | for key in keys.iter() {
58 | let val = js_sys::Reflect::get(target, &key)?;
59 | let key = key
60 | .as_string()
61 | .ok_or_else(|| "failed to extract object key")?;
62 | document.insert(key, value::inspect(&val)?);
63 | }
64 | Ok(document)
65 | }
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Mango
4 |
5 | A MongoDB driver for Deno.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ---
22 |
23 | ---
24 |
25 | > ⚠️ Work in progress. Expect breaking changes.
26 |
27 | ---
28 |
29 | ## Table of Contents
30 | - [Quick Start](#quick-start)
31 | - [Documentation](#documentation)
32 | - [Features](#features)
33 | - [Maintainers](#maintainers)
34 | - [Other](#other)
35 | - [Related](#related)
36 | - [Contribution](#contribution)
37 | - [Licence](#licence)
38 |
39 | ## Quick Start
40 |
41 | Subject to change, here as a placeholder mainly. In progress
42 |
43 | ```typescript
44 | import { Mango } from "https://deno.land/x/mango/mod.ts"
45 |
46 | const client = new Mango({
47 | // ... configs
48 | })
49 |
50 | const db = await client.connect()
51 |
52 | const profileCollection = db.collection("profiles")
53 |
54 | const profiles = await profileCollection.find({})
55 | ```
56 |
57 | ## Documentation
58 |
59 | In progress
60 |
61 | Link to doc.deno.land api documentation, or github pages, or add documentation here
62 |
63 | ## Features
64 |
65 | In progress
66 |
67 | - [x] Connecting to the database
68 | - [ ] Find documents for a given collection
69 | - [ ] ...
70 |
71 | ## Maintainers
72 |
73 | - Filippo Rossi ([@qu4k](https://github.com/qu4k))
74 | - Edward Bebbington ([@ebebbington](https://github.com/ebebbington))
75 | - Luca Casonato ([@lucacasonato](https://github.com/lucacasonato))
76 |
77 | ## Other
78 |
79 | ### Related
80 |
81 | - [bson](https://github.com/mongodb/bson-rust) - rust bson crate
82 |
83 | ### Contribution
84 |
85 | Pull request, issues and feedback are very welcome. Code style is formatted with `deno fmt` and `cargo fmt` and commit messages are done following Conventional Commits spec.
86 |
87 | ### Licence
88 |
89 | Copyright 2020, the denodrivers team. All rights reserved. MIT license.
90 |
--------------------------------------------------------------------------------
/bson/mod.ts:
--------------------------------------------------------------------------------
1 | import {
2 | from_bson_document as fromBsonDocument,
3 | to_bson_document as toBsonDocument,
4 | } from "./wasm/mango_bson.js";
5 |
6 | import * as types from "./types.ts";
7 |
8 | import { BinarySubtype } from "./types.ts";
9 | export { BinarySubtype, types };
10 |
11 | export function ObjectID(oid: string): types.ObjectID {
12 | return { $oid: oid };
13 | }
14 |
15 | export function DateTime(ms: number): types.DateTime {
16 | return { $date: { $numberLong: String(ms) } };
17 | }
18 |
19 | export function Double(val: number): types.Double {
20 | return { $numberDouble: String(val) };
21 | }
22 |
23 | export function Int32(val: number): types.Int32 {
24 | return { $numberInt: String(val) };
25 | }
26 |
27 | export function Int64(val: number): types.Int64 {
28 | return { $numberLong: String(val) };
29 | }
30 |
31 | export function Regex(val: RegExp): types.RegularExpression {
32 | return { $regularExpression: { pattern: val.source, options: val.flags } };
33 | }
34 |
35 | export function Timestamp(t: number, i: number): types.Timestamp {
36 | return { $timestamp: { t, i } };
37 | }
38 |
39 | export function Binary(
40 | payload: Uint8Array,
41 | subType: types.BinarySubtype,
42 | ): types.Binary {
43 | const output: string = Array.from(payload)
44 | .map((val): string => String.fromCharCode(val))
45 | .join("");
46 | const base64 = btoa(output);
47 | return { $binary: { base64, subType } };
48 | }
49 |
50 | export function MaxKey(): types.MaxKey {
51 | return { $maxKey: 1 };
52 | }
53 |
54 | export function MinKey(): types.MinKey {
55 | return { $minKey: 1 };
56 | }
57 |
58 | export function encode(object: types.BsonObject): Uint8Array {
59 | return toBsonDocument(object);
60 | }
61 |
62 | export function encodeDocuments(object: types.BsonObject[]): Uint8Array {
63 | const buffers = object.map(toBsonDocument);
64 | let total = 0;
65 | for (const buffer of buffers) {
66 | total += buffer.byteLength;
67 | }
68 | const final = new Uint8Array(total);
69 | let seek = 0;
70 | for (const buffer of buffers) {
71 | final.set(buffer, seek);
72 | seek += buffer.byteLength;
73 | }
74 | return final;
75 | }
76 |
77 | export function decode(buf: Uint8Array): types.BsonObject {
78 | // TODO(lucacasonato): rehydrate this
79 | return fromBsonDocument(buf);
80 | }
81 |
82 | /**
83 | * This function decodes all of the documents in the Uint8Array.
84 | */
85 | export function decodeDocuments(buf: Uint8Array): types.BsonObject[] {
86 | const view = new DataView(buf.buffer);
87 | let seek = 0;
88 | const documents = [];
89 | while (seek < buf.length) {
90 | const len = view.getInt32(seek, true);
91 | documents.push(decode(buf.subarray(seek, seek + len + 1)));
92 | seek += len;
93 | }
94 | return documents;
95 | }
96 |
--------------------------------------------------------------------------------
/protocol/op_msg.ts:
--------------------------------------------------------------------------------
1 | import {
2 | decode,
3 | decodeDocuments,
4 | encode,
5 | encodeDocuments,
6 | types,
7 | } from "../bson/mod.ts";
8 | import { assertEquals } from "../deps.ts";
9 |
10 | export type Section = Section0 | Section1;
11 |
12 | const encoder = new TextEncoder();
13 | const decoder = new TextDecoder();
14 |
15 | export interface OpMsg {
16 | sections: Section[];
17 | }
18 |
19 | export function serializeOpMsg(op: OpMsg): Uint8Array {
20 | const sections = op.sections.map(serializeSection);
21 | let sectionLen = 0;
22 | for (const section of sections) {
23 | sectionLen += section.byteLength;
24 | }
25 |
26 | const len = 4 + sectionLen;
27 |
28 | const view = new DataView(new ArrayBuffer(len));
29 | // flag bits
30 | view.setInt32(0, 0, true);
31 | let seek = 4;
32 | for (const section of sections) {
33 | new Uint8Array(view.buffer).set(section, seek);
34 | seek += section.byteLength;
35 | }
36 | return new Uint8Array(view.buffer);
37 | }
38 |
39 | function serializeSection(section: Section): Uint8Array {
40 | if (section.kind === 0) {
41 | return serializeSection0(section);
42 | } else if (section.kind === 1) {
43 | return serializeSection1(section);
44 | }
45 | throw new TypeError("Invalid section kind");
46 | }
47 |
48 | export interface Section0 {
49 | kind: 0;
50 | body: types.BsonObject;
51 | }
52 |
53 | function serializeSection0(section: Section0): Uint8Array {
54 | const body = encode(section.body);
55 | const buf = new Uint8Array(1 + body.byteLength);
56 | buf[0] = 0;
57 | buf.set(body, 1);
58 | return buf;
59 | }
60 |
61 | export interface Section1 {
62 | kind: 1;
63 | documentSequenceIdentifier: string;
64 | objects: types.BsonObject[];
65 | }
66 |
67 | function serializeSection1(section: Section1): Uint8Array {
68 | const documentSequenceIdentifier = encoder.encode(
69 | section.documentSequenceIdentifier,
70 | );
71 | const objects = encodeDocuments(section.objects);
72 | const buf = new Uint8Array(
73 | 1 + 4 + documentSequenceIdentifier.byteLength + 1 + objects.byteLength,
74 | );
75 | buf[0] = 0;
76 | new DataView(buf.buffer).setInt32(
77 | 1,
78 | 4 + documentSequenceIdentifier.byteLength + 1 + objects.byteLength,
79 | true,
80 | );
81 | buf.set(documentSequenceIdentifier, 1 + 4);
82 | buf.set(objects, 1 + 4 + documentSequenceIdentifier.byteLength + 1);
83 | return buf;
84 | }
85 |
86 | export function parseOpMsg(buf: Uint8Array): OpMsg {
87 | const view = new DataView(buf.buffer);
88 | const flagBits = view.getInt32(0, true);
89 | assertEquals(flagBits >> 16, 0);
90 |
91 | const sections: Section[] = [];
92 | let seek = 4;
93 | while (seek < buf.byteLength) {
94 | const kind = view.getInt8(seek);
95 | seek++;
96 | if (kind === 0) {
97 | const len = view.getInt32(seek, true);
98 | sections.push({ kind: 0, body: decode(buf.slice(seek, seek + len)) });
99 | seek += len;
100 | } else if (kind === 1) {
101 | const len = view.getInt32(seek, true);
102 | let documentSequenceIdentifierLen = 0;
103 | for (const byte of buf) {
104 | if (byte === 0) {
105 | break;
106 | }
107 | documentSequenceIdentifierLen++;
108 | }
109 | const documentSequenceIdentifier = decoder.decode(
110 | buf.subarray(4, 4 + documentSequenceIdentifierLen),
111 | );
112 | const objects = decodeDocuments(
113 | buf.slice(4 + documentSequenceIdentifierLen + 1),
114 | );
115 | sections.push({ kind: 1, documentSequenceIdentifier, objects });
116 | } else {
117 | throw new Error("Invalid section kind");
118 | }
119 | }
120 |
121 | return { sections };
122 | }
123 |
--------------------------------------------------------------------------------
/bson/src/encoder/extended.rs:
--------------------------------------------------------------------------------
1 | use bson::{oid::ObjectId, Bson};
2 | use chrono::prelude::*;
3 | use wasm_bindgen::JsValue;
4 |
5 | use super::number;
6 | use crate::Result;
7 |
8 | /// `{“$oid”: ””}`
9 | /// : A 24-character, big-endian hexadecimal string that represents the ObjectId bytes.
10 | fn oid(target: &JsValue) -> Result {
11 | let oid = target
12 | .as_string()
13 | .ok_or_else(|| "failed to extract object id value")?;
14 | Ok(Bson::ObjectId(ObjectId::with_string(&oid).map_err(
15 | |err| format!("error in ObjectID value: {}", err.to_string()),
16 | )?))
17 | }
18 |
19 | /// `{"$date": {"$numberLong": ""}}`
20 | /// : A 64-bit signed integer as string. The value represents milliseconds relative to the epoch.
21 | fn date(target: &JsValue) -> Result {
22 | let ms = js_sys::Reflect::get(target, &JsValue::from_str("$numberLong"))?;
23 | let ms = number::long(&ms)?;
24 | let secs = ms / 1e3 as i64; // [s]
25 | let nsecs = ((ms % 1e3 as i64) * 1e6 as i64) as u32; // [ns]
26 | let date = chrono::Utc.timestamp(secs, nsecs);
27 | Ok(Bson::DateTime(date))
28 | }
29 |
30 | /// {"$timestamp": {"t": , "i": }}
31 | /// : A positive integer for the seconds since epoch.
32 | /// : A positive integer for the increment.
33 | fn timestamp(target: &JsValue) -> Result {
34 | let t = js_sys::Reflect::get(target, &JsValue::from_str("t"))?;
35 | let i = js_sys::Reflect::get(target, &JsValue::from_str("i"))?;
36 | let t = t.as_f64().ok_or_else(|| "invalid t in $timestamp")?;
37 | let i = i.as_f64().ok_or_else(|| "invalid i in $timestamp")?;
38 | let time = (t / 1e3) as u32; // [s]
39 | let increment = i as u32;
40 | Ok(Bson::Timestamp(bson::Timestamp { time, increment }))
41 | }
42 |
43 | /// { "$regularExpression": { "pattern": "", "options": "" } }
44 | /// : A string that corresponds to the regular expression pattern.
45 | /// The string can contain valid JSON characters and unescaped double quote (") characters,
46 | /// but may not contain unescaped forward slash (/) characters.
47 | /// : A string that specifies BSON regular expression options (‘g’, ‘i’, ‘m’ and ‘s’) or an empty string "".
48 | /// Options other than (‘g’, ‘i’, ‘m’ and ‘s’) will be dropped when converting to this representation.
49 | /// !! The options MUST be in alphabetical order.
50 | fn regex(target: &JsValue) -> Result {
51 | let pattern = js_sys::Reflect::get(target, &JsValue::from_str("pattern"))?;
52 | let options = js_sys::Reflect::get(target, &JsValue::from_str("options"))?;
53 | let pattern = pattern
54 | .as_string()
55 | .ok_or_else(|| "invalid pattern in $regularExpression")?;
56 | let options = options
57 | .as_string()
58 | .ok_or_else(|| "invalid options in $regularExpression")?;
59 |
60 | // sort options...
61 | let mut chars = options.chars().collect::>();
62 | chars.sort_by(|a, b| a.cmp(b));
63 | let s = chars.into_iter().collect::();
64 | let options = String::from(s.trim());
65 |
66 | Ok(Bson::RegularExpression(bson::Regex { pattern, options }))
67 | }
68 |
69 | /// {"$binary": {"base64": , "subType": }}
70 | /// : Base64 encoded (with padding as “=”) payload string.
71 | /// : A one- or two-character hex string that corresponds to a BSON binary subtype.
72 | fn binary(target: &JsValue) -> Result {
73 | let bytes = js_sys::Reflect::get(target, &JsValue::from_str("base64"))?;
74 | let subtype = js_sys::Reflect::get(target, &JsValue::from_str("subType"))?;
75 | let bytes = bytes
76 | .as_string()
77 | .ok_or_else(|| "invalid base64 in $binary")?;
78 | let subtype = subtype
79 | .as_string()
80 | .ok_or_else(|| "invalid subType in $binary")?;
81 | let bytes = base64::decode(bytes)
82 | .map_err(|err| format!("invalid base64 in $binary: {}", err.to_string()))?;
83 | let subtype = hex::decode(subtype)
84 | .map_err(|err| format!("invalid subType in $binary: {}", err.to_string()))?;
85 |
86 | if subtype.len() == 1 {
87 | Ok(Bson::Binary(bson::Binary {
88 | bytes,
89 | subtype: subtype[0].into(),
90 | }))
91 | } else {
92 | Err("invalid subType in $binary".into())
93 | }
94 | }
95 |
96 | /// Inspect an extended JSON JsValue
97 | /// For reference: https://docs.mongodb.com/manual/reference/mongodb-extended-json/
98 | pub fn inspect(target: &JsValue) -> Result