Mutation Data Connector API types [GDC-594]

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6893
GitOrigin-RevId: edf0f72027197ae61bfa8d27d53eabf6ca626112
This commit is contained in:
Daniel Chambers 2022-11-29 14:35:20 +11:00 committed by hasura-bot
parent cca0b6e81a
commit ed79049637
48 changed files with 2480 additions and 31 deletions

View File

@ -111,12 +111,13 @@ The entry point to the reference agent application is a Fastify HTTP server. Raw
- `GET /capabilities`, which returns the capabilities of the agent and a schema that describes the type of the configuration expected to be sent on the `X-Hasura-DataConnector-Config` header
- `GET /schema`, which returns information about the provided _data schema_, its tables and their columns
- `POST /query`, which receives a query structure to be executed, encoded as the JSON request body, and returns JSON conforming to the schema described by the `/schema` endpoint, and contining the requested fields.
- `POST /query`, which receives a query structure to be executed, encoded as the JSON request body, and returns JSON containing the requested fields. The query will be over the data schema described by the `/schema` endpoint.
- `GET /health`, which can be used to either check if the agent is running, or if a particular data source is healthy
- `POST /mutation`, which receives a request to mutate (ie change) data described by the `/schema` endpoint.
The `/schema` and `/query` endpoints require the request to have the `X-Hasura-DataConnector-Config` header set. That header contains configuration information that agent can use to configure itself. For example, the header could contain a connection string to the database, if the agent requires a connection string to know how to connect to a specific database. The header must be a JSON object, but the specific properties that are required are up to the agent to define.
The `/schema`, `/query` and `/mutation` endpoints require the request to have the `X-Hasura-DataConnector-Config` header set. That header contains configuration information that agent can use to configure itself. For example, the header could contain a connection string to the database, if the agent requires a connection string to know how to connect to a specific database. The header must be a JSON object, but the specific properties that are required are up to the agent to define.
The `/schema` and `/query` endpoints also require the request to have the `X-Hasura-DataConnector-SourceName` header set. This header contains the name of the data source configured in HGE that will be querying the agent. This can be used by the agent to maintain things like connection pools and configuration maps on a per-source basis.
The `/schema`, `/query` and `/mutation` endpoints also require the request to have the `X-Hasura-DataConnector-SourceName` header set. This header contains the name of the data source configured in HGE that will be querying the agent. This can be used by the agent to maintain things like connection pools and configuration maps on a per-source basis.
We'll look at the implementation of each of the endpoints in turn.
@ -226,6 +227,37 @@ The `in_year` custom comparison operator is being used to request all employees
The example also defines two aggregate functions `min` and `max`, both of which have a result type of `DateTime`.
### Mutations capabilities
The agent can declare whether it supports mutations (ie. changing data) against its data source. If it supports mutations, it needs to declare a `mutations` capability with agent-specific values for the following properties:
```json
{
"capabilities": {
"mutations": {
"insert": {
"supports_nested_inserts": true
},
"update": {},
"delete": {},
"atomicity_support_level": "heterogeneous_operations",
"returning": {}
}
}
}
```
The agent is able to specify whether or not it supports inserts, updates and deletes separately. For inserts, it can specify whether it supports nested inserts, where the user can insert related rows nested inside the one row insert.
It also should specify its supported level of transactional atomicity when performing mutations. It can choose between the following levels:
- `row`: If multiple rows are affected in a single operation but one fails, only the failed row's changes will be reverted. For example, if one mutation operation inserts four rows, but one row fails, the other three rows will still be inserted, and the failed one will not.
- `single_operation`: If multiple rows are affected in a single operation but one fails, all affected rows in the operation will be reverted. For example, if one mutation operation inserts four rows, but one row fails, none of the rows will be inserted.
- `homogeneous_operations`: If multiple operations of only the same type exist in the one mutation request, a failure in one will result in all changes being reverted. For example, if one mutation request contains two insert operations, one to Table A and one to Table B, and Table B's insert fails, no rows will have been inserted into either Table A nor B.
- `heterogeneous_operations`: If multiple operations of any type exist in the one mutation request, a failure in one will result in all changes being reverted. For example, if one mutation request contains three operations, one to insert some rows, one to update some rows, and one to delete some rows, and the deletion fails, all changes (inserts, updates and deletes) will be reverted.
The preference would be to support the highest level of atomicity possible (ie `heteregeneous_operations` is preferred over `row`). It is also possible to omit the property, which would imply no atomicity at all (failures cannot be rolled back whatsoever).
The agent can also specify whether or not it supports `returning` data from mutations. This refers to the ability to return the data that was mutated by mutation operations (for example, the updated rows in an update, or the deleted rows in a delete).
### Schema
The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `graphql-engine`. It returns the following JSON object:
@ -1517,8 +1549,588 @@ Any non-200 response code from an Agent (except for the `/health` endpoint) will
```
{
"type": "uncaught-error", // This may be extended to more types in future
"message": String, // A plain-text message for display purposes
"details": Value // An arbitrary JSON Value containing error details
"type": String, // A specific error type, see below
"message": String, // A plain-text message for display purposes
"details": Value // An arbitrary JSON Value containing error details
}
```
The available error types are:
* `mutation-constraint-violation`: For when a mutation request fails because the mutation causes a violation of data constraints (for example, primary key constraint) in the data source
* `mutation-permission-check-failure`: For when a permissions check fails during a mutation and the mutation is rejected
* `uncaught-error`: For all other errors
### Mutations
The `POST /mutation` endpoint is invoked when the user issues a mutation GraphQL request to `graphql-engine`, assuming the agent has declared itself capable of mutations in its capabilities. The basic structure of a mutation request is as follows:
```jsonc
{
"table_relationships": [], // Any relationships between tables are described in here in the same manner as in queries
"operations": [ // A mutation request can contain multiple mutation operations
{
"type": "insert", // Also: "update" and "delete"
"returning_fields": { // The fields to return for every affected row
"ArtistId": {
"type": "column",
"column": "ArtistId",
"column_type": "number"
}
},
... // Other operation type-specific properties, detailed below
}
]
}
```
There are three types of mutation operations: `insert`, `update` and `delete`. A request can involve multiple mutation operations, potentially of differing types. A mutation operation can specify `returning_fields` which are the fields that are expected to be returned in the response for each row affected by the mutation operation.
The response to a mutation request takes this basic structure:
```jsonc
{
"operation_results": [ // There will be a result object per operation, returned here in the same order as in the request
{
"affected_rows": 1, // The number of rows affected by the mutation operation
"returning": [ // The rows that were affected; each row object contains the fields requested in `returning_fields`
{
"FieldName": "FieldValue"
}
]
}
]
}
```
If any mutation operation causes an error, for example, if a mutation violates a constraint such as a primary key or a foreign key constraint in an RDBMS, then an error should be returned as a response in the same manner as described in the [Reporting Errors](#reporting-errors) section. Changes should be rolled back to the extent described by the `atomicity_level` declared by in the agent's [mutation capabilities](#mutations-capabilities). The error type should be `mutation_constraint_violation` and the HTTP response code should be 400. For example:
```json
{
"type": "mutation-constraint-violation",
"message": "Violation of PRIMARY KEY constraint PK_Artist. Cannot insert duplicate key in table Artist. The duplicate key value is (1).", // Can be any helpfully descriptive error message
"details": { // Any helpful structured error information, the below is just an example
"constraint_name": "PK_Artist",
"table": ["Artist"],
"key_value": 1
}
}
```
If a mutation fails because it fails a permissions check (eg a `post-insert-check`), then the error code that should be used is `mutation-permission-check-failure`.
#### Insert Operations
Here's an example GraphQL mutation that inserts two artists:
```graphql
mutation InsertArtists {
insert_Artist(objects: [
{ArtistId: 300, Name: "Taylor Swift"},
{ArtistId: 301, Name: "Phil Collins"}
]) {
affected_rows
returning {
ArtistId
Name
}
}
}
```
This would result in a mutation request like this:
```json
{
"table_relationships": [],
"insert_schema": [
{
"table": ["Artist"],
"fields": {
"ArtistId": {
"type": "column",
"column": "ArtistId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
}
],
"operations": [
{
"type": "insert",
"table": ["Artist"],
"rows": [
[
{
"ArtistId": 300,
"Name": "Taylor Swift"
},
{
"ArtistId": 301,
"Name": "Phil Collins"
}
]
],
"post_insert_check": {
"type": "and",
"expressions": []
},
"returning_fields": {
"ArtistId": {
"type": "column",
"column": "ArtistId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
}
]
}
```
The first thing to notice is the `insert_schema` property at the mutation request level. This contains the definition of the fields that will be used inside any insert operation in this request on a per-table basis. The schema for the row data to insert is placed here, separate to the row data itself, in order to reduce the amount of duplicate data that would exist were it inlined into the row data structures themselves.
So, because in this request we are inserting into the Artist table, we have the definition of what "ArtistId" and "Name" properties mean when they are found in the rows to insert for the Artist table. In this case, both fields are columns (`"type": "column"`) with their names (`column`) and types (`column_type`) specified.
Next, let's break the `insert`-typed operation's properties down:
* `table`: specifies the table we're inserting rows into
* `rows`: An array of rows to insert. Each row is an object with properties, where what the properties correspond to (eg. column values) is defined by the `insert_schema`.
* `post_insert_check`: The post-insert check is an expression (in the same format as `Query`'s `where` property) that all inserted rows must match otherwise their insertion must be reverted. This expression comes from `graphql-engine`'s permissions system. The reason that it is a "post-insert" check is because it can involve joins via relationships to other tables and potentially data that is only available post-insert such as computed columns. If the agent knows it can compute the result of such a check without actually performing an insert, it is free to do so, but it must produce a result that is indistinguishable from that which was done post-insert. If the post-insert check fails, the mutation request should fail with an error using the error code `mutation-permission-check-failure`.
* `returning_fields`: This specifies a list of fields to return in the response. The property takes the same format as the `fields` property on Queries. It is expected that the specified fields will be returned for all rows affected by the insert (ie. all inserted rows).
The result of this request would be the following response:
```json
{
"operation_results": [
{
"affected_rows": 2,
"returning": [
{
"ArtistId": 300,
"Name": "Taylor Swift"
},
{
"ArtistId": 301,
"Name": "Phil Collins"
}
]
}
]
}
```
Notice that the two affected rows in `returning` are the two that we inserted.
#### Nested Insert Operations
If a user wishes to insert multiple related rows in one go, they can issue a nested insert GraphQL query:
```graphql
mutation InsertAlbum {
insert_Album(objects: [
{
AlbumId: 400,
Title: "Fearless",
Artist: {
data: {
ArtistId: 300,
Name: "Taylor Swift"
}
},
Tracks: {
data: [
{ TrackId: 4000, Name: "Fearless" },
{ TrackId: 4001, Name: "Fifteen" }
]
}
}
]) {
affected_rows
returning {
AlbumId
Title
Artist {
ArtistId
Name
}
Tracks {
TrackId
Name
}
}
}
}
```
This would result in the following request:
```json
{
"table_relationships": [
{
"source_table": ["Album"],
"relationships": {
"Artist": {
"target_table": ["Artist"],
"relationship_type": "object",
"column_mapping": {
"ArtistId": "ArtistId"
}
},
"Tracks": {
"target_table": ["Track"],
"relationship_type": "array",
"column_mapping": {
"AlbumId": "AlbumId"
}
}
}
}
],
"insert_schema": [
{
"table": ["Album"],
"fields": {
"AlbumId": {
"type": "column",
"column": "AlbumId",
"column_type": "number"
},
"Title": {
"type": "column",
"column": "Title",
"column_type": "string"
},
"Artist": {
"type": "object_relation",
"relationship": "Artist",
"insert_order": "before_parent"
},
"Tracks": {
"type": "array_relation",
"relationship": "Tracks"
},
}
},
{
"table": ["Artist"],
"fields": {
"ArtistId": {
"type": "column",
"column": "ArtistId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
},
{
"table": ["Track"],
"fields": {
"TrackId": {
"type": "column",
"column": "TrackId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
}
],
"operations": [
{
"type": "insert",
"table": ["Album"],
"rows": [
[
{
"AlbumId": 400,
"Title": "Fearless",
"Artist": {
"ArtistId": 300,
"Name": "Taylor Swift"
},
"Tracks": [
{
"TrackId": 4000,
"Name": "Fearless"
},
{
"TrackId": 4001,
"Name": "Fifteen"
}
]
}
]
],
"post_insert_check": {
"type": "and",
"expressions": []
},
"returning_fields": {
"AlbumId": {
"type": "column",
"column": "AlbumId",
"column_type": "number"
},
"Title": {
"type": "column",
"column": "Title",
"column_type": "string"
},
"Artist": {
"type": "relationship",
"relationship": "Artist",
"query": {
"fields": {
"ArtistId": {
"type": "column",
"column": "ArtistId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
}
},
"Tracks": {
"type": "relationship",
"relationship": "Tracks",
"query": {
"fields": {
"TrackId": {
"type": "column",
"column": "TrackId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Name",
"column_type": "string"
}
}
}
}
}
}
]
}
```
Note that there are two new types of fields in the `insert_schema` in this query to capture the nested inserts:
* `object_relation`: This captures a nested insert across an object relationship. In this case, we're inserting the related Artist row.
* `relationship`: The name of the relationship across which to insert the related row. The information about this relationship can be looked up in `table_relationships`.
* `insert_order`: This can be either `before_parent` or `after_parent` and indicates whether or not the related row needs to be inserted before the parent row or after it.
* `array_relation`: This captures a nested insert across an array relationship. In this case, we're inserting the related Tracks rows.
* `relationship`: The name of the relationship across which to insert the related rows. The information about this relationship can be looked up in `table_relationships`.
The agent is expected to set the necessary values of foreign key columns itself when inserting all the rows. In this example, the agent would:
* First insert the Artist.
* Then insert the Album, using the Artist.ArtistId primary key column for the Album.ArtistId foreign key column.
* Then insert the two Track rows, using the Album.AlbumId primary key column for the Track.AlbumId foreign key column.
This is particularly important where the value of primary keys are not known until they are generated in the database itself and cannot be provided by the user.
Note that in `returning_fields` we have used fields of type `relationship` to navigate relationships in the returned affected rows. This works in the same way as in Queries.
The response to this mutation request would be:
```json
{
"operation_results": [
{
"affected_rows": 4,
"returning": [
{
"AlbumId": 400,
"Title": "Fearless",
"Artist": {
"rows": [
{
"ArtistId": 300,
"Name": "Taylor Swift"
}
]
},
"Tracks": {
"rows": [
{
"TrackId": 4000,
"Name": "Fearless"
},
{
"ArtistId": 4001,
"Name": "Fifteen"
}
]
}
}
]
}
]
}
```
Note that relationship fields are returned in the response in the same fashion as they are in a Query response; ie. inside a nested object with `rows` and `aggregates` (if specified) properties.
#### Update Operations
Here's an example of a mutation that updates a Track row:
```graphql
mutation UpdateTrack {
update_Track(
where: { TrackId: { _eq: 1 } },
_inc: { Milliseconds: 100 },
_set: { UnitPrice: 2.50 }
) {
affected_rows
returning {
TrackId
Milliseconds
}
}
}
```
This would get translated into a mutation request like so:
```json
{
"table_relationships": [],
"operations": [
{
"type": "update",
"table": ["Track"],
"where": {
"type": "binary_op",
"operator": "equal",
"column": {
"name": "TrackId",
"column_type": "number"
},
"value": {
"type": "scalar",
"value": 1,
"value_type": "number"
}
},
"updates": [
{
"type": "increment",
"column": "Milliseconds",
"value": 100,
"value_type": "number"
},
{
"type": "set",
"column": "UnitPrice",
"value": 2.50,
"value_type": "number"
}
],
"post_update_check": {
"type": "and",
"expressions": []
},
"returning_fields": {
"TrackId": {
"type": "column",
"column": "TrackId",
"column_type": "number"
},
"Name": {
"type": "column",
"column": "Milliseconds",
"column_type": "number"
}
}
}
]
}
```
Breaking down the properties in the `update`-typed mutation operation:
* `table`: specifies the table we're updating rows in
* `where`: An expression (same as the expression in a Query's `where` property) that is used to select the matching rows to update
* `updates`: An array of `RowUpdate` objects that describe the individual updates to be applied to each row that matches the expression in `where`. There are two types of `RowUpdate`s:
* `increment` - This increments the specified column by the specified amount
* `set` - This sets the specified column to the specified value
* `post_update_check`: The post-update check is an expression (in the same format as `Query`'s `where` property) that all updated rows must match otherwise the changes made must be reverted. This expression comes from `graphql-engine`'s permissions system. The reason that it is a "post-update" check is because it operates on the post-update data (such as the results of increment updates), can involve joins via relationships to other tables, and can potentially involve data that is only available post-insert such as computed columns. If the agent knows it can compute the result of such a check without actually performing an update, it is free to do so, but it must produce a result that is indistinguishable from that which was done post-update. If the post-update check fails, the mutation request should fail with an error using the error code `mutation-permission-check-failure`.
* `returning_fields`: This specifies a list of fields to return in the response. The property takes the same format as the `fields` property on Queries. It is expected that the specified fields will be returned for all rows affected by the update (ie. all updated rows).
Update operations return responses that are the same as insert operations, except the affected rows in `returning` are naturally the updated rows instead.
#### Delete Operations
Here's an example of a mutation that deletes a Track row:
```graphql
mutation UpdateTrack {
delete_Track(
where: { TrackId: { _eq: 1 } },
) {
affected_rows
returning {
TrackId
Milliseconds
}
}
}
```
This would cause a mutation request to be send that looks like this:
```json
{
"table_relationships": [],
"operations": [
{
"type": "delete",
"table": ["Track"],
"where": {
"type": "binary_op",
"operator": "equal",
"column": {
"name": "TrackId",
"column_type": "number"
},
"value": {
"type": "scalar",
"value": 1,
"value_type": "number"
}
},
"returning_fields": {
"TrackId": {
"type": "column",
"column": "TrackId",
"column_type": "number"
}
}
}
]
}
```
Breaking down the properties in the `delete`-typed mutation operation:
* `table`: specifies the table we're deleting rows from
* `where`: An expression (same as the expression in a Query's `where` property) that is used to select the matching rows to delete
* `returning_fields`: This specifies a list of fields to return in the response. The property takes the same format as the `fields` property on Queries. It is expected that the specified fields will be returned for all rows affected by the deletion (ie. all deleted rows).
Delete operations return responses that are the same as insert and update operations, except the affected rows in `returning` are the deleted rows instead.

View File

@ -1,6 +1,6 @@
{
"name": "@hasura/dc-api-types",
"version": "0.16.0",
"version": "0.17.0",
"description": "Hasura GraphQL Engine Data Connector Agent API types",
"author": "Hasura (https://github.com/hasura/graphql-engine)",
"license": "Apache-2.0",

View File

@ -210,6 +210,71 @@
}
}
},
"/mutation": {
"post": {
"parameters": [
{
"in": "header",
"name": "X-Hasura-DataConnector-SourceName",
"required": true,
"schema": {
"type": "string"
}
},
{
"in": "header",
"name": "X-Hasura-DataConnector-Config",
"required": true,
"schema": {
"additionalProperties": true,
"nullable": false,
"type": "object"
}
}
],
"requestBody": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/MutationRequest"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/MutationResponse"
}
}
},
"description": ""
},
"400": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": " or `body` or `X-Hasura-DataConnector-Config` or `X-Hasura-DataConnector-SourceName`"
},
"500": {
"content": {
"application/json;charset=utf-8": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
},
"description": ""
}
}
}
},
"/health": {
"get": {
"parameters": [
@ -392,7 +457,49 @@
"type": "object"
},
"QueryCapabilities": {},
"MutationCapabilities": {},
"InsertCapabilities": {
"properties": {
"supports_nested_inserts": {
"default": false,
"description": "Whether or not nested inserts to related tables are supported",
"type": "boolean"
}
},
"type": "object"
},
"UpdateCapabilities": {},
"DeleteCapabilities": {},
"AtomicitySupportLevel": {
"description": "Describes the level of transactional atomicity the agent supports for mutation operations.\n'row': If multiple rows are affected in a single operation but one fails, only the failed row's changes will be reverted\n'single_operation': If multiple rows are affected in a single operation but one fails, all affected rows in the operation will be reverted\n'homogeneous_operations': If multiple operations of only the same type exist in the one mutation request, a failure in one will result in all changes being reverted\n'heterogeneous_operations': If multiple operations of any type exist in the one mutation request, a failure in one will result in all changes being reverted\n",
"enum": [
"row",
"single_operation",
"homogeneous_operations",
"heterogeneous_operations"
],
"type": "string"
},
"ReturningCapabilities": {},
"MutationCapabilities": {
"properties": {
"atomicity_support_level": {
"$ref": "#/components/schemas/AtomicitySupportLevel"
},
"delete": {
"$ref": "#/components/schemas/DeleteCapabilities"
},
"insert": {
"$ref": "#/components/schemas/InsertCapabilities"
},
"returning": {
"$ref": "#/components/schemas/ReturningCapabilities"
},
"update": {
"$ref": "#/components/schemas/UpdateCapabilities"
}
},
"type": "object"
},
"SubscriptionCapabilities": {},
"ScalarType": {
"additionalProperties": true,
@ -786,7 +893,9 @@
},
"ErrorResponseType": {
"enum": [
"uncaught-error"
"uncaught-error",
"mutation-constraint-violation",
"mutation-permission-check-failure"
],
"type": "string"
},
@ -1747,6 +1856,430 @@
],
"type": "object"
},
"MutationResponse": {
"properties": {
"operation_results": {
"description": "The results of each mutation operation, in the same order as they were received",
"items": {
"$ref": "#/components/schemas/MutationOperationResults"
},
"type": "array"
}
},
"required": [
"operation_results"
],
"type": "object"
},
"MutationOperationResults": {
"properties": {
"affected_rows": {
"description": "The number of rows affected by the mutation operation",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "number"
},
"returning": {
"description": "The rows affected by the mutation operation",
"items": {
"additionalProperties": {
"additionalProperties": true,
"anyOf": [
{
"$ref": "#/components/schemas/ColumnFieldValue"
},
{
"$ref": "#/components/schemas/QueryResponse"
},
{
"$ref": "#/components/schemas/NullColumnFieldValue"
}
]
},
"type": "object"
},
"nullable": true,
"type": "array"
}
},
"required": [
"affected_rows"
],
"type": "object"
},
"MutationRequest": {
"properties": {
"insert_schema": {
"description": "The schema by which to interpret row data specified in any insert operations in this request",
"items": {
"$ref": "#/components/schemas/TableInsertSchema"
},
"type": "array"
},
"operations": {
"description": "The mutation operations to perform",
"items": {
"$ref": "#/components/schemas/MutationOperation"
},
"type": "array"
},
"table_relationships": {
"description": "The relationships between tables involved in the entire mutation request",
"items": {
"$ref": "#/components/schemas/TableRelationships"
},
"type": "array"
}
},
"required": [
"table_relationships",
"insert_schema",
"operations"
],
"type": "object"
},
"ObjectRelationInsertionOrder": {
"enum": [
"before_parent",
"after_parent"
],
"type": "string"
},
"ObjectRelationInsertSchema": {
"properties": {
"insertion_order": {
"$ref": "#/components/schemas/ObjectRelationInsertionOrder"
},
"relationship": {
"description": "The name of the object relationship over which the related row must be inserted",
"type": "string"
},
"type": {
"enum": [
"object_relation"
],
"type": "string"
}
},
"required": [
"relationship",
"insertion_order",
"type"
],
"type": "object"
},
"ArrayRelationInsertSchema": {
"properties": {
"relationship": {
"description": "The name of the array relationship over which the related rows must be inserted",
"type": "string"
},
"type": {
"enum": [
"array_relation"
],
"type": "string"
}
},
"required": [
"relationship",
"type"
],
"type": "object"
},
"ColumnInsertSchema": {
"properties": {
"column": {
"description": "The name of the column that this field should be inserted into",
"type": "string"
},
"column_type": {
"$ref": "#/components/schemas/ScalarType"
},
"type": {
"enum": [
"column"
],
"type": "string"
}
},
"required": [
"column",
"column_type",
"type"
],
"type": "object"
},
"InsertFieldSchema": {
"discriminator": {
"mapping": {
"array_relation": "ArrayRelationInsertSchema",
"column": "ColumnInsertSchema",
"object_relation": "ObjectRelationInsertSchema"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/components/schemas/ObjectRelationInsertSchema"
},
{
"$ref": "#/components/schemas/ArrayRelationInsertSchema"
},
{
"$ref": "#/components/schemas/ColumnInsertSchema"
}
]
},
"TableInsertSchema": {
"properties": {
"fields": {
"additionalProperties": {
"$ref": "#/components/schemas/InsertFieldSchema"
},
"description": "The fields that will be found in the insert row data for the table and the schema for each field",
"type": "object"
},
"table": {
"$ref": "#/components/schemas/TableName"
}
},
"required": [
"table",
"fields"
],
"type": "object"
},
"ColumnInsertFieldValue": {
"additionalProperties": true
},
"ObjectRelationInsertFieldValue": {},
"RowObject": {
"additionalProperties": {
"additionalProperties": true,
"anyOf": [
{
"$ref": "#/components/schemas/ColumnInsertFieldValue"
},
{
"$ref": "#/components/schemas/ObjectRelationInsertFieldValue"
},
{
"$ref": "#/components/schemas/ArrayRelationInsertFieldValue"
},
{
"$ref": "#/components/schemas/NullColumnInsertFieldValue"
}
]
},
"type": "object"
},
"ArrayRelationInsertFieldValue": {
"items": {
"$ref": "#/components/schemas/RowObject"
},
"type": "array"
},
"NullColumnInsertFieldValue": {
"type": "null"
},
"InsertMutationOperation": {
"properties": {
"returning_fields": {
"additionalProperties": {
"$ref": "#/components/schemas/Field"
},
"default": {},
"description": "The fields to return for the rows affected by this insert operation",
"nullable": true,
"type": "object"
},
"rows": {
"description": "The rows to insert into the table",
"items": {
"$ref": "#/components/schemas/RowObject"
},
"type": "array"
},
"table": {
"$ref": "#/components/schemas/TableName"
},
"type": {
"enum": [
"insert"
],
"type": "string"
}
},
"required": [
"table",
"rows",
"type"
],
"type": "object"
},
"DeleteMutationOperation": {
"properties": {
"returning_fields": {
"additionalProperties": {
"$ref": "#/components/schemas/Field"
},
"default": {},
"description": "The fields to return for the rows affected by this delete operation",
"nullable": true,
"type": "object"
},
"table": {
"$ref": "#/components/schemas/TableName"
},
"type": {
"enum": [
"delete"
],
"type": "string"
},
"where": {
"$ref": "#/components/schemas/Expression"
}
},
"required": [
"table",
"type"
],
"type": "object"
},
"IncrementColumnRowUpdate": {
"properties": {
"column": {
"description": "The name of the column in the row",
"type": "string"
},
"type": {
"enum": [
"increment"
],
"type": "string"
},
"value": {
"additionalProperties": true,
"description": "The value to use for the column"
},
"value_type": {
"$ref": "#/components/schemas/ScalarType"
}
},
"required": [
"column",
"value",
"value_type",
"type"
],
"type": "object"
},
"SetColumnRowUpdate": {
"properties": {
"column": {
"description": "The name of the column in the row",
"type": "string"
},
"type": {
"enum": [
"set"
],
"type": "string"
},
"value": {
"additionalProperties": true,
"description": "The value to use for the column"
},
"value_type": {
"$ref": "#/components/schemas/ScalarType"
}
},
"required": [
"column",
"value",
"value_type",
"type"
],
"type": "object"
},
"RowUpdate": {
"discriminator": {
"mapping": {
"increment": "IncrementColumnRowUpdate",
"set": "SetColumnRowUpdate"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/components/schemas/IncrementColumnRowUpdate"
},
{
"$ref": "#/components/schemas/SetColumnRowUpdate"
}
]
},
"UpdateMutationOperation": {
"properties": {
"returning_fields": {
"additionalProperties": {
"$ref": "#/components/schemas/Field"
},
"default": {},
"description": "The fields to return for the rows affected by this update operation",
"nullable": true,
"type": "object"
},
"table": {
"$ref": "#/components/schemas/TableName"
},
"type": {
"enum": [
"update"
],
"type": "string"
},
"updates": {
"description": "The updates to make to the matched rows in the table",
"items": {
"$ref": "#/components/schemas/RowUpdate"
},
"type": "array"
},
"where": {
"$ref": "#/components/schemas/Expression"
}
},
"required": [
"table",
"updates",
"type"
],
"type": "object"
},
"MutationOperation": {
"discriminator": {
"mapping": {
"delete": "DeleteMutationOperation",
"insert": "InsertMutationOperation",
"update": "UpdateMutationOperation"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/components/schemas/InsertMutationOperation"
},
{
"$ref": "#/components/schemas/DeleteMutationOperation"
},
{
"$ref": "#/components/schemas/UpdateMutationOperation"
}
]
},
"RawResponse": {
"properties": {
"rows": {

View File

@ -9,6 +9,9 @@ export type { AnotherColumnComparison } from './models/AnotherColumnComparison';
export type { ApplyBinaryArrayComparisonOperator } from './models/ApplyBinaryArrayComparisonOperator';
export type { ApplyBinaryComparisonOperator } from './models/ApplyBinaryComparisonOperator';
export type { ApplyUnaryComparisonOperator } from './models/ApplyUnaryComparisonOperator';
export type { ArrayRelationInsertFieldValue } from './models/ArrayRelationInsertFieldValue';
export type { ArrayRelationInsertSchema } from './models/ArrayRelationInsertSchema';
export type { AtomicitySupportLevel } from './models/AtomicitySupportLevel';
export type { BinaryArrayComparisonOperator } from './models/BinaryArrayComparisonOperator';
export type { BinaryComparisonOperator } from './models/BinaryComparisonOperator';
export type { Capabilities } from './models/Capabilities';
@ -17,6 +20,8 @@ export type { ColumnCountAggregate } from './models/ColumnCountAggregate';
export type { ColumnField } from './models/ColumnField';
export type { ColumnFieldValue } from './models/ColumnFieldValue';
export type { ColumnInfo } from './models/ColumnInfo';
export type { ColumnInsertFieldValue } from './models/ColumnInsertFieldValue';
export type { ColumnInsertSchema } from './models/ColumnInsertSchema';
export type { ColumnNullability } from './models/ColumnNullability';
export type { ComparisonCapabilities } from './models/ComparisonCapabilities';
export type { ComparisonColumn } from './models/ComparisonColumn';
@ -25,6 +30,8 @@ export type { ComparisonValue } from './models/ComparisonValue';
export type { ConfigSchemaResponse } from './models/ConfigSchemaResponse';
export type { Constraint } from './models/Constraint';
export type { DataSchemaCapabilities } from './models/DataSchemaCapabilities';
export type { DeleteCapabilities } from './models/DeleteCapabilities';
export type { DeleteMutationOperation } from './models/DeleteMutationOperation';
export type { ErrorResponse } from './models/ErrorResponse';
export type { ErrorResponseType } from './models/ErrorResponseType';
export type { ExistsExpression } from './models/ExistsExpression';
@ -33,10 +40,22 @@ export type { ExplainCapabilities } from './models/ExplainCapabilities';
export type { ExplainResponse } from './models/ExplainResponse';
export type { Expression } from './models/Expression';
export type { Field } from './models/Field';
export type { IncrementColumnRowUpdate } from './models/IncrementColumnRowUpdate';
export type { InsertCapabilities } from './models/InsertCapabilities';
export type { InsertFieldSchema } from './models/InsertFieldSchema';
export type { InsertMutationOperation } from './models/InsertMutationOperation';
export type { MetricsCapabilities } from './models/MetricsCapabilities';
export type { MutationCapabilities } from './models/MutationCapabilities';
export type { MutationOperation } from './models/MutationOperation';
export type { MutationOperationResults } from './models/MutationOperationResults';
export type { MutationRequest } from './models/MutationRequest';
export type { MutationResponse } from './models/MutationResponse';
export type { NotExpression } from './models/NotExpression';
export type { NullColumnFieldValue } from './models/NullColumnFieldValue';
export type { NullColumnInsertFieldValue } from './models/NullColumnInsertFieldValue';
export type { ObjectRelationInsertFieldValue } from './models/ObjectRelationInsertFieldValue';
export type { ObjectRelationInsertionOrder } from './models/ObjectRelationInsertionOrder';
export type { ObjectRelationInsertSchema } from './models/ObjectRelationInsertSchema';
export type { OpenApiDiscriminator } from './models/OpenApiDiscriminator';
export type { OpenApiExternalDocumentation } from './models/OpenApiExternalDocumentation';
export type { OpenApiReference } from './models/OpenApiReference';
@ -63,18 +82,25 @@ export type { Relationship } from './models/Relationship';
export type { RelationshipCapabilities } from './models/RelationshipCapabilities';
export type { RelationshipField } from './models/RelationshipField';
export type { RelationshipType } from './models/RelationshipType';
export type { ReturningCapabilities } from './models/ReturningCapabilities';
export type { RowObject } from './models/RowObject';
export type { RowUpdate } from './models/RowUpdate';
export type { ScalarType } from './models/ScalarType';
export type { ScalarTypeCapabilities } from './models/ScalarTypeCapabilities';
export type { ScalarTypesCapabilities } from './models/ScalarTypesCapabilities';
export type { ScalarValueComparison } from './models/ScalarValueComparison';
export type { SchemaResponse } from './models/SchemaResponse';
export type { SetColumnRowUpdate } from './models/SetColumnRowUpdate';
export type { SingleColumnAggregate } from './models/SingleColumnAggregate';
export type { SingleColumnAggregateFunction } from './models/SingleColumnAggregateFunction';
export type { StarCountAggregate } from './models/StarCountAggregate';
export type { SubqueryComparisonCapabilities } from './models/SubqueryComparisonCapabilities';
export type { SubscriptionCapabilities } from './models/SubscriptionCapabilities';
export type { TableInfo } from './models/TableInfo';
export type { TableInsertSchema } from './models/TableInsertSchema';
export type { TableName } from './models/TableName';
export type { TableRelationships } from './models/TableRelationships';
export type { UnaryComparisonOperator } from './models/UnaryComparisonOperator';
export type { UnrelatedTable } from './models/UnrelatedTable';
export type { UpdateCapabilities } from './models/UpdateCapabilities';
export type { UpdateMutationOperation } from './models/UpdateMutationOperation';

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RowObject } from './RowObject';
export type ArrayRelationInsertFieldValue = Array<RowObject>;

View File

@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ArrayRelationInsertSchema = {
/**
* The name of the array relationship over which the related rows must be inserted
*/
relationship: string;
type: 'array_relation';
};

View File

@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Describes the level of transactional atomicity the agent supports for mutation operations.
* 'row': If multiple rows are affected in a single operation but one fails, only the failed row's changes will be reverted
* 'single_operation': If multiple rows are affected in a single operation but one fails, all affected rows in the operation will be reverted
* 'homogeneous_operations': If multiple operations of only the same type exist in the one mutation request, a failure in one will result in all changes being reverted
* 'heterogeneous_operations': If multiple operations of any type exist in the one mutation request, a failure in one will result in all changes being reverted
*
*/
export type AtomicitySupportLevel = 'row' | 'single_operation' | 'homogeneous_operations' | 'heterogeneous_operations';

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ColumnInsertFieldValue = {
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScalarType } from './ScalarType';
export type ColumnInsertSchema = {
/**
* The name of the column that this field should be inserted into
*/
column: string;
column_type: ScalarType;
type: 'column';
};

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type DeleteCapabilities = {
};

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
import type { Field } from './Field';
import type { TableName } from './TableName';
export type DeleteMutationOperation = {
/**
* The fields to return for the rows affected by this delete operation
*/
returning_fields?: Record<string, Field> | null;
table: TableName;
type: 'delete';
where?: Expression;
};

View File

@ -2,4 +2,4 @@
/* tslint:disable */
/* eslint-disable */
export type ErrorResponseType = 'uncaught-error';
export type ErrorResponseType = 'uncaught-error' | 'mutation-constraint-violation' | 'mutation-permission-check-failure';

View File

@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScalarType } from './ScalarType';
export type IncrementColumnRowUpdate = {
/**
* The name of the column in the row
*/
column: string;
type: 'increment';
/**
* The value to use for the column
*/
value: any;
value_type: ScalarType;
};

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type InsertCapabilities = {
/**
* Whether or not nested inserts to related tables are supported
*/
supports_nested_inserts?: boolean;
};

View File

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ArrayRelationInsertSchema } from './ArrayRelationInsertSchema';
import type { ColumnInsertSchema } from './ColumnInsertSchema';
import type { ObjectRelationInsertSchema } from './ObjectRelationInsertSchema';
export type InsertFieldSchema = (ObjectRelationInsertSchema | ArrayRelationInsertSchema | ColumnInsertSchema);

