server: Support the namespacing of table names in Data Connectors

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5329
GitOrigin-RevId: 5cf492bc2b09fef6250f4dd50f74f750f55ebe6a
This commit is contained in:
Daniel Chambers 2022-08-04 18:34:45 +10:00 committed by hasura-bot
parent bbafd0339c
commit c209b60239
34 changed files with 385 additions and 272 deletions

View File

@ -40,13 +40,13 @@ POST /v1/metadata
"kind": "reference",
"tables": [
{
"table": "Album",
"table": ["Album"],
"object_relationships": [
{
"name": "Artist",
"using": {
"manual_configuration": {
"remote_table": "Artist",
"remote_table": ["Artist"],
"column_mapping": {
"ArtistId": "ArtistId"
}
@ -56,13 +56,13 @@ POST /v1/metadata
]
},
{
"table": "Artist",
"table": ["Artist"],
"array_relationships": [
{
"name": "Album",
"using": {
"manual_configuration": {
"remote_table": "Album",
"remote_table": ["Album"],
"column_mapping": {
"ArtistId": "ArtistId"
}
@ -73,7 +73,9 @@ POST /v1/metadata
}
],
"configuration": {
"tables": [ "Artist", "Album" ]
"value": {
"tables": [ "Artist", "Album" ]
}
}
}
]
@ -165,7 +167,7 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
{
"tables": [
{
"name": "Artist",
"name": ["Artist"],
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
@ -184,7 +186,7 @@ The `GET /schema` endpoint is called whenever the metadata is (re)loaded by `gra
]
},
{
"name": "Album",
"name": ["Album"],
"primary_key": ["AlbumId"],
"description": "Collection of music albums created by artists",
"columns": [
@ -216,6 +218,8 @@ The `tables` section describes the two available tables, as well as their column
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"]`.
#### 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.
@ -239,7 +243,7 @@ and here is the resulting query request payload:
```json
{
"table": "Artist",
"table": ["Artist"],
"table_relationships": [],
"query": {
"where": {
@ -267,7 +271,7 @@ The implementation of the service is responsible for intepreting this data struc
Let's break down the request:
- The `table` field tells us which table to fetch the data from, namely the `Artist` table.
- The `table` field tells us which table to fetch the data from, namely the `Artist` table. The table name (ie. the array of strings) must be one that was returned previously by the `/schema` endpoint.
- The `table_relationships` field that lists any relationships used to join between tables in the query. This query does not use any relationships, so this is just an empty list here.
- The `query` field contains further information about how to query the specified table:
- The `where` field tells us that there is currently no (interesting) predicate being applied to the rows of the data set (just an empty conjunction, which ought to return every row).
@ -458,13 +462,13 @@ This will generate the following JSON query if the agent supports relationships:
```json
{
"table": "Artist",
"table": ["Artist"],
"table_relationships": [
{
"source_table": "Artist",
"source_table": ["Artist"],
"relationships": {
"ArtistAlbums": {
"target_table": "Album",
"target_table": ["Album"],
"relationship_type": "array",
"column_mapping": {
"ArtistId": "ArtistId"
@ -491,7 +495,6 @@ This will generate the following JSON query if the agent supports relationships:
"type": "and"
},
"offset": null,
"from": "albums",
"order_by": [],
"limit": null,
"fields": {
@ -523,7 +526,6 @@ Note the `Albums` field in particular, which traverses the `Artists` -> `Albums`
"type": "and"
},
"offset": null,
"from": "albums",
"order_by": [],
"limit": null,
"fields": {
@ -602,13 +604,13 @@ POST /v1/metadata
"kind": "reference",
"tables": [
{
"table": "Customer",
"table": ["Customer"],
"object_relationships": [
{
"name": "SupportRep",
"using": {
"manual_configuration": {
"remote_table": "Employee",
"remote_table": ["Employee"],
"column_mapping": {
"SupportRepId": "EmployeeId"
}
@ -639,7 +641,7 @@ POST /v1/metadata
]
},
{
"table": "Employee"
"table": ["Employee"]
}
],
"configuration": {}
@ -668,13 +670,13 @@ We would get the following query request JSON:
```json
{
"table": "Customer",
"table": ["Customer"],
"table_relationships": [
{
"source_table": "Customer",
"source_table": ["Customer"],
"relationships": {
"SupportRep": {
"target_table": "Employee",
"target_table": ["Employee"],
"relationship_type": "object",
"column_mapping": {
"SupportRepId": "EmployeeId"
@ -753,7 +755,7 @@ This would cause the following query request to be performed:
```json
{
"table": "Artist",
"table": ["Artist"],
"table_relationships": [],
"query": {
"aggregates": {
@ -796,7 +798,7 @@ query {
```json
{
"table": "Album",
"table": ["Album"],
"table_relationships": [],
"query": {
"aggregates": {
@ -848,7 +850,7 @@ The `nodes` part of the query ends up as standard `fields` in the `Query`, and t
```json
{
"table": "Artist",
"table": ["Artist"],
"table_relationships": [],
"query": {
"aggregates": {
@ -917,13 +919,13 @@ This would generate the following `QueryRequest`:
```json
{
"table": "Artist",
"table": ["Artist"],
"table_relationships": [
{
"source_table": "Artist",
"source_table": ["Artist"],
"relationships": {
"Albums": {
"target_table": "Album",
"target_table": ["Album"],
"relationship_type": "array",
"column_mapping": {
"ArtistId": "ArtistId"

View File

@ -17,9 +17,32 @@ This directory contains a barebones implementation of the Data Connector agent s
> docker run -it --rm -p 8100:8100 dc-reference-agent:latest
```
# Dataset
## Dataset
The dataset exposed by the reference agent is sourced from https://github.com/lerocha/chinook-database/
More specifically, the `ChinookData.xml.gz` file is a GZipped version of https://raw.githubusercontent.com/lerocha/chinook-database/ce27c48d9f375f81b7b68bacdfddf3c4458acc49/ChinookDatabase/DataSources/_Xml/ChinookData.xml
The `schema-tables.json` is manually derived from the schema of the data as can be seen from the `CREATE TABLE` etc DML statements in the various per-database-vendor SQL scripts that can be found in `/ChinookDatabase/DataSources` in that repo.
## Configuration
The reference agent supports some configuration properties that can be set via the `value` property of `configuration` on a source in Hasura metadata. The configuration is passed to the agent on each request via the `X-Hasura-DataConnector-Config` header.
The configuration that the reference agent can take supports two properties:
* `tables`: This is a list of table names that should be exposed by the agent. If omitted all Chinook dataset tables are exposed. If specified, it filters all available table names by the specified list.
* `schema`: If specified, this places all the tables within a schema of the specified name. For example, if `schema` is set to `my_schema`, all table names will be namespaced under `my_schema`, for example `["my_schema","Album"]`. If not specified, then tables are not namespaced, for example `["Album"]`.
Here's an example configuration that only exposes the Artist and Album tables, and namespaces them under `my_schema`:
```json
{
"tables": ["Artist", "Album"],
"schema": "my_schema"
}
```
Here's an example configuration that exposes all tables, un-namespaced:
```json
{}
```

View File

@ -2,7 +2,8 @@ import { FastifyRequest } from "fastify"
import { ConfigSchemaResponse } from "./types"
export type Config = {
tables: String[] | null
tables: string[] | null
schema: string | null
}
export const getConfig = (request: FastifyRequest): Config => {
@ -10,7 +11,8 @@ export const getConfig = (request: FastifyRequest): Config => {
const rawConfigJson = Array.isArray(configHeader) ? configHeader[0] : configHeader ?? "{}";
const config = JSON.parse(rawConfigJson);
return {
tables: config.tables ?? null
tables: config.tables ?? null,
schema: config.schema ?? null
}
}
@ -24,6 +26,11 @@ export const configSchema: ConfigSchemaResponse = {
type: "array",
items: { $ref: "#/otherSchemas/TableName" },
nullable: true
},
schema: {
description: "Name of the schema to place the tables in. Omit to have no schema for the tables",
type: "string",
nullable: true
}
}
},

View File

@ -1,10 +1,11 @@
import { SchemaResponse } from "../types"
import { SchemaResponse, TableName } from "../types"
import { Config } from "../config";
import xml2js from "xml2js"
import fs from "fs"
import stream from "stream"
import zlib from "zlib"
import { parseNumbers } from "xml2js/lib/processors";
import { tableNameEquals } from "../util";
export type StaticData = {
[tableName: string]: Record<string, string | number | boolean | null>[]
@ -40,16 +41,28 @@ export const loadStaticData = async (): Promise<StaticData> => {
return await data as StaticData;
}
export const filterAvailableTables = (staticData: StaticData, config : Config): StaticData => {
export const filterAvailableTables = (staticData: StaticData, config: Config): StaticData => {
return Object.fromEntries(
Object.entries(staticData).filter(([name, _]) => config.tables === null ? true : config.tables.indexOf(name) >= 0)
);
}
export const getTable = (staticData: StaticData, config: Config) => (tableName: TableName): Record<string, string | number | boolean | null>[] | undefined => {
if (config.schema) {
return tableName.length === 2 && tableName[0] === config.schema
? staticData[tableName[1]]
: undefined;
} else {
return tableName.length === 1
? staticData[tableName[0]]
: undefined;
}
}
const schema: SchemaResponse = {
tables: [
{
name: "Artist",
name: ["Artist"],
primary_key: ["ArtistId"],
description: "Collection of artists of music",
columns: [
@ -68,7 +81,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Album",
name: ["Album"],
primary_key: ["AlbumId"],
description: "Collection of music albums created by artists",
columns: [
@ -93,7 +106,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Customer",
name: ["Customer"],
primary_key: ["CustomerId"],
description: "Collection of customers who can buy tracks",
columns: [
@ -178,7 +191,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Employee",
name: ["Employee"],
primary_key: ["EmployeeId"],
description: "Collection of employees who work for the business",
columns: [
@ -275,7 +288,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Genre",
name: ["Genre"],
primary_key: ["GenreId"],
description: "Genres of music",
columns: [
@ -294,7 +307,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Invoice",
name: ["Invoice"],
primary_key: ["InvoiceId"],
description: "Collection of invoices of music purchases by a customer",
columns: [
@ -355,7 +368,7 @@ const schema: SchemaResponse = {
]
},
{
name: "InvoiceLine",
name: ["InvoiceLine"],
primary_key: ["InvoiceLineId"],
description: "Collection of track purchasing line items of invoices",
columns: [
@ -392,7 +405,7 @@ const schema: SchemaResponse = {
]
},
{
name: "MediaType",
name: ["MediaType"],
primary_key: ["MediaTypeId"],
description: "Collection of media types that tracks can be encoded in",
columns: [
@ -411,7 +424,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Playlist",
name: ["Playlist"],
primary_key: ["PlaylistId"],
description: "Collection of playlists",
columns: [
@ -430,7 +443,7 @@ const schema: SchemaResponse = {
]
},
{
name: "PlaylistTrack",
name: ["PlaylistTrack"],
primary_key: ["PlaylistId", "TrackId"],
description: "Associations between playlists and tracks",
columns: [
@ -449,7 +462,7 @@ const schema: SchemaResponse = {
]
},
{
name: "Track",
name: ["Track"],
primary_key: ["TrackId"],
description: "Collection of music tracks",
columns: [
@ -513,8 +526,22 @@ const schema: SchemaResponse = {
};
export const getSchema = (config: Config): SchemaResponse => {
const prefixSchemaToTableName = (tableName: TableName) =>
config.schema
? [config.schema, ...tableName]
: tableName;
const filteredTables = schema.tables.filter(table =>
config.tables === null ? true : config.tables.map(n => [n]).find(tableNameEquals(table.name)) !== undefined
);
const prefixedTables = filteredTables.map(table => ({
...table,
name: prefixSchemaToTableName(table.name),
}));
return {
...schema,
tables: schema.tables.filter(table => config.tables === null ? true : config.tables.indexOf(table.name) >= 0)
tables: prefixedTables
};
};

View File

@ -1,6 +1,6 @@
import Fastify from 'fastify';
import FastifyCors from '@fastify/cors';
import { filterAvailableTables, getSchema, loadStaticData } from './data';
import { filterAvailableTables, getSchema, getTable, loadStaticData } from './data';
import { queryData } from './query';
import { getConfig } from './config';
import { capabilitiesResponse } from './capabilities';
@ -34,7 +34,7 @@ server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (reque
server.log.info({ headers: request.headers, query: request.body, }, "query.request");
const config = getConfig(request);
const data = filterAvailableTables(staticData, config);
return queryData(data, request.body);
return queryData(getTable(data, config), request.body);
});
server.get("/health", async (request, response) => {

View File

@ -1,12 +1,7 @@
import { QueryRequest, TableRelationships, Relationship, Query, Field, OrderBy, Expression, BinaryComparisonOperator, UnaryComparisonOperator, BinaryArrayComparisonOperator, ComparisonColumn, ComparisonValue, ScalarValue, QueryResponse, Aggregate, SingleColumnAggregate, ColumnCountAggregate } from "./types";
import { coerceUndefinedToNull, crossProduct, unreachable, zip } from "./util";
import { QueryRequest, TableRelationships, Relationship, Query, Field, OrderBy, Expression, BinaryComparisonOperator, UnaryComparisonOperator, BinaryArrayComparisonOperator, ComparisonColumn, ComparisonValue, ScalarValue, QueryResponse, Aggregate, SingleColumnAggregate, ColumnCountAggregate, TableName } from "./types";
import { coerceUndefinedToNull, crossProduct, tableNameEquals, unreachable, zip } from "./util";
import * as math from "mathjs";
type StaticData = {
[tableName: string]: Record<string, ScalarValue>[]
}
type TableName = string
type RelationshipName = string
type ProjectedRow = {
@ -187,7 +182,7 @@ const paginateRows = (rows: Record<string, ScalarValue>[], offset: number | null
};
const makeFindRelationship = (allTableRelationships: TableRelationships[], tableName: TableName) => (relationshipName: RelationshipName): Relationship => {
const relationship = allTableRelationships.find(r => r.source_table === tableName)?.relationships?.[relationshipName];
const relationship = allTableRelationships.find(r => tableNameEquals(r.source_table)(tableName))?.relationships?.[relationshipName];
if (relationship === undefined)
throw `No relationship named ${relationshipName} found for table ${tableName}`;
else
@ -377,9 +372,9 @@ const calculateAggregates = (rows: Record<string, ScalarValue>[], aggregateReque
}));
};
export const queryData = (staticData: StaticData, queryRequest: QueryRequest) => {
export const queryData = (getTable: (tableName: TableName) => Record<string, ScalarValue>[] | undefined, queryRequest: QueryRequest) => {
const performQuery = (tableName: TableName, query: Query): QueryResponse => {
const rows = staticData[tableName];
const rows = getTable(tableName);
if (rows === undefined) {
throw `${tableName} is not a valid table`;
}

View File

@ -524,6 +524,13 @@
],
"type": "object"
},
"TableName": {
"description": "The fully qualified name of a table, where the last item in the array is the table name and any earlier items represent the namespacing of the table name",
"items": {
"type": "string"
},
"type": "array"
},
"ScalarType": {
"enum": [
"string",
@ -573,8 +580,7 @@
"type": "string"
},
"name": {
"description": "The name of the table",
"type": "string"
"$ref": "#/components/schemas/TableName"
},
"primary_key": {
"description": "The primary key of the table",
@ -654,8 +660,7 @@
"$ref": "#/components/schemas/Query"
},
"table": {
"description": "The name of the table to query",
"type": "string"
"$ref": "#/components/schemas/TableName"
},
"table_relationships": {
"description": "The relationships between tables involved in the entire query request",
@ -692,8 +697,7 @@
"$ref": "#/components/schemas/RelationshipType"
},
"target_table": {
"description": "The name of the target table in the relationship",
"type": "string"
"$ref": "#/components/schemas/TableName"
}
},
"required": [
@ -713,8 +717,7 @@
"type": "object"
},
"source_table": {
"description": "The name of the source table in the relationship",
"type": "string"
"$ref": "#/components/schemas/TableName"
}
},
"required": [

View File

@ -52,5 +52,6 @@ export type { SingleColumnAggregateFunction } from './models/SingleColumnAggrega
export type { StarCountAggregate } from './models/StarCountAggregate';
export type { SubscriptionCapabilities } from './models/SubscriptionCapabilities';
export type { TableInfo } from './models/TableInfo';
export type { TableName } from './models/TableName';
export type { TableRelationships } from './models/TableRelationships';
export type { UnaryComparisonOperator } from './models/UnaryComparisonOperator';

View File

@ -3,14 +3,12 @@
/* eslint-disable */
import type { Query } from './Query';
import type { TableName } from './TableName';
import type { TableRelationships } from './TableRelationships';
export type QueryRequest = {
query: Query;
/**
* The name of the table to query
*/
table: string;
table: TableName;
/**
* The relationships between tables involved in the entire query request
*/

View File

@ -3,6 +3,7 @@
/* eslint-disable */
import type { RelationshipType } from './RelationshipType';
import type { TableName } from './TableName';
export type Relationship = {
/**
@ -10,9 +11,6 @@ export type Relationship = {
*/
column_mapping: Record<string, string>;
relationship_type: RelationshipType;
/**
* The name of the target table in the relationship
*/
target_table: string;
target_table: TableName;
};

View File

@ -3,6 +3,7 @@
/* eslint-disable */
import type { ColumnInfo } from './ColumnInfo';
import type { TableName } from './TableName';
export type TableInfo = {
/**
@ -13,10 +14,7 @@ export type TableInfo = {
* Description of the table
*/
description?: string | null;
/**
* The name of the table
*/
name: string;
name: TableName;
/**
* The primary key of the table
*/

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* The fully qualified name of a table, where the last item in the array is the table name and any earlier items represent the namespacing of the table name
*/
export type TableName = Array<string>;

View File

@ -3,15 +3,13 @@
/* eslint-disable */
import type { Relationship } from './Relationship';
import type { TableName } from './TableName';
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;
source_table: TableName;
};

View File

@ -1,4 +1,6 @@
export const coerceUndefinedToNull = <T>(v: T | undefined): T | null => v === undefined ? null : v;
import { TableName } from "./types";
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}`) };
@ -14,3 +16,10 @@ export const zip = <T, U>(arr1: T[], arr2: U[]): [T, U][] => {
export const crossProduct = <T, U>(arr1: T[], arr2: U[]): [T, U][] => {
return arr1.flatMap(a1 => arr2.map(a2 => [a1, a2]) as [T, U][]);
};
export const tableNameEquals = (tableName1: TableName) => (tableName2: TableName): boolean => {
if (tableName1.length !== tableName2.length)
return false;
return zip(tableName1, tableName2).every(([n1, n2]) => n1 === n2);
}

View File

@ -9,13 +9,13 @@
"kind": "reference",
"tables": [
{
"table": "Album",
"table": ["Album"],
"object_relationships": [
{
"name": "Artist",
"using": {
"manual_configuration": {
"remote_table": "Artist",
"remote_table": ["Artist"],
"column_mapping": {
"ArtistId": "ArtistId"
}
@ -28,7 +28,7 @@
"name": "Tracks",
"using": {
"manual_configuration": {
"remote_table": "Track",
"remote_table": ["Track"],
"column_mapping": {
"AlbumId": "AlbumId"
}
@ -38,13 +38,13 @@
]
},
{
"table": "Artist",
"table": ["Artist"],
"array_relationships": [
{
"name": "Albums",
"using": {
"manual_configuration": {
"remote_table": "Album",
"remote_table": ["Album"],
"column_mapping": {
"ArtistId": "ArtistId"
}
@ -54,13 +54,13 @@
]
},
{
"table": "Customer",
"table": ["Customer"],
"object_relationships": [
{
"name": "SupportRep",
"using": {
"manual_configuration": {
"remote_table": "Employee",
"remote_table": ["Employee"],
"column_mapping": {
"SupportRepId": "EmployeeId"
}
@ -73,7 +73,7 @@
"name": "Invoices",
"using": {
"manual_configuration": {
"remote_table": "Invoice",
"remote_table": ["Invoice"],
"column_mapping": {
"CustomerId": "CustomerId"
}
@ -83,13 +83,13 @@
]
},
{
"table": "Employee",
"table": ["Employee"],
"object_relationships": [
{
"name": "ReportsTo",
"using": {
"manual_configuration": {
"remote_table": "Employee",
"remote_table": ["Employee"],
"column_mapping": {
"ReportsTo": "EmployeeId"
}
@ -102,7 +102,7 @@
"name": "SupportRepForCustomers",
"using": {
"manual_configuration": {
"remote_table": "Customer",
"remote_table": ["Customer"],
"column_mapping": {
"EmployeeId": "SupportRepId"
}
@ -113,7 +113,7 @@
"name": "DirectReports",
"using": {
"manual_configuration": {
"remote_table": "Employee",
"remote_table": ["Employee"],
"column_mapping": {
"EmployeeId": "ReportsTo"
}
@ -123,13 +123,13 @@
]
},
{
"table": "Genre",
"table": ["Genre"],
"array_relationships": [
{
"name": "Tracks",
"using": {
"manual_configuration": {
"remote_table": "Track",
"remote_table": ["Track"],
"column_mapping": {
"GenreId": "GenreId"
}
@ -139,13 +139,13 @@
]
},
{
"table": "Invoice",
"table": ["Invoice"],
"object_relationships": [
{
"name": "Customer",
"using": {
"manual_configuration": {
"remote_table": "Customer",
"remote_table": ["Customer"],
"column_mapping": {
"CustomerId": "CustomerId"
}
@ -158,7 +158,7 @@
"name": "InvoiceLines",
"using": {
"manual_configuration": {
"remote_table": "InvoiceLine",
"remote_table": ["InvoiceLine"],
"column_mapping": {
"InvoiceId": "InvoiceId"
}
@ -168,13 +168,13 @@
]
},
{
"table": "InvoiceLine",
"table": ["InvoiceLine"],
"object_relationships": [
{
"name": "Invoice",
"using": {
"manual_configuration": {
"remote_table": "Invoice",
"remote_table": ["Invoice"],
"column_mapping": {
"InvoiceId": "InvoiceId"
}
@ -185,7 +185,7 @@
"name": "Track",
"using": {
"manual_configuration": {
"remote_table": "Track",
"remote_table": ["Track"],
"column_mapping": {
"TrackId": "TrackId"
}
@ -195,13 +195,13 @@
]
},
{
"table": "MediaType",
"table": ["MediaType"],
"array_relationships": [
{
"name": "Tracks",
"using": {
"manual_configuration": {
"remote_table": "Track",
"remote_table": ["Track"],
"column_mapping": {
"MediaTypeId": "MediaTypeId"
}
@ -211,13 +211,13 @@
]
},
{
"table": "Playlist",
"table": ["Playlist"],
"array_relationships": [
{
"name": "PlaylistTracks",
"using": {
"manual_configuration": {
"remote_table": "PlaylistTrack",
"remote_table": ["PlaylistTrack"],
"column_mapping": {
"PlaylistId": "PlaylistId"
}
@ -227,13 +227,13 @@
]
},
{
"table": "PlaylistTrack",
"table": ["PlaylistTrack"],
"object_relationships": [
{
"name": "Playlist",
"using": {
"manual_configuration": {
"remote_table": "Playlist",
"remote_table": ["Playlist"],
"column_mapping": {
"PlaylistId": "PlaylistId"
}
@ -244,7 +244,7 @@
"name": "Track",
"using": {
"manual_configuration": {
"remote_table": "Track",
"remote_table": ["Track"],
"column_mapping": {
"TrackId": "TrackId"
}
@ -254,13 +254,13 @@
]
},
{
"table": "Track",
"table": ["Track"],
"object_relationships": [
{
"name": "Album",
"using": {
"manual_configuration": {
"remote_table": "Album",
"remote_table": ["Album"],
"column_mapping": {
"AlbumId": "AlbumId"
}
@ -271,7 +271,7 @@
"name": "MediaType",
"using": {
"manual_configuration": {
"remote_table": "MediaType",
"remote_table": ["MediaType"],
"column_mapping": {
"MediaTypeId": "MediaTypeId"
}
@ -282,7 +282,7 @@
"name": "Genre",
"using": {
"manual_configuration": {
"remote_table": "Genre",
"remote_table": ["Genre"],
"column_mapping": {
"GenreId": "GenreId"
}
@ -295,7 +295,7 @@
"name": "InvoiceLines",
"using": {
"manual_configuration": {
"remote_table": "InvoiceLine",
"remote_table": ["InvoiceLine"],
"column_mapping": {
"TrackId": "TrackId"
}
@ -306,7 +306,7 @@
"name": "PlaylistTracks",
"using": {
"manual_configuration": {
"remote_table": "PlaylistTrack",
"remote_table": ["PlaylistTrack"],
"column_mapping": {
"TrackId": "TrackId"
}

View File

@ -12,9 +12,10 @@ where
import Autodocodec
import Autodocodec.OpenAPI ()
import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)
import Data.Aeson (FromJSON, ToJSON)
import Data.Data (Data)
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty)
import Data.OpenApi (ToSchema)
import Data.Text (Text)
import GHC.Generics (Generic)
@ -23,14 +24,16 @@ import Prelude
--------------------------------------------------------------------------------
newtype TableName = TableName {unTableName :: Text}
newtype TableName = TableName {unTableName :: NonEmpty Text}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData, Hashable)
deriving newtype (FromJSONKey, ToJSONKey)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec TableName
instance HasCodec TableName where
codec = dimapCodec TableName unTableName textCodec
codec =
named "TableName" $
dimapCodec TableName unTableName codec
<?> "The fully qualified name of a table, where the last item in the array is the table name and any earlier items represent the namespacing of the table name"
--------------------------------------------------------------------------------

View File

@ -5,12 +5,14 @@ module Hasura.Backends.DataConnector.Adapter.Backend (CustomBooleanOperator (..)
import Data.Aeson qualified as J (ToJSON (..), Value)
import Data.Aeson.Extended (ToJSONKeyValue (..))
import Data.Aeson.Key (fromText)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Data.Text.Casing qualified as C
import Data.Text.Extended ((<<>))
import Hasura.Backends.DataConnector.Adapter.Types qualified as Adapter
import Hasura.Backends.DataConnector.IR.Aggregate qualified as IR.A
import Hasura.Backends.DataConnector.IR.Column qualified as IR.C
import Hasura.Backends.DataConnector.IR.Function qualified as IR.F
import Hasura.Backends.DataConnector.IR.Name qualified as IR.N
import Hasura.Backends.DataConnector.IR.OrderBy qualified as IR.O
import Hasura.Backends.DataConnector.IR.Scalar.Type qualified as IR.S.T
import Hasura.Backends.DataConnector.IR.Scalar.Value qualified as IR.S.V
@ -104,22 +106,21 @@ instance Backend 'DataConnector where
tableToFunction = coerce
tableGraphQLName :: TableName 'DataConnector -> Either QErr G.Name
tableGraphQLName name =
G.mkName (IR.N.unName name)
`onNothing` throw400 ValidationFailed ("TableName " <> IR.N.unName name <> " is not a valid GraphQL identifier")
tableGraphQLName name = do
let snakedName = snakeCaseTableName @'DataConnector name
G.mkName snakedName
`onNothing` throw400 ValidationFailed ("TableName " <> snakedName <> " is not a valid GraphQL identifier")
functionGraphQLName :: FunctionName 'DataConnector -> Either QErr G.Name
functionGraphQLName = error "functionGraphQLName: not implemented for the Data Connector backend."
snakeCaseTableName :: TableName 'DataConnector -> Text
snakeCaseTableName = IR.N.unName
snakeCaseTableName = Text.intercalate "_" . NonEmpty.toList . IR.T.unName
getTableIdentifier :: TableName 'DataConnector -> Either QErr C.GQLNameIdentifier
getTableIdentifier name = do
gqlTableName <-
G.mkName (IR.N.unName name)
`onNothing` throw400 ValidationFailed ("TableName " <> IR.N.unName name <> " is not a valid GraphQL identifier")
pure $ C.Identifier gqlTableName []
getTableIdentifier name@(IR.T.Name (prefix :| suffixes)) = do
(C.Identifier <$> G.mkName prefix <*> traverse G.mkNameSuffix suffixes)
`onNothing` throw400 ValidationFailed ("TableName " <> name <<> " is not a valid GraphQL identifier")
namingConventionSupport :: SupportedNamingCase
namingConventionSupport = OnlyHasuraCase

View File

@ -1,22 +1,33 @@
module Hasura.Backends.DataConnector.IR.Function (Name) where
module Hasura.Backends.DataConnector.IR.Function
( Name (..),
)
where
--------------------------------------------------------------------------------
import Data.Aeson (FromJSON, ToJSON, ToJSONKey (..))
import Data.Aeson.Types (toJSONKeyText)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Data.Text.Extended (ToTxt (..))
import Hasura.Base.ErrorValue qualified as ErrorValue
import Hasura.Base.ToErrorValue
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Hasura.Backends.DataConnector.IR.Name qualified as IR.N
newtype Name = Name {unName :: NonEmpty Text}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving newtype
( Cacheable,
FromJSON,
Hashable,
NFData,
ToJSON
)
--------------------------------------------------------------------------------
instance ToJSONKey Name where
toJSONKey = toJSONKeyText toTxt
-- | An alias for 'Name.Function' 'Name.Name's.
--
-- This alias is defined in its own module primarily for the convenience of
-- importing it qualified.
--
-- For example:
-- @
-- import Data.Coerce (coerce)
-- import Hasura.Experimental.IR.Function qualified as Function (Name)
--
-- example :: Function.Name
-- example = coerce @Text @Function.Name "function_name"
-- @
type Name = IR.N.Name 'IR.N.Function
instance ToTxt Name where
toTxt = Text.intercalate "." . NonEmpty.toList . unName
instance ToErrorValue Name where
toErrorValue = ErrorValue.squote . toTxt

View File

@ -43,12 +43,6 @@ newtype Name ty = Name {unName :: Text}
instance ToErrorValue (Name ty) where
toErrorValue = ErrorValue.squote . unName
instance Witch.From API.TableName (Name 'Table) where
from (API.TableName n) = Name n
instance Witch.From (Name 'Table) API.TableName where
from (Name n) = API.TableName n
instance Witch.From API.ColumnName (Name 'Column) where
from (API.ColumnName n) = Name n
@ -68,6 +62,4 @@ instance Witch.From (Name 'Relationship) API.RelationshipName where
-- shared abstraction.
data NameType
= Column
| Function
| Table
| Relationship

View File

@ -1,25 +1,49 @@
module Hasura.Backends.DataConnector.IR.Table
( Name,
( Name (..),
)
where
--------------------------------------------------------------------------------
import Data.Aeson (FromJSON (..), ToJSON, ToJSONKey (..), withText)
import Data.Aeson.Types (toJSONKeyText)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text qualified as Text
import Data.Text.Extended (ToTxt (..))
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Base.ErrorValue qualified as ErrorValue
import Hasura.Base.ToErrorValue
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Witch.From qualified as Witch
import Hasura.Backends.DataConnector.IR.Name qualified as IR.N
-- | The fully qualified name of a table. The last element in the list is the table name
-- and all other elements represent namespacing of the table name.
-- For example, for a database that has schemas, the name would be '[<schema>,<table name>]'
newtype Name = Name {unName :: NonEmpty Text}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving newtype
( Cacheable,
Hashable,
NFData,
ToJSON
)
--------------------------------------------------------------------------------
instance FromJSON Name where
parseJSON value =
Name <$> parseJSON value
-- Fallback parsing of a single string to support older metadata
<|> withText "Name" (\text -> pure . Name $ text :| []) value
-- | An alias for 'Name.Table' 'Name.Name's.
--
-- This alias is defined in its own module primarily for the convenience of
-- importing it qualified.
--
-- For example:
-- @
-- import Data.Coerce (coerce)
-- import Hasura.Experimental.IR.Table qualified as Table (Name)
--
-- example :: Table.Name
-- example = coerce @Text @Table.Name "table_name"
-- @
type Name = IR.N.Name 'IR.N.Table
instance ToJSONKey Name where
toJSONKey = toJSONKeyText toTxt
instance Witch.From API.TableName Name where
from (API.TableName n) = Name n
instance Witch.From Name API.TableName where
from (Name n) = API.TableName n
instance ToTxt Name where
toTxt = Text.intercalate "." . NonEmpty.toList . unName
instance ToErrorValue Name where
toErrorValue = ErrorValue.squote . toTxt

View File

@ -70,14 +70,14 @@ spec = do
describe "QueryRequest" $ do
let queryRequest =
QueryRequest
{ _qrTable = TableName "my_table",
{ _qrTable = TableName ["my_table"],
_qrTableRelationships = [],
_qrQuery = Query (Just mempty) Nothing Nothing Nothing Nothing Nothing
}
testToFromJSONToSchema
queryRequest
[aesonQQ|
{ "table": "my_table",
{ "table": ["my_table"],
"table_relationships": [],
"query": { "fields": {} } }
|]

View File

@ -34,14 +34,14 @@ spec = do
describe "Relationship" $ do
let relationship =
Relationship
{ _rTargetTable = TableName "target_table_name",
{ _rTargetTable = TableName ["target_table_name"],
_rRelationshipType = ObjectRelationship,
_rColumnMapping = [(ColumnName "outer_column", ColumnName "inner_column")]
}
testToFromJSONToSchema
relationship
[aesonQQ|
{ "target_table": "target_table_name",
{ "target_table": ["target_table_name"],
"relationship_type": "object",
"column_mapping": {
"outer_column": "inner_column"
@ -52,19 +52,19 @@ spec = do
describe "TableRelationships" $ do
let relationshipA =
Relationship
{ _rTargetTable = TableName "target_table_name_a",
{ _rTargetTable = TableName ["target_table_name_a"],
_rRelationshipType = ObjectRelationship,
_rColumnMapping = [(ColumnName "outer_column_a", ColumnName "inner_column_a")]
}
let relationshipB =
Relationship
{ _rTargetTable = TableName "target_table_name_b",
{ _rTargetTable = TableName ["target_table_name_b"],
_rRelationshipType = ArrayRelationship,
_rColumnMapping = [(ColumnName "outer_column_b", ColumnName "inner_column_b")]
}
let tableRelationships =
TableRelationships
{ _trSourceTable = TableName "source_table_name",
{ _trSourceTable = TableName ["source_table_name"],
_trRelationships =
[ (RelationshipName "relationship_a", relationshipA),
(RelationshipName "relationship_b", relationshipB)
@ -73,17 +73,17 @@ spec = do
testToFromJSONToSchema
tableRelationships
[aesonQQ|
{ "source_table": "source_table_name",
{ "source_table": ["source_table_name"],
"relationships": {
"relationship_a": {
"target_table": "target_table_name_a",
"target_table": ["target_table_name_a"],
"relationship_type": "object",
"column_mapping": {
"outer_column_a": "inner_column_a"
}
},
"relationship_b": {
"target_table": "target_table_name_b",
"target_table": ["target_table_name_b"],
"relationship_type": "array",
"column_mapping": {
"outer_column_b": "inner_column_b"

View File

@ -17,27 +17,27 @@ import Test.Hspec
spec :: Spec
spec = do
describe "TableName" $ do
testToFromJSONToSchema (TableName "my_table_name") [aesonQQ|"my_table_name"|]
testToFromJSONToSchema (TableName ["my_table_name"]) [aesonQQ|["my_table_name"]|]
jsonOpenApiProperties genTableName
describe "TableInfo" $ do
describe "minimal" $
testToFromJSONToSchema
(TableInfo (TableName "my_table_name") [] Nothing Nothing)
(TableInfo (TableName ["my_table_name"]) [] Nothing Nothing)
[aesonQQ|
{ "name": "my_table_name",
{ "name": ["my_table_name"],
"columns": []
}
|]
describe "non-minimal" $
testToFromJSONToSchema
( TableInfo
(TableName "my_table_name")
(TableName ["my_table_name"])
[ColumnInfo (ColumnName "id") StringTy False Nothing]
(Just [ColumnName "id"])
(Just "my description")
)
[aesonQQ|
{ "name": "my_table_name",
{ "name": ["my_table_name"],
"columns": [{"name": "id", "type": "string", "nullable": false}],
"primary_key": ["id"],
"description": "my description"
@ -46,7 +46,7 @@ spec = do
jsonOpenApiProperties genTableInfo
genTableName :: MonadGen m => m TableName
genTableName = TableName <$> text (linear 0 10) unicode
genTableName = TableName <$> Gen.nonEmpty (linear 1 3) (text (linear 0 10) unicode)
genTableInfo :: MonadGen m => m TableInfo
genTableInfo =

View File

@ -12,8 +12,10 @@ module Hasura.Backends.DataConnector.RQLGenerator.GenCommon
where
import Data.Functor.Const
import Hasura.Backends.DataConnector.IR.Function qualified as FunctionName
import Hasura.Backends.DataConnector.IR.Name qualified as Name
import Hasura.Backends.DataConnector.IR.Scalar.Type qualified as ScalarType
import Hasura.Backends.DataConnector.IR.Table qualified as TableName
import Hasura.Generator.Common (defaultRange, genArbitraryUnicodeText, genHashMap)
import Hasura.Prelude (coerce, fmap, pure, ($), (<$>), (<*>))
import Hasura.RQL.IR
@ -22,7 +24,8 @@ import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.Relationships.Local
import Hasura.SQL.Backend
import Hedgehog (MonadGen)
import Hedgehog.Gen (bool_, choice, element, list)
import Hedgehog.Gen (bool_, choice, element, list, nonEmpty)
import Hedgehog.Internal.Range (linear)
--------------------------------------------------------------------------------
@ -83,7 +86,7 @@ genColumn :: MonadGen m => m (Column 'DataConnector)
genColumn = coerce <$> genArbitraryUnicodeText defaultRange
genTableName :: MonadGen m => m (TableName 'DataConnector)
genTableName = coerce <$> genArbitraryUnicodeText defaultRange
genTableName = coerce <$> nonEmpty (linear 1 3) (genArbitraryUnicodeText defaultRange)
genScalarType :: MonadGen m => m (ScalarType 'DataConnector)
genScalarType =
@ -94,7 +97,7 @@ genScalarType =
]
genFunctionName :: MonadGen m => m (FunctionName 'DataConnector)
genFunctionName = coerce <$> genArbitraryUnicodeText defaultRange
genFunctionName = coerce <$> nonEmpty (linear 1 3) (genArbitraryUnicodeText defaultRange)
genFunctionArgumentExp :: MonadGen m => m (FunctionArgumentExp 'DataConnector a)
genFunctionArgumentExp = pure (Const ())

View File

@ -74,11 +74,14 @@ import Data.Aeson.Lens (_Bool, _Number, _String)
import Data.Bifunctor (bimap)
import Data.ByteString (ByteString)
import Data.ByteString.Lazy qualified as BSL
import Data.CaseInsensitive (CI)
import Data.CaseInsensitive qualified as CI
import Data.FileEmbed (embedFile, makeRelativeToProject)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.List (find, sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Scientific (Scientific)
import Data.Text (Text)
@ -105,11 +108,14 @@ chinookXml :: XML.Document
chinookXml = XML.parseLBS_ XML.def . GZip.decompress $ BSL.fromStrict chinookXmlBS
readTableFromXmlIntoRows :: API.TableName -> [KeyMap API.FieldValue]
readTableFromXmlIntoRows (API.TableName tableName) =
readTableFromXmlIntoRows tableName =
rowToJsonObject <$> tableRows
where
tableNameToXmlTag :: API.TableName -> CI Text
tableNameToXmlTag (API.TableName names) = CI.mk . Text.intercalate "_" $ NonEmpty.toList names
tableRows :: [XML.Element]
tableRows = chinookXml ^.. XML.root . XML.nodes . traverse . XML._Element . XML.named (CI.mk tableName)
tableRows = chinookXml ^.. XML.root . XML.nodes . traverse . XML._Element . XML.named (tableNameToXmlTag tableName)
rowToJsonObject :: XML.Element -> KeyMap API.FieldValue
rowToJsonObject element =
@ -131,8 +137,11 @@ readTableFromXmlIntoRows (API.TableName tableName) =
else API.mkColumnFieldValue $ J.String textValue
in (name, value)
mkTableName :: Text -> API.TableName
mkTableName = API.TableName . (:| [])
artistsTableName :: API.TableName
artistsTableName = API.TableName "Artist"
artistsTableName = mkTableName "Artist"
artistsRows :: [KeyMap API.FieldValue]
artistsRows = sortBy "ArtistId" $ readTableFromXmlIntoRows artistsTableName
@ -155,7 +164,7 @@ artistsTableRelationships =
)
albumsTableName :: API.TableName
albumsTableName = API.TableName "Album"
albumsTableName = mkTableName "Album"
albumsRows :: [KeyMap API.FieldValue]
albumsRows = sortBy "AlbumId" $ readTableFromXmlIntoRows albumsTableName
@ -179,7 +188,7 @@ tracksRelationshipName :: API.RelationshipName
tracksRelationshipName = API.RelationshipName "Tracks"
customersTableName :: API.TableName
customersTableName = API.TableName "Customer"
customersTableName = mkTableName "Customer"
customersRows :: [KeyMap API.FieldValue]
customersRows = sortBy "CustomerId" $ readTableFromXmlIntoRows customersTableName
@ -198,7 +207,7 @@ supportRepRelationshipName :: API.RelationshipName
supportRepRelationshipName = API.RelationshipName "SupportRep"
employeesTableName :: API.TableName
employeesTableName = API.TableName "Employee"
employeesTableName = mkTableName "Employee"
employeesRows :: [KeyMap API.FieldValue]
employeesRows = sortBy "EmployeeId" $ readTableFromXmlIntoRows employeesTableName
@ -221,25 +230,25 @@ supportRepForCustomersRelationshipName :: API.RelationshipName
supportRepForCustomersRelationshipName = API.RelationshipName "SupportRepForCustomers"
invoicesTableName :: API.TableName
invoicesTableName = API.TableName "Invoice"
invoicesTableName = mkTableName "Invoice"
invoicesRows :: [KeyMap API.FieldValue]
invoicesRows = sortBy "InvoiceId" $ readTableFromXmlIntoRows invoicesTableName
invoiceLinesTableName :: API.TableName
invoiceLinesTableName = API.TableName "InvoiceLine"
invoiceLinesTableName = mkTableName "InvoiceLine"
invoiceLinesRows :: [KeyMap API.FieldValue]
invoiceLinesRows = sortBy "InvoiceLineId" $ readTableFromXmlIntoRows invoiceLinesTableName
mediaTypesTableName :: API.TableName
mediaTypesTableName = API.TableName "MediaType"
mediaTypesTableName = mkTableName "MediaType"
mediaTypesRows :: [KeyMap API.FieldValue]
mediaTypesRows = sortBy "MediaTypeId" $ readTableFromXmlIntoRows mediaTypesTableName
tracksTableName :: API.TableName
tracksTableName = API.TableName "Track"
tracksTableName = mkTableName "Track"
tracksRows :: [KeyMap API.FieldValue]
tracksRows = sortBy "TrackId" $ readTableFromXmlIntoRows tracksTableName

View File

@ -1,6 +1,6 @@
[
{
"name": "Artist",
"name": ["Artist"],
"primary_key": ["ArtistId"],
"description": "Collection of artists of music",
"columns": [
@ -19,7 +19,7 @@
]
},
{
"name": "Album",
"name": ["Album"],
"primary_key": ["AlbumId"],
"description": "Collection of music albums created by artists",
"columns": [
@ -44,7 +44,7 @@
]
},
{
"name": "Customer",
"name": ["Customer"],
"primary_key": ["CustomerId"],
"description": "Collection of customers who can buy tracks",
"columns": [
@ -129,7 +129,7 @@
]
},
{
"name": "Employee",
"name": ["Employee"],
"primary_key": ["EmployeeId"],
"description": "Collection of employees who work for the business",
"columns": [
@ -226,7 +226,7 @@
]
},
{
"name": "Genre",
"name": ["Genre"],
"primary_key": ["GenreId"],
"description": "Genres of music",
"columns": [
@ -245,7 +245,7 @@
]
},
{
"name": "Invoice",
"name": ["Invoice"],
"primary_key": ["InvoiceId"],
"description": "Collection of invoices of music purchases by a customer",
"columns": [
@ -306,7 +306,7 @@
]
},
{
"name": "InvoiceLine",
"name": ["InvoiceLine"],
"primary_key": ["InvoiceLineId"],
"description": "Collection of track purchasing line items of invoices",
"columns": [
@ -343,7 +343,7 @@
]
},
{
"name": "MediaType",
"name": ["MediaType"],
"primary_key": ["MediaTypeId"],
"description": "Collection of media types that tracks can be encoded in",
"columns": [
@ -362,7 +362,7 @@
]
},
{
"name": "Playlist",
"name": ["Playlist"],
"primary_key": ["PlaylistId"],
"description": "Collection of playlists",
"columns": [
@ -381,7 +381,7 @@
]
},
{
"name": "PlaylistTrack",
"name": ["PlaylistTrack"],
"primary_key": ["PlaylistId", "TrackId"],
"description": "Associations between playlists and tracks",
"columns": [
@ -400,7 +400,7 @@
]
},
{
"name": "Track",
"name": ["Track"],
"primary_key": ["TrackId"],
"description": "Collection of music tracks",
"columns": [

View File

@ -23,6 +23,9 @@ data MockConfig = MockConfig
_queryResponse :: API.QueryRequest -> API.QueryResponse
}
mkTableName :: Text -> API.TableName
mkTableName = API.TableName . (:| [])
-- | Stock Capabilities for a Chinook Agent
capabilities :: API.CapabilitiesResponse
capabilities =
@ -62,7 +65,7 @@ schema =
API.SchemaResponse
{ API.srTables =
[ API.TableInfo
{ API.dtiName = API.TableName "Artist",
{ API.dtiName = mkTableName "Artist",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "ArtistId",
@ -81,7 +84,7 @@ schema =
API.dtiDescription = Just "Collection of artists of music"
},
API.TableInfo
{ API.dtiName = API.TableName "Album",
{ API.dtiName = mkTableName "Album",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "AlbumId",
@ -106,7 +109,7 @@ schema =
API.dtiDescription = Just "Collection of music albums created by artists"
},
API.TableInfo
{ API.dtiName = API.TableName "Genre",
{ API.dtiName = mkTableName "Genre",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "GenreId",
@ -125,7 +128,7 @@ schema =
API.dtiDescription = Just "Genres of music"
},
API.TableInfo
{ API.dtiName = API.TableName "Invoice",
{ API.dtiName = mkTableName "Invoice",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "InvoiceId",
@ -186,7 +189,7 @@ schema =
API.dtiDescription = Just "Collection of invoices of music purchases by a customer"
},
API.TableInfo
{ API.dtiName = API.TableName "InvoiceLine",
{ API.dtiName = mkTableName "InvoiceLine",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "InvoiceLineId",
@ -223,7 +226,7 @@ schema =
API.dtiDescription = Just "Collection of track purchasing line items of invoices"
},
API.TableInfo
{ API.dtiName = API.TableName "MediaType",
{ API.dtiName = mkTableName "MediaType",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "MediaTypeId",
@ -242,7 +245,7 @@ schema =
API.dtiDescription = Just "Collection of media types that tracks can be encoded in"
},
API.TableInfo
{ API.dtiName = API.TableName "Track",
{ API.dtiName = mkTableName "Track",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "TrackId",

View File

@ -40,36 +40,36 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Album
- table: [Album]
object_relationships:
- name: Artist
using:
manual_configuration:
remote_table: Artist
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: Artist
- table: [Artist]
array_relationships:
- name: Albums
using:
manual_configuration:
remote_table: Album
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
- table: Invoice
- table: [Invoice]
array_relationships:
- name: InvoiceLines
using:
manual_configuration:
remote_table: InvoiceLine
remote_table: [InvoiceLine]
column_mapping:
InvoiceId: InvoiceId
- table: InvoiceLine
- table: [InvoiceLine]
object_relationships:
- name: Invoice
using:
manual_configuration:
remote_table: Invoice
remote_table: [Invoice]
column_mapping:
InvoiceId: InvoiceId
configuration: {}

View File

@ -43,36 +43,36 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Album
- table: [Album]
object_relationships:
- name: Artist
using:
manual_configuration:
remote_table: Artist
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: Artist
- table: [Artist]
array_relationships:
- name: Albums
using:
manual_configuration:
remote_table: Album
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
- table: Invoice
- table: [Invoice]
array_relationships:
- name: InvoiceLines
using:
manual_configuration:
remote_table: InvoiceLine
remote_table: [InvoiceLine]
column_mapping:
InvoiceId: InvoiceId
- table: InvoiceLine
- table: [InvoiceLine]
object_relationships:
- name: Invoice
using:
manual_configuration:
remote_table: Invoice
remote_table: [Invoice]
column_mapping:
InvoiceId: InvoiceId
configuration: {}
@ -139,15 +139,15 @@ tests opts = describe "Aggregate Query Tests" $ do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Artist",
{ _qrTable = API.TableName ("Artist" :| []),
_qrTableRelationships =
[ API.TableRelationships
{ _trSourceTable = API.TableName "Artist",
{ _trSourceTable = API.TableName ("Artist" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Albums",
API.Relationship
{ _rTargetTable = API.TableName "Album",
{ _rTargetTable = API.TableName ("Album" :| []),
_rRelationshipType = API.ArrayRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
}
@ -270,15 +270,15 @@ tests opts = describe "Aggregate Query Tests" $ do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Invoice",
{ _qrTable = API.TableName ("Invoice" :| []),
_qrTableRelationships =
[ API.TableRelationships
{ _trSourceTable = API.TableName "Invoice",
{ _trSourceTable = API.TableName ("Invoice" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "InvoiceLines",
API.Relationship
{ _rTargetTable = API.TableName "InvoiceLine",
{ _rTargetTable = API.TableName ("InvoiceLine" :| []),
_rRelationshipType = API.ArrayRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "InvoiceId", API.ColumnName "InvoiceId")]
}

View File

@ -47,7 +47,7 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Album
- table: [Album]
configuration:
custom_root_fields:
select: albums
@ -63,10 +63,10 @@ sourceMetadata =
- name: artist
using:
manual_configuration:
remote_table: Artist
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: Artist
- table: [Artist]
configuration:
custom_root_fields:
select: artists
@ -80,7 +80,7 @@ sourceMetadata =
- name: albums
using:
manual_configuration:
remote_table: Album
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
configuration: {}
@ -123,7 +123,7 @@ tests opts = do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Album",
{ _qrTable = API.TableName ("Album" :| []),
_qrTableRelationships = [],
_qrQuery =
API.Query
@ -185,7 +185,7 @@ tests opts = do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Album",
{ _qrTable = API.TableName ("Album" :| []),
_qrTableRelationships = [],
_qrQuery =
API.Query

View File

@ -43,20 +43,20 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Genre
- table: MediaType
- table: Track
- table: [Genre]
- table: [MediaType]
- table: [Track]
object_relationships:
- name: Genre
using:
manual_configuration:
remote_table: Genre
remote_table: [Genre]
column_mapping:
GenreId: GenreId
- name: MediaType
using:
manual_configuration:
remote_table: MediaType
remote_table: [MediaType]
column_mapping:
MediaTypeId: MediaTypeId
configuration: {}
@ -118,22 +118,22 @@ tests opts = do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Track",
{ _qrTable = API.TableName ("Track" :| []),
_qrTableRelationships =
[ API.TableRelationships
{ _trSourceTable = API.TableName "Track",
{ _trSourceTable = API.TableName ("Track" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Genre",
API.Relationship
{ _rTargetTable = API.TableName "Genre",
{ _rTargetTable = API.TableName ("Genre" :| []),
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "GenreId", API.ColumnName "GenreId")]
}
),
( API.RelationshipName "MediaType",
API.Relationship
{ _rTargetTable = API.TableName "MediaType",
{ _rTargetTable = API.TableName ("MediaType" :| []),
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping =
HashMap.fromList

View File

@ -47,7 +47,7 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Album
- table: [Album]
configuration:
custom_root_fields:
select: albums
@ -63,10 +63,10 @@ sourceMetadata =
- name: artist
using:
manual_configuration:
remote_table: Artist
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: Artist
- table: [Artist]
configuration:
custom_root_fields:
select: artists
@ -80,7 +80,7 @@ sourceMetadata =
- name: albums
using:
manual_configuration:
remote_table: Album
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
configuration:
@ -132,7 +132,7 @@ tests opts = do
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName "Album",
{ _qrTable = API.TableName ("Album" :| []),
_qrTableRelationships = [],
_qrQuery =
API.Query

View File

@ -46,7 +46,7 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Album
- table: [Album]
configuration:
custom_root_fields:
select: albums
@ -62,10 +62,10 @@ tables:
- name: artist
using:
manual_configuration:
remote_table: Artist
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: Artist
- table: [Artist]
configuration:
custom_root_fields:
select: artists
@ -79,22 +79,22 @@ tables:
- name: albums
using:
manual_configuration:
remote_table: Album
remote_table: [Album]
column_mapping:
ArtistId: ArtistId
- table: Playlist
- table: PlaylistTrack
- table: [Playlist]
- table: [PlaylistTrack]
object_relationships:
- name: Playlist
using:
manual_configuration:
remote_table: Playlist
remote_table: [Playlist]
column_mapping:
PlaylistId: PlaylistId
- name: Track
using:
manual_configuration:
remote_table: Track
remote_table: [Track]
column_mapping:
TrackId: TrackId
- table: Track

View File

@ -49,12 +49,12 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: Employee
- table: [Employee]
array_relationships:
- name: SupportRepForCustomers
using:
manual_configuration:
remote_table: Customer
remote_table: [Customer]
column_mapping:
EmployeeId: SupportRepId
select_permissions:
@ -69,12 +69,12 @@ sourceMetadata =
SupportRepForCustomers:
Country:
_ceq: [ "$", "Country" ]
- table: Customer
- table: [Customer]
object_relationships:
- name: SupportRep
using:
manual_configuration:
remote_table: Employee
remote_table: [Employee]
column_mapping:
SupportRepId: EmployeeId
select_permissions: