diff --git a/dc-agents/README.md b/dc-agents/README.md index 62998e29f8c..295e2f33278 100644 --- a/dc-agents/README.md +++ b/dc-agents/README.md @@ -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. diff --git a/dc-agents/dc-api-types/package.json b/dc-agents/dc-api-types/package.json index bac72250aba..7240e689d24 100644 --- a/dc-agents/dc-api-types/package.json +++ b/dc-agents/dc-api-types/package.json @@ -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", diff --git a/dc-agents/dc-api-types/src/agent.openapi.json b/dc-agents/dc-api-types/src/agent.openapi.json index 8743eb3ec96..3c52dd64f39 100644 --- a/dc-agents/dc-api-types/src/agent.openapi.json +++ b/dc-agents/dc-api-types/src/agent.openapi.json @@ -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": { diff --git a/dc-agents/dc-api-types/src/index.ts b/dc-agents/dc-api-types/src/index.ts index 0bbe08b20a5..3d7ac040fc3 100644 --- a/dc-agents/dc-api-types/src/index.ts +++ b/dc-agents/dc-api-types/src/index.ts @@ -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'; diff --git a/dc-agents/dc-api-types/src/models/ArrayRelationInsertFieldValue.ts b/dc-agents/dc-api-types/src/models/ArrayRelationInsertFieldValue.ts new file mode 100644 index 00000000000..014827922a1 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ArrayRelationInsertFieldValue.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { RowObject } from './RowObject'; + +export type ArrayRelationInsertFieldValue = Array; diff --git a/dc-agents/dc-api-types/src/models/ArrayRelationInsertSchema.ts b/dc-agents/dc-api-types/src/models/ArrayRelationInsertSchema.ts new file mode 100644 index 00000000000..63f4519a75e --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ArrayRelationInsertSchema.ts @@ -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'; +}; + diff --git a/dc-agents/dc-api-types/src/models/AtomicitySupportLevel.ts b/dc-agents/dc-api-types/src/models/AtomicitySupportLevel.ts new file mode 100644 index 00000000000..3288fc7d348 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/AtomicitySupportLevel.ts @@ -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'; diff --git a/dc-agents/dc-api-types/src/models/ColumnInsertFieldValue.ts b/dc-agents/dc-api-types/src/models/ColumnInsertFieldValue.ts new file mode 100644 index 00000000000..8559792143d --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ColumnInsertFieldValue.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ColumnInsertFieldValue = { +}; + diff --git a/dc-agents/dc-api-types/src/models/ColumnInsertSchema.ts b/dc-agents/dc-api-types/src/models/ColumnInsertSchema.ts new file mode 100644 index 00000000000..c254b3b8d5a --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ColumnInsertSchema.ts @@ -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'; +}; + diff --git a/dc-agents/dc-api-types/src/models/DeleteCapabilities.ts b/dc-agents/dc-api-types/src/models/DeleteCapabilities.ts new file mode 100644 index 00000000000..b79b553701f --- /dev/null +++ b/dc-agents/dc-api-types/src/models/DeleteCapabilities.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type DeleteCapabilities = { +}; + diff --git a/dc-agents/dc-api-types/src/models/DeleteMutationOperation.ts b/dc-agents/dc-api-types/src/models/DeleteMutationOperation.ts new file mode 100644 index 00000000000..cd24b89c3e6 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/DeleteMutationOperation.ts @@ -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 | null; + table: TableName; + type: 'delete'; + where?: Expression; +}; + diff --git a/dc-agents/dc-api-types/src/models/ErrorResponseType.ts b/dc-agents/dc-api-types/src/models/ErrorResponseType.ts index cb893517743..f5926799de9 100644 --- a/dc-agents/dc-api-types/src/models/ErrorResponseType.ts +++ b/dc-agents/dc-api-types/src/models/ErrorResponseType.ts @@ -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'; diff --git a/dc-agents/dc-api-types/src/models/IncrementColumnRowUpdate.ts b/dc-agents/dc-api-types/src/models/IncrementColumnRowUpdate.ts new file mode 100644 index 00000000000..24637305da6 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/IncrementColumnRowUpdate.ts @@ -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; +}; + diff --git a/dc-agents/dc-api-types/src/models/InsertCapabilities.ts b/dc-agents/dc-api-types/src/models/InsertCapabilities.ts new file mode 100644 index 00000000000..078aadcf89e --- /dev/null +++ b/dc-agents/dc-api-types/src/models/InsertCapabilities.ts @@ -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; +}; + diff --git a/dc-agents/dc-api-types/src/models/InsertFieldSchema.ts b/dc-agents/dc-api-types/src/models/InsertFieldSchema.ts new file mode 100644 index 00000000000..4e0c4d7f0cd --- /dev/null +++ b/dc-agents/dc-api-types/src/models/InsertFieldSchema.ts @@ -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); + diff --git a/dc-agents/dc-api-types/src/models/InsertMutationOperation.ts b/dc-agents/dc-api-types/src/models/InsertMutationOperation.ts new file mode 100644 index 00000000000..5dc7ae65a59 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/InsertMutationOperation.ts @@ -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 | null; + /** + * The rows to insert into the table + */ + rows: Array; + table: TableName; + type: 'insert'; +}; + diff --git a/dc-agents/dc-api-types/src/models/MutationCapabilities.ts b/dc-agents/dc-api-types/src/models/MutationCapabilities.ts index 85d10e00b1d..2072c6de849 100644 --- a/dc-agents/dc-api-types/src/models/MutationCapabilities.ts +++ b/dc-agents/dc-api-types/src/models/MutationCapabilities.ts @@ -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; }; diff --git a/dc-agents/dc-api-types/src/models/MutationOperation.ts b/dc-agents/dc-api-types/src/models/MutationOperation.ts new file mode 100644 index 00000000000..a8c95512c6c --- /dev/null +++ b/dc-agents/dc-api-types/src/models/MutationOperation.ts @@ -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); + diff --git a/dc-agents/dc-api-types/src/models/MutationOperationResults.ts b/dc-agents/dc-api-types/src/models/MutationOperationResults.ts new file mode 100644 index 00000000000..f2c9e19c4ad --- /dev/null +++ b/dc-agents/dc-api-types/src/models/MutationOperationResults.ts @@ -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> | null; +}; + diff --git a/dc-agents/dc-api-types/src/models/MutationRequest.ts b/dc-agents/dc-api-types/src/models/MutationRequest.ts new file mode 100644 index 00000000000..80354b05ac0 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/MutationRequest.ts @@ -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; + /** + * The mutation operations to perform + */ + operations: Array; + /** + * The relationships between tables involved in the entire mutation request + */ + table_relationships: Array; +}; + diff --git a/dc-agents/dc-api-types/src/models/MutationResponse.ts b/dc-agents/dc-api-types/src/models/MutationResponse.ts new file mode 100644 index 00000000000..0d45546677f --- /dev/null +++ b/dc-agents/dc-api-types/src/models/MutationResponse.ts @@ -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; +}; + diff --git a/dc-agents/dc-api-types/src/models/NullColumnInsertFieldValue.ts b/dc-agents/dc-api-types/src/models/NullColumnInsertFieldValue.ts new file mode 100644 index 00000000000..6a6d5540a3a --- /dev/null +++ b/dc-agents/dc-api-types/src/models/NullColumnInsertFieldValue.ts @@ -0,0 +1,5 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type NullColumnInsertFieldValue = null; diff --git a/dc-agents/dc-api-types/src/models/ObjectRelationInsertFieldValue.ts b/dc-agents/dc-api-types/src/models/ObjectRelationInsertFieldValue.ts new file mode 100644 index 00000000000..f3fab509b14 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ObjectRelationInsertFieldValue.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ObjectRelationInsertFieldValue = { +}; + diff --git a/dc-agents/dc-api-types/src/models/ObjectRelationInsertSchema.ts b/dc-agents/dc-api-types/src/models/ObjectRelationInsertSchema.ts new file mode 100644 index 00000000000..ddcc4472078 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ObjectRelationInsertSchema.ts @@ -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'; +}; + diff --git a/dc-agents/dc-api-types/src/models/ObjectRelationInsertionOrder.ts b/dc-agents/dc-api-types/src/models/ObjectRelationInsertionOrder.ts new file mode 100644 index 00000000000..f02dcb7b6db --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ObjectRelationInsertionOrder.ts @@ -0,0 +1,5 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ObjectRelationInsertionOrder = 'before_parent' | 'after_parent'; diff --git a/dc-agents/dc-api-types/src/models/ReturningCapabilities.ts b/dc-agents/dc-api-types/src/models/ReturningCapabilities.ts new file mode 100644 index 00000000000..6b6434aeaa0 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/ReturningCapabilities.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type ReturningCapabilities = { +}; + diff --git a/dc-agents/dc-api-types/src/models/RowObject.ts b/dc-agents/dc-api-types/src/models/RowObject.ts new file mode 100644 index 00000000000..c3ecd750d3f --- /dev/null +++ b/dc-agents/dc-api-types/src/models/RowObject.ts @@ -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; diff --git a/dc-agents/dc-api-types/src/models/RowUpdate.ts b/dc-agents/dc-api-types/src/models/RowUpdate.ts new file mode 100644 index 00000000000..2ea4aa08852 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/RowUpdate.ts @@ -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); + diff --git a/dc-agents/dc-api-types/src/models/SetColumnRowUpdate.ts b/dc-agents/dc-api-types/src/models/SetColumnRowUpdate.ts new file mode 100644 index 00000000000..8e3eb8f3174 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/SetColumnRowUpdate.ts @@ -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; +}; + diff --git a/dc-agents/dc-api-types/src/models/TableInsertSchema.ts b/dc-agents/dc-api-types/src/models/TableInsertSchema.ts new file mode 100644 index 00000000000..ed0a383def1 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/TableInsertSchema.ts @@ -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; + table: TableName; +}; + diff --git a/dc-agents/dc-api-types/src/models/UpdateCapabilities.ts b/dc-agents/dc-api-types/src/models/UpdateCapabilities.ts new file mode 100644 index 00000000000..625323c1be7 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/UpdateCapabilities.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type UpdateCapabilities = { +}; + diff --git a/dc-agents/dc-api-types/src/models/UpdateMutationOperation.ts b/dc-agents/dc-api-types/src/models/UpdateMutationOperation.ts new file mode 100644 index 00000000000..7d95da149ea --- /dev/null +++ b/dc-agents/dc-api-types/src/models/UpdateMutationOperation.ts @@ -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 | null; + table: TableName; + type: 'update'; + /** + * The updates to make to the matched rows in the table + */ + updates: Array; + where?: Expression; +}; + diff --git a/dc-agents/package-lock.json b/dc-agents/package-lock.json index f297b5d34a0..56807bb3c8f 100644 --- a/dc-agents/package-lock.json +++ b/dc-agents/package-lock.json @@ -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", diff --git a/dc-agents/reference/package-lock.json b/dc-agents/reference/package-lock.json index 4cbd7a92476..47f6f15e541 100644 --- a/dc-agents/reference/package-lock.json +++ b/dc-agents/reference/package-lock.json @@ -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", diff --git a/dc-agents/reference/package.json b/dc-agents/reference/package.json index ea6ab1271f3..4afafd9b0da 100644 --- a/dc-agents/reference/package.json +++ b/dc-agents/reference/package.json @@ -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", diff --git a/dc-agents/sqlite/package-lock.json b/dc-agents/sqlite/package-lock.json index c7ac434cd5f..c4ff71cc696 100644 --- a/dc-agents/sqlite/package-lock.json +++ b/dc-agents/sqlite/package-lock.json @@ -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", diff --git a/dc-agents/sqlite/package.json b/dc-agents/sqlite/package.json index fe4b321d58f..9edc521b623 100644 --- a/dc-agents/sqlite/package.json +++ b/dc-agents/sqlite/package.json @@ -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", diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 633828a067a..f0c74946a71 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -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 diff --git a/server/lib/dc-api/dc-api.cabal b/server/lib/dc-api/dc-api.cabal index 3bb2f83c32d..e2344fae357 100644 --- a/server/lib/dc-api/dc-api.cabal +++ b/server/lib/dc-api/dc-api.cabal @@ -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 diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API.hs index b0bf87347d1..6ae9b4124ac 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API.hs @@ -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 diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs index 11655b72bcb..6b9c855053c 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0.hs @@ -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 diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs index 57228377ec0..003dfa9f985 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs @@ -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) diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/ErrorResponse.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/ErrorResponse.hs index dffd34258b1..b068d34175d 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/ErrorResponse.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/ErrorResponse.hs @@ -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, diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs new file mode 100644 index 00000000000..8e7a616cc37 --- /dev/null +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Mutations.hs @@ -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 diff --git a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs index 725aca4c1ac..3e0a6a290f1 100644 --- a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs +++ b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs @@ -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 diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs index 8c609bf16fa..248e062758d 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs @@ -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 {} diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs new file mode 100644 index 00000000000..9cb5cfa705a --- /dev/null +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/MutationsSpec.hs @@ -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)) diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs index b49b6ffd309..27ff41c3b39 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/QuerySpec.hs @@ -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 =