View File

@ -0,0 +1,21 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Field } from './Field';
import type { RowObject } from './RowObject';
import type { TableName } from './TableName';
export type InsertMutationOperation = {
/**
* The fields to return for the rows affected by this insert operation
*/
returning_fields?: Record<string, Field> | null;
/**
* The rows to insert into the table
*/
rows: Array<RowObject>;
table: TableName;
type: 'insert';
};

View File

@ -2,6 +2,17 @@
/* tslint:disable */
/* eslint-disable */
import type { AtomicitySupportLevel } from './AtomicitySupportLevel';
import type { DeleteCapabilities } from './DeleteCapabilities';
import type { InsertCapabilities } from './InsertCapabilities';
import type { ReturningCapabilities } from './ReturningCapabilities';
import type { UpdateCapabilities } from './UpdateCapabilities';
export type MutationCapabilities = {
atomicity_support_level?: AtomicitySupportLevel;
delete?: DeleteCapabilities;
insert?: InsertCapabilities;
returning?: ReturningCapabilities;
update?: UpdateCapabilities;
};

View File

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { DeleteMutationOperation } from './DeleteMutationOperation';
import type { InsertMutationOperation } from './InsertMutationOperation';
import type { UpdateMutationOperation } from './UpdateMutationOperation';
export type MutationOperation = (InsertMutationOperation | DeleteMutationOperation | UpdateMutationOperation);

