Skip to content

Commit

Permalink
implement transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
zth committed Sep 28, 2023
1 parent eb18069 commit 88d4fb2
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 38 deletions.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 := <str>$title,
status := <PublishStatus>$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<movie>` instead of `array<movie>`.
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions cli/EdgeDbGenerator.res
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
)
})
Expand Down
18 changes: 18 additions & 0 deletions dbTestProject/src/Movies.res
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ let movieByTitle = (client, ~title) => {
title: title,
})
}

let _ = %edgeql(`
# @name AddActor
insert Person {
name := <str>$name
}
`)

// Workaround until new release of rescript-embed-lang
let addActor = Movies__edgeDb.AddActor.transaction

let _ = %edgeql(`
# @name RemoveActor
delete Person filter .id = <uuid>$id
`)

// Workaround until new release of rescript-embed-lang
let removeActor = Movies__edgeDb.RemoveActor.transaction
52 changes: 51 additions & 1 deletion dbTestProject/src/__generated__/Movies__edgeDb.res

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions dbTestProject/test/TestProject.test.res
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
27 changes: 25 additions & 2 deletions src/EdgeDB.res
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module Transaction = {
type t

@module("edgedb") external make: unit => t = "createClient"

@send external execute: (t, string, ~args: 'args=?) => promise<unit> = "execute"

@send external query: (t, string, ~args: 'args=?) => promise<array<'result>> = "query"
Expand Down Expand Up @@ -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)
}
}
66 changes: 35 additions & 31 deletions test/__snapshots__/EdgeDbGeneratorTests.test.mjs.snap
Original file line number Diff line number Diff line change
@@ -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 = <str>$movieTitle\`
type args = {movieTitle: string}
type response = {title: string, status: [#Published | #Unpublished]}
let query = (client: EdgeDB.Client.t, args: args): promise<result<response, EdgeDB.Error.errorFromOperation>> => {
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,
Expand Down Expand Up @@ -73,3 +42,38 @@ exports[`extracting queries from ReScript documents it can extract from docs #2
} filter
.id = <uuid>$userId"
`;
exports[`generate file 1`] = `
{
"contents":
"// @sourceHash b71c2e4fb6a0ba65311c5fce81401d7a
module FindMovie = {
let queryText = \`select Movie {
title,
status,
actors: {
name,
age
}
} filter
.title = <str>$movieTitle\`
type args = {movieTitle: string}
type response = {title: string, status: [#Published | #Unpublished]}
let query = (client: EdgeDB.Client.t, args: args): promise<result<response, EdgeDB.Error.errorFromOperation>> => {
client->EdgeDB.QueryHelpers.singleRequired(queryText, ~args)
}
let transaction = (transaction: EdgeDB.Transaction.t, args: args): promise<result<response, EdgeDB.Error.errorFromOperation>> => {
transaction->EdgeDB.TransactionHelpers.singleRequired(queryText, ~args)
}
}
"
,
"hash": "b71c2e4fb6a0ba65311c5fce81401d7a",
"path": "SomeQueryFile__edgeDb.res",
}
`;

0 comments on commit 88d4fb2

Please sign in to comment.