diff --git a/__tests__/error.re b/__tests__/error.re index 93ddd09..9a4612f 100644 --- a/__tests__/error.re +++ b/__tests__/error.re @@ -5,66 +5,71 @@ let connect = () => describe("MySql2 Error Handling", () => { let conn = connect(); - afterAll(() => MySql2.close(conn)); - let accessDeniedTest = "Should respond with an access denied error"; - testAsync(accessDeniedTest, finish => { - let c = MySql2.connect(~password="s0m3 g@rb@g3 pw", ()); - let sql = "SELECT 1+1 AS result"; - MySql2.execute(c, sql, None, res => { - switch res { - | `Select(_,_) => fail("unexpected_select_result") |> finish - | `Mutation(_,_) => fail("unexpected_mutation_result") |> finish - | `Error(e) => { - Expect.expect(() => raise(e)) + testAsync( + accessDeniedTest, + finish => { + let c = MySql2.connect(~password="s0m3 g@rb@g3 pw", ()); + let sql = "SELECT 1+1 AS result"; + MySql2.execute(c, sql, None, res => + switch (res) { + | `Select(_, _) => fail("unexpected_select_result") |> finish + | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish + | `Error(e) => + Expect.expect(() => + raise(e) + ) |> Expect.toThrowMessage("ER_ACCESS_DENIED_ERROR") |> finish } - } - }) - }); - + ); + }, + ); let syntaxErrorTest = "Should respond with an error on invalid SQL syntax."; - testAsync(syntaxErrorTest, finish => { - let sql = "SELECT invalid, AS result"; - MySql2.execute(conn, sql, None, res => { - switch res { - | `Select(_,_) => fail("unexpected_select_result") |> finish - | `Mutation(_,_) => fail("unexpected_mutation_result") |> finish - | `Error(e) => { - Expect.expect(() => raise(e)) + testAsync( + syntaxErrorTest, + finish => { + let sql = "SELECT invalid, AS result"; + MySql2.execute(conn, sql, None, res => + switch (res) { + | `Select(_, _) => fail("unexpected_select_result") |> finish + | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish + | `Error(e) => + Expect.expect(() => + raise(e) + ) |> Expect.toThrowMessage("ER_PARSE_ERROR") |> finish } - } - }) - }); - + ); + }, + ); let emptyErrorTest = "Should parse out an empty error with defaults"; - test(emptyErrorTest, () => { - try ( - Js.Exn.raiseError("IDKWTM") - ) { - | Js.Exn.Error(e) => { - let exn = MySql2.Error.from_js(e); - Expect.expect(() => raise(exn)) - |> Expect.toThrowMessage("99999 (99999) - IDKWTM") - } + test(emptyErrorTest, () => + try (Js.Exn.raiseError("IDKWTM")) { + | Js.Exn.Error(e) => + let exn = MySql2.Error.from_js(e); + Expect.expect(() => + raise(exn) + ) + |> Expect.toThrowMessage("99999 (99999) - IDKWTM"); } - }); - + ); let nonErrorObjectTest = "Should return a defaulted error"; - test(nonErrorObjectTest, () => { + test(nonErrorObjectTest, () => try ( - /* Use raw JS here to throw a garbage exception object */ - [%raw {|(function () { throw {} })()|}] + [%raw + /* Use raw JS here to throw a garbage exception object */ + {|(function () { throw {} })()|} + ] ) { - | Js.Exn.Error(e) => { - let exn = MySql2.Error.from_js(e); - Expect.expect(()=> raise(exn)) - |> Expect.toThrowMessage("UNKNOWN - 99999 (99999) - EMPTY_MESSAGE") - } + | Js.Exn.Error(e) => + let exn = MySql2.Error.from_js(e); + Expect.expect(() => + raise(exn) + ) + |> Expect.toThrowMessage("UNKNOWN - 99999 (99999) - EMPTY_MESSAGE"); } - }); + ); }); diff --git a/__tests__/parse_response.re b/__tests__/parse_response.re index 977e96a..5678fe7 100644 --- a/__tests__/parse_response.re +++ b/__tests__/parse_response.re @@ -2,25 +2,29 @@ open Jest; describe("MySql2.parse_response", () => { test("Should return an error when given an unexpected boolean.", () => { - let invalid = Js.Json.boolean(Js.true_); + let invalid = Js.Json.boolean(true); let message = switch (MySql2.parse_response(invalid, [||])) { - | `Select(_,_) => Failure("invalid_select_result") - | `Mutation(_,_) => Failure("invalid_mutation_result") + | `Select(_, _) => Failure("invalid_select_result") + | `Mutation(_, _) => Failure("invalid_mutation_result") | `Error(e) => e }; - Expect.expect(() => raise(message)) + Expect.expect(() => + raise(message) + ) |> Expect.toThrowMessage("invalid_driver_result"); }); test("Should return an error when given an unexpected string", () => { let invalid = Js.Json.string("invalid"); let message = switch (MySql2.parse_response(invalid, [||])) { - | `Select(_,_) => Failure("invalid_select_result") - | `Mutation(_,_) => Failure("invalid_mutation_result") + | `Select(_, _) => Failure("invalid_select_result") + | `Mutation(_, _) => Failure("invalid_mutation_result") | `Error(e) => e }; - Expect.expect(() => raise(message)) + Expect.expect(() => + raise(message) + ) |> Expect.toThrowMessage("invalid_driver_result"); }); }); diff --git a/__tests__/query.re b/__tests__/query.re index e26cb41..02f2963 100644 --- a/__tests__/query.re +++ b/__tests__/query.re @@ -5,26 +5,26 @@ let connect = () => type insert = { affected_rows: int, - insert_id: option(int) + insert_id: option(int), }; type thing = { id: int, - code: string + code: string, }; -let raiseError = (exn) => exn |> raise; +let raiseError = exn => exn |> raise; let onSelect = (next, fn, res) => - switch res { - | `Error(e) => raise(e); + switch (res) { + | `Error(e) => raise(e) | `Mutation(_) => fail("unexpected_mutation_result") |> next | `Select(rows, meta) => fn(rows, meta, next) -}; + }; let onMutation = (next, fn, res) => - switch res { - | `Error(e) => raise(e); + switch (res) { + | `Error(e) => raise(e) | `Mutation(count, id) => fn(count, id, next) | `Select(_, _) => fail("unexpected_select_result") |> next }; @@ -33,14 +33,19 @@ describe("Raw SQL Query Test", () => { let conn = connect(); afterAll(() => MySql2.close(conn)); testAsync("Expect a test database to be listed", finish => - MySql2.execute(conn, "SHOW DATABASES", None, onSelect(finish, (rows, _, next) => - rows - |> Js.Array.map(Json.Decode.dict(Json.Decode.string)) - |> Js.Array.map(x => Js.Dict.unsafeGet(x, "Database")) - |> Expect.expect - |> Expect.toContain("test") - |> next - )) + MySql2.execute( + conn, + "SHOW DATABASES", + None, + onSelect(finish, (rows, _, next) => + rows + |> Js.Array.map(Json.Decode.dict(Json.Decode.string)) + |> Js.Array.map(x => Js.Dict.unsafeGet(x, "Database")) + |> Expect.expect + |> Expect.toContain("test") + |> next + ), + ) ); }); @@ -54,70 +59,97 @@ describe("Raw SQL Query Test Sequence", () => { ) |}; let drop = next => - MySql2.execute(conn, "DROP TABLE IF EXISTS `test`.`simple`", None, (res) => + MySql2.execute(conn, "DROP TABLE IF EXISTS `test`.`simple`", None, res => switch (res) { | `Error(e) => raiseError(e) - | `Mutation(_,_) => next() - | `Select(_,_) => failwith("unexpected_select_result") + | `Mutation(_, _) => next() + | `Select(_, _) => failwith("unexpected_select_result") } ); let create = next => - MySql2.execute(conn, table_sql, None, (res) => + MySql2.execute(conn, table_sql, None, res => switch (res) { | `Error(e) => raiseError(e) - | `Mutation(_,_) => next() - | `Select(_,_) => failwith("unexpected_select_result") + | `Mutation(_, _) => next() + | `Select(_, _) => failwith("unexpected_select_result") } ); beforeAllAsync(finish => drop(() => create(finish))); afterAll(() => MySql2.close(conn)); testAsync("Expect a mutation result for an INSERT query", finish => { let sql = "INSERT INTO `test`.`simple` (`code`) VALUES ('foo')"; - MySql2.execute(conn, sql, None, onMutation(finish, (count, id, next) => { - let countIsOne = count == 1; - let idIsOne = id == 1; - Expect.expect([|countIsOne, idIsOne|]) - |> Expect.toBeSupersetOf([|true, true|]) - |> next - })) + MySql2.execute( + conn, + sql, + None, + onMutation( + finish, + (count, id, next) => { + let countIsOne = count == 1; + let idIsOne = id == 1; + Expect.expect([|countIsOne, idIsOne|]) + |> Expect.toBeSupersetOf([|true, true|]) + |> next; + }, + ), + ); }); testAsync("Expect a mutation result for an UPDATE query", finish => { let sql = "UPDATE `test`.`simple` SET `code`='foo2' WHERE code='foo'"; - MySql2.execute(conn, sql, None, onMutation(finish, (count, id, next) => { - let countIsOne = count == 1; - let idIsZero = id == 0; - Expect.expect([|countIsOne, idIsZero|]) - |> Expect.toBeSupersetOf([|true, true|]) - |> next - })) + MySql2.execute( + conn, + sql, + None, + onMutation( + finish, + (count, id, next) => { + let countIsOne = count == 1; + let idIsZero = id == 0; + Expect.expect([|countIsOne, idIsZero|]) + |> Expect.toBeSupersetOf([|true, true|]) + |> next; + }, + ), + ); }); testAsync("Expect a SELECT NULL to return an empty array", finish => { let sql = "SELECT NULL FROM `test`.`simple` WHERE false"; let decoder = Json.Decode.dict(Json.Decode.nullable(Json.Decode.string)); - MySql2.execute(conn, sql, None, onSelect(finish, (rows, _, next) => { - Belt_Array.map(rows, decoder) - |> Expect.expect - |> Expect.toHaveLength(0) - |> next - })) + MySql2.execute( + conn, + sql, + None, + onSelect(finish, (rows, _, next) => + Belt_Array.map(rows, decoder) + |> Expect.expect + |> Expect.toHaveLength(0) + |> next + ), + ); }); - testAsync("Expect a SELECT * to respond with all the columns", finish =>{ + testAsync("Expect a SELECT * to respond with all the columns", finish => { let sql = "SELECT * FROM `test`.`simple`"; - let decoder = json => Json.Decode.({ - id: json |> field("id", int), - code: json |> field("code", string) - }); + let decoder = json => + Json.Decode.{ + id: json |> field("id", int), + code: json |> field("code", string), + }; let first_row = x => { let idIsOne = x[0].id == 1; let codeIsFoo = x[0].code == "foo"; [|idIsOne, codeIsFoo|]; }; - MySql2.execute(conn, sql, None, onSelect(finish, (rows, _, next) => { - Belt_Array.map(rows, decoder) - |> first_row - |> Expect.expect - |> Expect.toBeSupersetOf([|true, true|]) - |> next - })) + MySql2.execute( + conn, + sql, + None, + onSelect(finish, (rows, _, next) => + Belt_Array.map(rows, decoder) + |> first_row + |> Expect.expect + |> Expect.toBeSupersetOf([|true, true|]) + |> next + ), + ); }); }); diff --git a/__tests__/with_params.re b/__tests__/with_params.re index b07ed84..999ecee 100644 --- a/__tests__/with_params.re +++ b/__tests__/with_params.re @@ -1,57 +1,69 @@ open Jest; let connect = () => - MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ~password="", ~database="test", ()); + MySql2.connect( + ~host="127.0.0.1", + ~port=3306, + ~user="root", + ~password="", + ~database="test", + (), + ); type result = {result: int}; describe("Test parameter interpolation", () => { let conn = connect(); - let decoder = json => Json.Decode.({ - result: json |> field("result", int) - }); + let decoder = json => Json.Decode.{result: json |> field("result", int)}; afterAll(() => MySql2.close(conn)); - describe("Standard (positional) parameters", () => { + describe("Standard (positional) parameters", () => testAsync("Expect parameters to be substituted properly", finish => { let sql = "SELECT 1 + ? + ? AS result"; - let params = Some(`Positional( - Belt_Array.map([|5,6|], Json.Encode.int) |> Json.Encode.jsonArray - )); - MySql2.execute(conn, sql, params, (res) => + let params = + Some( + `Positional( + Belt_Array.map([|5, 6|], Json.Encode.int) + |> Json.Encode.jsonArray, + ), + ); + MySql2.execute(conn, sql, params, res => switch (res) { | `Error(e) => raise(e) - | `Mutation(_,_) => fail("unexpected_mutation_result") |> finish + | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish | `Select(rows, _) => - Belt_Array.map(rows, decoder) - |> Belt_Array.map(_, x => x.result) - |> Expect.expect - |> Expect.toBeSupersetOf([|12|]) - |> finish + Belt_Array.map(rows, decoder) + |> Belt_Array.map(_, x => x.result) + |> Expect.expect + |> Expect.toBeSupersetOf([|12|]) + |> finish } - ) - }); - }); - describe("Named parameters", () => { + ); + }) + ); + describe("Named parameters", () => testAsync("Expect parameters to be substituted properly", finish => { let sql = "SELECT :x + :y AS result"; - let params = Some(`Named( - Json.Encode.object_([ - ("x", Json.Encode.int(1)), - ("y", Json.Encode.int(2)), - ]) - )); - MySql2.execute(conn, sql, params, (res) => + let params = + Some( + `Named( + Json.Encode.object_([ + ("x", Json.Encode.int(1)), + ("y", Json.Encode.int(2)), + ]), + ), + ); + MySql2.execute(conn, sql, params, res => switch (res) { | `Error(e) => raise(e) - | `Mutation(_,_) => fail("unexpected_mutation_result") |> finish + | `Mutation(_, _) => fail("unexpected_mutation_result") |> finish | `Select(rows, _) => - Belt_Array.map(rows, decoder) - |> Belt_Array.map(_, x => x.result) - |> Expect.expect - |> Expect.toBeSupersetOf([|3|]) - |> finish + Belt_Array.map(rows, decoder) + |> Belt_Array.map(_, x => x.result) + |> Expect.expect + |> Expect.toBeSupersetOf([|3|]) + |> finish } ); - }); - }); + }) + ); }); diff --git a/examples/prepared_statements.re b/examples/prepared_statements.re index b97de8d..ab7c64a 100644 --- a/examples/prepared_statements.re +++ b/examples/prepared_statements.re @@ -1,27 +1,32 @@ +let conn = MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); -let conn = - MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); - -let positional = Some(`Positional( - Belt_Array.map([|5,6|], Json.Encode.int) |> Json.Encode.jsonArray -)); +let positional = + Some( + `Positional( + Belt_Array.map([|5, 6|], Json.Encode.int) |> Json.Encode.jsonArray, + ), + ); MySql2.execute(conn, "SELECT 1 + ? + ? AS result", positional, res => - switch res { + switch (res) { | `Error(e) => Js.log2("ERROR: ", e) | `Mutation(count, id) => Js.log3("Mutation: ", count, id) | `Select(rows, meta) => Js.log3("Select: ", rows, meta) } ); -let named = Some(`Named( - Json.Encode.object_([ - ("x", Json.Encode.int(1)), - ("y", Json.Encode.int(2)), - ]) -)); +let named = + Some( + `Named( + Json.Encode.object_([ + ("x", Json.Encode.int(1)), + ("y", Json.Encode.int(2)), + ]), + ), + ); + MySql2.execute(conn, "SELECT :x + :y AS result", named, res => - switch res { + switch (res) { | `Error(e) => Js.log2("ERROR: ", e) | `Mutation(count, id) => Js.log3("Mutation: ", count, id) | `Select(rows, meta) => Js.log3("Select: ", rows, meta) diff --git a/examples/simple.ml b/examples/simple.ml deleted file mode 100644 index 0d92730..0000000 --- a/examples/simple.ml +++ /dev/null @@ -1,37 +0,0 @@ -let conn = MySql2.connect ~host:"127.0.0.1" ~port:3306 ~user:"root" () - -let test_handler res = - match res with - | `Error e -> Js.log2 "ERROR: " e - | `Select (rows, meta) -> Js.log3 "SELECT: " rows meta - | `Mutation (count, id) -> Js.log3 "MUTATION: " count id - -let _ = MySql2.execute conn "SHOW DATABASES" None test_handler - -let table_sql = {| - CREATE TABLE IF NOT EXISTS test.simple ( - id bigint(20) NOT NULL AUTO_INCREMENT - , code varchar(32) NOT NULL - , PRIMARY KEY(id) - ) -|} - -let _ = MySql2.execute conn table_sql None test_handler - -let simple_insert_sql = "INSERT INTO test.simple (code) VALUES ('foo')" - -let _ = MySql2.execute conn simple_insert_sql None test_handler - -let simple_update_sql = "UPDATE test.simple SET code='foo2' WHERE code='foo'" - -let _ = MySql2.execute conn simple_update_sql None test_handler - -let _ = MySql2.execute - conn - "SELECT NULL FROM test.simple WHERE false" - None - test_handler - -let _ = MySql2.execute conn "SELECT * FROM test.simple" None test_handler - -let _ = MySql2.close conn diff --git a/examples/simple.re b/examples/simple.re new file mode 100644 index 0000000..184e83a --- /dev/null +++ b/examples/simple.re @@ -0,0 +1,39 @@ +let conn = MySql2.connect(~host="127.0.0.1", ~port=3306, ~user="root", ()); + +let test_handler = + fun + | `Error(e) => Js.log2("ERROR: ", e) + | `Select(rows, meta) => Js.log3("SELECT: ", rows, meta) + | `Mutation(count, id) => Js.log3("MUTATION: ", count, id); + +let _ = MySql2.execute(conn, "SHOW DATABASES", None, test_handler); + +let table_sql = {| + CREATE TABLE IF NOT EXISTS test.simple ( + id bigint(20) NOT NULL AUTO_INCREMENT + , code varchar(32) NOT NULL + , PRIMARY KEY(id) + ) +|}; + +let _ = MySql2.execute(conn, table_sql, None, test_handler); + +let simple_insert_sql = "INSERT INTO test.simple (code) VALUES ('foo')"; + +let _ = MySql2.execute(conn, simple_insert_sql, None, test_handler); + +let simple_update_sql = "UPDATE test.simple SET code='foo2' WHERE code='foo'"; + +let _ = MySql2.execute(conn, simple_update_sql, None, test_handler); + +let _ = + MySql2.execute( + conn, + "SELECT NULL FROM test.simple WHERE false", + None, + test_handler, + ); + +let _ = MySql2.execute(conn, "SELECT * FROM test.simple", None, test_handler); + +let _ = MySql2.close(conn); diff --git a/package.json b/package.json index 36cc7a1..c2c591b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "author": "Nathan Sculli ", "license": "MIT", "dependencies": { + "@glennsl/bs-json": "^1.3.2", + "bs-platform": "^3.0.0", "mysql2": "^1.5.1" }, "devDependencies": { diff --git a/src/MySql2.ml b/src/MySql2.ml deleted file mode 100644 index 3c63ccf..0000000 --- a/src/MySql2.ml +++ /dev/null @@ -1,161 +0,0 @@ -module Connection = struct - - type t - - module Config = struct - type t - - external make : - ?host:string -> - ?port:int -> - ?user:string -> - ?password:string -> - ?database:string -> - unit ->t = "" [@@bs.obj] - end - - external createConnection : Config.t -> t = "" [@@bs.module "mysql2" ] - external close : t -> unit = "end" [@@bs.send] - - let make ?host ?port ?user ?password ?database _ = - createConnection - (Config.make ?host ?port ?user ?password ?database ()) -end - -module Options = struct - type t = < - sql: string; - values: Js.Json.t Js.Nullable.t; - namedPlaceholders: Js.boolean; - > Js.t - - let make sql values is_named = - [%bs.obj { sql; values; namedPlaceholders = is_named; }] - - let from_params sql params = - match params with - | None -> make sql Js.Nullable.null Js.false_ - | Some p -> match p with - | `Named json -> make sql (Js.Nullable.return json) Js.true_ - | `Positional json -> make sql (Js.Nullable.return json) Js.false_ - -end - -module Error = struct - let default_code = "99999" - let default_errno = 99999 - let default_message = "EMPTY_MESSAGE" - let default_name = "UNKNOWN" - - external name : Js.Exn.t -> string Js.Nullable.t = "" [@@bs.get] - external message : Js.Exn.t -> string Js.Nullable.t = "" [@@bs.get] - external code : Js.Exn.t -> string Js.Nullable.t = "" [@@bs.get] - external errno : Js.Exn.t -> int Js.Nullable.t = "" [@@bs.get] - external sql_state : Js.Exn.t -> string Js.Nullable.t = "sqlState" - [@@bs.get] - external sql_message : Js.Exn.t -> string Js.Nullable.t = "sqlMessage" - [@@bs.get] - - let with_default default nullable = - nullable - |> Js.Nullable.toOption - |> Js.Option.getWithDefault default - - let from_js exn = - let name = exn |> name |> with_default default_name in - let message = exn |> message |> with_default default_message in - let code = exn |> code |> with_default default_code in - let errno = exn |> errno |> with_default default_errno in - let sql_state = exn |> sql_state in - let sql_message = exn |> sql_message in - Failure {j|$name - $code ($errno) - $message - ($sql_state) $sql_message|j} -end - -type connection = Connection.t -type meta_record = { - catalog: string; - schema: string; - name: string; - orgName: string; - table: string; - orgTable: string; - characterSet: int; - columnLength: int; - columnType: int; - flags: int; - decimals: int - } -type meta = meta_record array -type params = - [ `Named of Js.Json.t - | `Positional of Js.Json.t - ] option -type rows = Js.Json.t array -type callback = - [ `Error of exn - | `Mutation of int * int - | `Select of rows * meta - ] -> - unit - -let decode_meta_record json = Json.Decode.({ - catalog = json |> field "catalog" string; - schema = json |> field "schema" string; - name = json |> field "name" string; - orgName = json |> field "orgName" string; - table = json |> field "table" string; - orgTable = json |> field "orgTable" string; - characterSet = json |> field "characterSet" int; - columnLength = json |> field "columnLength" int; - columnType = json |> field "columnType" int; - flags = json |> field "flags" int; - decimals = json |> field "decimals" int; -}) - -let result_mutation json = Json.Decode.( - let changes = json |> field "affectedRows" (withDefault 0 int) in - let last_id = json |> field "insertId" (withDefault 0 int) in - `Mutation (changes, last_id) -) - -let result_select rows meta = `Select ( - rows, - (Belt_Array.map meta decode_meta_record) -) - -let close = Connection.close -let connect = Connection.make - -external execute : - Connection.t -> - Options.t -> - (Js.Exn.t Js.Nullable.t -> Js.Json.t -> Js.Json.t array -> unit) -> - unit = "execute" -[@@bs.send] - -external query : - Connection.t -> - Options.t -> - (Js.Exn.t Js.Nullable.t -> Js.Json.t -> Js.Json.t array -> unit) -> - unit = "query" -[@@bs.send] - -let parse_response json meta = - match Js.Json.classify json with - | Js.Json.JSONObject _ -> result_mutation json - | Js.Json.JSONArray rows -> result_select rows meta - | _ -> `Error (Failure - {|MySql2Error - (UNKNOWN_RESPONSE_TYPE) - invalid_driver_result|} - ) - -let execute conn sql params callback = - let options = Options.from_params sql params in - let fn = if options##namedPlaceholders == Js.true_ - then execute - else query - in - fn conn options (fun exn res meta -> - match (Js.Nullable.toOption exn) with - | Some e -> callback (`Error (Error.from_js e)) - | None -> callback (parse_response res meta) - ) diff --git a/src/MySql2.mli b/src/MySql2.mli deleted file mode 100644 index 1d3c845..0000000 --- a/src/MySql2.mli +++ /dev/null @@ -1,55 +0,0 @@ -type params = - [ `Named of Js.Json.t - | `Positional of Js.Json.t - ] option -type rows = Js.Json.t array -type meta_record = { - catalog: string; - schema: string; - name: string; - orgName: string; - table: string; - orgTable: string; - characterSet: int; - columnLength: int; - columnType: int; - flags: int; - decimals: int -} -type meta = meta_record array - -module Connection : sig - type t -end - -module Error : sig - val from_js : Js.Exn.t -> exn -end - -type connection = Connection.t -type callback = - [ `Error of exn - | `Mutation of int * int - | `Select of rows * meta - ] -> - unit - -val close : connection -> unit - -val connect : - ?host:string -> - ?port:int -> - ?user:string -> - ?password:string -> - ?database:string -> - unit -> connection - -val execute : connection -> string -> params -> callback -> unit - -val parse_response : - Js.Json.t -> - Js.Json.t array -> - [> `Error of exn - | `Mutation of int * int - | `Select of rows * meta - ] diff --git a/src/MySql2.re b/src/MySql2.re new file mode 100644 index 0000000..5b50e10 --- /dev/null +++ b/src/MySql2.re @@ -0,0 +1,178 @@ +module Connection = { + type t; + module Config = { + type t; + [@bs.obj] + external make : + ( + ~host: string=?, + ~port: int=?, + ~user: string=?, + ~password: string=?, + ~database: string=?, + unit + ) => + t = + ""; + }; + [@bs.module "mysql2"] external createConnection : Config.t => t = ""; + [@bs.send] external close : t => unit = "end"; + let make = (~host=?, ~port=?, ~user=?, ~password=?, ~database=?, _) => + createConnection( + Config.make(~host?, ~port?, ~user?, ~password?, ~database?, ()), + ); +}; + +module Options = { + type t = { + . + "sql": string, + "values": Js.Nullable.t(Js.Json.t), + "namedPlaceholders": bool, + }; + let make = (sql, values, is_named) => { + "sql": sql, + "values": values, + "namedPlaceholders": is_named, + }; + let from_params = sql => + fun + | None => make(sql, Js.Nullable.null, false) + | Some(p) => + p + |> ( + fun + | `Named(json) => make(sql, Js.Nullable.return(json), true) + | `Positional(json) => make(sql, Js.Nullable.return(json), false) + ); +}; + +module Error = { + let default_code = "99999"; + let default_errno = 99999; + let default_message = "EMPTY_MESSAGE"; + let default_name = "UNKNOWN"; + [@bs.get] external name : Js.Exn.t => Js.Nullable.t(string) = ""; + [@bs.get] external message : Js.Exn.t => Js.Nullable.t(string) = ""; + [@bs.get] external code : Js.Exn.t => Js.Nullable.t(string) = ""; + [@bs.get] external errno : Js.Exn.t => Js.Nullable.t(int) = ""; + [@bs.get] + external sql_state : Js.Exn.t => Js.Nullable.t(string) = "sqlState"; + [@bs.get] + external sql_message : Js.Exn.t => Js.Nullable.t(string) = "sqlMessage"; + let with_default = (default, nullable) => + nullable |> Js.Nullable.toOption |> Js.Option.getWithDefault(default); + let from_js = exn => { + let name = exn |> name |> with_default(default_name); + let message = exn |> message |> with_default(default_message); + let code = exn |> code |> with_default(default_code); + let errno = exn |> errno |> with_default(default_errno); + let sql_state = exn |> sql_state; + let sql_message = exn |> sql_message; + Failure( + {j|$name - $code ($errno) - $message - ($sql_state) $sql_message|j}, + ); + }; +}; + +type connection = Connection.t; + +type meta_record = { + catalog: string, + schema: string, + name: string, + orgName: string, + table: string, + orgTable: string, + characterSet: int, + columnLength: int, + columnType: int, + flags: int, + decimals: int, +}; + +type meta = array(meta_record); + +type params = option([ | `Named(Js.Json.t) | `Positional(Js.Json.t)]); + +type rows = array(Js.Json.t); + +type callback = + [ | `Error(exn) | `Mutation(int, int) | `Select(rows, meta)] => unit; + +let decode_meta_record = json => + Json.Decode.{ + catalog: json |> field("catalog", string), + schema: json |> field("schema", string), + name: json |> field("name", string), + orgName: json |> field("orgName", string), + table: json |> field("table", string), + orgTable: json |> field("orgTable", string), + characterSet: json |> field("characterSet", int), + columnLength: json |> field("columnLength", int), + columnType: json |> field("columnType", int), + flags: json |> field("flags", int), + decimals: json |> field("decimals", int), + }; + +let result_mutation = json => { + open Json.Decode; + let changes = json |> field("affectedRows", withDefault(0, int)); + let last_id = json |> field("insertId", withDefault(0, int)); + `Mutation((changes, last_id)); +}; + +let result_select = (rows, meta) => + `Select((rows, Belt_Array.map(meta, decode_meta_record))); + +let close = Connection.close; + +let connect = Connection.make; + +[@bs.send] +external execute : + ( + Connection.t, + Options.t, + (Js.Nullable.t(Js.Exn.t), Js.Json.t, array(Js.Json.t)) => unit + ) => + unit = + "execute"; + +[@bs.send] +external query : + ( + Connection.t, + Options.t, + (Js.Nullable.t(Js.Exn.t), Js.Json.t, array(Js.Json.t)) => unit + ) => + unit = + "query"; + +let parse_response = (json, meta) => + switch (json |> Js.Json.classify) { + | Js.Json.JSONObject(_) => result_mutation(json) + | Js.Json.JSONArray(rows) => result_select(rows, meta) + | _ => + `Error( + Failure( + {|MySql2Error - (UNKNOWN_RESPONSE_TYPE) - invalid_driver_result|}, + ), + ) + }; + +let execute = (conn, sql, params, callback) => { + let options = params |> Options.from_params(sql); + let fn = + if (options##namedPlaceholders) { + execute; + } else { + query; + }; + fn(conn, options, (exn, res, meta) => + switch (exn |> Js.Nullable.toOption) { + | Some(e) => callback(`Error(e |> Error.from_js)) + | None => callback(meta |> parse_response(res)) + } + ); +}; diff --git a/src/MySql2.rei b/src/MySql2.rei new file mode 100644 index 0000000..4847a86 --- /dev/null +++ b/src/MySql2.rei @@ -0,0 +1,47 @@ +type params = option([ | `Named(Js.Json.t) | `Positional(Js.Json.t)]); + +type rows = array(Js.Json.t); + +type meta_record = { + catalog: string, + schema: string, + name: string, + orgName: string, + table: string, + orgTable: string, + characterSet: int, + columnLength: int, + columnType: int, + flags: int, + decimals: int, +}; + +type meta = array(meta_record); + +module Connection: {type t;}; + +module Error: {let from_js: Js.Exn.t => exn;}; + +type connection = Connection.t; + +type callback = + [ | `Error(exn) | `Mutation(int, int) | `Select(rows, meta)] => unit; + +let close: connection => unit; + +let connect: + ( + ~host: string=?, + ~port: int=?, + ~user: string=?, + ~password: string=?, + ~database: string=?, + unit + ) => + connection; + +let execute: (connection, string, params, callback) => unit; + +let parse_response: + (Js.Json.t, array(Js.Json.t)) => + [> | `Error(exn) | `Mutation(int, int) | `Select(rows, meta)]; diff --git a/yarn.lock b/yarn.lock index 2f2dabb..4df3a23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,9 +16,9 @@ dependencies: jest "^22.0.4" -"@glennsl/bs-json@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@glennsl/bs-json/-/bs-json-1.2.0.tgz#8a2175458cf425614d20bb5542a2eb92b09bd4fc" +"@glennsl/bs-json@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@glennsl/bs-json/-/bs-json-1.3.2.tgz#89869b1ed4fa0f8cc8a00046d2327273be5e71d3" abab@^1.0.4: version "1.0.4" @@ -397,9 +397,9 @@ browser-resolve@^1.11.2: dependencies: resolve "1.1.7" -bs-platform@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-2.2.3.tgz#d905ae10a5f3621e6a739041dfa0b58483a2174f" +bs-platform@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-3.0.0.tgz#38f200730db52fdea37819376b6ac3dfb20244c0" bser@^2.0.0: version "2.0.0"