Generate TypeScript types for the Data Connector reference agent from the OpenAPI schema

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4855
GitOrigin-RevId: 4cc09d6706e67c69fbbedef72ff816365b9f7b4e
This commit is contained in:
Daniel Chambers 2022-06-29 17:42:51 +10:00 committed by hasura-bot
parent 4b2ca2ea63
commit 59ffce9ac1
70 changed files with 2476 additions and 864 deletions

View File

@ -32,8 +32,8 @@ constraints: any.Cabal ==3.2.1.0,
any.attoparsec-iso8601 ==1.0.2.1,
any.authenticate-oauth ==1.7,
any.auto-update ==0.1.6,
any.autodocodec ==0.1.0.0,
any.autodocodec-openapi3 ==0.2.0.0,
any.autodocodec ==0.1.0.1,
any.autodocodec-openapi3 ==0.2.1.0,
any.barbies ==2.0.3.1,
any.base ==4.14.3.0,
any.base-compat ==0.11.2,
@ -352,4 +352,4 @@ constraints: any.Cabal ==3.2.1.0,
any.xml-types ==0.3.8,
any.yaml ==0.11.7.0,
any.zlib ==0.6.2.3,
index-state: hackage.haskell.org 2022-06-13T15:16:34Z
index-state: hackage.haskell.org 2022-06-20T06:51:52Z

View File

@ -210,7 +210,7 @@ Notice that the names of tables and columns are used in the metadata document to
#### Type definitions
The `SchemaResponse` TypeScript type from [the reference implementation](./reference/src/types/schema.ts) describes the valid response body for the `GET /schema` endpoint.
The `SchemaResponse` TypeScript type from [the reference implementation](./reference/src/types/index.ts) describes the valid response body for the `GET /schema` endpoint.
### Responding to queries
@ -672,4 +672,4 @@ The key point of interest here is in the `where` field where we are comparing be
#### Type Definitions
The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/query.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type.
The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/index.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type.

File diff suppressed because it is too large Load Diff

View File

@ -18,12 +18,12 @@
"typecheck": "tsc --noEmit",
"start": "ts-node ./src/index.ts",
"start-no-typecheck": "ts-node --transpileOnly ./src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"generate-types": "./scripts/generate-types.sh"
},
"dependencies": {
"@fastify/cors": "^7.0.0",
"fastify": "^3.29.0",
"openapi3-ts": "^2.0.2",
"pino-pretty": "^8.0.0",
"xml2js": "^0.4.23"
},
@ -31,6 +31,7 @@
"@tsconfig/node16": "^1.0.2",
"@types/node": "^16.11.38",
"@types/xml2js": "^0.4.11",
"openapi-typescript-codegen": "^0.23.0",
"ts-node": "^10.8.1",
"typescript": "^4.7.3"
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." >/dev/null 2>&1 && pwd )" # ... https://stackoverflow.com/a/246128/176841
cd "$PROJECT_ROOT"
TYPES_DIR="./src/types"
SCHEMA_FILE="$TYPES_DIR/agent.openapi.json"
mkdir -p $TYPES_DIR
if [ ! -f $SCHEMA_FILE ] ; then
echo "$SCHEMA_FILE does not exist, re-generating it using the agent test suite"
cabal run test:tests-dc-api -- export-openapi-spec | tail -n 1 | jq > $SCHEMA_FILE
fi
echo "Deleting existing generated model..."
rm -rf "$TYPES_DIR/models"
rm -f "$TYPES_DIR/index.ts"
echo "Generating model from $SCHEMA_FILE..."
openapi --useUnionTypes --input $SCHEMA_FILE --output $TYPES_DIR --exportServices false --exportCore false --indent 2

View File