View File

@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ColumnFieldValue } from './ColumnFieldValue';
import type { NullColumnFieldValue } from './NullColumnFieldValue';
import type { QueryResponse } from './QueryResponse';
export type MutationOperationResults = {
/**
* The number of rows affected by the mutation operation
*/
affected_rows: number;
/**
* The rows affected by the mutation operation
*/
returning?: Array<Record<string, (ColumnFieldValue | QueryResponse | NullColumnFieldValue)>> | null;
};

View File

@ -0,0 +1,23 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MutationOperation } from './MutationOperation';
import type { TableInsertSchema } from './TableInsertSchema';
import type { TableRelationships } from './TableRelationships';
export type MutationRequest = {
/**
* The schema by which to interpret row data specified in any insert operations in this request
*/
insert_schema: Array<TableInsertSchema>;
/**
* The mutation operations to perform
*/
operations: Array<MutationOperation>;
/**
* The relationships between tables involved in the entire mutation request
*/
table_relationships: Array<TableRelationships>;
};

View File

@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { MutationOperationResults } from './MutationOperationResults';
export type MutationResponse = {
/**
* The results of each mutation operation, in the same order as they were received
*/
operation_results: Array<MutationOperationResults>;
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type NullColumnInsertFieldValue = null;

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ObjectRelationInsertFieldValue = {
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ObjectRelationInsertionOrder } from './ObjectRelationInsertionOrder';
export type ObjectRelationInsertSchema = {
insertion_order: ObjectRelationInsertionOrder;
/**
* The name of the object relationship over which the related row must be inserted
*/
relationship: string;
type: 'object_relation';
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ObjectRelationInsertionOrder = 'before_parent' | 'after_parent';

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ReturningCapabilities = {
};

View File

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ArrayRelationInsertFieldValue } from './ArrayRelationInsertFieldValue';
import type { ColumnInsertFieldValue } from './ColumnInsertFieldValue';
import type { NullColumnInsertFieldValue } from './NullColumnInsertFieldValue';
import type { ObjectRelationInsertFieldValue } from './ObjectRelationInsertFieldValue';
export type RowObject = Record<string, (ColumnInsertFieldValue | ObjectRelationInsertFieldValue | ArrayRelationInsertFieldValue | NullColumnInsertFieldValue)>;

View File

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { IncrementColumnRowUpdate } from './IncrementColumnRowUpdate';
import type { SetColumnRowUpdate } from './SetColumnRowUpdate';
export type RowUpdate = (IncrementColumnRowUpdate | SetColumnRowUpdate);

View File

@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScalarType } from './ScalarType';
export type SetColumnRowUpdate = {
/**
* The name of the column in the row
*/
column: string;
type: 'set';
/**
* The value to use for the column
*/
value: any;
value_type: ScalarType;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { InsertFieldSchema } from './InsertFieldSchema';
import type { TableName } from './TableName';
export type TableInsertSchema = {
/**
* The fields that will be found in the insert row data for the table and the schema for each field
*/
fields: Record<string, InsertFieldSchema>;
table: TableName;
};

View File

@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UpdateCapabilities = {
};

View File

@ -0,0 +1,23 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
import type { Field } from './Field';
import type { RowUpdate } from './RowUpdate';
import type { TableName } from './TableName';
export type UpdateMutationOperation = {
/**
* The fields to return for the rows affected by this update operation
*/
returning_fields?: Record<string, Field> | null;
table: TableName;
type: 'update';
/**
* The updates to make to the matched rows in the table
*/
updates: Array<RowUpdate>;
where?: Expression;
};

View File

@ -24,7 +24,7 @@
},
"dc-api-types": {
"name": "@hasura/dc-api-types",
"version": "0.16.0",
"version": "0.17.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",
@ -1197,7 +1197,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^7.0.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify": "^3.29.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",
@ -1781,7 +1781,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify": "^4.4.0",
"fastify-metrics": "^9.2.1",
"nanoid": "^3.3.4",
@ -3125,7 +3125,7 @@
"version": "file:reference",
"requires": {
"@fastify/cors": "^7.0.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"@tsconfig/node16": "^1.0.3",
"@types/node": "^16.11.49",
"@types/xml2js": "^0.4.11",
@ -3514,7 +3514,7 @@
"version": "file:sqlite",
"requires": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"@tsconfig/node16": "^1.0.3",
"@types/node": "^16.11.49",
"@types/sqlite3": "^3.1.8",

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^7.0.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify": "^3.29.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",
@ -44,7 +44,7 @@
}
},
"node_modules/@hasura/dc-api-types": {
"version": "0.16.0",
"version": "0.17.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@fastify/cors": "^7.0.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify": "^3.29.0",
"mathjs": "^11.0.0",
"pino-pretty": "^8.0.0",

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify": "^4.4.0",
"fastify-metrics": "^9.2.1",
"nanoid": "^3.3.4",
@ -54,7 +54,7 @@
"license": "MIT"
},
"node_modules/@hasura/dc-api-types": {
"version": "0.16.0",
"version": "0.17.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

@ -22,7 +22,7 @@
},
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.16.0",
"@hasura/dc-api-types": "0.17.0",
"fastify-metrics": "^9.2.1",
"fastify": "^4.4.0",
"nanoid": "^3.3.4",

