From 88d4fb2236b2d598e1fc8630e5fb23813cf32f45 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 28 Sep 2023 18:08:45 +0200 Subject: [PATCH] implement transaction --- README.md | 29 +++++++- cli/EdgeDbGenerator.res | 8 +++ dbTestProject/src/Movies.res | 18 +++++ .../src/__generated__/Movies__edgeDb.res | 52 ++++++++++++++- dbTestProject/test/TestProject.test.res | 27 ++++++++ package.json | 2 +- src/EdgeDB.res | 27 +++++++- .../EdgeDbGeneratorTests.test.mjs.snap | 66 ++++++++++--------- 8 files changed, 191 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 4292bc4..142a5f7 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,29 @@ let movies = await client->findMovies({ There's just one thing to notice in relation to regular EdgeQL - we require you to put a comment at the top of your query with a `@name` annotation, naming the query. This is because we need to be able to discern which query is which in the current ReScript file, since you can put as many queries as you want in the same ReScript file. +### Using transactions + +There's a `transaction` function emitted for each EdgeQL query. You can use that to do your operation in a transaction: + +```rescript +let client = EdgeDB.Client.make() + +// Remember to define your EdgeQL using a module, so you get easy access to all generated functions. +module InsertMovie = %edgeql(` + # @name insertMovie + insert Movie { + title := $title, + status := $status + }`) + +await client->EdgeDB.Client.transaction(async tx => { + await tx->InsertMovie.transaction({ + title: "Jalla Jalla", + status: #Published + }) +}) +``` + ### Cardinality EdgeDB and `rescript-edgedb` automatically manages the cardinality of each query for you. That means that you can always trust the return types of your query. For example, adding `limit 1` to the `findMovies` query above would make the return types `option` instead of `array`. @@ -127,10 +150,10 @@ Yes, you should. This ensures building the project doesn't _have_ to rely on a r ## WIP -- [ ] Simple transactions support -- [ ] CLI to statically prevent overfetching +- [x] Simple transactions support +- [x] CLI to statically prevent overfetching - [ ] Improve CLI docs -- [ ] Test/example project +- [x] Test/example project - [ ] Figure out publishing - [ ] Generate docs using new ReScript doc generation diff --git a/cli/EdgeDbGenerator.res b/cli/EdgeDbGenerator.res index 1c1ca4e..ae1e657 100644 --- a/cli/EdgeDbGenerator.res +++ b/cli/EdgeDbGenerator.res @@ -414,6 +414,14 @@ ${params.types.distinctTypes->Set.values->Iterator.toArray->Array.joinWith("\n\n : ""}${extraInFnArgs}): ${returnType} => { client->EdgeDB.QueryHelpers.${method}(queryText${hasArgs ? ", ~args" : ""}${extraInFnApply}) } + + let transaction = (transaction: EdgeDB.Transaction.t${hasArgs + ? `, args: args` + : ""}${extraInFnArgs}): ${returnType} => { + transaction->EdgeDB.TransactionHelpers.${method}(queryText${hasArgs + ? ", ~args" + : ""}${extraInFnApply}) + } }\n\n`, ) }) diff --git a/dbTestProject/src/Movies.res b/dbTestProject/src/Movies.res index dc95077..811b63e 100644 --- a/dbTestProject/src/Movies.res +++ b/dbTestProject/src/Movies.res @@ -35,3 +35,21 @@ let movieByTitle = (client, ~title) => { title: title, }) } + +let _ = %edgeql(` + # @name AddActor + insert Person { + name := $name + } +`) + +// Workaround until new release of rescript-embed-lang +let addActor = Movies__edgeDb.AddActor.transaction + +let _ = %edgeql(` + # @name RemoveActor + delete Person filter .id = $id +`) + +// Workaround until new release of rescript-embed-lang +let removeActor = Movies__edgeDb.RemoveActor.transaction diff --git a/dbTestProject/src/__generated__/Movies__edgeDb.res b/dbTestProject/src/__generated__/Movies__edgeDb.res index 2f395b9..0792277 100644 --- a/dbTestProject/src/__generated__/Movies__edgeDb.res +++ b/dbTestProject/src/__generated__/Movies__edgeDb.res @@ -1,4 +1,4 @@ -// @sourceHash 03b34eabb42c7bd614713646823911b8 +// @sourceHash 16f6ce89084f097aa7cf3106229fe5a9 module AllMovies = { let queryText = `select Movie { id, @@ -25,6 +25,10 @@ module AllMovies = { let query = (client: EdgeDB.Client.t): promise> => { client->EdgeDB.QueryHelpers.many(queryText) } + + let transaction = (transaction: EdgeDB.Transaction.t): promise> => { + transaction->EdgeDB.TransactionHelpers.many(queryText) + } } module MovieByTitle = { @@ -59,5 +63,51 @@ module MovieByTitle = { let query = (client: EdgeDB.Client.t, args: args, ~onError=?): promise> => { client->EdgeDB.QueryHelpers.single(queryText, ~args, ~onError?) } + + let transaction = (transaction: EdgeDB.Transaction.t, args: args, ~onError=?): promise> => { + transaction->EdgeDB.TransactionHelpers.single(queryText, ~args, ~onError?) + } +} + +module AddActor = { + let queryText = `insert Person { + name := $name + }` + + type args = { + name: string, + } + + type response = { + id: string, + } + + let query = (client: EdgeDB.Client.t, args: args): promise> => { + client->EdgeDB.QueryHelpers.singleRequired(queryText, ~args) + } + + let transaction = (transaction: EdgeDB.Transaction.t, args: args): promise> => { + transaction->EdgeDB.TransactionHelpers.singleRequired(queryText, ~args) + } +} + +module RemoveActor = { + let queryText = `delete Person filter .id = $id` + + type args = { + id: string, + } + + type response = { + id: string, + } + + let query = (client: EdgeDB.Client.t, args: args, ~onError=?): promise> => { + client->EdgeDB.QueryHelpers.single(queryText, ~args, ~onError?) + } + + let transaction = (transaction: EdgeDB.Transaction.t, args: args, ~onError=?): promise> => { + transaction->EdgeDB.TransactionHelpers.single(queryText, ~args, ~onError?) + } } diff --git a/dbTestProject/test/TestProject.test.res b/dbTestProject/test/TestProject.test.res index 667bb0d..3bc50c7 100644 --- a/dbTestProject/test/TestProject.test.res +++ b/dbTestProject/test/TestProject.test.res @@ -35,6 +35,33 @@ describe("fetching data", () => { await client->Movies.movieByTitle(~title="The Great Adventure 2"), )->Expect.toMatchSnapshot }) + + testAsync("running in a transaction", async () => { + let res = await client->EdgeDB.Client.transaction( + async transaction => { + await transaction->Movies.addActor({name: "Bruce Willis"}) + }, + ) + + expect( + switch res { + | Ok({id}) => + let removed = await client->EdgeDB.Client.transaction( + async transaction => { + await transaction->Movies.removeActor({id: id}) + }, + ) + switch removed { + | Some({id}) => + // Just for the unused CLI output + let _id = id + | None => () + } + id->String.length > 2 + | Error(_) => false + }, + )->Expect.toBe(true) + }) }) test("run unused selections CLI", () => { diff --git a/package.json b/package.json index fffb046..d829cea 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "src/EdgeDB.mjs", "bin": "dist/Cli.js", "scripts": { - "test": "bun test test/", + "test": "bun test test/*.test.mjs", "build:res": "rescript", "build": "npm run build:res && esbuild --external:edgedb --external:chokidar --platform=node --bundle cli/Cli.mjs --outfile=dist/Cli.js" }, diff --git a/src/EdgeDB.res b/src/EdgeDB.res index f371b34..b84c0ce 100644 --- a/src/EdgeDB.res +++ b/src/EdgeDB.res @@ -1,8 +1,6 @@ module Transaction = { type t - @module("edgedb") external make: unit => t = "createClient" - @send external execute: (t, string, ~args: 'args=?) => promise = "execute" @send external query: (t, string, ~args: 'args=?) => promise> = "query" @@ -400,3 +398,28 @@ module QueryHelpers = { | exception Exn.Error(err) => Error(err->Error.fromExn) } } + +module TransactionHelpers = { + /** Returns all found items as an array. */ + let many = (client, query, ~args=?) => Transaction.query(client, query, ~args) + + /** Returns a single item, if one was found. */ + let single = async (client, query, ~args=?, ~onError=?) => + switch await Transaction.querySingle(client, query, ~args) { + | Value(v) => Some(v) + | Null => None + | exception Exn.Error(err) => + switch onError { + | None => () + | Some(onError) => onError(err->Error.fromExn) + } + None + } + + /** Assumes exactly one item is going to be found, and errors if that's not the case. */ + let singleRequired = async (client, query, ~args=?) => + switch await Transaction.queryRequiredSingle(client, query, ~args) { + | v => Ok(v) + | exception Exn.Error(err) => Error(err->Error.fromExn) + } +} diff --git a/test/__snapshots__/EdgeDbGeneratorTests.test.mjs.snap b/test/__snapshots__/EdgeDbGeneratorTests.test.mjs.snap index 3bec80d..46369ff 100644 --- a/test/__snapshots__/EdgeDbGeneratorTests.test.mjs.snap +++ b/test/__snapshots__/EdgeDbGeneratorTests.test.mjs.snap @@ -1,36 +1,5 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`generate file 1`] = ` -{ - "contents": -"// @sourceHash 43efb7cecb4f08bcf22e679cf6cdeed5 -module FindMovie = { - let queryText = \`select Movie { - title, - status, - actors: { - name, - age - } - } filter - .title = $movieTitle\` - - type args = {movieTitle: string} - - type response = {title: string, status: [#Published | #Unpublished]} - - let query = (client: EdgeDB.Client.t, args: args): promise> => { - client->EdgeDB.QueryHelpers.singleRequired(queryText, ~args) - } -} - -" -, - "hash": "43efb7cecb4f08bcf22e679cf6cdeed5", - "path": "SomeQueryFile__edgeDb.res", -} -`; - exports[`extracting queries from ReScript documents it can extract from docs #1 1`] = ` "select Movie { title, @@ -73,3 +42,38 @@ exports[`extracting queries from ReScript documents it can extract from docs #2 } filter .id = $userId" `; + +exports[`generate file 1`] = ` +{ + "contents": +"// @sourceHash b71c2e4fb6a0ba65311c5fce81401d7a +module FindMovie = { + let queryText = \`select Movie { + title, + status, + actors: { + name, + age + } + } filter + .title = $movieTitle\` + + type args = {movieTitle: string} + + type response = {title: string, status: [#Published | #Unpublished]} + + let query = (client: EdgeDB.Client.t, args: args): promise> => { + client->EdgeDB.QueryHelpers.singleRequired(queryText, ~args) + } + + let transaction = (transaction: EdgeDB.Transaction.t, args: args): promise> => { + transaction->EdgeDB.TransactionHelpers.singleRequired(queryText, ~args) + } +} + +" +, + "hash": "b71c2e4fb6a0ba65311c5fce81401d7a", + "path": "SomeQueryFile__edgeDb.res", +} +`;