@ -1,15 +1,5 @@
import { ConfigSchemaResponse, configSchema } from "./config"
export type Relationships = {}
export type Capabilities = {
relationships: Relationships
}
export type CapabilitiesResponse = {
capabilities: Capabilities,
configSchemas: ConfigSchemaResponse,
}
import { configSchema } from "./config"
import { CapabilitiesResponse } from "./types"
export const capabilitiesResponse: CapabilitiesResponse = {
capabilities: { relationships: {} },

View File

@ -1,15 +1,10 @@
import { FastifyRequest } from "fastify"
import { SchemaObject } from "openapi3-ts"
import { ConfigSchemaResponse } from "./types"
export type Config = {
tables: String[] | null
}
export type ConfigSchemaResponse = {
configSchema: SchemaObject,
otherSchemas: { [schemaName: string]: SchemaObject },
}
export const getConfig = (request: FastifyRequest): Config => {
const configHeader = request.headers["x-hasura-dataconnector-config"];
const rawConfigJson = Array.isArray(configHeader) ? configHeader[0] : configHeader ?? "{}";

View File

@ -1,4 +1,4 @@
import { SchemaResponse, ScalarType } from "../types/schema"
import { SchemaResponse } from "../types"
import { Config } from "../config";
import xml2js from "xml2js"
import fs from "fs"
@ -43,13 +43,13 @@ const schema: SchemaResponse = {
columns: [
{
name: "ArtistId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Artist primary key identifier"
},
{
name: "Name",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The name of the artist"
}
@ -62,19 +62,19 @@ const schema: SchemaResponse = {
columns: [
{
name: "AlbumId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Album primary key identifier"
},
{
name: "Title",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The title of the album"
},
{
name: "ArtistId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "The ID of the artist that created this album"
}
@ -87,79 +87,79 @@ const schema: SchemaResponse = {
columns: [
{
name: "CustomerId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Customer primary key identifier"
},
{
name: "FirstName",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The customer's first name"
},
{
name: "LastName",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The customer's last name"
},
{
name: "Company",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's company name"
},
{
name: "Address",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's address line (street number, street)"
},
{
name: "City",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's address city"
},
{
name: "State",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's address state"
},
{
name: "Country",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's address country"
},
{
name: "PostalCode",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's address postal code"
},
{
name: "Phone",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's phone number"
},
{
name: "Fax",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The customer's fax number"
},
{
name: "Email",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The customer's email address"
},
{
name: "SupportRepId",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The ID of the Employee who is this customer's support representative"
}
@ -172,85 +172,85 @@ const schema: SchemaResponse = {
columns: [
{
name: "EmployeeId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Employee primary key identifier"
},
{
name: "FirstName",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The employee's first name"
},
{
name: "LastName",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The employee's last name"
},
{
name: "Title",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's job title"
},
{
name: "BirthDate",
type: ScalarType.String, // Ought to be DateTime but we don't have a type for this yet
type: "string", // Ought to be DateTime but we don't have a type for this yet
nullable: true,
description: "The employee's birth date"
},
{
name: "HireDate",
type: ScalarType.String, // Ought to be DateTime but we don't have a type for this yet
type: "string", // Ought to be DateTime but we don't have a type for this yet
nullable: true,
description: "The employee's birth date"
},
{
name: "Address",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's address line (street number, street)"
},
{
name: "City",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's address city"
},
{
name: "State",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's address state"
},
{
name: "Country",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's address country"
},
{
name: "PostalCode",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's address postal code"
},
{
name: "Phone",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's phone number"
},
{
name: "Fax",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The employee's fax number"
},
{
name: "Email",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The employee's email address"
},
@ -263,13 +263,13 @@ const schema: SchemaResponse = {
columns: [
{
name: "GenreId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Genre primary key identifier"
},
{
name: "Name",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The name of the genre"
}
@ -282,55 +282,55 @@ const schema: SchemaResponse = {
columns: [
{
name: "InvoiceId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Invoice primary key identifier"
},
{
name: "CustomerId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "ID of the customer who bought the music"
},
{
name: "InvoiceDate",
type: ScalarType.String, // Ought to be DateTime but we don't have a type for this yet
type: "string", // Ought to be DateTime but we don't have a type for this yet
nullable: false,
description: "Date of the invoice"
},
{
name: "BillingAddress",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The invoice's billing address line (street number, street)"
},
{
name: "BillingCity",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The invoice's billing address city"
},
{
name: "BillingState",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The invoice's billing address state"
},
{
name: "BillingCountry",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The invoice's billing address country"
},
{
name: "BillingPostalCode",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The invoice's billing address postal code"
},
{
name: "Total",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "The total amount due on the invoice"
},
@ -343,31 +343,31 @@ const schema: SchemaResponse = {
columns: [
{
name: "InvoiceLineId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Invoice Line primary key identifier"
},
{
name: "InvoiceId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "ID of the invoice the line belongs to"
},
{
name: "TrackId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "ID of the music track being purchased"
},
{
name: "UnitPrice",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Price of each individual track unit"
},
{
name: "Quantity",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Quantity of the track purchased"
},
@ -380,13 +380,13 @@ const schema: SchemaResponse = {
columns: [
{
name: "MediaTypeId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Media Type primary key identifier"
},
{
name: "Name",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The name of the media type format"
},
@ -399,13 +399,13 @@ const schema: SchemaResponse = {
columns: [
{
name: "PlaylistId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "Playlist primary key identifier"
},
{
name: "Name",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The name of the playlist"
},
@ -419,13 +419,13 @@ const schema: SchemaResponse = {
// columns: [
// {
// name: "PlaylistId",
// type: ScalarType.Number,
// type: "number",
// nullable: false,
// description: "The ID of the playlist"
// },
// {
// name: "TrackId",
// type: ScalarType.Number,
// type: "number",
// nullable: true,
// description: "The ID of the track"
// },
@ -438,55 +438,55 @@ const schema: SchemaResponse = {
columns: [
{
name: "TrackId",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "The ID of the track"
},
{
name: "Name",
type: ScalarType.String,
type: "string",
nullable: false,
description: "The name of the track"
},
{
name: "AlbumId",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The ID of the album the track belongs to"
},
{
name: "MediaTypeId",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The ID of the media type the track is encoded with"
},
{
name: "GenreId",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The ID of the genre of the track"
},
{
name: "Composer",
type: ScalarType.String,
type: "string",
nullable: true,
description: "The name of the composer of the track"
},
{
name: "Milliseconds",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "The length of the track in milliseconds"
},
{
name: "Bytes",
type: ScalarType.Number,
type: "number",
nullable: true,
description: "The size of the track in bytes"
},
{
name: "UnitPrice",
type: ScalarType.Number,
type: "number",
nullable: false,
description: "The price of the track"
},

View File

@ -1,11 +1,10 @@
import Fastify from 'fastify';
import Fastify from 'fastify';
import FastifyCors from '@fastify/cors';
import { SchemaResponse } from './types/schema';
import { ProjectedRow, QueryRequest } from './types/query';
import { filterAvailableTables, getSchema, loadStaticData } from './data';
import { queryData } from './query';
import { getConfig } from './config';
import { CapabilitiesResponse, capabilitiesResponse} from './capabilities';
import { capabilitiesResponse } from './capabilities';
import { CapabilitiesResponse, SchemaResponse, QueryRequest, QueryResponse } from './types';
const port = Number(process.env.PORT) || 8100;
const server = Fastify({ logger: { prettyPrint: true } });
@ -31,7 +30,7 @@ server.get<{ Reply: SchemaResponse }>("/schema", async (request, _response) => {
return getSchema(config);
});
server.post<{ Body: QueryRequest, Reply: ProjectedRow[] }>("/query", async (request, _response) => {
server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (request, _response) => {
server.log.info({ headers: request.headers, query: request.body, }, "query.request");
const config = getConfig(request);
const data = filterAvailableTables(staticData, config);

View File

@ -1,49 +1,56 @@
import { Expression, Fields, BinaryComparisonOperator, OrderBy, OrderType, ProjectedRow, Query, QueryResponse, RelationshipType, ScalarValue, UnaryComparisonOperator, ComparisonValue, BinaryArrayComparisonOperator, QueryRequest, TableName, ComparisonColumn, TableRelationships, Relationship, RelationshipName } from "./types/query";
import { QueryRequest, TableRelationships, Relationship, Query, Field, OrderBy, Expression, BinaryComparisonOperator, UnaryComparisonOperator, BinaryArrayComparisonOperator, ComparisonColumn, ComparisonValue, ScalarValue, QueryResponse } from "./types";
import { coerceUndefinedToNull, crossProduct, unreachable, zip } from "./util";
type StaticData = {
[tableName: string]: Record<string, ScalarValue>[]
}
type TableName = string
type RelationshipName = string
type ProjectedRow = {
[fieldName: string]: ScalarValue | ProjectedRow[] | ProjectedRow
}
const prettyPrintBinaryComparisonOperator = (operator: BinaryComparisonOperator): string => {
switch (operator) {
case BinaryComparisonOperator.GreaterThan: return ">";
case BinaryComparisonOperator.GreaterThanOrEqual: return ">=";
case BinaryComparisonOperator.LessThan: return "<";
case BinaryComparisonOperator.LessThanOrEqual: return "<=";
case BinaryComparisonOperator.Equal: return "==";
case "greater_than": return ">";
case "greater_than_or_equal": return ">=";
case "less_than": return "<";
case "less_than_or_equal": return "<=";
case "equal": return "==";
default: return unreachable(operator);
};
};
const prettyPrintBinaryArrayComparisonOperator = (operator: BinaryArrayComparisonOperator): string => {
switch (operator) {
case BinaryArrayComparisonOperator.In: return "IN";
case "in": return "IN";
default: return unreachable(operator);
};
};
const prettyPrintUnaryComparisonOperator = (operator: UnaryComparisonOperator): string => {
switch (operator) {
case UnaryComparisonOperator.IsNull: return "IS NULL";
case "is_null": return "IS NULL";
default: return unreachable(operator);
};
};
const getBinaryComparisonOperatorEvaluator = (operator: BinaryComparisonOperator): ((left: ScalarValue, right: ScalarValue) => boolean) => {
switch (operator) {
case BinaryComparisonOperator.GreaterThan: return (a, b) => a !== null && b !== null && a > b;
case BinaryComparisonOperator.GreaterThanOrEqual: return (a, b) => a !== null && b !== null && a >= b;
case BinaryComparisonOperator.LessThan: return (a, b) => a !== null && b !== null && a < b;
case BinaryComparisonOperator.LessThanOrEqual: return (a, b) => a !== null && b !== null && a <= b;
case BinaryComparisonOperator.Equal: return (a, b) => a !== null && b !== null && a === b;
case "greater_than": return (a, b) => a !== null && b !== null && a > b;
case "greater_than_or_equal": return (a, b) => a !== null && b !== null && a >= b;
case "less_than": return (a, b) => a !== null && b !== null && a < b;
case "less_than_or_equal": return (a, b) => a !== null && b !== null && a <= b;
case "equal": return (a, b) => a !== null && b !== null && a === b;
default: return unreachable(operator);
};
};
const getBinaryArrayComparisonOperatorEvaluator = (operator: BinaryArrayComparisonOperator): ((left: ScalarValue, right: ScalarValue[]) => boolean) => {
switch (operator) {
case BinaryArrayComparisonOperator.In: return (a, bs) => a !== null && bs.includes(a);
case "in": return (a, bs) => a !== null && bs.includes(a);
default: return unreachable(operator);
};
};
@ -51,7 +58,7 @@ const getBinaryArrayComparisonOperatorEvaluator = (operator: BinaryArrayComparis
const getUnaryComparisonOperatorEvaluator = (operator: UnaryComparisonOperator): ((value: ScalarValue) => boolean) => {
switch (operator) {
case UnaryComparisonOperator.IsNull: return (v) => v === null;
case "is_null": return (v) => v === null;
default: return unreachable(operator);
};
};
@ -162,7 +169,7 @@ const sortRows = (rows: Record<string, ScalarValue>[], orderBy: OrderBy[]): Reco
? -1
: 1;
return ordering === OrderType.Descending ? -compared : compared;
return ordering === "desc" ? -compared : compared;
}, 0)
);
@ -191,7 +198,7 @@ const createFilterExpressionForRelationshipJoin = (row: Record<string, ScalarVal
.map(([outerValue, innerColumnName]) => {
return {
type: "binary_op",
operator: BinaryComparisonOperator.Equal,
operator: "equal",
column: {
path: [],
name: innerColumnName,
@ -222,7 +229,7 @@ const addRelationshipFilterToQuery = (row: Record<string, ScalarValue>, relation
}
};
const buildFieldsForPathedComparisonColumn = (comparisonColumn: ComparisonColumn): Fields => {
const buildFieldsForPathedComparisonColumn = (comparisonColumn: ComparisonColumn): Record<string, Field> => {
const [relationshipName, ...remainingPath] = comparisonColumn.path;
if (relationshipName === undefined) {
return {
@ -276,7 +283,7 @@ const makeGetComparisonColumnValues = (findRelationship: (relationshipName: Rela
}
};
const projectRow = (fields: Fields, findRelationship: (relationshipName: RelationshipName) => Relationship, performQuery: (tableName: TableName, query: Query) => ProjectedRow[]) => (row: Record<string, ScalarValue>): ProjectedRow => {
const projectRow = (fields: Record<string, Field>, findRelationship: (relationshipName: RelationshipName) => Relationship, performQuery: (tableName: TableName, query: Query) => ProjectedRow[]) => (row: Record<string, ScalarValue>): ProjectedRow => {
const projectedRow: ProjectedRow = {};
for (const [fieldName, field] of Object.entries(fields)) {
@ -289,11 +296,11 @@ const projectRow = (fields: Fields, findRelationship: (relationshipName: Relatio
const relationship = findRelationship(field.relationship);
const subquery = addRelationshipFilterToQuery(row, relationship, field.query);
switch (relationship.relationship_type) {
case RelationshipType.Object:
case "object":
projectedRow[fieldName] = subquery ? coerceUndefinedToNull(performQuery(relationship.target_table, subquery)[0]) : null;
break;
case RelationshipType.Array:
case "array":
projectedRow[fieldName] = subquery ? performQuery(relationship.target_table, subquery) : [];
break;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type { AndExpression } from './models/AndExpression';
export type { AnotherColumnComparison } from './models/AnotherColumnComparison';
export type { ApplyBinaryArrayComparisonExpression } from './models/ApplyBinaryArrayComparisonExpression';
export type { ApplyBinaryComparisonOperator } from './models/ApplyBinaryComparisonOperator';
export type { ApplyUnaryComparisonOperator } from './models/ApplyUnaryComparisonOperator';
export type { BinaryArrayComparisonOperator } from './models/BinaryArrayComparisonOperator';
export type { BinaryComparisonOperator } from './models/BinaryComparisonOperator';
export type { BooleanOperators } from './models/BooleanOperators';
export type { Capabilities } from './models/Capabilities';
export type { CapabilitiesResponse } from './models/CapabilitiesResponse';
export type { ColumnField } from './models/ColumnField';
export type { ColumnInfo } from './models/ColumnInfo';
export type { ComparisonColumn } from './models/ComparisonColumn';
export type { ComparisonOperators } from './models/ComparisonOperators';
export type { ComparisonValue } from './models/ComparisonValue';
export type { ConfigSchemaResponse } from './models/ConfigSchemaResponse';
export type { Expression } from './models/Expression';
export type { Field } from './models/Field';
export type { FilteringCapabilities } from './models/FilteringCapabilities';
export type { MutationCapabilities } from './models/MutationCapabilities';
export type { NotExpression } from './models/NotExpression';
export type { OpenApiDiscriminator } from './models/OpenApiDiscriminator';
export type { OpenApiExternalDocumentation } from './models/OpenApiExternalDocumentation';
export type { OpenApiReference } from './models/OpenApiReference';
export type { OpenApiSchema } from './models/OpenApiSchema';
export type { OpenApiXml } from './models/OpenApiXml';
export type { OrderBy } from './models/OrderBy';
export type { OrderType } from './models/OrderType';
export type { OrExpression } from './models/OrExpression';
export type { Query } from './models/Query';
export type { QueryCapabilities } from './models/QueryCapabilities';
export type { QueryRequest } from './models/QueryRequest';
export type { QueryResponse } from './models/QueryResponse';
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 { ScalarType } from './models/ScalarType';
export type { ScalarValue } from './models/ScalarValue';
export type { ScalarValueComparison } from './models/ScalarValueComparison';
export type { SchemaResponse } from './models/SchemaResponse';
export type { SubscriptionCapabilities } from './models/SubscriptionCapabilities';
export type { TableInfo } from './models/TableInfo';
export type { TableRelationships } from './models/TableRelationships';
export type { UnaryComparisonOperator } from './models/UnaryComparisonOperator';

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
export type AndExpression = {
expressions: Array<Expression>;
type: 'and';
};

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ComparisonColumn } from './ComparisonColumn';
export type AnotherColumnComparison = {
column: ComparisonColumn;
type: 'column';
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BinaryArrayComparisonOperator } from './BinaryArrayComparisonOperator';
import type { ComparisonColumn } from './ComparisonColumn';
import type { ScalarValue } from './ScalarValue';
export type ApplyBinaryArrayComparisonExpression = {
column: ComparisonColumn;
operator: BinaryArrayComparisonOperator;
type: 'binary_arr_op';
values: Array<ScalarValue>;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BinaryComparisonOperator } from './BinaryComparisonOperator';
import type { ComparisonColumn } from './ComparisonColumn';
import type { ComparisonValue } from './ComparisonValue';
export type ApplyBinaryComparisonOperator = {
column: ComparisonColumn;
operator: BinaryComparisonOperator;
type: 'binary_op';
value: ComparisonValue;
};

View File

@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ComparisonColumn } from './ComparisonColumn';
import type { UnaryComparisonOperator } from './UnaryComparisonOperator';
export type ApplyUnaryComparisonOperator = {
column: ComparisonColumn;
operator: UnaryComparisonOperator;
type: 'unary_op';
};

View File

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

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type BinaryComparisonOperator = 'less_than' | 'less_than_or_equal' | 'greater_than' | 'greater_than_or_equal' | 'equal';

View File

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

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { FilteringCapabilities } from './FilteringCapabilities';
import type { MutationCapabilities } from './MutationCapabilities';
import type { QueryCapabilities } from './QueryCapabilities';
import type { RelationshipCapabilities } from './RelationshipCapabilities';
import type { SubscriptionCapabilities } from './SubscriptionCapabilities';
export type Capabilities = {
filtering?: FilteringCapabilities;
mutations?: MutationCapabilities;
queries?: QueryCapabilities;
relationships?: RelationshipCapabilities;
subscriptions?: SubscriptionCapabilities;
};

View File

@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Capabilities } from './Capabilities';
import type { ConfigSchemaResponse } from './ConfigSchemaResponse';
export type CapabilitiesResponse = {
capabilities: Capabilities;
configSchemas: ConfigSchemaResponse;
};

View File

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ColumnField = {
column: string;
type: 'column';
};

View File

@ -0,0 +1,22 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScalarType } from './ScalarType';
export type ColumnInfo = {
/**
* Column description
*/
description?: string | null;
/**
* Column name
*/
name: string;
/**
* Is column nullable
*/
nullable: boolean;
type: ScalarType;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ComparisonColumn = {
/**
* The name of the column
*/
name: string;
/**
* The relationship path from the current query table to the table that contains the specified column. Empty array means the current query table.
*/
path: Array<string>;
};

View File

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

View File

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

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { OpenApiSchema } from './OpenApiSchema';
export type ConfigSchemaResponse = {
configSchema: OpenApiSchema;
otherSchemas: Record<string, OpenApiSchema>;
};

View File

@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AndExpression } from './AndExpression';
import type { ApplyBinaryArrayComparisonExpression } from './ApplyBinaryArrayComparisonExpression';
import type { ApplyBinaryComparisonOperator } from './ApplyBinaryComparisonOperator';
import type { ApplyUnaryComparisonOperator } from './ApplyUnaryComparisonOperator';
import type { NotExpression } from './NotExpression';
import type { OrExpression } from './OrExpression';
export type Expression = (AndExpression | OrExpression | NotExpression | ApplyBinaryComparisonOperator | ApplyBinaryArrayComparisonExpression | ApplyUnaryComparisonOperator);

View File

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

View File

@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BooleanOperators } from './BooleanOperators';
import type { ComparisonOperators } from './ComparisonOperators';
export type FilteringCapabilities = {
booleanOperators: BooleanOperators;
comparisonOperators: ComparisonOperators;
};

View File

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

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
export type NotExpression = {
expression: Expression;
type: 'not';
};

View File

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OpenApiDiscriminator = {
mapping?: Record<string, string>;
propertyName: string;
};

View File

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OpenApiExternalDocumentation = {
description?: string;
url: string;
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OpenApiReference = {
$ref: string;
};

View File

@ -0,0 +1,47 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { OpenApiDiscriminator } from './OpenApiDiscriminator';
import type { OpenApiExternalDocumentation } from './OpenApiExternalDocumentation';
import type { OpenApiReference } from './OpenApiReference';
import type { OpenApiXml } from './OpenApiXml';
export type OpenApiSchema = {
additionalProperties?: any;
allOf?: Array<(OpenApiSchema | OpenApiReference)>;
anyOf?: Array<(OpenApiSchema | OpenApiReference)>;
default?: any;
deprecated?: boolean;
description?: string;
discriminator?: OpenApiDiscriminator;
enum?: Array<any>;
example?: any;
exclusiveMaximum?: boolean;
exclusiveMinimum?: boolean;
externalDocs?: OpenApiExternalDocumentation;
format?: string;
items?: (OpenApiSchema | OpenApiReference);
maxItems?: number;
maxLength?: number;
maxProperties?: number;
maximum?: number;
minItems?: number;
minLength?: number;
minProperties?: number;
minimum?: number;
multipleOf?: number;
not?: (OpenApiSchema | OpenApiReference);
nullable?: boolean;
oneOf?: Array<(OpenApiSchema | OpenApiReference)>;
pattern?: string;
properties?: Record<string, (OpenApiSchema | OpenApiReference)>;
readOnly?: boolean;
required?: Array<string>;
title?: string;
type?: 'array' | 'boolean' | 'integer' | 'number' | 'object' | 'string';
uniqueItems?: boolean;
writeOnly?: boolean;
xml?: OpenApiXml;
};

View File

@ -0,0 +1,12 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OpenApiXml = {
attribute?: boolean;
name?: string;
namespace?: string;
prefix?: string;
wrapped?: boolean;
};

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
export type OrExpression = {
expressions: Array<Expression>;
type: 'or';
};

View File

@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { OrderType } from './OrderType';
export type OrderBy = {
/**
* Column to order by
*/
column: string;
ordering: OrderType;
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OrderType = 'asc' | 'desc';

View File

@ -0,0 +1,28 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
import type { Field } from './Field';
import type { OrderBy } from './OrderBy';
export type Query = {
/**
* Fields of the query
*/
fields: Record<string, Field>;
/**
* Optionally limit to N results
*/
limit?: number | null;
/**
* Optionally offset from the Nth result
*/
offset?: number | null;
/**
* Optionally order the results by the value of one or more fields
*/
order_by?: Array<OrderBy> | null;
where?: Expression;
};

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type QueryCapabilities = {
/**
* Does the agent support querying a table by primary key?
*/
supportsPrimaryKeys: boolean;
};

View File

@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Query } from './Query';
import type { TableRelationships } from './TableRelationships';
export type QueryRequest = {
query: Query;
/**
* The name of the table to query
*/
table: string;
/**
* The relationships between tables involved in the entire query request
*/
table_relationships: Array<TableRelationships>;
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type QueryResponse = Array<Record<string, any>>;

View File

@ -0,0 +1,18 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RelationshipType } from './RelationshipType';
export type Relationship = {
/**
* A mapping between columns on the source table to columns on the target table
*/
column_mapping: Record<string, string>;
relationship_type: RelationshipType;
/**
* The name of the target table in the relationship
*/
target_table: string;
};

View File

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

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Query } from './Query';
export type RelationshipField = {
query: Query;
/**
* The name of the relationship to follow for the subquery
*/
relationship: string;
type: 'relationship';
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RelationshipType = 'object' | 'array';

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ScalarType = 'string' | 'number' | 'bool';

View File

@ -0,0 +1,6 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ScalarValue = (string | number | boolean | null);

View File

@ -0,0 +1,11 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ScalarValue } from './ScalarValue';
export type ScalarValueComparison = {
type: 'scalar';
value: ScalarValue;
};

View File

@ -0,0 +1,13 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TableInfo } from './TableInfo';
export type SchemaResponse = {
/**
* Available tables
*/
tables: Array<TableInfo>;
};

View File

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

View File

@ -0,0 +1,25 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ColumnInfo } from './ColumnInfo';
export type TableInfo = {
/**
* The columns of the table
*/
columns: Array<ColumnInfo>;
/**
* Description of the table
*/
description?: string | null;
/**
* The name of the table
*/
name: string;
/**
* The primary key of the table
*/
primary_key?: string | null;
};

View File

@ -0,0 +1,17 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Relationship } from './Relationship';
export type TableRelationships = {
/**
* A map of relationships from the source table to target tables. The key of the map is the relationship name
*/
relationships: Record<string, Relationship>;
/**
* The name of the source table in the relationship
*/
source_table: string;
};

View File

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

View File

@ -1,148 +0,0 @@
export type QueryRequest = {
table: TableName,
table_relationships: TableRelationships[],
query: Query,
}
export type TableName = string
export type TableRelationships = {
source_table: TableName,
relationships: { [relationshipName: RelationshipName]: Relationship }
}
export type Relationship = {
target_table: TableName,
relationship_type: RelationshipType,
column_mapping: { [source: SourceColumnName]: TargetColumnName },
}
export type SourceColumnName = ColumnName
export type TargetColumnName = ColumnName
export type RelationshipName = string
export enum RelationshipType {
Object = "object",
Array = "array"
}
export type Query = {
fields: Fields,
limit?: number | null,
offset?: number | null,
where?: Expression | null,
order_by?: OrderBy[],
}
export type Fields = { [fieldName: string]: Field }
export type Field = ColumnField | RelationshipField
export type ColumnName = string
export type ColumnField = {
type: "column",
column: ColumnName,
}
export type RelationshipField = {
type: "relationship",
relationship: RelationshipName
query: Query,
}
export type ScalarValue = string | number | boolean | null
export type ComparisonColumn = {
path: RelationshipName[],
name: ColumnName,
}
export type ComparisonValue =
| AnotherColumnComparisonValue
| ScalarComparisonValue
export type AnotherColumnComparisonValue = {
type: "column",
column: ComparisonColumn,
}
export type ScalarComparisonValue = {
type: "scalar",
value: ScalarValue,
}
export type Expression =
| AndExpression
| OrExpression
| NotExpression
| ApplyBinaryComparisonOperatorExpression
| ApplyBinaryArrayComparisonOperatorExpression
| ApplyUnaryComparisonOperatorExpression
export type AndExpression = {
type: "and",
expressions: Expression[],
}
export type OrExpression = {
type: "or",
expressions: Expression[],
}
export type NotExpression = {
type: "not",
expression: Expression,
}
export type ApplyBinaryComparisonOperatorExpression = {
type: "binary_op",
operator: BinaryComparisonOperator,
column: ComparisonColumn,
value: ComparisonValue,
}
export type ApplyBinaryArrayComparisonOperatorExpression = {
type: "binary_arr_op",
operator: BinaryArrayComparisonOperator,
column: ComparisonColumn,
values: ScalarValue[],
}
export type ApplyUnaryComparisonOperatorExpression = {
type: "unary_op",
operator: UnaryComparisonOperator,
column: ComparisonColumn,
}
export enum BinaryComparisonOperator {
LessThan = "less_than",
LessThanOrEqual = "less_than_or_equal",
GreaterThan = "greater_than",
GreaterThanOrEqual = "greater_than_or_equal",
Equal = "equal",
}
export enum BinaryArrayComparisonOperator {
In = "in",
}
export enum UnaryComparisonOperator {
IsNull = "is_null",
}
export type OrderBy = {
column: ColumnName,
ordering: OrderType,
}
export enum OrderType {
Ascending = "asc",
Descending = "desc",
}
export type QueryResponse = ProjectedRow[]
export type ProjectedRow = {
[fieldName: string]: ScalarValue | ProjectedRow[] | ProjectedRow
}

View File

@ -1,23 +0,0 @@
export type SchemaResponse = {
tables: Table[],
}
export type Table = {
name: string,
columns: ColumnInfo[],
primary_key?: string | null,
description?: string | null
}
export type ColumnInfo = {
name: string,
type: ScalarType,
nullable: boolean,
description?: string | null
}
export enum ScalarType {
String = "string",
Number = "number",
Boolean = "bool"
}

View File

@ -1,7 +1,6 @@
export const coerceUndefinedToNull = <T>(v: T | undefined): T | null => v === undefined ? null : v;
export const unreachable = (x: never): never => { throw new Error(`Unreachable code reached! The types lied! 😭 Unexpected value: ${x}`) };
;
export const zip = <T, U>(arr1: T[], arr2: U[]): [T, U][] => {
const length = Math.min(arr1.length, arr2.length);

View File

@ -7,13 +7,12 @@ module Hasura.Backends.DataConnector.API
ConfigHeader,
SourceNameHeader,
SourceName,
openApiSchemaJson,
openApiSchema,
Routes (..),
apiClient,
)
where
import Data.Aeson qualified as J
import Data.Data (Proxy (..))
import Data.OpenApi (OpenApi)
import Data.Text (Text)
@ -22,7 +21,6 @@ import Servant.API
import Servant.API.Generic
import Servant.Client (Client, ClientM, client)
import Servant.OpenApi
import Prelude
--------------------------------------------------------------------------------
-- Servant Routes
@ -68,13 +66,6 @@ type Api = CapabilitiesApi :<|> SchemaApi :<|> QueryApi
openApiSchema :: OpenApi
openApiSchema = toOpenApi (Proxy @Api)
-- | The OpenAPI 3.0 schema for the API
--
-- This is not exposed as the 'OpenApi' type because we need to do some hackery in
-- the serialized JSON to work around some limitations in the openapi3 library
openApiSchemaJson :: J.Value
openApiSchemaJson = V0.fixExternalSchemaRefsInComponentSchemas $ J.toJSON openApiSchema
apiClient :: Client ClientM (NamedRoutes Routes)
apiClient =
client (Proxy @(NamedRoutes Routes))

View File

@ -22,7 +22,7 @@ import Data.Aeson (FromJSON, ToJSON)
import Data.Data (Data, Proxy (..))
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Hashable (Hashable)
import Data.OpenApi (NamedSchema (..), OpenApiType (OpenApiObject), Referenced (..), Schema (..), ToSchema (..))
import Data.OpenApi (NamedSchema (..), OpenApiType (OpenApiObject), Schema (..), ToSchema (..), declareSchemaRef)
import GHC.Generics (Generic)
import Hasura.Backends.DataConnector.API.V0.ConfigSchema (ConfigSchemaResponse)
import Prelude
@ -137,8 +137,8 @@ instance HasCodec CapabilitiesResponse where
instance ToSchema CapabilitiesResponse where
declareNamedSchema _ = do
capabilitiesSchema <- declareNamedSchema (Proxy @Capabilities)
configSchemasSchema <- declareNamedSchema (Proxy @ConfigSchemaResponse)
capabilitiesSchemaRef <- declareSchemaRef (Proxy @Capabilities)
configSchemasSchemaRef <- declareSchemaRef (Proxy @ConfigSchemaResponse)
let schema =
mempty
{ _schemaType = Just OpenApiObject,
@ -146,8 +146,8 @@ instance ToSchema CapabilitiesResponse where
_schemaRequired = ["capabilities", "configSchemas"],
_schemaProperties =
InsOrdHashMap.fromList
[ ("capabilities", Inline $ _namedSchemaSchema capabilitiesSchema),
("configSchemas", Inline $ _namedSchemaSchema configSchemasSchema)
[ ("capabilities", capabilitiesSchemaRef),
("configSchemas", configSchemasSchemaRef)
]
}

View File

@ -4,8 +4,6 @@ module Hasura.Backends.DataConnector.API.V0.ConfigSchema
( Config (..),
ConfigSchemaResponse (..),
validateConfigAgainstConfigSchema,
fixExternalSchemaRefsInComponentSchemas,
fixExternalSchemaRefsInSchema,
)
where
@ -20,8 +18,9 @@ import Data.ByteString.Lazy qualified as BSL
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
import Data.Hashable (Hashable)
import Data.Maybe (fromMaybe)
import Data.OpenApi (AdditionalProperties (..), Definitions, NamedSchema (..), OpenApiType (..), Reference (..), Referenced (..), Schema (..), ToParamSchema (..), ToSchema (..), ValidationError)
import Data.OpenApi (AdditionalProperties (..), Definitions, NamedSchema (..), OpenApiItems (..), OpenApiType (..), Reference (..), Referenced (..), Schema (..), ToParamSchema (..), ToSchema (..), ValidationError)
import Data.OpenApi qualified as OpenApi
import Data.OpenApi.Declare (Declare, MonadDeclare (..))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
@ -75,33 +74,184 @@ instance Autodocodec.HasCodec ConfigSchemaResponse where
codec = Autodocodec.codecViaAeson "Configuration schemas"
instance ToSchema ConfigSchemaResponse where
declareNamedSchema _ =
pure $ NamedSchema (Just "ConfigSchemaResponse") schema
where
schema :: Schema
schema =
declareNamedSchema _ = do
openApiSchemaRef <- declareOpenApiSchema
let otherSchemasSchema =
mempty
{ _schemaType = Just OpenApiObject,
_schemaNullable = Just False,
_schemaAdditionalProperties = Just $ AdditionalPropertiesSchema openApiSchemaRef
}
let schema =
mempty
{ _schemaType = Just OpenApiObject,
_schemaNullable = Just False,
_schemaRequired = ["configSchema", "otherSchemas"],
_schemaProperties =
InsOrdHashMap.fromList
[ ("configSchema", openApiSchemaSchema),
[ ("configSchema", openApiSchemaRef),
("otherSchemas", Inline otherSchemasSchema)
]
}
pure $ NamedSchema (Just "ConfigSchemaResponse") schema
otherSchemasSchema :: Schema
otherSchemasSchema =
-- | Declares the schema for the OpenAPI Schema type (and its dependent types) and
-- returns a reference that can be used to refer to it from other schemas.
--
-- This is a transcription of the schemas defined here:
-- https://raw.githubusercontent.com/OAI/OpenAPI-Specification/80c781e479f85ac67001ceb3e7e410e25d2a561b/schemas/v3.0/schema.json#/definitions/Schema
--
-- Unfortunately using external references to the above schema tends to make many
-- OpenAPI type generators choke, so importing the relevant schemas into our spec
-- is a pragmatic workaround.
declareOpenApiSchema :: Declare (Definitions Schema) (Referenced Schema)
declareOpenApiSchema = do
declare $
InsOrdHashMap.fromList
[ openApiSchema,
openApiReference,
openApiDiscriminator,
openApiExternalDocumentation,
openApiXml
]
pure . Ref $ Reference "OpenApiSchema"
where
openApiSchema :: (Text, Schema)
openApiSchema =
("OpenApiSchema",)
mempty
{ _schemaType = Just OpenApiObject,
_schemaNullable = Just False,
_schemaAdditionalProperties = Just $ AdditionalPropertiesSchema openApiSchemaSchema
_schemaProperties =
InsOrdHashMap.fromList
[ ("title", Inline mempty {_schemaType = Just OpenApiString}),
("multipleOf", Inline mempty {_schemaType = Just OpenApiNumber, _schemaMinimum = Just 0, _schemaExclusiveMinimum = Just True}),
("maximum", Inline mempty {_schemaType = Just OpenApiNumber}),
("exclusiveMaximum", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("minimum", Inline mempty {_schemaType = Just OpenApiNumber}),
("exclusiveMinimum", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("maxLength", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0}),
("minLength", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0, _schemaDefault = Just $ Number 0}),
("pattern", Inline mempty {_schemaType = Just OpenApiString, _schemaFormat = Just "regex"}),
("maxItems", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0}),
("minItems", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0, _schemaDefault = Just $ Number 0}),
("uniqueItems", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("maxProperties", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0}),
("minProperties", Inline mempty {_schemaType = Just OpenApiInteger, _schemaMinimum = Just 0, _schemaDefault = Just $ Number 0}),
( "required",
Inline
mempty
{ _schemaType = Just OpenApiArray,
_schemaItems = Just . OpenApiItemsObject $ Inline mempty {_schemaType = Just OpenApiString},
_schemaMinItems = Just 1,
_schemaUniqueItems = Just True
}
),
( "enum",
Inline
mempty
{ _schemaType = Just OpenApiArray,
_schemaItems = Just . OpenApiItemsObject $ Inline mempty,
_schemaMinItems = Just 1,
_schemaUniqueItems = Just False
}
),
("type", Inline mempty {_schemaType = Just OpenApiString, _schemaEnum = Just ["array", "boolean", "integer", "number", "object", "string"]}),
("not", Inline mempty {_schemaOneOf = Just schemaOrReference}),
("allOf", Inline mempty {_schemaType = Just OpenApiArray, _schemaItems = Just . OpenApiItemsObject $ Inline mempty {_schemaOneOf = Just schemaOrReference}}),
("oneOf", Inline mempty {_schemaType = Just OpenApiArray, _schemaItems = Just . OpenApiItemsObject $ Inline mempty {_schemaOneOf = Just schemaOrReference}}),
("anyOf", Inline mempty {_schemaType = Just OpenApiArray, _schemaItems = Just . OpenApiItemsObject $ Inline mempty {_schemaOneOf = Just schemaOrReference}}),
("items", Inline mempty {_schemaOneOf = Just schemaOrReference}),
("properties", Inline mempty {_schemaType = Just OpenApiObject, _schemaAdditionalProperties = Just . AdditionalPropertiesSchema $ Inline mempty {_schemaOneOf = Just schemaOrReference}}),
( "additionalProperties",
Inline
mempty
{ _schemaAdditionalProperties = Just . AdditionalPropertiesSchema $ Inline mempty {_schemaOneOf = Just $ schemaOrReference <> [Inline mempty {_schemaType = Just OpenApiBoolean}]},
_schemaDefault = Just $ Bool True
}
),
("description", Inline mempty {_schemaType = Just OpenApiString}),
("format", Inline mempty {_schemaType = Just OpenApiString}),
("default", Inline mempty),
("nullable", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("discriminator", Ref . Reference $ fst openApiDiscriminator),
("readOnly", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("writeOnly", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("example", Inline mempty),
("externalDocs", Ref . Reference $ fst openApiExternalDocumentation),
("deprecated", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("xml", Ref . Reference $ fst openApiXml)
],
-- Note: Technically OpenAPI schemas should be able to define extension properties but since OpenAPI itself doesn't
-- support defining patternProperties, I can't define them here. 😢
-- "patternProperties": { "^x-": {} }
-- _schemaPatternProperties =
_schemaAdditionalProperties = Just $ AdditionalPropertiesAllowed False
}
openApiSchemaSchema :: Referenced Schema
openApiSchemaSchema =
Ref (Reference "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/80c781e479f85ac67001ceb3e7e410e25d2a561b/schemas/v3.0/schema.json#/definitions/Schema")
openApiReference :: (Text, Schema)
openApiReference =
("OpenApiReference",)
mempty
{ _schemaType = Just OpenApiObject,
_schemaRequired = ["$ref"],
-- Note: This is technically defined using "patternProperties" with the property name regex ^\$ref$
-- but OpenAPI doesn't support patternProperties ironically, so this is close enough
_schemaProperties = InsOrdHashMap.fromList [("$ref", Inline mempty {_schemaType = Just OpenApiString, _schemaFormat = Just "uri-reference"})]
}
schemaOrReference :: [Referenced Schema]
schemaOrReference = [Ref . Reference $ fst openApiSchema, Ref . Reference $ fst openApiReference]
openApiDiscriminator :: (Text, Schema)
openApiDiscriminator =
("OpenApiDiscriminator",)
mempty
{ _schemaType = Just OpenApiObject,
_schemaRequired = ["propertyName"],
_schemaProperties =
InsOrdHashMap.fromList
[ ("propertyName", Inline mempty {_schemaType = Just OpenApiString}),
("mapping", Inline mempty {_schemaType = Just OpenApiObject, _schemaAdditionalProperties = Just . AdditionalPropertiesSchema $ Inline mempty {_schemaType = Just OpenApiString}})
]
}
openApiExternalDocumentation :: (Text, Schema)
openApiExternalDocumentation =
("OpenApiExternalDocumentation",)
mempty
{ _schemaType = Just OpenApiObject,
_schemaRequired = ["url"],
_schemaProperties =
InsOrdHashMap.fromList
[ ("description", Inline mempty {_schemaType = Just OpenApiString}),
("url", Inline mempty {_schemaType = Just OpenApiString, _schemaFormat = Just "uri-reference"})
],
-- Note: Technically external docs should be able to define extension properties but since OpenAPI itself doesn't
-- support defining patternProperties, I can't define them here. 😢
-- "patternProperties": { "^x-": {} }
-- _schemaPatternProperties =
_schemaAdditionalProperties = Just $ AdditionalPropertiesAllowed False
}
openApiXml :: (Text, Schema)
openApiXml =
("OpenApiXml",)
mempty
{ _schemaType = Just OpenApiObject,
_schemaProperties =
InsOrdHashMap.fromList
[ ("name", Inline mempty {_schemaType = Just OpenApiString}),
("namespace", Inline mempty {_schemaType = Just OpenApiString, _schemaFormat = Just "uri"}),
("prefix", Inline mempty {_schemaType = Just OpenApiString}),
("attribute", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False}),
("wrapped", Inline mempty {_schemaType = Just OpenApiBoolean, _schemaDefault = Just $ Bool False})
],
-- Note: Technically XML should be able to define extension properties but since OpenAPI itself doesn't
-- support defining patternProperties, I can't define them here. 😢
-- "patternProperties": { "^x-": {} }
-- _schemaPatternProperties =
_schemaAdditionalProperties = Just $ AdditionalPropertiesAllowed False
}
-- | Rewrites the config schema internal refs to the form that openapi3 expects when it deserialized them
--
@ -157,26 +307,3 @@ rewriteSchemaRefs rewriteRefText schemaObj =
validateConfigAgainstConfigSchema :: ConfigSchemaResponse -> Config -> [ValidationError]
validateConfigAgainstConfigSchema ConfigSchemaResponse {..} (Config config) =
OpenApi.validateJSON _csrOtherSchemas _csrConfigSchema (Object config)
-- | Fixes any refs in schemas that are external refs to an http-based URL.
-- Note that this is limited to schemas in the components/schemas section.
-- This is used to specifically address the external refs defined by the
-- OpenAPI schema spec of the 'ConfigSchemaResponse' type.
--
-- This works around a limitation in the openapi3 library where it does not
-- understand the concept of external refs and will always assume any defined
-- ref refers to a schema inside the top level OpenApi document itself.
-- Practically, this means that #/components/schemas/ gets mashed onto the
-- front of any external ref :(
fixExternalSchemaRefsInComponentSchemas :: Value -> Value
fixExternalSchemaRefsInComponentSchemas openApiObj =
openApiObj
& key "components" . key "schemas" . members %~ fixExternalSchemaRefsInSchema
fixExternalSchemaRefsInSchema :: Value -> Value
fixExternalSchemaRefsInSchema = rewriteSchemaRefs fixExternalHttpSchemaRef
fixExternalHttpSchemaRef :: Text -> Text
fixExternalHttpSchemaRef = \case
(Text.stripPrefix "#/components/schemas/http" -> Just suffix) -> "http" <> suffix
other -> other

View File

@ -113,7 +113,10 @@ deriving via Autodocodec Field instance ToSchema Field
-- endpoint encoded as a list of JSON objects.
newtype QueryResponse = QueryResponse {getQueryResponse :: [Object]}
deriving newtype (Eq, Ord, Show, NFData)
deriving (ToJSON, FromJSON, ToSchema) via Autodocodec [Object]
deriving (ToJSON, FromJSON, ToSchema) via Autodocodec QueryResponse
instance HasCodec QueryResponse where
codec = named "QueryResponse" $ dimapCodec QueryResponse getQueryResponse codec
$(makeLenses ''QueryRequest)
$(makeLenses ''Query)

View File

@ -31,5 +31,5 @@ data Type
instance HasCodec Type where
codec =
named "Type" $
named "ScalarType" $
disjointStringConstCodec [(StringTy, "string"), (NumberTy, "number"), (BoolTy, "bool")]

View File

@ -42,7 +42,7 @@ import Data.Text.Extended
import Data.Text.Lazy qualified as LT
import Data.Text.Lazy.Encoding qualified as TL
import GHC.Stats.Extended qualified as RTS
import Hasura.Backends.DataConnector.API (openApiSchemaJson)
import Hasura.Backends.DataConnector.API (openApiSchema)
import Hasura.Backends.Postgres.Execute.Types
import Hasura.Base.Error
import Hasura.EncJSON
@ -1078,7 +1078,7 @@ httpApp setupHook corsCfg serverCtx enableConsole consoleAssetsDir enableTelemet
spockAction encodeQErr id $
mkGetHandler $ do
onlyAdmin
return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue openApiSchemaJson) [])
return (emptyHttpLogMetadata @m, JSONResp $ HttpResponse (encJFromJValue openApiSchema) [])
Spock.get "api/swagger/json" $
spockAction encodeQErr id $
mkGetHandler $ do

View File

@ -65,8 +65,8 @@ spec = do
|]
testToFromJSON val jsonVal
it "produces the correct OpenAPI Spec once external schema refs are fixed up" $
fixExternalSchemaRefsInSchema (toJSON $ toSchema (Proxy @ConfigSchemaResponse))
it "OpenAPI spec is as expected" $
toJSON (toSchema (Proxy @ConfigSchemaResponse))
`shouldBe` [aesonQQ|
{
"required": [
@ -77,11 +77,11 @@ spec = do
"nullable": false,
"properties": {
"configSchema": {
"$ref": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/80c781e479f85ac67001ceb3e7e410e25d2a561b/schemas/v3.0/schema.json#/definitions/Schema"
"$ref": "#/components/schemas/OpenApiSchema"
},
"otherSchemas": {
"additionalProperties": {
"$ref": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/80c781e479f85ac67001ceb3e7e410e25d2a561b/schemas/v3.0/schema.json#/definitions/Schema"
"$ref": "#/components/schemas/OpenApiSchema"
},
"type": "object",
"nullable": false

View File

@ -6,7 +6,7 @@ import Control.Monad ((>=>))
import Data.Aeson.Text (encodeToLazyText)
import Data.Proxy (Proxy (..))
import Data.Text.Lazy.IO qualified as Text
import Hasura.Backends.DataConnector.API (Routes (..), apiClient, openApiSchemaJson)
import Hasura.Backends.DataConnector.API (Routes (..), apiClient, openApiSchema)
import Hasura.Backends.DataConnector.API qualified as API
import Network.HTTP.Client (defaultManagerSettings, newManager)
import Servant.API (NamedRoutes)
@ -38,7 +38,7 @@ main = do
agentCapabilities <- getAgentCapabilities api _toAgentCapabilities
runSpec (tests api testSourceName _toAgentConfig agentCapabilities) (applyTestConfig defaultConfig testOptions) >>= evaluateSummary
ExportOpenAPISpec ->
Text.putStrLn $ encodeToLazyText openApiSchemaJson
Text.putStrLn $ encodeToLazyText openApiSchema
pure ()

View File

@ -27,6 +27,7 @@ import Data.Aeson qualified as Aeson
import Data.IORef qualified as I
import Harness.Backend.DataConnector.MockAgent
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Http (healthCheck)
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
import Harness.Test.Context (BackendType (DataConnector), Options, defaultBackendTypeString)
import Harness.TestEnvironment (TestEnvironment)
@ -48,10 +49,11 @@ dataconnector:
mockBackendConfig :: Aeson.Value
mockBackendConfig =
let backendType = defaultBackendTypeString $ DataConnector
agentUri = "http://127.0.0.1:" <> show mockAgentPort <> "/"
in [yaml|
dataconnector:
*backendType:
uri: "http://127.0.0.1:65006/"
uri: *agentUri
|]
--------------------------------------------------------------------------------
@ -114,6 +116,7 @@ mkLocalTestEnvironmentMock _ = do
maeConfig <- I.newIORef chinookMock
maeQuery <- I.newIORef Nothing
maeThreadId <- forkIO $ runMockServer maeConfig maeQuery
healthCheck $ "http://127.0.0.1:" <> show mockAgentPort <> "/healthz"
pure $ MockAgentEnvironment {..}
-- | Load the agent schema into HGE.

View File

@ -1,6 +1,7 @@
module Harness.Backend.DataConnector.MockAgent
( MockConfig (..),
chinookMock,
mockAgentPort,
runMockServer,
)
where
@ -220,12 +221,22 @@ mockQueryHandler :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> AP
mockQueryHandler mcfg mquery _sourceName _cfg query = liftIO $ do
handler <- fmap _queryResponse $ I.readIORef mcfg
I.writeIORef mquery (Just query)
pure $ handler (error "WTF DUDE")
pure $ handler query
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server API.Api
dcMockableServer mcfg mquery = mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery
type HealthcheckApi =
"healthz"
:> Get '[JSON] ()
healthcheckHandler :: Handler ()
healthcheckHandler = pure ()
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> Server (API.Api :<|> HealthcheckApi)
dcMockableServer mcfg mquery = (mockCapabilitiesHandler mcfg :<|> mockSchemaHandler mcfg :<|> mockQueryHandler mcfg mquery) :<|> healthcheckHandler
mockAgentPort :: Warp.Port
mockAgentPort = 65006
runMockServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> IO ()
runMockServer mcfg mquery = do
let app = serve (Proxy :: Proxy API.Api) $ dcMockableServer mcfg mquery
Warp.run 65006 app
let app = serve (Proxy :: Proxy (API.Api :<|> HealthcheckApi)) $ dcMockableServer mcfg mquery
Warp.run mockAgentPort app