View File

@ -1096,6 +1096,7 @@ test-suite graphql-engine-tests
Hasura.Backends.DataConnector.API.V0.ColumnSpec
Hasura.Backends.DataConnector.API.V0.ConfigSchemaSpec
Hasura.Backends.DataConnector.API.V0.ExpressionSpec
Hasura.Backends.DataConnector.API.V0.MutationsSpec
Hasura.Backends.DataConnector.API.V0.OrderBySpec
Hasura.Backends.DataConnector.API.V0.QuerySpec
Hasura.Backends.DataConnector.API.V0.RelationshipsSpec

View File

@ -78,6 +78,7 @@ library
Hasura.Backends.DataConnector.API.V0.ConfigSchema
Hasura.Backends.DataConnector.API.V0.ErrorResponse
Hasura.Backends.DataConnector.API.V0.Expression
Hasura.Backends.DataConnector.API.V0.Mutations
Hasura.Backends.DataConnector.API.V0.OrderBy
Hasura.Backends.DataConnector.API.V0.Query
Hasura.Backends.DataConnector.API.V0.Explain

View File

@ -6,6 +6,7 @@ module Hasura.Backends.DataConnector.API
Api,
CapabilitiesResponses,
QueryResponses,
MutationResponses,
SchemaApi,
SchemaResponses,
QueryApi,
@ -98,6 +99,8 @@ queryCase defaultAction queryAction errorAction union = do
type QueryResponses = '[V0.QueryResponse, V0.ErrorResponse, V0.ErrorResponse400]
type MutationResponses = '[V0.MutationResponse, V0.ErrorResponse, V0.ErrorResponse400]
type QueryApi =
"query"
:> SourceNameHeader Required
@ -112,6 +115,13 @@ type ExplainApi =
:> ReqBody '[JSON] V0.QueryRequest
:> Post '[JSON] V0.ExplainResponse
type MutationApi =
"mutation"
:> SourceNameHeader Required
:> ConfigHeader Required
:> ReqBody '[JSON] V0.MutationRequest
:> UVerb 'POST '[JSON] MutationResponses
type HealthApi =
"health"
:> SourceNameHeader Optional
@ -158,18 +168,20 @@ data Routes mode = Routes
_query :: mode :- QueryApi,
-- | 'POST /explain'
_explain :: mode :- ExplainApi,
-- | 'POST /mutation'
_mutation :: mode :- MutationApi,
-- | 'GET /health'
_health :: mode :- HealthApi,
-- | 'GET /metrics'
_metrics :: mode :- MetricsApi,
-- | 'GET /metrics'
-- | 'GET /raw'
_raw :: mode :- RawApi
}
deriving stock (Generic)
-- | servant-openapi3 does not (yet) support NamedRoutes so we need to compose the
-- API the old-fashioned way using :<|> for use by @toOpenApi@
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi :<|> ExplainApi :<|> HealthApi :<|> MetricsApi :<|> RawApi
type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi :<|> ExplainApi :<|> MutationApi :<|> HealthApi :<|> MetricsApi :<|> RawApi
-- | Provide an OpenApi 3.0 schema for the API
openApiSchema :: OpenApi

