Add mutability properties to the Data Connector schema API [GDC-664]

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7190
GitOrigin-RevId: ce602b5e5cc5aee8716ff3f7a036b18b3bf47188
This commit is contained in:
Daniel Chambers 2022-12-08 13:05:28 +11:00 committed by hasura-bot
parent 1dd9e19b69
commit c14fd3ba4c
25 changed files with 1046 additions and 294 deletions

View File

@ -267,6 +267,7 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
"tables": [
{
"name": ["Artist"],
"type": "table",
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
@ -274,18 +275,26 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "Artist primary key identifier"
"description": "Artist primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": true,
"description": "The name of the artist"
"description": "The name of the artist",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Album"],
"type": "table",
"primary_key": ["AlbumId"],
"description": "Collection of music albums created by artists",
"columns": [
@ -293,32 +302,49 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
"name": "AlbumId",
"type": "number",
"nullable": false,
"description": "Album primary key identifier"
"description": "Album primary key identifier",
"insertable": true,
"updatable": false,
},
{
"name": "Title",
"type": "string",
"nullable": false,
"description": "The title of the album"
"description": "The title of the album",
"insertable": true,
"updatable": true
},
{
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "The ID of the artist that created this album"
"description": "The ID of the artist that created this album",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
}
]
}
```
The `tables` section describes the two available tables, as well as their columns, including types and nullability information.
The `tables` section describes two available tables, as well as their columns, including types and nullability information.
Notice that the names of tables and columns are used in the metadata document to describe tracked tables and relationships.
Table names are described as an array of strings. This allows agents to fully qualify their table names with whatever namespacing requirements they have. For example, if the agent connects to a database that puts tables inside schemas, the agent could use table names such as `["my_schema", "my_table"]`.
The `type` of a table can be either a "table" or a "view".
Tables have mutability properties, namely, whether they are "insertable", "updatable" and "deletable", which refers to whether rows can be inserted/updated/deleted from the table. Typically, in an RDBMS, tables are insertable, updatable and deletable, but views may not be. However, an agent may declare the mutability properties in any combination that suits its data source.
Columns also have "insertable" and "updatable" mutability properties. Typically, in an RDBMS, computed columns are neither insertable not updatable, primary keys are insertable but not updatable and normal columns are insertable and updatable. Agents may declare whatever combination suits their data source.
If the agent declares a lack of mutability support in its capabilities, it should not declare tables/columns as mutable in its schema here.
#### Type definitions
The `SchemaResponse` TypeScript type from [the reference implementation](./reference/src/types/index.ts) describes the valid response body for the `GET /schema` endpoint.
@ -2134,4 +2160,3 @@ Breaking down the properties in the `delete`-typed mutation operation:
* `returning_fields`: This specifies a list of fields to return in the response. The property takes the same format as the `fields` property on Queries. It is expected that the specified fields will be returned for all rows affected by the deletion (ie. all deleted rows).
Delete operations return responses that are the same as insert and update operations, except the affected rows in `returning` are the deleted rows instead.

View File

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

View File

@ -934,6 +934,13 @@
},
"type": "array"
},
"TableType": {
"enum": [
"table",
"view"
],
"type": "string"
},
"ColumnInfo": {
"properties": {
"description": {
@ -941,6 +948,11 @@
"nullable": true,
"type": "string"
},
"insertable": {
"default": false,
"description": "Whether or not the column can be inserted into",
"type": "boolean"
},
"name": {
"description": "Column name",
"type": "string"
@ -951,6 +963,11 @@
},
"type": {
"$ref": "#/components/schemas/ScalarType"
},
"updatable": {
"default": false,
"description": "Whether or not the column can be updated",
"type": "boolean"
}
},
"required": [
@ -988,6 +1005,11 @@
},
"type": "array"
},
"deletable": {
"default": false,
"description": "Whether or not existing rows can be deleted in the table",
"type": "boolean"
},
"description": {
"description": "Description of the table",
"nullable": true,
@ -1001,6 +1023,11 @@
"description": "Foreign key constraints",
"type": "object"
},
"insertable": {
"default": false,
"description": "Whether or not new rows can be inserted into the table",
"type": "boolean"
},
"name": {
"$ref": "#/components/schemas/TableName"
},
@ -1011,6 +1038,14 @@
"type": "string"
},
"type": "array"
},
"type": {
"$ref": "#/components/schemas/TableType"
},
"updatable": {
"default": false,
"description": "Whether or not existing rows can be updated in the table",
"type": "boolean"
}
},
"required": [

View File

@ -101,6 +101,7 @@ 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 { TableType } from './models/TableType';
export type { UnaryComparisonOperator } from './models/UnaryComparisonOperator';
export type { UnrelatedTable } from './models/UnrelatedTable';
export type { UpdateCapabilities } from './models/UpdateCapabilities';

View File

@ -9,6 +9,10 @@ export type ColumnInfo = {
* Column description
*/
description?: string | null;
/**
* Whether or not the column can be inserted into
*/
insertable?: boolean;
/**
* Column name
*/
@ -18,5 +22,9 @@ export type ColumnInfo = {
*/
nullable: boolean;
type: ScalarType;
/**
* Whether or not the column can be updated
*/
updatable?: boolean;
};

View File

@ -5,12 +5,17 @@
import type { ColumnInfo } from './ColumnInfo';
import type { Constraint } from './Constraint';
import type { TableName } from './TableName';
import type { TableType } from './TableType';
export type TableInfo = {
/**
* The columns of the table
*/
columns: Array<ColumnInfo>;
/**
* Whether or not existing rows can be deleted in the table
*/
deletable?: boolean;
/**
* Description of the table
*/
@ -19,10 +24,19 @@ export type TableInfo = {
* Foreign key constraints
*/
foreign_keys?: Record<string, Constraint>;
/**
* Whether or not new rows can be inserted into the table
*/
insertable?: boolean;
name: TableName;
/**
* The primary key of the table
*/
primary_key?: Array<string>;
type?: TableType;
/**
* Whether or not existing rows can be updated in the table
*/
updatable?: boolean;
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TableType = 'table' | 'view';

View File

@ -24,7 +24,7 @@
},
"dc-api-types": {
"name": "@hasura/dc-api-types",
"version": "0.18.0",
"version": "0.19.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.18.0",
"@hasura/dc-api-types": "0.19.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.18.0",
"@hasura/dc-api-types": "0.19.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.18.0",
"@hasura/dc-api-types": "0.19.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.18.0",
"@hasura/dc-api-types": "0.19.0",
"@tsconfig/node16": "^1.0.3",
"@types/node": "^16.11.49",
"@types/sqlite3": "^3.1.8",

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^7.0.0",
"@hasura/dc-api-types": "0.18.0",
"@hasura/dc-api-types": "0.19.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.18.0",
"version": "0.19.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

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

View File

@ -100,6 +100,7 @@ const schema: SchemaResponse = {
tables: [
{
name: ["Artist"],
type: "table",
primary_key: ["ArtistId"],
description: "Collection of artists of music",
columns: [
@ -107,18 +108,26 @@ const schema: SchemaResponse = {
name: "ArtistId",
type: "number",
nullable: false,
description: "Artist primary key identifier"
description: "Artist primary key identifier",
insertable: false,
updatable: false,
},
{
name: "Name",
type: "string",
nullable: true,
description: "The name of the artist"
description: "The name of the artist",
insertable: false,
updatable: false,
}
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Album"],
type: "table",
primary_key: ["AlbumId"],
foreign_keys: {
"Artist": {
@ -134,24 +143,34 @@ const schema: SchemaResponse = {
name: "AlbumId",
type: "number",
nullable: false,
description: "Album primary key identifier"
description: "Album primary key identifier",
insertable: false,
updatable: false,
},
{
name: "Title",
type: "string",
nullable: false,
description: "The title of the album"
description: "The title of the album",
insertable: false,
updatable: false,
},
{
name: "ArtistId",
type: "number",
nullable: false,
description: "The ID of the artist that created this album"
description: "The ID of the artist that created this album",
insertable: false,
updatable: false,
}
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Customer"],
type: "table",
primary_key: ["CustomerId"],
foreign_keys: {
"CustomerSupportRep": {
@ -167,84 +186,114 @@ const schema: SchemaResponse = {
name: "CustomerId",
type: "number",
nullable: false,
description: "Customer primary key identifier"
description: "Customer primary key identifier",
insertable: false,
updatable: false,
},
{
name: "FirstName",
type: "string",
nullable: false,
description: "The customer's first name"
description: "The customer's first name",
insertable: false,
updatable: false,
},
{
name: "LastName",
type: "string",
nullable: false,
description: "The customer's last name"
description: "The customer's last name",
insertable: false,
updatable: false,
},
{
name: "Company",
type: "string",
nullable: true,
description: "The customer's company name"
description: "The customer's company name",
insertable: false,
updatable: false,
},
{
name: "Address",
type: "string",
nullable: true,
description: "The customer's address line (street number, street)"
description: "The customer's address line (street number, street)",
insertable: false,
updatable: false,
},
{
name: "City",
type: "string",
nullable: true,
description: "The customer's address city"
description: "The customer's address city",
insertable: false,
updatable: false,
},
{
name: "State",
type: "string",
nullable: true,
description: "The customer's address state"
description: "The customer's address state",
insertable: false,
updatable: false,
},
{
name: "Country",
type: "string",
nullable: true,
description: "The customer's address country"
description: "The customer's address country",
insertable: false,
updatable: false,
},
{
name: "PostalCode",
type: "string",
nullable: true,
description: "The customer's address postal code"
description: "The customer's address postal code",
insertable: false,
updatable: false,
},
{
name: "Phone",
type: "string",
nullable: true,
description: "The customer's phone number"
description: "The customer's phone number",
insertable: false,
updatable: false,
},
{
name: "Fax",
type: "string",
nullable: true,
description: "The customer's fax number"
description: "The customer's fax number",
insertable: false,
updatable: false,
},
{
name: "Email",
type: "string",
nullable: false,
description: "The customer's email address"
description: "The customer's email address",
insertable: false,
updatable: false,
},
{
name: "SupportRepId",
type: "number",
nullable: true,
description: "The ID of the Employee who is this customer's support representative"
description: "The ID of the Employee who is this customer's support representative",
insertable: false,
updatable: false,
}
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Employee"],
type: "table",
primary_key: ["EmployeeId"],
foreign_keys: {
"EmployeeReportsTo": {
@ -260,96 +309,130 @@ const schema: SchemaResponse = {
name: "EmployeeId",
type: "number",
nullable: false,
description: "Employee primary key identifier"
description: "Employee primary key identifier",
insertable: false,
updatable: false,
},
{
name: "LastName",
type: "string",
nullable: false,
description: "The employee's last name"
description: "The employee's last name",
insertable: false,
updatable: false,
},
{
name: "FirstName",
type: "string",
nullable: false,
description: "The employee's first name"
description: "The employee's first name",
insertable: false,
updatable: false,
},
{
name: "Title",
type: "string",
nullable: true,
description: "The employee's job title"
description: "The employee's job title",
insertable: false,
updatable: false,
},
{
name: "ReportsTo",
type: "number",
nullable: true,
description: "The employee's manager"
description: "The employee's manager",
insertable: false,
updatable: false,
},
{
name: "BirthDate",
type: "DateTime",
nullable: true,
description: "The employee's birth date"
description: "The employee's birth date",
insertable: false,
updatable: false,
},
{
name: "HireDate",
type: "DateTime",
nullable: true,
description: "The employee's hire date"
description: "The employee's hire date",
insertable: false,
updatable: false,
},
{
name: "Address",
type: "string",
nullable: true,
description: "The employee's address line (street number, street)"
description: "The employee's address line (street number, street)",
insertable: false,
updatable: false,
},
{
name: "City",
type: "string",
nullable: true,
description: "The employee's address city"
description: "The employee's address city",
insertable: false,
updatable: false,
},
{
name: "State",
type: "string",
nullable: true,
description: "The employee's address state"
description: "The employee's address state",
insertable: false,
updatable: false,
},
{
name: "Country",
type: "string",
nullable: true,
description: "The employee's address country"
description: "The employee's address country",
insertable: false,
updatable: false,
},
{
name: "PostalCode",
type: "string",
nullable: true,
description: "The employee's address postal code"
description: "The employee's address postal code",
insertable: false,
updatable: false,
},
{
name: "Phone",
type: "string",
nullable: true,
description: "The employee's phone number"
description: "The employee's phone number",
insertable: false,
updatable: false,
},
{
name: "Fax",
type: "string",
nullable: true,
description: "The employee's fax number"
description: "The employee's fax number",
insertable: false,
updatable: false,
},
{
name: "Email",
type: "string",
nullable: true,
description: "The employee's email address"
description: "The employee's email address",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Genre"],
type: "table",
primary_key: ["GenreId"],
description: "Genres of music",
columns: [
@ -357,18 +440,26 @@ const schema: SchemaResponse = {
name: "GenreId",
type: "number",
nullable: false,
description: "Genre primary key identifier"
description: "Genre primary key identifier",
insertable: false,
updatable: false,
},
{
name: "Name",
type: "string",
nullable: true,
description: "The name of the genre"
description: "The name of the genre",
insertable: false,
updatable: false,
}
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Invoice"],
type: "table",
primary_key: ["InvoiceId"],
foreign_keys: {
"InvoiceCustomer": {
@ -384,60 +475,82 @@ const schema: SchemaResponse = {
name: "InvoiceId",
type: "number",
nullable: false,
description: "Invoice primary key identifier"
description: "Invoice primary key identifier",
insertable: false,
updatable: false,
},
{
name: "CustomerId",
type: "number",
nullable: false,
description: "ID of the customer who bought the music"
description: "ID of the customer who bought the music",
insertable: false,
updatable: false,
},
{
name: "InvoiceDate",
type: "DateTime",
nullable: false,
description: "Date of the invoice"
description: "Date of the invoice",
insertable: false,
updatable: false,
},
{
name: "BillingAddress",
type: "string",
nullable: true,
description: "The invoice's billing address line (street number, street)"
description: "The invoice's billing address line (street number, street)",
insertable: false,
updatable: false,
},
{
name: "BillingCity",
type: "string",
nullable: true,
description: "The invoice's billing address city"
description: "The invoice's billing address city",
insertable: false,
updatable: false,
},
{
name: "BillingState",
type: "string",
nullable: true,
description: "The invoice's billing address state"
description: "The invoice's billing address state",
insertable: false,
updatable: false,
},
{
name: "BillingCountry",
type: "string",
nullable: true,
description: "The invoice's billing address country"
description: "The invoice's billing address country",
insertable: false,
updatable: false,
},
{
name: "BillingPostalCode",
type: "string",
nullable: true,
description: "The invoice's billing address postal code"
description: "The invoice's billing address postal code",
insertable: false,
updatable: false,
},
{
name: "Total",
type: "number",
nullable: false,
description: "The total amount due on the invoice"
description: "The total amount due on the invoice",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["InvoiceLine"],
type: "table",
primary_key: ["InvoiceLineId"],
foreign_keys: {
"Invoice": {
@ -459,36 +572,50 @@ const schema: SchemaResponse = {
name: "InvoiceLineId",
type: "number",
nullable: false,
description: "Invoice Line primary key identifier"
description: "Invoice Line primary key identifier",
insertable: false,
updatable: false,
},
{
name: "InvoiceId",
type: "number",
nullable: false,
description: "ID of the invoice the line belongs to"
description: "ID of the invoice the line belongs to",
insertable: false,
updatable: false,
},
{
name: "TrackId",
type: "number",
nullable: false,
description: "ID of the music track being purchased"
description: "ID of the music track being purchased",
insertable: false,
updatable: false,
},
{
name: "UnitPrice",
type: "number",
nullable: false,
description: "Price of each individual track unit"
description: "Price of each individual track unit",
insertable: false,
updatable: false,
},
{
name: "Quantity",
type: "number",
nullable: false,
description: "Quantity of the track purchased"
description: "Quantity of the track purchased",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["MediaType"],
type: "table",
primary_key: ["MediaTypeId"],
description: "Collection of media types that tracks can be encoded in",
columns: [
@ -496,18 +623,26 @@ const schema: SchemaResponse = {
name: "MediaTypeId",
type: "number",
nullable: false,
description: "Media Type primary key identifier"
description: "Media Type primary key identifier",
insertable: false,
updatable: false,
},
{
name: "Name",
type: "string",
nullable: true,
description: "The name of the media type format"
description: "The name of the media type format",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Playlist"],
type: "table",
primary_key: ["PlaylistId"],
description: "Collection of playlists",
columns: [
@ -515,18 +650,26 @@ const schema: SchemaResponse = {
name: "PlaylistId",
type: "number",
nullable: false,
description: "Playlist primary key identifier"
description: "Playlist primary key identifier",
insertable: false,
updatable: false,
},
{
name: "Name",
type: "string",
nullable: true,
description: "The name of the playlist"
description: "The name of the playlist",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["PlaylistTrack"],
type: "table",
primary_key: ["PlaylistId", "TrackId"],
foreign_keys: {
"Playlist": {
@ -548,18 +691,26 @@ const schema: SchemaResponse = {
name: "PlaylistId",
type: "number",
nullable: false,
description: "The ID of the playlist"
description: "The ID of the playlist",
insertable: false,
updatable: false,
},
{
name: "TrackId",
type: "number",
nullable: false,
description: "The ID of the track"
description: "The ID of the track",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
{
name: ["Track"],
type: "table",
primary_key: ["TrackId"],
foreign_keys: {
"Album": {
@ -587,57 +738,78 @@ const schema: SchemaResponse = {
name: "TrackId",
type: "number",
nullable: false,
description: "The ID of the track"
description: "The ID of the track",
insertable: false,
updatable: false,
},
{
name: "Name",
type: "string",
nullable: false,
description: "The name of the track"
description: "The name of the track",
insertable: false,
updatable: false,
},
{
name: "AlbumId",
type: "number",
nullable: true,
description: "The ID of the album the track belongs to"
description: "The ID of the album the track belongs to",
insertable: false,
updatable: false,
},
{
name: "MediaTypeId",
type: "number",
nullable: false,
description: "The ID of the media type the track is encoded with"
description: "The ID of the media type the track is encoded with",
insertable: false,
updatable: false,
},
{
name: "GenreId",
type: "number",
nullable: true,
description: "The ID of the genre of the track"
description: "The ID of the genre of the track",
insertable: false,
updatable: false,
},
{
name: "Composer",
type: "string",
nullable: true,
description: "The name of the composer of the track"
description: "The name of the composer of the track",
insertable: false,
updatable: false,
},
{
name: "Milliseconds",
type: "number",
nullable: false,
description: "The length of the track in milliseconds"
description: "The length of the track in milliseconds",
insertable: false,
updatable: false,
},
{
name: "Bytes",
type: "number",
nullable: true,
description: "The size of the track in bytes"
description: "The size of the track in bytes",
insertable: false,
updatable: false,
},
{
name: "UnitPrice",
type: "number",
nullable: false,
description: "The price of the track"
description: "The price of the track",
insertable: false,
updatable: false,
},
]
],
insertable: false,
updatable: false,
deletable: false,
},
]
};

View File

@ -10,7 +10,7 @@
"license": "Apache-2.0",
"dependencies": {
"@fastify/cors": "^8.1.0",
"@hasura/dc-api-types": "0.18.0",
"@hasura/dc-api-types": "0.19.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.18.0",
"version": "0.19.0",
"license": "Apache-2.0",
"devDependencies": {
"@tsconfig/node16": "^1.0.3",

View File

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

View File

@ -22,6 +22,19 @@ export const capabilitiesResponse: CapabilitiesResponse = {
supports_relations: true
}
},
... (
envToBool('MUTATIONS')
? {
mutations: {
atomicity_support_level: "heterogeneous_operations",
insert: { supports_nested_inserts: true },
update: {},
delete: {},
returning: {},
}
}
: {}
),
explain: {},
raw: {},
... ( envToBool('METRICS') ? { metrics: {} } : {} )

View File

@ -1,7 +1,7 @@
import { SchemaResponse, ScalarType, ColumnInfo, TableInfo, Constraint } from "@hasura/dc-api-types"
import { Config } from "./config";
import { connect, SqlLogger } from './db';
import { logDeep } from "./util";
import { envToBool } from "./util";
var sqliteParser = require('sqlite-parser');
@ -35,13 +35,17 @@ function determineScalarType(datatype: Datatype): ScalarType {
}
}
function getColumns(ast: any[]) : ColumnInfo[] {
function getColumns(ast: any[], primaryKeys: string[], mutationsEnabled: boolean) : ColumnInfo[] {
return ast.map(c => {
return ({
const isPrimaryKey = primaryKeys.includes(c.name);
return {
name: c.name,
type: determineScalarType(c.datatype),
nullable: nullableCast(c.definition)
})
nullable: nullableCast(c.definition),
insertable: mutationsEnabled,
updatable: mutationsEnabled && !isPrimaryKey,
};
})
}
@ -56,20 +60,25 @@ function nullableCast(ds: any[]): boolean {
const formatTableInfo = (config: Config) => (info: TableInfoInternal): TableInfo => {
const ast = sqliteParser(info.sql);
const ddl = ddlColumns(ast);
const columnsDdl = getColumnsDdl(ast);
const primaryKeys = ddlPKs(ast);
const foreignKeys = ddlFKs(config, ast);
const primaryKey = primaryKeys.length > 0 ? { primary_key: primaryKeys } : {};
const foreignKey = foreignKeys.length > 0 ? { foreign_keys: Object.fromEntries(foreignKeys) } : {};
const tableName = config.explicit_main_schema ? ["main", info.name] : [info.name];
// TODO: Should we include something for the description here?
const mutationsEnabled = envToBool('MUTATIONS');
return {
name: tableName,
type: "table",
...primaryKey,
...foreignKey,
description: info.sql,
columns: getColumns(ddl)
columns: getColumns(columnsDdl, primaryKeys, mutationsEnabled),
insertable: mutationsEnabled,
updatable: mutationsEnabled,
deletable: mutationsEnabled,
}
}
@ -100,7 +109,7 @@ function includeTable(config: Config, table: TableInfoInternal): boolean {
* @param ddl - The output of sqlite-parser
* @returns - List of columns as present in the output of sqlite-parser.
*/
function ddlColumns(ddl: any): any[] {
function getColumnsDdl(ddl: any): any[] {
if(ddl.type != 'statement' || ddl.variant != 'list') {
throw new Error("Encountered a non-statement or non-list when parsing DDL for table.");
}

View File

@ -11,6 +11,7 @@ import Control.Lens qualified as Lens
import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Lens
import Data.Aeson.Types qualified as J
import Data.List.NonEmpty qualified as NE
import Data.Vector qualified as Vector
import Harness.Backend.DataConnector.Chinook qualified as Chinook
@ -24,6 +25,7 @@ import Harness.Test.Fixture qualified as Fixture
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment)
import Harness.TestEnvironment qualified as TestEnvironment
import Harness.Yaml (shouldReturnYaml, shouldReturnYamlF)
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Prelude
import Test.Hspec (SpecWith, describe, it, pendingWith)
@ -103,6 +105,14 @@ schemaInspectionTests opts = describe "Schema and Source Inspection" $ do
let removeDescriptions (J.Object o) = J.Object (KM.delete "description" (removeDescriptions <$> o))
removeDescriptions (J.Array a) = J.Array (removeDescriptions <$> a)
removeDescriptions x = x
let mutationsCapabilities =
TestEnvironment.backendTypeConfig testEnvironment
>>= BackendType.backendCapabilities
>>= J.parseMaybe J.parseJSON
>>= API._cMutations
let supportsInserts = isJust $ mutationsCapabilities >>= API._mcInsertCapabilities
let supportsUpdates = isJust $ mutationsCapabilities >>= API._mcUpdateCapabilities
let supportsDeletes = isJust $ mutationsCapabilities >>= API._mcDeleteCapabilities
case BackendType.backendSourceName <$> TestEnvironment.backendTypeConfig testEnvironment of
Nothing -> pendingWith "Backend not found for testEnvironment"
@ -125,13 +135,21 @@ schemaInspectionTests opts = describe "Schema and Source Inspection" $ do
- name: GenreId
nullable: false
type: number
insertable: *supportsInserts
updatable: false
- name: Name
nullable: true
type: string
insertable: *supportsInserts
updatable: *supportsUpdates
name:
- Genre
type: table
primary_key:
- GenreId
insertable: *supportsInserts
updatable: *supportsUpdates
deletable: *supportsDeletes
|]
describe "get_source_kind_capabilities" $ do

View File

@ -1,13 +1,14 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE TemplateHaskell #-}
--
module Hasura.Backends.DataConnector.API.V0.Column
( ColumnInfo (..),
ciName,
ciType,
ciNullable,
ciDescription,
ciInsertable,
ciUpdatable,
ColumnName (..),
)
where
@ -44,7 +45,9 @@ data ColumnInfo = ColumnInfo
{ _ciName :: ColumnName,
_ciType :: API.V0.Scalar.ScalarType,
_ciNullable :: Bool,
_ciDescription :: Maybe Text
_ciDescription :: Maybe Text,
_ciInsertable :: Bool,
_ciUpdatable :: Bool
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData, Hashable)
@ -58,5 +61,7 @@ instance HasCodec ColumnInfo where
<*> requiredField "type" "Column type" .= _ciType
<*> requiredField "nullable" "Is column nullable" .= _ciNullable
<*> optionalFieldOrNull "description" "Column description" .= _ciDescription
<*> optionalFieldWithDefault "insertable" False "Whether or not the column can be inserted into" .= _ciInsertable
<*> optionalFieldWithDefault "updatable" False "Whether or not the column can be updated" .= _ciUpdatable
$(makeLenses ''ColumnInfo)

View File

@ -1,15 +1,20 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE TemplateHaskell #-}
--
module Hasura.Backends.DataConnector.API.V0.Table
( TableInfo (..),
( TableName (..),
TableInfo (..),
tiName,
tiType,
tiColumns,
tiPrimaryKey,
tiForeignKeys,
tiDescription,
TableName (..),
tiInsertable,
tiUpdatable,
tiDeletable,
TableType (..),
ForeignKeys (..),
ConstraintName (..),
Constraint (..),
@ -56,10 +61,14 @@ instance HasCodec TableName where
-- | Table schema data from the 'SchemaResponse'.
data TableInfo = TableInfo
{ _tiName :: TableName,
_tiType :: TableType,
_tiColumns :: [API.V0.ColumnInfo],
_tiPrimaryKey :: [API.V0.ColumnName],
_tiForeignKeys :: ForeignKeys,
_tiDescription :: Maybe Text
_tiDescription :: Maybe Text,
_tiInsertable :: Bool,
_tiUpdatable :: Bool,
_tiDeletable :: Bool
}
deriving stock (Eq, Ord, Show, Generic)
deriving anyclass (NFData, Hashable)
@ -70,10 +79,31 @@ instance HasCodec TableInfo where
object "TableInfo" $
TableInfo
<$> requiredField "name" "The name of the table" .= _tiName
<*> optionalFieldWithDefault "type" Table "The type of table" .= _tiType
<*> requiredField "columns" "The columns of the table" .= _tiColumns
<*> optionalFieldWithOmittedDefault "primary_key" [] "The primary key of the table" .= _tiPrimaryKey
<*> optionalFieldWithOmittedDefault "foreign_keys" (ForeignKeys mempty) "Foreign key constraints" .= _tiForeignKeys
<*> optionalFieldOrNull "description" "Description of the table" .= _tiDescription
<*> optionalFieldWithDefault "insertable" False "Whether or not new rows can be inserted into the table" .= _tiInsertable
<*> optionalFieldWithDefault "updatable" False "Whether or not existing rows can be updated in the table" .= _tiUpdatable
<*> optionalFieldWithDefault "deletable" False "Whether or not existing rows can be deleted in the table" .= _tiDeletable
--------------------------------------------------------------------------------
data TableType
= Table
| View
deriving stock (Eq, Ord, Show, Generic, Enum, Bounded)
deriving anyclass (NFData, Hashable)
instance HasCodec TableType where
codec =
named "TableType" $
( stringConstCodec
[ (Table, "table"),
(View, "view")
]
)
--------------------------------------------------------------------------------
@ -85,6 +115,8 @@ newtype ForeignKeys = ForeignKeys {unForeignKeys :: HashMap ConstraintName Const
instance HasCodec ForeignKeys where
codec = dimapCodec ForeignKeys unForeignKeys $ codec @(HashMap ConstraintName Constraint)
--------------------------------------------------------------------------------
newtype ConstraintName = ConstraintName {unConstraintName :: Text}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving newtype (FromJSON, ToJSON, FromJSONKey, ToJSONKey)
@ -105,5 +137,7 @@ instance HasCodec Constraint where
<$> requiredField "foreign_table" "The table referenced by the foreign key in the child table." .= _cForeignTable
<*> requiredField "column_mapping" "The columns on which you want want to define the foreign key." .= _cColumnMapping
--------------------------------------------------------------------------------
$(makeLenses ''TableInfo)
$(makeLenses ''Constraint)

View File

@ -1,6 +1,7 @@
[
{
"name": ["Artist"],
"type": "table",
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
@ -8,23 +9,31 @@
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "Artist primary key identifier"
"description": "Artist primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": true,
"description": "The name of the artist"
"description": "The name of the artist",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Album"],
"type": "table",
"primary_key": ["AlbumId"],
"foreign_keys": {
"Artist": {
"column_mapping": {
"ArtistId": "ArtistId"
"ArtistId": "ArtistId"
},
"foreign_table": ["Artist"]
}
@ -35,24 +44,34 @@
"name": "AlbumId",
"type": "number",
"nullable": false,
"description": "Album primary key identifier"
"description": "Album primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Title",
"type": "string",
"nullable": false,
"description": "The title of the album"
"description": "The title of the album",
"insertable": true,
"updatable": true
},
{
"name": "ArtistId",
"type": "number",
"nullable": false,
"description": "The ID of the artist that created this album"
"description": "The ID of the artist that created this album",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Customer"],
"type": "table",
"primary_key": ["CustomerId"],
"foreign_keys": {
"CustomerSupportRep": {
@ -68,84 +87,114 @@
"name": "CustomerId",
"type": "number",
"nullable": false,
"description": "Customer primary key identifier"
"description": "Customer primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "FirstName",
"type": "string",
"nullable": false,
"description": "The customer's first name"
"description": "The customer's first name",
"insertable": true,
"updatable": true
},
{
"name": "LastName",
"type": "string",
"nullable": false,
"description": "The customer's last name"
"description": "The customer's last name",
"insertable": true,
"updatable": true
},
{
"name": "Company",
"type": "string",
"nullable": true,
"description": "The customer's company name"
"description": "The customer's company name",
"insertable": true,
"updatable": true
},
{
"name": "Address",
"type": "string",
"nullable": true,
"description": "The customer's address line (street number, street)"
"description": "The customer's address line (street number, street)",
"insertable": true,
"updatable": true
},
{
"name": "City",
"type": "string",
"nullable": true,
"description": "The customer's address city"
"description": "The customer's address city",
"insertable": true,
"updatable": true
},
{
"name": "State",
"type": "string",
"nullable": true,
"description": "The customer's address state"
"description": "The customer's address state",
"insertable": true,
"updatable": true
},
{
"name": "Country",
"type": "string",
"nullable": true,
"description": "The customer's address country"
"description": "The customer's address country",
"insertable": true,
"updatable": true
},
{
"name": "PostalCode",
"type": "string",
"nullable": true,
"description": "The customer's address postal code"
"description": "The customer's address postal code",
"insertable": true,
"updatable": true
},
{
"name": "Phone",
"type": "string",
"nullable": true,
"description": "The customer's phone number"
"description": "The customer's phone number",
"insertable": true,
"updatable": true
},
{
"name": "Fax",
"type": "string",
"nullable": true,
"description": "The customer's fax number"
"description": "The customer's fax number",
"insertable": true,
"updatable": true
},
{
"name": "Email",
"type": "string",
"nullable": false,
"description": "The customer's email address"
"description": "The customer's email address",
"insertable": true,
"updatable": true
},
{
"name": "SupportRepId",
"type": "number",
"nullable": true,
"description": "The ID of the Employee who is this customer's support representative"
"description": "The ID of the Employee who is this customer's support representative",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Employee"],
"type": "table",
"primary_key": ["EmployeeId"],
"foreign_keys": {
"EmployeeReportsTo": {
@ -161,96 +210,130 @@
"name": "EmployeeId",
"type": "number",
"nullable": false,
"description": "Employee primary key identifier"
"description": "Employee primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "LastName",
"type": "string",
"nullable": false,
"description": "The employee's last name"
"description": "The employee's last name",
"insertable": true,
"updatable": true
},
{
"name": "FirstName",
"type": "string",
"nullable": false,
"description": "The employee's first name"
"description": "The employee's first name",
"insertable": true,
"updatable": true
},
{
"name": "Title",
"type": "string",
"nullable": true,
"description": "The employee's job title"
"description": "The employee's job title",
"insertable": true,
"updatable": true
},
{
"name": "ReportsTo",
"type": "number",
"nullable": true,
"description": "The employee's report"
"description": "The employee's manager",
"insertable": true,
"updatable": true
},
{
"name": "BirthDate",
"type": "DateTime",
"nullable": true,
"description": "The employee's birth date"
"description": "The employee's birth date",
"insertable": true,
"updatable": true
},
{
"name": "HireDate",
"type": "DateTime",
"nullable": true,
"description": "The employee's hire date"
"description": "The employee's hire date",
"insertable": true,
"updatable": true
},
{
"name": "Address",
"type": "string",
"nullable": true,
"description": "The employee's address line (street number, street)"
"description": "The employee's address line (street number, street)",
"insertable": true,
"updatable": true
},
{
"name": "City",
"type": "string",
"nullable": true,
"description": "The employee's address city"
"description": "The employee's address city",
"insertable": true,
"updatable": true
},
{
"name": "State",
"type": "string",
"nullable": true,
"description": "The employee's address state"
"description": "The employee's address state",
"insertable": true,
"updatable": true
},
{
"name": "Country",
"type": "string",
"nullable": true,
"description": "The employee's address country"
"description": "The employee's address country",
"insertable": true,
"updatable": true
},
{
"name": "PostalCode",
"type": "string",
"nullable": true,
"description": "The employee's address postal code"
"description": "The employee's address postal code",
"insertable": true,
"updatable": true
},
{
"name": "Phone",
"type": "string",
"nullable": true,
"description": "The employee's phone number"
"description": "The employee's phone number",
"insertable": true,
"updatable": true
},
{
"name": "Fax",
"type": "string",
"nullable": true,
"description": "The employee's fax number"
"description": "The employee's fax number",
"insertable": true,
"updatable": true
},
{
"name": "Email",
"type": "string",
"nullable": true,
"description": "The employee's email address"
"description": "The employee's email address",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Genre"],
"type": "table",
"primary_key": ["GenreId"],
"description": "Genres of music",
"columns": [
@ -258,18 +341,26 @@
"name": "GenreId",
"type": "number",
"nullable": false,
"description": "Genre primary key identifier"
"description": "Genre primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": true,
"description": "The name of the genre"
"description": "The name of the genre",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Invoice"],
"type": "table",
"primary_key": ["InvoiceId"],
"foreign_keys": {
"InvoiceCustomer": {
@ -285,60 +376,82 @@
"name": "InvoiceId",
"type": "number",
"nullable": false,
"description": "Invoice primary key identifier"
"description": "Invoice primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "CustomerId",
"type": "number",
"nullable": false,
"description": "ID of the customer who bought the music"
"description": "ID of the customer who bought the music",
"insertable": true,
"updatable": true
},
{
"name": "InvoiceDate",
"type": "DateTime",
"nullable": false,
"description": "Date of the invoice"
"description": "Date of the invoice",
"insertable": true,
"updatable": true
},
{
"name": "BillingAddress",
"type": "string",
"nullable": true,
"description": "The invoice's billing address line (street number, street)"
"description": "The invoice's billing address line (street number, street)",
"insertable": true,
"updatable": true
},
{
"name": "BillingCity",
"type": "string",
"nullable": true,
"description": "The invoice's billing address city"
"description": "The invoice's billing address city",
"insertable": true,
"updatable": true
},
{
"name": "BillingState",
"type": "string",
"nullable": true,
"description": "The invoice's billing address state"
"description": "The invoice's billing address state",
"insertable": true,
"updatable": true
},
{
"name": "BillingCountry",
"type": "string",
"nullable": true,
"description": "The invoice's billing address country"
"description": "The invoice's billing address country",
"insertable": true,
"updatable": true
},
{
"name": "BillingPostalCode",
"type": "string",
"nullable": true,
"description": "The invoice's billing address postal code"
"description": "The invoice's billing address postal code",
"insertable": true,
"updatable": true
},
{
"name": "Total",
"type": "number",
"nullable": false,
"description": "The total amount due on the invoice"
"description": "The total amount due on the invoice",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["InvoiceLine"],
"type": "table",
"primary_key": ["InvoiceLineId"],
"foreign_keys": {
"Invoice": {
@ -360,36 +473,50 @@
"name": "InvoiceLineId",
"type": "number",
"nullable": false,
"description": "Invoice Line primary key identifier"
"description": "Invoice Line primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "InvoiceId",
"type": "number",
"nullable": false,
"description": "ID of the invoice the line belongs to"
"description": "ID of the invoice the line belongs to",
"insertable": true,
"updatable": true
},
{
"name": "TrackId",
"type": "number",
"nullable": false,
"description": "ID of the music track being purchased"
"description": "ID of the music track being purchased",
"insertable": true,
"updatable": true
},
{
"name": "UnitPrice",
"type": "number",
"nullable": false,
"description": "Price of each individual track unit"
"description": "Price of each individual track unit",
"insertable": true,
"updatable": true
},
{
"name": "Quantity",
"type": "number",
"nullable": false,
"description": "Quantity of the track purchased"
"description": "Quantity of the track purchased",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["MediaType"],
"type": "table",
"primary_key": ["MediaTypeId"],
"description": "Collection of media types that tracks can be encoded in",
"columns": [
@ -397,18 +524,26 @@
"name": "MediaTypeId",
"type": "number",
"nullable": false,
"description": "Media Type primary key identifier"
"description": "Media Type primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": true,
"description": "The name of the media type format"
"description": "The name of the media type format",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Playlist"],
"type": "table",
"primary_key": ["PlaylistId"],
"description": "Collection of playlists",
"columns": [
@ -416,18 +551,26 @@
"name": "PlaylistId",
"type": "number",
"nullable": false,
"description": "Playlist primary key identifier"
"description": "Playlist primary key identifier",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": true,
"description": "The name of the playlist"
"description": "The name of the playlist",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["PlaylistTrack"],
"type": "table",
"primary_key": ["PlaylistId", "TrackId"],
"foreign_keys": {
"Playlist": {
@ -449,18 +592,26 @@
"name": "PlaylistId",
"type": "number",
"nullable": false,
"description": "The ID of the playlist"
"description": "The ID of the playlist",
"insertable": true,
"updatable": false
},
{
"name": "TrackId",
"type": "number",
"nullable": false,
"description": "The ID of the track"
"description": "The ID of the track",
"insertable": true,
"updatable": false
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
},
{
"name": ["Track"],
"type": "table",
"primary_key": ["TrackId"],
"foreign_keys": {
"Album": {
@ -488,56 +639,77 @@
"name": "TrackId",
"type": "number",
"nullable": false,
"description": "The ID of the track"
"description": "The ID of the track",
"insertable": true,
"updatable": false
},
{
"name": "Name",
"type": "string",
"nullable": false,
"description": "The name of the track"
"description": "The name of the track",
"insertable": true,
"updatable": true
},
{
"name": "AlbumId",
"type": "number",
"nullable": true,
"description": "The ID of the album the track belongs to"
"description": "The ID of the album the track belongs to",
"insertable": true,
"updatable": true
},
{
"name": "MediaTypeId",
"type": "number",
"nullable": false,
"description": "The ID of the media type the track is encoded with"
"description": "The ID of the media type the track is encoded with",
"insertable": true,
"updatable": true
},
{
"name": "GenreId",
"type": "number",
"nullable": true,
"description": "The ID of the genre of the track"
"description": "The ID of the genre of the track",
"insertable": true,
"updatable": true
},
{
"name": "Composer",
"type": "string",
"nullable": true,
"description": "The name of the composer of the track"
"description": "The name of the composer of the track",
"insertable": true,
"updatable": true
},
{
"name": "Milliseconds",
"type": "number",
"nullable": false,
"description": "The length of the track in milliseconds"
"description": "The length of the track in milliseconds",
"insertable": true,
"updatable": true
},
{
"name": "Bytes",
"type": "number",
"nullable": true,
"description": "The size of the track in bytes"
"description": "The size of the track in bytes",
"insertable": true,
"updatable": true
},
{
"name": "UnitPrice",
"type": "number",
"nullable": false,
"description": "The price of the track"
"description": "The price of the track",
"insertable": true,
"updatable": true
}
]
],
"insertable": true,
"updatable": true,
"deletable": true
}
]

View File

@ -9,11 +9,13 @@ import Control.Monad (forM_)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class (MonadIO)
import Data.Aeson (Value (..), toJSON)
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Lens (_Object)
import Data.Foldable (find)
import Data.HashMap.Strict qualified as HashMap
import Data.List (sort, sortOn)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Maybe (isJust)
import Data.Text qualified as Text
import Hasura.Backends.DataConnector.API qualified as API
import Test.AgentClient (AgentClientT, HasAgentClient, getSchemaGuarded)
@ -28,6 +30,10 @@ import Prelude
spec :: TestData -> API.SourceName -> API.Config -> API.Capabilities -> AgentTestSpec
spec TestData {..} sourceName config API.Capabilities {..} = describe "schema API" $ do
let supportsInserts = isJust $ _cMutations >>= API._mcInsertCapabilities
let supportsUpdates = isJust $ _cMutations >>= API._mcUpdateCapabilities
let supportsDeletes = isJust $ _cMutations >>= API._mcDeleteCapabilities
it "returns the Chinook tables" $ do
let extractTableNames = sort . fmap API._tiName
tableNames <- (extractTableNames . API._srTables) <$> getSchemaGuarded sourceName config
@ -36,7 +42,7 @@ spec TestData {..} sourceName config API.Capabilities {..} = describe "schema AP
tableNames `jsonShouldBe` expectedTableNames
testPerTable "returns the correct columns in the Chinook tables" $ \expectedTable@API.TableInfo {..} -> do
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> getSchemaGuarded sourceName config
actualTable <- find (\t -> API._tiName t == _tiName) . API._srTables <$> getSchemaGuarded sourceName config
-- We remove some properties here so that we don't compare them since they vary between agent implementations
let extractJsonForComparison table =
@ -46,16 +52,53 @@ spec TestData {..} sourceName config API.Capabilities {..} = describe "schema AP
column
& _Object . at "type" .~ Nothing -- Types can vary between agents since underlying datatypes can change
& _Object . at "description" .~ Nothing -- Descriptions are not supported by all agents
-- If the agent only supports nullable columns, we make all columns nullable
let setExpectedColumnNullability columns =
if API._dscColumnNullability _cDataSchema == API.OnlyNullableColumns
then columns & traverse %~ (_Object . at "nullable" ?~ Bool True)
else columns
let actualJsonColumns = extractJsonForComparison <$> tables
let expectedJsonColumns = Just . setExpectedColumnNullability $ extractJsonForComparison expectedTable
let actualJsonColumns = extractJsonForComparison <$> actualTable
let expectedJsonColumns =
expectedTable
& extractJsonForComparison
-- If the agent only supports nullable columns, we make all columns nullable
& applyWhen
(API._dscColumnNullability _cDataSchema == API.OnlyNullableColumns)
(traverse %~ (_Object . at "nullable" ?~ Bool True))
-- If the agent doesn't support insert mutations then all columns should not be insertable
& applyWhen
(not supportsInserts)
(traverse %~ (_Object . at "insertable" ?~ Bool False))
-- If agent doesn't support update mutations then all columns should not be updatable
& applyWhen
(not supportsUpdates)
(traverse %~ (_Object . at "updatable" ?~ Bool False))
& Just
actualJsonColumns `jsonShouldBe` expectedJsonColumns
testPerTable "returns the correct mutability in the Chinook tables" $ \expectedTable@API.TableInfo {..} -> do
actualTable <- find (\t -> API._tiName t == _tiName) . API._srTables <$> getSchemaGuarded sourceName config
let extractJsonForComparison (table :: API.TableInfo) =
toJSON table
& _Object %~ (KeyMap.filterWithKey (\prop _value -> prop `elem` ["insertable", "updatable", "deletable"]))
let actualComparisonJson = extractJsonForComparison <$> actualTable
let expectedComparisonJson =
expectedTable
& extractJsonForComparison
-- If the agent doesn't support insert mutations then the table should not be insertable
& applyWhen
(not supportsInserts)
(_Object . at "insertable" ?~ Bool False)
-- If the agent doesn't support update mutations then the table should not be updatable
& applyWhen
(not supportsUpdates)
(_Object . at "updatable" ?~ Bool False)
-- If the agent doesn't support delete mutations then the table should not be deletable
& applyWhen
(not supportsDeletes)
(_Object . at "deletable" ?~ Bool False)
& Just
actualComparisonJson `jsonShouldBe` expectedComparisonJson
if API._dscSupportsPrimaryKeys _cDataSchema
then testPerTable "returns the correct primary keys for the Chinook tables" $ \API.TableInfo {..} -> do
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> getSchemaGuarded sourceName config
@ -90,3 +133,7 @@ spec TestData {..} sourceName config API.Capabilities {..} = describe "schema AP
forM_ _tdSchemaTables $ \expectedTable@API.TableInfo {..} -> do
it (Text.unpack . NonEmpty.last $ API.unTableName _tiName) $
test expectedTable
applyWhen :: Bool -> (a -> a) -> a -> a
applyWhen True f x = f x
applyWhen False _ x = x

View File

@ -96,316 +96,425 @@ schema =
{ API._srTables =
[ API.TableInfo
{ API._tiName = mkTableName "Artist",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "ArtistId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Artist primary key identifier"
API._ciDescription = Just "Artist primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Name",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The name of the artist"
API._ciDescription = Just "The name of the artist",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "ArtistId"],
API._tiDescription = Just "Collection of artists of music",
API._tiForeignKeys = API.ForeignKeys mempty
API._tiForeignKeys = API.ForeignKeys mempty,
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Album",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "AlbumId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Album primary key identifier"
API._ciDescription = Just "Album primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Title",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The title of the album"
API._ciDescription = Just "The title of the album",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "ArtistId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The ID of the artist that created the album"
API._ciDescription = Just "The ID of the artist that created the album",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "AlbumId"],
API._tiDescription = Just "Collection of music albums created by artists",
API._tiForeignKeys =
API.ForeignKeys $
HashMap.singleton (API.ConstraintName "Artist") (API.Constraint (mkTableName "Artist") (HashMap.singleton (API.ColumnName "ArtistId") (API.ColumnName "ArtistId")))
HashMap.singleton (API.ConstraintName "Artist") (API.Constraint (mkTableName "Artist") (HashMap.singleton (API.ColumnName "ArtistId") (API.ColumnName "ArtistId"))),
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Customer",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "CustomerId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Customer primary key identifier"
API._ciDescription = Just "Customer primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "FirstName",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The customer's first name"
API._ciDescription = Just "The customer's first name",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "LastName",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The customer's last name"
API._ciDescription = Just "The customer's last name",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Company",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's company name"
API._ciDescription = Just "The customer's company name",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Address",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's address line (street number, street)"
API._ciDescription = Just "The customer's address line (street number, street)",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "City",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's address city"
API._ciDescription = Just "The customer's address city",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "State",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's address state"
API._ciDescription = Just "The customer's address state",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Country",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's address country"
API._ciDescription = Just "The customer's address country",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "PostalCode",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's address postal code"
API._ciDescription = Just "The customer's address postal code",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Phone",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's phone number"
API._ciDescription = Just "The customer's phone number",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Fax",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The customer's fax number"
API._ciDescription = Just "The customer's fax number",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Email",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The customer's email address"
API._ciDescription = Just "The customer's email address",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "SupportRepId",
API._ciType = API.NumberTy,
API._ciNullable = True,
API._ciDescription = Just "The ID of the Employee who is this customer's support representative"
API._ciDescription = Just "The ID of the Employee who is this customer's support representative",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "CustomerId"],
API._tiDescription = Just "Collection of customers who can buy tracks",
API._tiForeignKeys =
API.ForeignKeys $
HashMap.singleton (API.ConstraintName "CustomerSupportRep") (API.Constraint (mkTableName "Employee") (HashMap.singleton (API.ColumnName "SupportRepId") (API.ColumnName "EmployeeId")))
HashMap.singleton (API.ConstraintName "CustomerSupportRep") (API.Constraint (mkTableName "Employee") (HashMap.singleton (API.ColumnName "SupportRepId") (API.ColumnName "EmployeeId"))),
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Employee",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "EmployeeId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Employee primary key identifier"
API._ciDescription = Just "Employee primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "LastName",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The employee's last name"
API._ciDescription = Just "The employee's last name",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "FirstName",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The employee's first name"
API._ciDescription = Just "The employee's first name",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Title",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's job title"
API._ciDescription = Just "The employee's job title",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "ReportsTo",
API._ciType = API.NumberTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's report"
API._ciDescription = Just "The employee's report",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BirthDate",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's birth date"
API._ciDescription = Just "The employee's birth date",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "HireDate",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's hire date"
API._ciDescription = Just "The employee's hire date",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Address",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's address line (street number, street)"
API._ciDescription = Just "The employee's address line (street number, street)",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "City",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's address city"
API._ciDescription = Just "The employee's address city",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "State",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's address state"
API._ciDescription = Just "The employee's address state",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Country",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's address country"
API._ciDescription = Just "The employee's address country",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "PostalCode",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's address postal code"
API._ciDescription = Just "The employee's address postal code",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Phone",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's phone number"
API._ciDescription = Just "The employee's phone number",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Fax",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's fax number"
API._ciDescription = Just "The employee's fax number",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Email",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The employee's email address"
API._ciDescription = Just "The employee's email address",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "EmployeeId"],
API._tiDescription = Just "Collection of employees who work for the business",
API._tiForeignKeys =
API.ForeignKeys $
HashMap.singleton (API.ConstraintName "EmployeeReportsTo") (API.Constraint (mkTableName "Employee") (HashMap.singleton (API.ColumnName "ReportsTo") (API.ColumnName "EmployeeId")))
HashMap.singleton (API.ConstraintName "EmployeeReportsTo") (API.Constraint (mkTableName "Employee") (HashMap.singleton (API.ColumnName "ReportsTo") (API.ColumnName "EmployeeId"))),
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Genre",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "GenreId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Genre primary key identifier"
API._ciDescription = Just "Genre primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Name",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The name of the genre"
API._ciDescription = Just "The name of the genre",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "GenreId"],
API._tiDescription = Just "Genres of music",
API._tiForeignKeys = API.ForeignKeys mempty
API._tiForeignKeys = API.ForeignKeys mempty,
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Invoice",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "InvoiceId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Invoice primary key identifier"
API._ciDescription = Just "Invoice primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "CustomerId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "ID of the customer who bought the music"
API._ciDescription = Just "ID of the customer who bought the music",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "InvoiceDate",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "Date of the invoice"
API._ciDescription = Just "Date of the invoice",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BillingAddress",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The invoice's billing address line (street number, street)"
API._ciDescription = Just "The invoice's billing address line (street number, street)",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BillingCity",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The invoice's billing address city"
API._ciDescription = Just "The invoice's billing address city",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BillingState",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The invoice's billing address state"
API._ciDescription = Just "The invoice's billing address state",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BillingCountry",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The invoice's billing address country"
API._ciDescription = Just "The invoice's billing address country",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "BillingPostalCode",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The invoice's billing address postal code"
API._ciDescription = Just "The invoice's billing address postal code",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Total",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The total amount due on the invoice"
API._ciDescription = Just "The total amount due on the invoice",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "InvoiceId"],
@ -413,40 +522,54 @@ schema =
API._tiForeignKeys =
API.ForeignKeys $
HashMap.singleton (API.ConstraintName "InvoiceCustomer") $
API.Constraint (mkTableName "Customer") (HashMap.singleton (API.ColumnName "CustomerId") (API.ColumnName "CustomerId"))
API.Constraint (mkTableName "Customer") (HashMap.singleton (API.ColumnName "CustomerId") (API.ColumnName "CustomerId")),
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "InvoiceLine",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "InvoiceLineId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Invoice Line primary key identifier"
API._ciDescription = Just "Invoice Line primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "InvoiceId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "ID of the invoice the line belongs to"
API._ciDescription = Just "ID of the invoice the line belongs to",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "TrackId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "ID of the music track being purchased"
API._ciDescription = Just "ID of the music track being purchased",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "UnitPrice",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Price of each individual track unit"
API._ciDescription = Just "Price of each individual track unit",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Quantity",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Quantity of the track purchased"
API._ciDescription = Just "Quantity of the track purchased",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "InvoiceLineId"],
@ -456,84 +579,114 @@ schema =
HashMap.fromList
[ (API.ConstraintName "Invoice", API.Constraint (mkTableName "Invoice") (HashMap.singleton (API.ColumnName "InvoiceId") (API.ColumnName "InvoiceId"))),
(API.ConstraintName "Track", API.Constraint (mkTableName "Track") (HashMap.singleton (API.ColumnName "TrackId") (API.ColumnName "TrackId")))
]
],
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "MediaType",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "MediaTypeId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "Media Type primary key identifier"
API._ciDescription = Just "Media Type primary key identifier",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Name",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The name of the media type format"
API._ciDescription = Just "The name of the media type format",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "MediaTypeId"],
API._tiDescription = Just "Collection of media types that tracks can be encoded in",
API._tiForeignKeys = API.ForeignKeys mempty
API._tiForeignKeys = API.ForeignKeys mempty,
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "Track",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo
{ API._ciName = API.ColumnName "TrackId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The ID of the track"
API._ciDescription = Just "The ID of the track",
API._ciInsertable = True,
API._ciUpdatable = False
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Name",
API._ciType = API.StringTy,
API._ciNullable = False,
API._ciDescription = Just "The name of the track"
API._ciDescription = Just "The name of the track",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "AlbumId",
API._ciType = API.NumberTy,
API._ciNullable = True,
API._ciDescription = Just "The ID of the album the track belongs to"
API._ciDescription = Just "The ID of the album the track belongs to",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "MediaTypeId",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The ID of the media type the track is encoded with"
API._ciDescription = Just "The ID of the media type the track is encoded with",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "GenreId",
API._ciType = API.NumberTy,
API._ciNullable = True,
API._ciDescription = Just "The ID of the genre of the track"
API._ciDescription = Just "The ID of the genre of the track",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Composer",
API._ciType = API.StringTy,
API._ciNullable = True,
API._ciDescription = Just "The name of the composer of the track"
API._ciDescription = Just "The name of the composer of the track",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Milliseconds",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The length of the track in milliseconds"
API._ciDescription = Just "The length of the track in milliseconds",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "Bytes",
API._ciType = API.NumberTy,
API._ciNullable = True,
API._ciDescription = Just "The size of the track in bytes"
API._ciDescription = Just "The size of the track in bytes",
API._ciInsertable = True,
API._ciUpdatable = True
},
API.ColumnInfo
{ API._ciName = API.ColumnName "UnitPrice",
API._ciType = API.NumberTy,
API._ciNullable = False,
API._ciDescription = Just "The price of the track"
API._ciDescription = Just "The price of the track",
API._ciInsertable = True,
API._ciUpdatable = True
}
],
API._tiPrimaryKey = [API.ColumnName "TrackId"],
@ -544,21 +697,28 @@ schema =
[ (API.ConstraintName "Album", API.Constraint (mkTableName "Album") (HashMap.singleton (API.ColumnName "AlbumId") (API.ColumnName "AlbumId"))),
(API.ConstraintName "Genre", API.Constraint (mkTableName "Genre") (HashMap.singleton (API.ColumnName "GenreId") (API.ColumnName "GenreId"))),
(API.ConstraintName "MediaType", API.Constraint (mkTableName "MediaType") (HashMap.singleton (API.ColumnName "MediaTypeId") (API.ColumnName "MediaTypeId")))
]
],
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
},
API.TableInfo
{ API._tiName = mkTableName "MyCustomScalarsTable",
API._tiType = API.Table,
API._tiColumns =
[ API.ColumnInfo (API.ColumnName "MyIntColumn") (API.CustomTy "MyInt") False Nothing,
API.ColumnInfo (API.ColumnName "MyFloatColumn") (API.CustomTy "MyFloat") False Nothing,
API.ColumnInfo (API.ColumnName "MyStringColumn") (API.CustomTy "MyString") False Nothing,
API.ColumnInfo (API.ColumnName "MyBooleanColumn") (API.CustomTy "MyBoolean") False Nothing,
API.ColumnInfo (API.ColumnName "MyIDColumn") (API.CustomTy "MyID") False Nothing,
API.ColumnInfo (API.ColumnName "MyAnythingColumn") (API.CustomTy "MyAnything") False Nothing
[ API.ColumnInfo (API.ColumnName "MyIntColumn") (API.CustomTy "MyInt") False Nothing True True,
API.ColumnInfo (API.ColumnName "MyFloatColumn") (API.CustomTy "MyFloat") False Nothing True True,
API.ColumnInfo (API.ColumnName "MyStringColumn") (API.CustomTy "MyString") False Nothing True True,
API.ColumnInfo (API.ColumnName "MyBooleanColumn") (API.CustomTy "MyBoolean") False Nothing True True,
API.ColumnInfo (API.ColumnName "MyIDColumn") (API.CustomTy "MyID") False Nothing True True,
API.ColumnInfo (API.ColumnName "MyAnythingColumn") (API.CustomTy "MyAnything") False Nothing True True
],
API._tiPrimaryKey = [],
API._tiDescription = Nothing,
API._tiForeignKeys = API.ForeignKeys mempty
API._tiForeignKeys = API.ForeignKeys mempty,
API._tiInsertable = True,
API._tiUpdatable = True,
API._tiDeletable = True
}
]
}

View File

@ -183,30 +183,32 @@ resolveDatabaseMetadata' ::
SourceTypeCustomization ->
m (Either QErr (ResolvedSource 'DataConnector))
resolveDatabaseMetadata' _ sc@DC.SourceConfig {_scSchema = API.SchemaResponse {..}, ..} customization =
-- We need agents to provide the foreign key contraints inside 'API.SchemaResponse'
let foreignKeys = fmap API._tiForeignKeys _srTables
tables = Map.fromList $ do
API.TableInfo {..} <- _srTables
let primaryKeyColumns = Seq.fromList $ coerce <$> _tiPrimaryKey
let meta =
RQL.T.T.DBTableMetadata
{ _ptmiOid = OID 0,
{ _ptmiOid = OID 0, -- TODO: This is wrong and needs to be fixed. It is used for diffing tables and seeing what's new/deleted/altered, so reusing 0 for all tables is problematic.
_ptmiColumns = do
API.ColumnInfo {..} <- _tiColumns
pure $
RQL.T.C.RawColumnInfo
{ rciName = Witch.from _ciName,
rciPosition = 1,
rciPosition = 1, -- TODO: This is very wrong and needs to be fixed. It is used for diffing tables and seeing what's new/deleted/altered, so reusing 1 for all columns is problematic.
rciType = DC.mkScalarType _scCapabilities _ciType,
rciIsNullable = _ciNullable,
rciDescription = fmap GQL.Description _ciDescription,
-- TODO: Add Column Mutability to the 'TableInfo'
rciMutability = RQL.T.C.ColumnMutability False False
rciMutability = RQL.T.C.ColumnMutability _ciInsertable _ciUpdatable
},
_ptmiPrimaryKey = RQL.T.T.PrimaryKey (RQL.T.T.Constraint (DC.ConstraintName "") (OID 0)) <$> NESeq.nonEmptySeq primaryKeyColumns,
_ptmiUniqueConstraints = mempty,
_ptmiForeignKeys = buildForeignKeySet foreignKeys,
_ptmiViewInfo = Just $ RQL.T.T.ViewInfo False False False,
_ptmiViewInfo =
( if _tiType == API.Table && _tiInsertable && _tiUpdatable && _tiDeletable
then Nothing
else Just $ RQL.T.T.ViewInfo _tiInsertable _tiUpdatable _tiDeletable
),
_ptmiDescription = fmap PGDescription _tiDescription,
_ptmiExtraTableMetadata = ()
}

View File

@ -19,23 +19,25 @@ spec = do
testToFromJSONToSchema (ColumnName "my_column_name") [aesonQQ|"my_column_name"|]
jsonOpenApiProperties genColumnName
describe "ColumnInfo" $ do
describe "without description" $
testToFromJSONToSchema
(ColumnInfo (ColumnName "my_column_name") StringTy False Nothing)
describe "minimal" $
testFromJSON
(ColumnInfo (ColumnName "my_column_name") StringTy False Nothing False False)
[aesonQQ|
{ "name": "my_column_name",
"type": "string",
"nullable": false
}
|]
describe "with description" $
describe "non-minimal" $
testToFromJSONToSchema
(ColumnInfo (ColumnName "my_column_name") NumberTy True (Just "My column description"))
(ColumnInfo (ColumnName "my_column_name") NumberTy True (Just "My column description") True True)
[aesonQQ|
{ "name": "my_column_name",
"type": "number",
"nullable": true,
"description": "My column description"
"description": "My column description",
"insertable": true,
"updatable": true
}
|]
jsonOpenApiProperties genColumnInfo
@ -50,3 +52,5 @@ genColumnInfo =
<*> genScalarType
<*> Gen.bool
<*> Gen.maybe (genArbitraryAlphaNumText defaultRange)
<*> Gen.bool
<*> Gen.bool

View File

@ -22,8 +22,8 @@ spec = do
jsonOpenApiProperties genTableName
describe "TableInfo" $ do
describe "minimal" $
testToFromJSONToSchema
(TableInfo (TableName ["my_table_name"]) [] [] (ForeignKeys mempty) Nothing)
testFromJSON
(TableInfo (TableName ["my_table_name"]) Table [] [] (ForeignKeys mempty) Nothing False False False)
[aesonQQ|
{ "name": ["my_table_name"],
"columns": []
@ -33,30 +33,43 @@ spec = do
testToFromJSONToSchema
( TableInfo
(TableName ["my_table_name"])
[ColumnInfo (ColumnName "id") StringTy False Nothing]
View
[ColumnInfo (ColumnName "id") StringTy False Nothing False False]
[ColumnName "id"]
(ForeignKeys mempty)
(Just "my description")
True
True
True
)
[aesonQQ|
{ "name": ["my_table_name"],
"columns": [{"name": "id", "type": "string", "nullable": false}],
"type": "view",
"columns": [{"name": "id", "type": "string", "nullable": false, "insertable": false, "updatable": false}],
"primary_key": ["id"],
"description": "my description"
"description": "my description",
"insertable": true,
"updatable": true,
"deletable": true
}
|]
describe "foreign-key" $
testToFromJSONToSchema
( TableInfo
(TableName ["my_table_name"])
[ColumnInfo (ColumnName "id") StringTy False Nothing]
Table
[ColumnInfo (ColumnName "id") StringTy False Nothing False False]
[ColumnName "id"]
(ForeignKeys $ HashMap.singleton (ConstraintName "Artist") (Constraint (TableName ["artist_table"]) (HashMap.singleton (ColumnName "ArtistId") (ColumnName "ArtistId"))))
(Just "my description")
False
False
False
)
[aesonQQ|
{ "name": ["my_table_name"],
"columns": [{"name": "id", "type": "string", "nullable": false}],
"type": "table",
"columns": [{"name": "id", "type": "string", "nullable": false, "insertable": false, "updatable": false}],
"primary_key": ["id"],
"description": "my description",
"foreign_keys": {
@ -66,7 +79,10 @@ spec = do
"ArtistId": "ArtistId"
}
}
}
},
"insertable": false,
"updatable": false,
"deletable": false
}
|]
jsonOpenApiProperties genTableInfo
@ -85,12 +101,19 @@ genConstraint =
let mapping = genHashMap genColumnName genColumnName defaultRange
in Constraint <$> genTableName <*> mapping
genTableType :: MonadGen m => m TableType
genTableType = Gen.enumBounded
-- | Note: this generator is intended for serialization tests only and does not ensure valid Foreign Key Constraints.
genTableInfo :: (MonadGen m, GenBase m ~ Identity) => m TableInfo
genTableInfo =
TableInfo
<$> genTableName
<*> genTableType
<*> Gen.list defaultRange genColumnInfo
<*> Gen.list defaultRange genColumnName
<*> genForeignKeys
<*> Gen.maybe (genArbitraryAlphaNumText defaultRange)
<*> Gen.bool
<*> Gen.bool
<*> Gen.bool

View File

@ -1,6 +1,7 @@
-- | Some helper functions for testing Aeson instances
module Test.Aeson.Utils
( testToFromJSON,
( testFromJSON,
testToFromJSON,
validateToJSONOpenApi,
testToFromJSONToSchema,
jsonRoundTrip,
@ -26,10 +27,14 @@ import Hedgehog.Internal.Range
import Test.Hspec
import Test.Hspec.Hedgehog
testToFromJSON :: (HasCallStack, Eq a, Show a, FromJSON a, ToJSON a) => a -> Value -> Spec
testToFromJSON a v = do
testFromJSON :: (HasCallStack, Eq a, Show a, FromJSON a) => a -> Value -> Spec
testFromJSON a v = do
it "parses from JSON" $
parseEither parseJSON v `shouldBe` Right a
testToFromJSON :: (HasCallStack, Eq a, Show a, FromJSON a, ToJSON a) => a -> Value -> Spec
testToFromJSON a v = do
testFromJSON a v
it "encodes to JSON" $
toJSON a `shouldBe` v