View File

@ -5,6 +5,7 @@ module Hasura.Backends.DataConnector.API.V0
module ConfigSchema,
module Expression,
module ErrorResponse,
module Mutations,
module OrderBy,
module Query,
module Raw,
@ -23,6 +24,7 @@ import Hasura.Backends.DataConnector.API.V0.ConfigSchema as ConfigSchema
import Hasura.Backends.DataConnector.API.V0.ErrorResponse as ErrorResponse
import Hasura.Backends.DataConnector.API.V0.Explain as Explain
import Hasura.Backends.DataConnector.API.V0.Expression as Expression
import Hasura.Backends.DataConnector.API.V0.Mutations as Mutations
import Hasura.Backends.DataConnector.API.V0.OrderBy as OrderBy
import Hasura.Backends.DataConnector.API.V0.Query as Query
import Hasura.Backends.DataConnector.API.V0.Raw as Raw

View File

@ -12,6 +12,11 @@ module Hasura.Backends.DataConnector.API.V0.Capabilities
ColumnNullability (..),
QueryCapabilities (..),
MutationCapabilities (..),
InsertCapabilities (..),
UpdateCapabilities (..),
DeleteCapabilities (..),
AtomicitySupportLevel (..),
ReturningCapabilities (..),
SubscriptionCapabilities (..),
ComparisonOperators (..),
AggregateFunctions (..),
@ -142,13 +147,91 @@ instance HasCodec QueryCapabilities where
codec =
object "QueryCapabilities" $ pure QueryCapabilities
data MutationCapabilities = MutationCapabilities {}
data MutationCapabilities = MutationCapabilities
{ _mcInsertCapabilities :: Maybe InsertCapabilities,
_mcUpdateCapabilities :: Maybe UpdateCapabilities,
_mcDeleteCapabilities :: Maybe DeleteCapabilities,
_mcAtomicitySupportLevel :: Maybe AtomicitySupportLevel,
_mcReturningCapabilities :: Maybe ReturningCapabilities
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec MutationCapabilities
instance HasCodec MutationCapabilities where
codec = object "MutationCapabilities" $ pure MutationCapabilities
codec =
object "MutationCapabilities" $
MutationCapabilities
<$> optionalField "insert" "Whether or not the agent supports insert mutations" .= _mcInsertCapabilities
<*> optionalField "update" "Whether or not the agent supports update mutations" .= _mcUpdateCapabilities
<*> optionalField "delete" "Whether or not the agent supports delete mutations" .= _mcDeleteCapabilities
<*> optionalField "atomicity_support_level" "What level of transactional atomicity does the agent support for mutations" .= _mcAtomicitySupportLevel
<*> optionalField "returning" "Whether or not the agent supports returning the mutation-affected rows" .= _mcReturningCapabilities
data InsertCapabilities = InsertCapabilities
{ _icSupportsNestedInserts :: Bool
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec InsertCapabilities
instance HasCodec InsertCapabilities where
codec =
object "InsertCapabilities" $
InsertCapabilities
<$> optionalFieldWithDefault "supports_nested_inserts" False "Whether or not nested inserts to related tables are supported" .= _icSupportsNestedInserts
data UpdateCapabilities = UpdateCapabilities {}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec UpdateCapabilities
instance HasCodec UpdateCapabilities where
codec =
object "UpdateCapabilities" $ pure UpdateCapabilities
data DeleteCapabilities = DeleteCapabilities {}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec DeleteCapabilities
instance HasCodec DeleteCapabilities where
codec =
object "DeleteCapabilities" $ pure DeleteCapabilities
data AtomicitySupportLevel
= RowAtomicity
| SingleOperationAtomicity
| HomogeneousOperationsAtomicity
| HeterogeneousOperationsAtomicity
deriving stock (Eq, Ord, Show, Generic, Data, Enum, Bounded)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec AtomicitySupportLevel
instance HasCodec AtomicitySupportLevel where
codec =
named "AtomicitySupportLevel" $
stringConstCodec
[ (RowAtomicity, "row"),
(SingleOperationAtomicity, "single_operation"),
(HomogeneousOperationsAtomicity, "homogeneous_operations"),
(HeterogeneousOperationsAtomicity, "heterogeneous_operations")
]
<??> [ "Describes the level of transactional atomicity the agent supports for mutation operations.",
"'row': If multiple rows are affected in a single operation but one fails, only the failed row's changes will be reverted",
"'single_operation': If multiple rows are affected in a single operation but one fails, all affected rows in the operation will be reverted",
"'homogeneous_operations': If multiple operations of only the same type exist in the one mutation request, a failure in one will result in all changes being reverted",
"'heterogeneous_operations': If multiple operations of any type exist in the one mutation request, a failure in one will result in all changes being reverted"
]
data ReturningCapabilities = ReturningCapabilities {}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec ReturningCapabilities
instance HasCodec ReturningCapabilities where
codec =
object "ReturningCapabilities" $ pure ReturningCapabilities
data SubscriptionCapabilities = SubscriptionCapabilities {}
deriving stock (Eq, Ord, Show, Generic, Data)

View File

@ -37,12 +37,18 @@ import Prelude
data ErrorResponseType
= UncaughtError
| MutationConstraintViolation
| MutationPermissionCheckFailure
deriving stock (Eq, Show, Generic)
instance HasCodec ErrorResponseType where
codec =
named "ErrorResponseType" $
stringConstCodec [(UncaughtError, "uncaught-error")]
stringConstCodec
[ (UncaughtError, "uncaught-error"),
(MutationConstraintViolation, "mutation-constraint-violation"),
(MutationPermissionCheckFailure, "mutation-permission-check-failure")
]
data ErrorResponse = ErrorResponse
{ _crType :: ErrorResponseType,

View File

@ -0,0 +1,476 @@
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE OverloadedLists #-}
module Hasura.Backends.DataConnector.API.V0.Mutations
( MutationRequest (..),
TableInsertSchema (..),
InsertFieldSchema (..),
ColumnInsertSchema (..),
ObjectRelationInsertSchema (..),
ObjectRelationInsertionOrder (..),
ArrayRelationInsertSchema (..),
MutationOperation (..),
InsertMutationOperation (..),
RowObject (..),
InsertFieldValue,
mkColumnInsertFieldValue,
mkObjectRelationInsertFieldValue,
mkArrayRelationInsertFieldValue,
deserializeAsColumnInsertFieldValue,
deserializeAsObjectRelationInsertFieldValue,
deserializeAsArrayRelationInsertFieldValue,
_ColumnInsertFieldValue,
_ObjectRelationInsertFieldValue,
_ArrayRelationInsertFieldValue,
UpdateMutationOperation (..),
RowUpdate (..),
RowColumnValue (..),
DeleteMutationOperation (..),
MutationResponse (..),
MutationOperationResults (..),
)
where
import Autodocodec.Extended
import Autodocodec.OpenAPI ()
import Control.Arrow (ArrowChoice (left))
import Control.Lens (Lens', lens, prism')
import Control.Lens.Combinators (Prism')
import Data.Aeson (FromJSON, ToJSON)
import Data.Aeson qualified as J
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.OpenApi (ToSchema)
import Data.Scientific (Scientific)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Show (appPrec, appPrec1)
import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Expression qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Query qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Relationships qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Scalar qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0
import Servant.API (HasStatus (..))
-- | A request to perform a batch of 'MutationOperation's.
--
-- The table relationships and insert schema represent metadata that will be
-- used by agents interpreting the operations, and are shared across all operations.
data MutationRequest = MutationRequest
{ _mrTableRelationships :: [API.V0.TableRelationships],
_mrInsertSchema :: [TableInsertSchema],
_mrOperations :: [MutationOperation]
}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec MutationRequest
instance HasCodec MutationRequest where
codec :: JSONCodec MutationRequest
codec =
object "MutationRequest" $
MutationRequest
<$> requiredField "table_relationships" "The relationships between tables involved in the entire mutation request"
.= _mrTableRelationships
<*> requiredField "insert_schema" "The schema by which to interpret row data specified in any insert operations in this request"
.= _mrInsertSchema
<*> requiredField "operations" "The mutation operations to perform"
.= _mrOperations
-- | Describes the fields that may be found in rows that are requested to be inserted
-- in an 'InsertMutationOperation' in the 'MutationRequest' (ie. a 'RowObject').
--
-- This metadata about rows to be inserted has been extracted out of the rows
-- themselves to reduce the amount of duplicated information in the serialized request.
-- Without doing this, every row inserted into the same table would repeat the same
-- schema information over and over.
data TableInsertSchema = TableInsertSchema
{ _tisTable :: API.V0.TableName,
_tisFields :: HashMap API.V0.FieldName InsertFieldSchema
}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec TableInsertSchema
instance HasCodec TableInsertSchema where
codec :: JSONCodec TableInsertSchema
codec =
object "TableInsertSchema" $
TableInsertSchema
<$> requiredField "table" "The name of the table"
.= _tisTable
<*> requiredField "fields" "The fields that will be found in the insert row data for the table and the schema for each field"
.= _tisFields
-- | The schema of a particular field in a row to be inserted (ie. a 'RowObject').
data InsertFieldSchema
= -- | The field represents a particular column in the table
ColumnInsert ColumnInsertSchema
| -- | The field represents a row to be inserted in a separate table that
-- is related to the current table by an object relationship
ObjectRelationInsert ObjectRelationInsertSchema
| -- | The field represents a collection of rows to be inserted in a
-- separate table that is related to the current table by an array relationship
ArrayRelationInsert ArrayRelationInsertSchema
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec InsertFieldSchema
instance HasCodec InsertFieldSchema where
codec =
named "InsertFieldSchema" $
object "InsertFieldSchema" $
discriminatedUnionCodec "type" enc dec
where
columnInsertSchemaObjectCodec :: ObjectCodec ColumnInsertSchema ColumnInsertSchema
columnInsertSchemaObjectCodec =
ColumnInsertSchema
<$> requiredField "column" "The name of the column that this field should be inserted into"
.= _cisColumn
<*> requiredField "column_type" "The scalar type of the column"
.= _cisColumnType
objectRelationInsertSchemaObjectCodec :: ObjectCodec ObjectRelationInsertSchema ObjectRelationInsertSchema
objectRelationInsertSchemaObjectCodec =
ObjectRelationInsertSchema
<$> requiredField "relationship" "The name of the object relationship over which the related row must be inserted"
.= _orisRelationship
<*> requiredField "insertion_order" "The order in which to insert the related row, relative to its parent"
.= _orisInsertionOrder
arrayRelationInsertSchemaObjectCodec :: ObjectCodec ArrayRelationInsertSchema ArrayRelationInsertSchema
arrayRelationInsertSchemaObjectCodec =
ArrayRelationInsertSchema
<$> requiredField "relationship" "The name of the array relationship over which the related rows must be inserted"
.= _arisRelationship
enc = \case
ColumnInsert insertOp -> ("column", mapToEncoder insertOp columnInsertSchemaObjectCodec)
ObjectRelationInsert updateOp -> ("object_relation", mapToEncoder updateOp objectRelationInsertSchemaObjectCodec)
ArrayRelationInsert deleteOp -> ("array_relation", mapToEncoder deleteOp arrayRelationInsertSchemaObjectCodec)
dec =
HashMap.fromList
[ ("column", ("ColumnInsertSchema", mapToDecoder ColumnInsert columnInsertSchemaObjectCodec)),
("object_relation", ("ObjectRelationInsertSchema", mapToDecoder ObjectRelationInsert objectRelationInsertSchemaObjectCodec)),
("array_relation", ("ArrayRelationInsertSchema", mapToDecoder ArrayRelationInsert arrayRelationInsertSchemaObjectCodec))
]
-- | Describes a field in a row to be inserted that represents a column in the table
data ColumnInsertSchema = ColumnInsertSchema
{ _cisColumn :: API.V0.ColumnName,
_cisColumnType :: API.V0.ScalarType
}
deriving stock (Eq, Ord, Show)
-- | Describes a field in a row to be inserted that represents a row to be inserted in
-- a separate table tha is related to the current table by an object relationship
data ObjectRelationInsertSchema = ObjectRelationInsertSchema
{ _orisRelationship :: API.V0.RelationshipName,
_orisInsertionOrder :: ObjectRelationInsertionOrder
}
deriving stock (Eq, Ord, Show)
-- | Describes whether the object-related row needs to be inserted before or after the
-- parent row. This is important in one-to-one object relationships where the order
-- of insertion needs to be clarified
data ObjectRelationInsertionOrder
= BeforeParent
| AfterParent
deriving stock (Eq, Ord, Show, Enum, Bounded)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec ObjectRelationInsertionOrder
instance HasCodec ObjectRelationInsertionOrder where
codec =
named "ObjectRelationInsertionOrder" $
( stringConstCodec
[ (BeforeParent, "before_parent"),
(AfterParent, "after_parent")
]
)
-- | Describes a field in a row to be inserted that represents a collection of rows to
-- be inserted in a separate table tha is related to the current table by an array relationship
data ArrayRelationInsertSchema = ArrayRelationInsertSchema
{ _arisRelationship :: API.V0.RelationshipName
}
deriving stock (Eq, Ord, Show)
-- | Represents a particular mutation operation to perform against a table
data MutationOperation
= InsertOperation InsertMutationOperation
| UpdateOperation UpdateMutationOperation
| DeleteOperation DeleteMutationOperation
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec MutationOperation
instance HasCodec MutationOperation where
codec =
named "MutationOperation" $
object "MutationOperation" $
discriminatedUnionCodec "type" enc dec
where
insertMutationOperationObjectCodec :: ObjectCodec InsertMutationOperation InsertMutationOperation
insertMutationOperationObjectCodec =
InsertMutationOperation
<$> requiredField "table" "The name of the table to insert rows in"
.= _imoTable
<*> requiredField "rows" "The rows to insert into the table"
.= _imoRows
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this insert operation"
.= _imoReturningFields
updateMutationOperationObjectCodec :: ObjectCodec UpdateMutationOperation UpdateMutationOperation
updateMutationOperationObjectCodec =
UpdateMutationOperation
<$> requiredField "table" "The name of the table to update rows in"
.= _umoTable
<*> optionalFieldOrNull "where" "The filter by which to select rows to update"
.= _umoWhere
<*> requiredField "updates" "The updates to make to the matched rows in the table"
.= _umoUpdates
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this update operation"
.= _umoReturningFields
deleteMutationOperationObjectCodec :: ObjectCodec DeleteMutationOperation DeleteMutationOperation
deleteMutationOperationObjectCodec =
DeleteMutationOperation
<$> requiredField "table" "The name of the table to delete rows from"
.= _dmoTable
<*> optionalFieldOrNull "where" "The filter by which to select rows to delete"
.= _dmoWhere
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this delete operation"
.= _dmoReturningFields
enc = \case
InsertOperation insertOp -> ("insert", mapToEncoder insertOp insertMutationOperationObjectCodec)
UpdateOperation updateOp -> ("update", mapToEncoder updateOp updateMutationOperationObjectCodec)
DeleteOperation deleteOp -> ("delete", mapToEncoder deleteOp deleteMutationOperationObjectCodec)
dec =
HashMap.fromList
[ ("insert", ("InsertMutationOperation", mapToDecoder InsertOperation insertMutationOperationObjectCodec)),
("update", ("UpdateMutationOperation", mapToDecoder UpdateOperation updateMutationOperationObjectCodec)),
("delete", ("DeleteMutationOperation", mapToDecoder DeleteOperation deleteMutationOperationObjectCodec))
]
-- | Describes a collection of rows that should be inserted into a particular table.
--
-- The schema of the rows must be interpreted by looking at the insert schema on 'MutationRequest'
data InsertMutationOperation = InsertMutationOperation
{ -- | The table to insert into
_imoTable :: API.V0.TableName,
-- | The rows to insert into the table
_imoRows :: [RowObject],
-- | The fields to return that represent a projection over the set of rows inserted
-- after they are inserted (after insertion they include calculated columns, relations etc)
_imoReturningFields :: HashMap API.V0.FieldName API.V0.Field
}
deriving stock (Eq, Ord, Show)
-- | A row to be inserted into a table. It is mapping from a 'API.V0.FieldName' to a 'InsertFieldValue'.
-- The field name must be looked up in the insert schema defined in the 'MutationRequest' in order to know
-- how to deserialize the 'InsertFieldValue' correctly.
--
-- Note that the field name is not the same as a table column name. The column name (if the field
-- represent a table column) can be found in the insert schema.
newtype RowObject = RowObject {unRowObject :: HashMap API.V0.FieldName InsertFieldValue}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RowObject
instance HasCodec RowObject where
codec = named "RowObject" $ dimapCodec RowObject unRowObject codec
-- | Represents the value of a field in a row to be inserted. There are three diffferent types of
-- row field value types: column, object relation and array relation. ColumnInsertFieldValues represent
-- a table column in the row to be inserted. ObjectRelationInsertFieldValues represent a row in another
-- object-related table that needs to be inserted. ArrayRelationInsertFieldValues represent rows in
-- another array-related table that need to be inserted.
--
-- Unfortunately, the JSON representation of these three different types wholly overlap with each other.
-- We chose not to explicitly discriminate them (eg. wrapped in an object with a type property) because
-- we wanted to keep the JSON that represents row data to insert as terse as possible. This means in
-- order to know which type any particular 'InsertFieldValue' actually is, you must look at the
-- 'TableInsertSchema' and look up the field in question to see the matching 'InsertFieldSchema', which
-- will tell you what type it is. Then, you can explicitly deserialize the 'InsertFieldValue' as the type
-- it is supposed to be.
--
-- Explicit serialization functions:
-- * 'mkColumnInsertFieldValue'
-- * 'mkObjectRelationInsertFieldValue'
-- * 'mkArrayRelationInsertFieldValue'
--
-- Explicit deserialization functions:
-- * 'deserializeAsColumnInsertFieldValue'
-- * 'deserializeAsObjectRelationInsertFieldValue'
-- * 'deserializeAsArrayRelationInsertFieldValue'
newtype InsertFieldValue = InsertFieldValue J.Value
deriving stock (Eq, Ord, Show)
deriving (ToJSON, FromJSON, ToSchema) via Autodocodec InsertFieldValue
mkColumnInsertFieldValue :: J.Value -> InsertFieldValue
mkColumnInsertFieldValue = InsertFieldValue
mkObjectRelationInsertFieldValue :: RowObject -> InsertFieldValue
mkObjectRelationInsertFieldValue = InsertFieldValue . J.toJSON
mkArrayRelationInsertFieldValue :: [RowObject] -> InsertFieldValue
mkArrayRelationInsertFieldValue = InsertFieldValue . J.toJSON
deserializeAsColumnInsertFieldValue :: InsertFieldValue -> J.Value
deserializeAsColumnInsertFieldValue (InsertFieldValue value) = value
deserializeAsObjectRelationInsertFieldValue :: InsertFieldValue -> Either Text RowObject
deserializeAsObjectRelationInsertFieldValue (InsertFieldValue value) =
case J.fromJSON value of
J.Error s -> Left $ T.pack s
J.Success rowObject -> Right rowObject
deserializeAsArrayRelationInsertFieldValue :: InsertFieldValue -> Either Text [RowObject]
deserializeAsArrayRelationInsertFieldValue (InsertFieldValue value) =
case J.fromJSON value of
J.Error s -> Left $ T.pack s
J.Success rowObjects -> Right rowObjects
_ColumnInsertFieldValue :: Lens' InsertFieldValue J.Value
_ColumnInsertFieldValue = lens deserializeAsColumnInsertFieldValue (const mkColumnInsertFieldValue)
_ObjectRelationInsertFieldValue :: Prism' InsertFieldValue RowObject
_ObjectRelationInsertFieldValue = prism' mkObjectRelationInsertFieldValue (either (const Nothing) Just . deserializeAsObjectRelationInsertFieldValue)
_ArrayRelationInsertFieldValue :: Prism' InsertFieldValue [RowObject]
_ArrayRelationInsertFieldValue = prism' mkArrayRelationInsertFieldValue (either (const Nothing) Just . deserializeAsArrayRelationInsertFieldValue)
instance HasCodec InsertFieldValue where
codec =
dimapCodec encode decode $
possiblyJointEitherCodec anyJsonValueCodec (possiblyJointEitherCodec (possiblyJointEitherCodec objectRelationInsertFieldValueCodec arrayRelationInsertFieldValueCodec) nullColumnFieldValue)
where
arrayRelationInsertFieldValueCodec :: JSONCodec [RowObject]
arrayRelationInsertFieldValueCodec = named "ArrayRelationInsertFieldValue" $ codec
objectRelationInsertFieldValueCodec :: JSONCodec RowObject
objectRelationInsertFieldValueCodec = named "ObjectRelationInsertFieldValue" $ codec
anyJsonValueCodec :: JSONCodec J.Value
anyJsonValueCodec = named "ColumnInsertFieldValue" valueCodec
-- We have to explicitly call out null as a separate named type in OpenAPI
-- to get the typescript type-generator to recognise null as a valid value here
nullColumnFieldValue :: JSONCodec ()
nullColumnFieldValue = named "NullColumnInsertFieldValue" nullCodec
encode :: Either J.Value (Either (Either RowObject [RowObject]) ()) -> InsertFieldValue
encode = InsertFieldValue . either id (either (either J.toJSON J.toJSON) (const J.Null))
decode :: InsertFieldValue -> Either J.Value (Either (Either RowObject [RowObject]) ())
decode = Left . deserializeAsColumnInsertFieldValue
-- | Describes a update against a table that can modify a certain subset of rows
data UpdateMutationOperation = UpdateMutationOperation
{ -- | The table to update rows in
_umoTable :: API.V0.TableName,
-- | An expression to select which rows should be updated
_umoWhere :: Maybe API.V0.Expression,
-- | The updates to perform against each row
_umoUpdates :: [RowUpdate],
-- | The fields to return that represent a projection over the set of rows updated
-- after they are updated (ie. with their updated values)
_umoReturningFields :: HashMap API.V0.FieldName API.V0.Field
}
deriving stock (Eq, Ord, Show)
-- | Describes an update to be performed on a row
data RowUpdate
= -- | A particular column in the row should have its value incremented
IncrementColumn RowColumnValue
| -- | A particular column in the row should have its value overwritten
SetColumn RowColumnValue
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RowUpdate
instance HasCodec RowUpdate where
codec =
named "RowUpdate" $
object "RowUpdate" $
discriminatedUnionCodec "type" enc dec
where
enc = \case
IncrementColumn rowColumnValue -> ("increment", mapToEncoder rowColumnValue rowColumnValueObjectCodec)
SetColumn rowColumnValue -> ("set", mapToEncoder rowColumnValue rowColumnValueObjectCodec)
dec =
HashMap.fromList
[ ("increment", ("IncrementColumnRowUpdate", mapToDecoder IncrementColumn rowColumnValueObjectCodec)),
("set", ("SetColumnRowUpdate", mapToDecoder SetColumn rowColumnValueObjectCodec))
]
-- | The value of a particular column
data RowColumnValue = RowColumnValue
{ _rcvColumn :: API.V0.ColumnName,
_rcvValue :: J.Value,
_rcvValueType :: API.V0.ScalarType
}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec RowColumnValue
instance HasCodec RowColumnValue where
codec =
object "RowColumnValue" rowColumnValueObjectCodec
rowColumnValueObjectCodec :: ObjectCodec RowColumnValue RowColumnValue
rowColumnValueObjectCodec =
RowColumnValue
<$> requiredField "column" "The name of the column in the row"
.= _rcvColumn
<*> requiredField "value" "The value to use for the column"
.= _rcvValue
<*> requiredField "value_type" "The scalar type of the value"
.= _rcvValueType
-- | Describes a set of rows to be deleted from a table
data DeleteMutationOperation = DeleteMutationOperation
{ -- | The table to delete rows from
_dmoTable :: API.V0.TableName,
-- | An expression with which to select the rows to be deleted
_dmoWhere :: Maybe API.V0.Expression,
-- | The fields to return that represent a projection over the set of rows that will be deleted
-- with their values before they are deleted
_dmoReturningFields :: HashMap API.V0.FieldName API.V0.Field
}
deriving stock (Eq, Ord, Show)
-- | Represents the response from a 'MutationRequest'
data MutationResponse = MutationResponse
{ -- | A matching list of results per 'MutationOperation' in the request, in the same order
_mrOperationResults :: [MutationOperationResults]
}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec MutationResponse
instance HasCodec MutationResponse where
codec :: JSONCodec MutationResponse
codec =
object "MutationResponse" $
MutationResponse
<$> requiredField "operation_results" "The results of each mutation operation, in the same order as they were received"
.= _mrOperationResults
instance HasStatus MutationResponse where
type StatusOf MutationResponse = 200
-- | The results of a particular 'MutationOperation'
data MutationOperationResults = MutationOperationResults
{ -- | The number of rows affected by the 'MutationOperation'
_morAffectedRows :: Int,
-- | A projection of the rows affected by the mutation, if requested by the operation
-- via its returning fields definition
_morReturning :: Maybe [HashMap API.V0.FieldName API.V0.FieldValue]
}
deriving stock (Eq, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec MutationOperationResults
instance HasCodec MutationOperationResults where
codec :: JSONCodec MutationOperationResults
codec =
object "MutationOperationResults" $
MutationOperationResults
<$> requiredField "affected_rows" "The number of rows affected by the mutation operation"
.= _morAffectedRows
<*> optionalFieldOrNull "returning" "The rows affected by the mutation operation"
.= _morReturning

View File

@ -571,6 +571,10 @@ mockQueryHandler mcfg mquery mQueryCfg _sourceName queryConfig query = liftIO $
explainHandler :: API.SourceName -> API.Config -> API.QueryRequest -> Handler API.ExplainResponse
explainHandler _sourceName _queryConfig _query = pure $ API.ExplainResponse [] ""
-- Returns an empty mutation response for now
mutationsHandler :: API.SourceName -> API.Config -> API.MutationRequest -> Handler (Union API.MutationResponses)
mutationsHandler _ _ _ = pure . inject . SOP.I $ API.MutationResponse []
healthcheckHandler :: Maybe API.SourceName -> Maybe API.Config -> Handler NoContent
healthcheckHandler _sourceName _config = pure NoContent
@ -586,6 +590,7 @@ dcMockableServer mcfg mquery mQueryConfig =
:<|> mockSchemaHandler mcfg mQueryConfig
:<|> mockQueryHandler mcfg mquery mQueryConfig
:<|> explainHandler
:<|> mutationsHandler
:<|> healthcheckHandler
:<|> metricsHandler
:<|> rawHandler

View File

@ -58,7 +58,28 @@ genQueryCapabilities :: MonadGen m => m QueryCapabilities
genQueryCapabilities = pure QueryCapabilities
genMutationCapabilities :: MonadGen m => m MutationCapabilities
genMutationCapabilities = pure MutationCapabilities {}
genMutationCapabilities =
MutationCapabilities
<$> Gen.maybe genInsertCapabilities
<*> Gen.maybe genUpdateCapabilities
<*> Gen.maybe genDeleteCapabilities
<*> Gen.maybe genAtomicitySupportLevel
<*> Gen.maybe genReturningCapabilities
genInsertCapabilities :: MonadGen m => m InsertCapabilities
genInsertCapabilities = InsertCapabilities <$> Gen.bool
genUpdateCapabilities :: MonadGen m => m UpdateCapabilities
genUpdateCapabilities = pure UpdateCapabilities
genDeleteCapabilities :: MonadGen m => m DeleteCapabilities
genDeleteCapabilities = pure DeleteCapabilities
genAtomicitySupportLevel :: MonadGen m => m AtomicitySupportLevel
genAtomicitySupportLevel = Gen.enumBounded
genReturningCapabilities :: MonadGen m => m ReturningCapabilities
genReturningCapabilities = pure ReturningCapabilities
genSubscriptionCapabilities :: MonadGen m => m SubscriptionCapabilities
genSubscriptionCapabilities = pure SubscriptionCapabilities {}

View File

@ -0,0 +1,327 @@
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
module Hasura.Backends.DataConnector.API.V0.MutationsSpec
( spec,
)
where
import Data.Aeson
import Data.Aeson.QQ.Simple (aesonQQ)
import Hasura.Backends.DataConnector.API.V0
import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName)
import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression)
import Hasura.Backends.DataConnector.API.V0.QuerySpec (genField, genFieldMap, genFieldValue)
import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName, genTableRelationships)
import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType)
import Hasura.Backends.DataConnector.API.V0.TableSpec (genTableName)
import Hasura.Generator.Common (defaultRange)
import Hasura.Prelude
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Test.Aeson.Utils (genValue, jsonOpenApiProperties, testToFromJSONToSchema)
import Test.Hspec
spec :: Spec
spec = do
describe "MutationRequest" $ do
testToFromJSONToSchema
(MutationRequest [] [] [])
[aesonQQ|
{ "table_relationships": [],
"insert_schema": [],
"operations": [] }
|]
jsonOpenApiProperties genMutationRequest
describe "TableInsertSchema" $ do
testToFromJSONToSchema
(TableInsertSchema (TableName ["my_table"]) [])
[aesonQQ|
{ "table": ["my_table"],
"fields": {} }
|]
jsonOpenApiProperties genTableInsertSchema
describe "InsertFieldSchema" $ do
describe "ColumnInsert" $ do
testToFromJSONToSchema
(ColumnInsert (ColumnInsertSchema (ColumnName "my_column") NumberTy))
[aesonQQ|
{ "type": "column",
"column": "my_column",
"column_type": "number" }
|]
describe "ObjectRelationInsert" $ do
testToFromJSONToSchema
(ObjectRelationInsert (ObjectRelationInsertSchema (RelationshipName "my_relation") BeforeParent))
[aesonQQ|
{ "type": "object_relation",
"relationship": "my_relation",
"insertion_order": "before_parent" }
|]
describe "ArrayRelationInsert" $ do
testToFromJSONToSchema
(ArrayRelationInsert (ArrayRelationInsertSchema (RelationshipName "my_relation")))
[aesonQQ|
{ "type": "array_relation",
"relationship": "my_relation" }
|]
jsonOpenApiProperties genInsertFieldSchema
describe "MutationOperation" $ do
let returningFields = [(FieldName "field", ColumnField (ColumnName "my_column") StringTy)]
describe "InsertOperation" $ do
testToFromJSONToSchema
(InsertOperation (InsertMutationOperation (TableName ["my_table"]) [] returningFields))
[aesonQQ|
{ "type": "insert",
"table": ["my_table"],
"rows": [],
"returning_fields": {
"field": {
"type": "column",
"column": "my_column",
"column_type": "string"
}
}
}
|]
describe "UpdateOperation" $ do
testToFromJSONToSchema
(UpdateOperation (UpdateMutationOperation (TableName ["my_table"]) (Just $ And []) [] returningFields))
[aesonQQ|
{ "type": "update",
"table": ["my_table"],
"where": { "type": "and", "expressions": [] },
"updates": [],
"returning_fields": {
"field": {
"type": "column",
"column": "my_column",
"column_type": "string"
}
}
}
|]
describe "DeleteOperation" $ do
testToFromJSONToSchema
(DeleteOperation (DeleteMutationOperation (TableName ["my_table"]) (Just $ And []) returningFields))
[aesonQQ|
{ "type": "delete",
"table": ["my_table"],
"where": { "type": "and", "expressions": [] },
"returning_fields": {
"field": {
"type": "column",
"column": "my_column",
"column_type": "string"
}
}
}
|]
jsonOpenApiProperties genMutationOperation
describe "RowObject" $ do
testToFromJSONToSchema
(RowObject [(FieldName "row_field", mkColumnInsertFieldValue $ String "row_field_value")])
[aesonQQ|
{ "row_field": "row_field_value" }
|]
jsonOpenApiProperties genRowObject
describe "InsertFieldValue" $ do
describe "ColumnInsertFieldValue" $ do
describe "Object" $
testToFromJSONToSchema
(mkColumnInsertFieldValue $ Object [("property", "Wow")])
[aesonQQ| { "property": "Wow" } |]
describe "String" $ do
testToFromJSONToSchema
(mkColumnInsertFieldValue $ String "Test")
[aesonQQ| "Test" |]
describe "Number" $ do
testToFromJSONToSchema
(mkColumnInsertFieldValue $ Number 123)
[aesonQQ| 123 |]
describe "Bool" $ do
testToFromJSONToSchema
(mkColumnInsertFieldValue $ Bool True)
[aesonQQ| true |]
describe "Null" $ do
testToFromJSONToSchema
(mkColumnInsertFieldValue Null)
[aesonQQ| null |]
describe "ObjectRelationInsertFieldValue" $ do
testToFromJSONToSchema
(mkObjectRelationInsertFieldValue $ RowObject [(FieldName "row_field", mkColumnInsertFieldValue $ String "row_field_value")])
[aesonQQ| { "row_field": "row_field_value" } |]
describe "ArrayRelationInsertFieldValue" $ do
testToFromJSONToSchema
(mkArrayRelationInsertFieldValue $ [RowObject [(FieldName "row_field", mkColumnInsertFieldValue $ String "row_field_value")]])
[aesonQQ| [{ "row_field": "row_field_value" }] |]
jsonOpenApiProperties genInsertFieldValue
describe "ObjectRelationInsertionOrder" $ do
describe "BeforeParent" $
testToFromJSONToSchema BeforeParent [aesonQQ|"before_parent"|]
describe "AfterParent" $
testToFromJSONToSchema AfterParent [aesonQQ|"after_parent"|]
jsonOpenApiProperties genObjectRelationInsertionOrder
describe "RowUpdate" $ do
describe "IncrementColumnRowUpdate" $
testToFromJSONToSchema
(IncrementColumn $ RowColumnValue (ColumnName "my_column") (Number 10) NumberTy)
[aesonQQ|
{ "type": "increment",
"column": "my_column",
"value": 10,
"value_type": "number" }
|]
describe "SetColumnRowUpdate" $
testToFromJSONToSchema
(SetColumn $ RowColumnValue (ColumnName "my_column") (Number 10) NumberTy)
[aesonQQ|
{ "type": "set",
"column": "my_column",
"value": 10,
"value_type": "number" }
|]
jsonOpenApiProperties genRowUpdate
describe "RowColumnValue" $ do
testToFromJSONToSchema
(RowColumnValue (ColumnName "my_column") (String "a value") StringTy)
[aesonQQ|
{ "column": "my_column",
"value": "a value",
"value_type": "string" }
|]
jsonOpenApiProperties genRowColumnValue
describe "MutationResponse" $ do
testToFromJSONToSchema
(MutationResponse [])
[aesonQQ|
{ "operation_results": [] }
|]
jsonOpenApiProperties genMutationResponse
describe "MutationOperationResults" $ do
testToFromJSONToSchema
(MutationOperationResults 64 (Just [[(FieldName "field", mkColumnFieldValue $ String "field_value")]]))
[aesonQQ|
{ "affected_rows": 64,
"returning": [
{ "field": "field_value" }
]
}
|]
jsonOpenApiProperties genMutationOperationResults
genMutationRequest :: Gen MutationRequest
genMutationRequest =
MutationRequest
<$> Gen.list defaultRange genTableRelationships
<*> Gen.list defaultRange genTableInsertSchema
<*> Gen.list defaultRange genMutationOperation
genTableInsertSchema :: Gen TableInsertSchema
genTableInsertSchema =
TableInsertSchema
<$> genTableName
<*> genFieldMap genInsertFieldSchema
genInsertFieldSchema :: (MonadGen m, GenBase m ~ Identity) => m InsertFieldSchema
genInsertFieldSchema =
Gen.choice
[ ColumnInsert <$> genColumnInsertSchema,
ObjectRelationInsert <$> genObjectRelationInsertSchema,
ArrayRelationInsert <$> genArrayRelationInsertSchema
]
genColumnInsertSchema :: (MonadGen m, GenBase m ~ Identity) => m ColumnInsertSchema
genColumnInsertSchema =
ColumnInsertSchema
<$> genColumnName
<*> genScalarType
genObjectRelationInsertSchema :: MonadGen m => m ObjectRelationInsertSchema
genObjectRelationInsertSchema =
ObjectRelationInsertSchema
<$> genRelationshipName
<*> genObjectRelationInsertionOrder
genObjectRelationInsertionOrder :: MonadGen m => m ObjectRelationInsertionOrder
genObjectRelationInsertionOrder = Gen.enumBounded
genArrayRelationInsertSchema :: MonadGen m => m ArrayRelationInsertSchema
genArrayRelationInsertSchema = ArrayRelationInsertSchema <$> genRelationshipName
genMutationOperation :: Gen MutationOperation
genMutationOperation =
Gen.choice
[ InsertOperation <$> genInsertMutationOperation,
UpdateOperation <$> genUpdateMutationOperation,
DeleteOperation <$> genDeleteMutationOperation
]
genInsertMutationOperation :: Gen InsertMutationOperation
genInsertMutationOperation =
InsertMutationOperation
<$> genTableName
<*> Gen.list defaultRange genRowObject
<*> genFieldMap genField
genRowObject :: Gen RowObject
genRowObject = RowObject <$> genFieldMap genInsertFieldValue
genInsertFieldValue :: Gen InsertFieldValue
genInsertFieldValue =
Gen.recursive
Gen.choice
[mkColumnInsertFieldValue <$> genValue]
[ mkObjectRelationInsertFieldValue <$> genRowObject,
mkArrayRelationInsertFieldValue <$> Gen.list defaultRange genRowObject
]
genUpdateMutationOperation :: Gen UpdateMutationOperation
genUpdateMutationOperation =
UpdateMutationOperation
<$> genTableName
<*> Gen.maybe genExpression
<*> Gen.list defaultRange genRowUpdate
<*> genFieldMap genField
genRowUpdate :: (MonadGen m, GenBase m ~ Identity) => m RowUpdate
genRowUpdate =
Gen.choice
[ IncrementColumn <$> genRowColumnValue,
SetColumn <$> genRowColumnValue
]
genRowColumnValue :: (MonadGen m, GenBase m ~ Identity) => m RowColumnValue
genRowColumnValue =
RowColumnValue
<$> genColumnName
<*> genValue
<*> genScalarType
genDeleteMutationOperation :: Gen DeleteMutationOperation
genDeleteMutationOperation =
DeleteMutationOperation
<$> genTableName
<*> Gen.maybe genExpression
<*> genFieldMap genField
genMutationResponse :: Gen MutationResponse
genMutationResponse =
MutationResponse
<$> Gen.list defaultRange genMutationOperationResults
genMutationOperationResults :: Gen MutationOperationResults
genMutationOperationResults =
MutationOperationResults
<$> Gen.int defaultRange
<*> Gen.maybe (Gen.list defaultRange (genFieldMap genFieldValue))

View File

@ -1,7 +1,14 @@
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
module Hasura.Backends.DataConnector.API.V0.QuerySpec (spec) where
module Hasura.Backends.DataConnector.API.V0.QuerySpec
( spec,
genFieldName,
genFieldMap,
genField,
genFieldValue,
)
where
import Data.Aeson qualified as J
import Data.Aeson.QQ.Simple (aesonQQ)
@ -14,7 +21,7 @@ import Hasura.Backends.DataConnector.API.V0.OrderBySpec (genOrderBy)
import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName, genTableRelationships)
import Hasura.Backends.DataConnector.API.V0.ScalarSpec (genScalarType)
import Hasura.Backends.DataConnector.API.V0.TableSpec (genTableName)
import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumText)
import Hasura.Generator.Common (defaultRange, genArbitraryAlphaNumText, genHashMap)
import Hasura.Prelude
import Hedgehog
import Hedgehog.Gen qualified as Gen
@ -154,8 +161,7 @@ genFieldName :: Gen FieldName
genFieldName = FieldName <$> genArbitraryAlphaNumText defaultRange
genFieldMap :: Gen value -> Gen (HashMap FieldName value)
genFieldMap genValue' =
HashMap.fromList <$> Gen.list defaultRange ((,) <$> genFieldName <*> genValue')
genFieldMap genValue' = genHashMap genFieldName genValue' defaultRange
genRelationshipField :: Gen RelationshipField
genRelationshipField =