Sqlite reference agent

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5263
Co-authored-by: Daniel Chambers <1214352+daniel-chambers@users.noreply.github.com>
GitOrigin-RevId: fb5cf3cb80ab59cd5ee0d064e3f776c062856aa3
This commit is contained in:
Lyndon Maydwell 2022-08-05 17:11:15 +10:00 committed by hasura-bot
parent 162e51c668
commit fc907201a0
71 changed files with 6932 additions and 0 deletions

3
dc-agents/sqlite/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
./*.sqlite

1
dc-agents/sqlite/.nvmrc Normal file
View File

@ -0,0 +1 @@
lts/gallium

View File

@ -0,0 +1,21 @@
FROM node:16-alpine
WORKDIR /app
COPY package.json .
COPY package-lock.json .
RUN npm ci
COPY tsconfig.json .
COPY src src
# This is just to ensure everything compiles ahead of time.
# We'll actually run using ts-node to ensure we get TypesScript
# stack traces if something fails at runtime.
RUN npm run typecheck
EXPOSE 8100
# We don't bother doing typechecking when we run (only TS->JS transpiling)
# because we checked it above already. This uses less memory at runtime.
CMD [ "npm", "run", "--silent", "start-no-typecheck" ]

150
dc-agents/sqlite/README.md Normal file
View File

@ -0,0 +1,150 @@
# Data Connector Agent for SQLite
This directory contains an SQLite implementation of a data connector agent.
It can use local SQLite database files as referenced by the "db" config field.
## Capabilities
The SQLite agent currently supports the following capabilities:
* [x] GraphQL Schema
* [x] GraphQL Queries
* [ ] GraphQL Mutations
* [x] Relationships
* [x] Aggregations
* [ ] Exposing Foreign-Key Information
* [ ] Subscriptions
* [ ] Streaming Subscriptions
Note: You are able to get detailed metadata about the agent's capabilities by
`GET`ting the `/capabilities` endpoint of the running agent.
## Requirements
* NodeJS 16
* SQLite `>= 3.38.0` or compiled in JSON support
* Required for the json_group_array() and json_group_object() aggregate SQL functions
* https://www.sqlite.org/json1.html#jgrouparray
## Build & Run
```sh
npm install
npm run build
npm run start
```
Or a simple dev-loop via `entr`:
```sh
echo src/**/*.ts | xargs -n1 echo | DB_READONLY=y entr -r npm run start
```
## Options / Environment Variables
* ENV: `PORT=[INT]` - Port for agent to listen on. 8100 by default.
* ENV: `PERMISSIVE_CORS={1|true|yes}` - Allows all requests - Useful for testing with SwaggerUI. Turn off on production.
* ENV: `DB_CREATE={1|true|yes}` - Allows new databases to be created, not permitted by default.
* ENV: `DB_READONLY={1|true|yes}` - Makes databases readonly, they are read-write by default.
* ENV: `DB_ALLOW_LIST=DB1[,DB2]*` - Restrict what databases can be connected to.
* ENV: `DB_PRIVATECACHE` - Keep caches between connections private. Shared by default.
* ENV: `DEBUGGING_TAGS` - Outputs xml style tags in query comments for deugging purposes.
## Agent usage
The agent is configured as per the configuration schema.
The only required field is `db` which specifies a local sqlite database to use.
The schema is exposed via introspection, but you can limit which tables are referenced by
* Explicitly enumerating them via the `tables` field, or
* Toggling the `include_sqlite_meta_tables` to include or exclude sqlite meta tables.
## Docker Build & Run
```
> docker build . -t dc-sqlite-agent:latest
> docker run -it --rm -p 8100:8100 dc-sqlite-agent:latest
```
You will want to mount a volume with your database(s) so that they can be referenced in configuration.
## Dataset
The dataset used for testing the reference agent is sourced from:
* https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sql
## Testing Changes to the Agent
Run:
```sh
cabal run graphql-engine:test:tests-dc-api -- test --agent-base-url http://localhost:8100 --agent-config '{"db": "db.chinook2.sqlite"}'
```
From the HGE repo.
## TODO
* [ ] Pull reference types from a package rather than checked-in files
* [x] Health Check
* [x] DB Specific Health Checks
* [x] Schema
* [x] Capabilities
* [x] Query
* [x] Array Relationships
* [x] Object Relationships
* [x] Ensure everything is escaped correctly - https://sequelize.org/api/v6/class/src/sequelize.js~sequelize#instance-method-escape
* [ ] Or... Use parameterized queries if possible - https://sequelize.org/docs/v6/core-concepts/raw-queries/#bind-parameter
* [x] Run test-suite from SDK
* [x] Remove old queries module
* [x] Relationships / Joins
* [x] Rename `resultTT` and other badly named types in the `schema.ts` module
* [x] Add ENV Variable for restriction on what databases can be used
* [x] Update to the latest types
* [x] Port back to hge codebase as an official reference agent
* [x] Make escapeSQL global to the query module
* [x] Make CORS permissions configurable
* [x] Optional DB Allowlist
* [x] Fix SDK Test suite to be more flexible about descriptions
* [x] READONLY option
* [x] CREATE option
* [x] Don't create DB option
* [x] Aggregate queries
* [x] Verbosity settings
* [x] Cache settings
* [x] Missing WHERE clause from object relationships
* [x] Reuse `find_table_relationship` in more scenarios
* [x] ORDER clause in aggregates breaks SQLite parser for some reason
* [x] Check that looped exist check doesn't cause name conflicts
* [ ] `NOT EXISTS IS NULL` != `EXISTS IS NOT NULL`, Example:
sqlite> create table test(testid string);
sqlite> .schema
CREATE TABLE test(testid string);
sqlite> select 1 where exists(select * from test where testid is null);
sqlite> select 1 where exists(select * from test where testid is not null);
sqlite> select 1 where not exists(select * from test where testid is null);
1
sqlite> select 1 where not exists(select * from test where testid is not null);
1
sqlite> insert into test(testid) values('foo');
sqlite> insert into test(testid) values(NULL);
sqlite> select * from test;
foo
sqlite> select 1 where exists(select * from test where testid is null);
1
sqlite> select 1 where exists(select * from test where testid is not null);
1
sqlite> select 1 where not exists(select * from test where testid is null);
sqlite> select 1 where exists(select * from test where testid is not null);
1
# Known Bugs
## Tricky Aggregates may have logic bug
Replicate by running the agent test-suite.

3832
dc-agents/sqlite/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
{
"name": "dc-agent-reference",
"version": "1.0.0",
"description": "Reference implementation of a Data Connector Agent for Hasura GraphQL Engine",
"author": "Hasura (https://github.com/hasura/graphql-engine)",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/hasura/graphql-engine.git"
},
"bugs": {
"url": "https://github.com/hasura/graphql-engine/issues"
},
"homepage": "https://github.com/hasura/graphql-engine#readme",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"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"
},
"dependencies": {
"@fastify/cors": "^7.0.0",
"fastify": "^3.29.0",
"openapi3-ts": "^2.0.2",
"pino-pretty": "^8.0.0",
"sequelize": "^6.21.2",
"sqlite": "^4.1.1",
"sqlite-parser": "^1.0.1",
"sqlite3": "^5.0.8",
"sqlstring-sqlite": "^0.1.1",
"xml2js": "^0.4.23"
},
"devDependencies": {
"@tsconfig/node16": "^1.0.2",
"@types/node": "^16.11.38",
"@types/sqlite3": "^3.1.8",
"@types/xml2js": "^0.4.11",
"ts-node": "^10.8.1",
"typescript": "^4.7.4"
}
}

View File

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

View File

@ -0,0 +1,60 @@
import { FastifyRequest } from "fastify"
import { SchemaObject } from "openapi3-ts"
export type Config = {
db: string,
tables: String[] | null,
meta: Boolean
}
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 ?? "{}";
const config = JSON.parse(rawConfigJson);
return {
db: config.db,
tables: config.tables ?? null,
meta: config.include_sqlite_meta_tables ?? false
}
}
export const configSchema: ConfigSchemaResponse = {
configSchema: {
type: "object",
nullable: false,
properties: {
db: {
description: "The SQLite database file to use.",
type: "string"
},
tables: {
description: "List of tables to make available in the schema and for querying",
type: "array",
items: { $ref: "#/otherSchemas/TableName" },
nullable: true
},
include_sqlite_meta_tables: {
description: "By default index tables, etc are not included, set this to true to include them.",
type: "boolean",
nullable: true
},
DEBUG: {
description: "For debugging.",
type: "object",
additionalProperties: true,
nullable: true
}
}
},
otherSchemas: {
TableName: {
nullable: false,
type: "string"
}
}
}

View File

@ -0,0 +1,36 @@
import { Config } from "./config";
import { Sequelize } from 'sequelize';
import { env } from "process";
import { stringToBool } from "./util";
import SQLite from 'sqlite3';
export function connect(config: Config): Sequelize {
if(env.DB_ALLOW_LIST != null) {
if(!env.DB_ALLOW_LIST.split(',').includes(config.db)) {
throw new Error(`Database ${config.db} is not present in DB_ALLOW_LIST 😭`);
}
}
// See https://github.com/TryGhost/node-sqlite3/wiki/API#new-sqlite3databasefilename--mode--callback
// mode (optional): One or more of
// * OPEN_READONLY
// * OPEN_READWRITE
// * OPEN_CREATE
// * OPEN_FULLMUTEX
// * OPEN_URI
// * OPEN_SHAREDCACHE
// * OPEN_PRIVATECACHE
// The default value is OPEN_READWRITE | OPEN_CREATE | OPEN_FULLMUTEX.
const readMode = stringToBool(process.env['DB_READONLY']) ? SQLite.OPEN_READONLY : SQLite.OPEN_READWRITE;
const createMode = stringToBool(process.env['DB_CREATE']) ? SQLite.OPEN_CREATE : 0; // Flag style means 0=off
const cacheMode = stringToBool(process.env['DB_PRIVATECACHE']) ? SQLite.OPEN_PRIVATECACHE : SQLite.OPEN_SHAREDCACHE;
const mode = readMode | createMode | cacheMode;
const db = new Sequelize({
dialect: 'sqlite',
storage: config.db,
dialectOptions: { mode: mode }
});
return db;
};

View File

@ -0,0 +1,108 @@
import Fastify from 'fastify';
import FastifyCors from '@fastify/cors';
import { getSchema } from './schema';
import { queryData } from './query';
import { getConfig } from './config';
import { CapabilitiesResponse, capabilitiesResponse} from './capabilities';
import { connect } from './db';
import { stringToBool } from './util';
import { QueryResponse, SchemaResponse, QueryRequest } from './types';
import * as fs from 'fs'
const port = Number(process.env.PORT) || 8100;
const server = Fastify({ logger: { prettyPrint: true } });
if(stringToBool(process.env['PERMISSIVE_CORS'])) {
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
server.register(FastifyCors, {
origin: true,
methods: ["GET", "POST", "OPTIONS"],
allowedHeaders: ["X-Hasura-DataConnector-Config", "X-Hasura-DataConnector-SourceName"]
});
}
server.get<{ Reply: CapabilitiesResponse }>("/capabilities", async (request, _response) => {
server.log.info({ headers: request.headers, query: request.body, }, "capabilities.request");
return capabilitiesResponse;
});
server.get<{ Reply: SchemaResponse }>("/schema", async (request, _response) => {
server.log.info({ headers: request.headers, query: request.body, }, "schema.request");
const config = getConfig(request);
return getSchema(config);
});
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);
return queryData(config, request.body);
});
server.get("/health", async (request, response) => {
const config = getConfig(request);
response.type('application/json');
if(config.db == null) {
server.log.info({ headers: request.headers, query: request.body, }, "health.request");
// response.statusCode = 204;
return { "status": "ok" };
} else {
server.log.info({ headers: request.headers, query: request.body, }, "health.db.request");
const db = connect(config);
const [r, m] = await db.query('select 1 where 1 = 1');
if(r && JSON.stringify(r) == '[{"1":1}]') {
response.statusCode = 204;
return { "status": "ok" };
} else {
response.statusCode = 500;
return { "error": "problem executing query", "query_result": r };
}
}
});
server.get("/swagger.json", async (request, response) => {
fs.readFile('src/types/agent.openapi.json', (err, fileBuffer) => {
response.type('application/json');
response.send(err || fileBuffer)
})
})
server.get("/", async (request, response) => {
response.type('text/html');
return `<!DOCTYPE html>
<html>
<head>
<title>Hasura Data Connectors SQLite Agent</title>
</head>
<body>
<h1>Hasura Data Connectors SQLite Agent</h1>
<p>See <a href="https://github.com/hasura/graphql-engine#hasura-graphql-engine">
the GraphQL Engine repository</a> for more information.</p>
<ul>
<li><a href="/">GET / - This Page</a>
<li><a href="/capabilities">GET /capabilities - Capabilities Metadata</a>
<li><a href="/schema">GET /schema - Agent Schema</a>
<li><a href="/query">POST /query - Query Handler</a>
<li><a href="/health">GET /health - Healthcheck</a>
<li><a href="/swagger.json">GET /swagger.json - Swagger JSON</a>
</ul>
</body>
</html>
`;
})
process.on('SIGINT', () => {
server.log.info("interrupted");
process.exit(0);
});
const start = async () => {
try {
await server.listen(port, "0.0.0.0");
}
catch (err) {
server.log.fatal(err);
process.exit(1);
}
};
start();

View File

@ -0,0 +1,487 @@
import { Config } from "./config";
import { connect } from "./db";
import { coerceUndefinedOrNullToEmptyArray, coerceUndefinedToNull, omap, last, coerceUndefinedOrNullToEmptyRecord, stringToBool, logDeep, isEmptyObject, tableNameEquals } from "./util";
import {
Expression,
BinaryComparisonOperator,
ComparisonValue,
QueryRequest,
ComparisonColumn,
TableRelationships,
Relationship,
RelationshipField,
BinaryArrayComparisonOperator,
OrderBy,
QueryResponse,
Field,
Aggregate,
TableName,
} from "./types";
const SqlString = require('sqlstring-sqlite');
/** Helper type for convenience. Uses the sqlstring-sqlite library, but should ideally use the function in sequalize.
*/
type Fields = Record<string, Field>
type Aggregates = Record<string, Aggregate>
function escapeString(x: any): string {
return SqlString.escape(x);
}
/**
*
* @param identifier: Unescaped name. E.g. 'Alb"um'
* @returns Escaped name. E.g. '"Alb\"um"'
*/
function escapeIdentifier(identifier: string): string {
// TODO: Review this function since the current implementation is off the cuff.
const result = identifier.replace(/\\/g,"\\\\").replace(/"/g,'\\"');
return `"${result}"`;
}
function extractRawTableName(tableName: TableName): string {
if (tableName.length === 1)
return tableName[0];
else
throw new Error(`${tableName.join(".")} is not a valid table`);
}
/**
*
* @param tableName: Unescaped table name. E.g. 'Alb"um'
* @returns Escaped table name. E.g. '"Alb\"um"'
*/
function escapeTableName(tableName: TableName): string {
return escapeIdentifier(extractRawTableName(tableName));
}
function json_object(ts: Array<TableRelationships>, fields: Fields, table: TableName): string {
const result = omap(fields, (k,v) => {
switch(v.type) {
case "column":
return [`${escapeString(k)}, ${escapeIdentifier(v.column)}`];
case "relationship":
const result = ts.flatMap((x) => {
if(tableNameEquals(x.source_table)(table)) {
const rel = x.relationships[v.relationship];
if(rel) {
return [`'${k}', ${relationship(ts, rel, v, table)}`];
}
}
return [];
});
if(result.length < 1) {
console.log("Couldn't find relationship for field", k, v, ts);
}
return result;
}
}).flatMap((e) => e).join(", ");
return tag('json_object', `JSON_OBJECT(${result})`);
}
function where_clause(ts: Array<TableRelationships>, w: Expression | null, t: TableName): Array<string> {
if(w === null) {
return [];
} else {
switch(w.type) {
case "not":
const aNot = where_clause(ts, w.expression, t);
if(aNot.length > 0) {
return [`(NOT ${aNot})`];
}
break;
case "and":
const aAnd = w.expressions.flatMap(x => where_clause(ts, x, t));
if(aAnd.length > 0) {
return [`(${aAnd.join(" AND ")})`];
}
break;
case "or":
const aOr = w.expressions.flatMap(x => where_clause(ts, x, t));
if(aOr.length > 0) {
return [`(${aOr.join(" OR ")})`];
}
break;
case "unary_op":
switch(w.operator) {
case 'is_null':
if(w.column.path.length < 1) {
return [`(${escapeIdentifier(w.column.name)} IS NULL)`];
} else {
return [exists(ts, w.column, t, 'IS NULL')];
}
default:
if(w.column.path.length < 1) {
return [`(${escapeIdentifier(w.column.name)} ${w.operator})`];
} else {
return [exists(ts, w.column, t, w.operator)];
}
}
case "binary_op":
const bop = bop_op(w.operator);
if(w.column.path.length < 1) {
return [`${escapeIdentifier(w.column.name)} ${bop} ${bop_val(w.value, t)}`];
} else {
return [exists(ts, w.column, t, `${bop} ${bop_val(w.value, t)}`)];
}
case "binary_arr_op":
const bopA = bop_array(w.operator);
if(w.column.path.length < 1) {
return [`(${escapeIdentifier(w.column.name)} ${bopA} (${w.values.map(v => escapeString(v)).join(", ")}))`];
} else {
return [exists(ts,w.column,t, `${bopA} (${w.values.map(v => escapeString(v)).join(", ")})`)];
}
}
return [];
}
}
function exists(ts: Array<TableRelationships>, c: ComparisonColumn, t: TableName, o: string): string {
// NOTE: An N suffix doesn't guarantee that conflicts are avoided.
const r = join_path(ts, t, c.path, 0);
const f = `FROM ${r.f.map(x => `${x.from} AS ${x.as}`).join(', ')}`;
return tag('exists',`EXISTS (SELECT 1 ${f} WHERE ${[...r.j, `${last(r.f).as}.${escapeIdentifier(c.name)} ${o}`].join(' AND ')})`);
}
/** Develops a from clause for an operation with a path - a relationship referenced column
*
* Artist [Albums] Title
* FROM Album Album_PATH_XX ...
* WHERE Album_PATH_XX.ArtistId = Artist.ArtistId
* Album_PATH_XX.Title IS NULL
*
* @param ts
* @param table
* @param path
* @returns the from clause for the EXISTS query
*/
function join_path(ts: TableRelationships[], table: TableName, path: Array<string>, level: number): {f: Array<{from: string, as: string}>, j: string[]} {
const r = find_table_relationship(ts, table);
if(path.length < 1) {
return {f: [], j: []};
} else if(r === null) {
throw new Error(`Couldn't find relationship ${ts}, ${table.join(".")} - This shouldn't happen.`);
} else {
const x = r.relationships[path[0]];
const n = join_path(ts, x.target_table, path.slice(1), level+1);
const m =
omap(
x.column_mapping,
(sourceColumnName,targetColumnName) =>
`${depthQualifyIdentifier(level-1,extractRawTableName(table))}.${escapeIdentifier(sourceColumnName)} = ${depthQualifyIdentifier(level, extractRawTableName(x.target_table))}.${escapeIdentifier(targetColumnName)}`
)
.join(' AND ');
return {f: [{from: escapeTableName(x.target_table), as: depthQualifyIdentifier(level, extractRawTableName(x.target_table))}, ...n.f], j: [m, ...n.j]};
}
}
function depthQualifyIdentifier(depth: number, identifier:string): string {
if(depth < 0) {
return escapeIdentifier(identifier);
} else {
return escapeIdentifier(`${identifier}_${depth}`);
}
}
/**
*
* @param ts Array of Table Relationships
* @param t Table Name
* @returns Relationships matching table-name
*/
function find_table_relationship(ts: Array<TableRelationships>, t: TableName): TableRelationships | null {
for(var i = 0; i < ts.length; i++) {
const r = ts[i];
if(tableNameEquals(r.source_table)(t)) {
return r;
}
}
return null;
}
function cast_aggregate_function(f: string): string {
switch(f) {
case 'avg':
case 'max':
case 'min':
case 'sum':
case 'total':
return f;
default:
throw new Error(`Aggregate function ${f} is not supported by SQLite. See: https://www.sqlite.org/lang_aggfunc.html`);
}
}
/**
* Builds an Aggregate query expression.
*
* NOTE: ORDER Clauses are currently broken due to SQLite parser issue.
*
* @param table
* @param aggregates
* @param innerFromClauses
* @returns
*/
function aggregates_query(
table: TableName,
aggregates: Aggregates,
innerFromClauses: string,
): Array<string> {
if(isEmptyObject(aggregates)) {
return [];
} else {
const aggregate_pairs = omap(aggregates, (k,v) => {
switch(v.type) {
case 'star_count':
return `${escapeString(k)}, COUNT(*)`;
case 'column_count':
if(v.distinct) {
return `${escapeString(k)}, COUNT(DISTINCT ${escapeIdentifier(v.column)})`;
} else {
return `${escapeString(k)}, COUNT(${escapeIdentifier(v.column)})`;
}
case 'single_column':
return `${escapeString(k)}, ${cast_aggregate_function(v.function)}(${escapeIdentifier(v.column)})`;
}
}).join(', ');
return [`'aggregates', (SELECT JSON_OBJECT(${aggregate_pairs}) FROM (SELECT * from ${escapeTableName(table)} ${innerFromClauses}))`]
}
}
function array_relationship(
ts: Array<TableRelationships>,
table: TableName,
wJoin: Array<string>,
fields: Fields,
aggregates: Aggregates,
wWhere: Expression | null,
wLimit: number | null,
wOffset: number | null,
wOrder: Array<OrderBy>,
): string {
const innerFromClauses = `${where(ts, wWhere, wJoin, table)} ${order(wOrder)} ${limit(wLimit)} ${offset(wOffset)}`;
const aggregateSelect = aggregates_query(table, aggregates, innerFromClauses);
const fieldSelect = isEmptyObject(fields) ? [] : [`'rows', JSON_GROUP_ARRAY(j)`];
const fieldFrom = isEmptyObject(fields) ? '' : (() => {
// NOTE: The order of table prefixes are currently assumed to be from "parent" to "child".
// NOTE: The reuse of the 'j' identifier should be safe due to scoping. This is confirmed in testing.
if(wOrder.length < 1) {
return `FROM ( SELECT ${json_object(ts, fields, table)} AS j FROM ${escapeTableName(table)} ${innerFromClauses})`;
} else {
const innerSelect = `SELECT * FROM ${escapeTableName(table)} ${innerFromClauses}`;
return `FROM (SELECT ${json_object(ts, fields, table)} AS j FROM (${innerSelect}) AS ${table})`;
}
})()
return tag('array_relationship',`(SELECT JSON_OBJECT(${[...fieldSelect, ...aggregateSelect].join(', ')}) ${fieldFrom})`);
}
function object_relationship(
ts: Array<TableRelationships>,
table: TableName,
wJoin: Array<string>,
fields: Fields,
): string {
// NOTE: The order of table prefixes are from "parent" to "child".
const innerFrom = `${escapeTableName(table)} ${where(ts, null, wJoin, table)}`;
return tag('object_relationship',
`(SELECT JSON_OBJECT('rows', JSON_ARRAY(${json_object(ts, fields, table)})) AS j FROM ${innerFrom})`);
}
function relationship(ts: Array<TableRelationships>, r: Relationship, field: RelationshipField, table: TableName): string {
const wJoin = omap(
r.column_mapping,
(k,v) => `${escapeTableName(table)}.${escapeIdentifier(k)} = ${escapeTableName(r.target_table)}.${escapeIdentifier(v)}`
);
switch(r.relationship_type) {
case 'object':
return tag('relationship', object_relationship(
ts,
r.target_table,
wJoin,
coerceUndefinedOrNullToEmptyRecord(field.query.fields),
));
case 'array':
return tag('relationship', array_relationship(
ts,
r.target_table,
wJoin,
coerceUndefinedOrNullToEmptyRecord(field.query.fields),
coerceUndefinedOrNullToEmptyRecord(field.query.aggregates),
coerceUndefinedToNull(field.query.where),
coerceUndefinedToNull(field.query.limit),
coerceUndefinedToNull(field.query.offset),
coerceUndefinedOrNullToEmptyArray(field.query.order_by),
));
}
}
// TODO: There is a bug in this implementation where vals can reference columns with paths.
function bop_col(c: ComparisonColumn, t: TableName): string {
if(c.path.length < 1) {
return tag('bop_col', `${escapeTableName(t)}.${escapeIdentifier(c.name)}`);
} else {
throw new Error(`bop_col shouldn't be handling paths.`);
}
}
function bop_array(o: BinaryArrayComparisonOperator): string {
switch(o) {
case 'in': return tag('bop_array','IN');
default: return tag('bop_array', o);
}
}
function bop_op(o: BinaryComparisonOperator): string {
let result = o;
switch(o) {
case 'equal': result = "="; break;
case 'greater_than': result = ">"; break;
case 'greater_than_or_equal': result = ">="; break;
case 'less_than': result = "<"; break;
case 'less_than_or_equal': result = "<="; break;
}
return tag('bop_op',result);
}
function bop_val(v: ComparisonValue, t: TableName): string {
switch(v.type) {
case "column": return tag('bop_val', bop_col(v.column, t));
case "scalar": return tag('bop_val', escapeString(v.value));
}
}
function order(o: Array<OrderBy>): string {
if(o.length < 1) {
return "";
}
const result = o.map(e => `${e.column} ${e.ordering}`).join(', ');
return tag('order',`ORDER BY ${result}`);
}
/**
* @param whereExpression Nested expression used in the associated where clause
* @param joinArray Join clauses
* @returns string representing the combined where clause
*/
function where(ts: Array<TableRelationships>, whereExpression: Expression | null, joinArray: Array<string>, t: TableName): string {
const clauses = [...where_clause(ts, whereExpression, t), ...joinArray];
if(clauses.length < 1) {
return "";
} else {
return tag('where',`WHERE ${clauses.join(" AND ")}`);
}
}
function limit(l: number | null): string {
if(l === null) {
return "";
} else {
return tag('limit',`LIMIT ${l}`);
}
}
function offset(o: number | null): string {
if(o === null) {
return "";
} else {
return tag('offset', `OFFSET ${o}`);
}
}
/** Top-Level Query Function.
*/
function query(request: QueryRequest): string {
const result = array_relationship(
request.table_relationships,
request.table,
[],
coerceUndefinedOrNullToEmptyRecord(request.query.fields),
coerceUndefinedOrNullToEmptyRecord(request.query.aggregates),
coerceUndefinedToNull(request.query.where),
coerceUndefinedToNull(request.query.limit),
coerceUndefinedToNull(request.query.offset),
coerceUndefinedOrNullToEmptyArray(request.query.order_by),
);
return tag('query', `SELECT ${result} as data`);
}
/** Format the DB response into a /query response.
*
* Note: There should always be one result since 0 rows still generates an empty JSON array.
*/
function output(rows: any): QueryResponse {
return JSON.parse(rows[0].data);
}
const DEBUGGING_TAGS = stringToBool(process.env['DEBUGGING_TAGS']);
/** Function to add SQL comments to the generated SQL to tag which procedures generated what text.
*
* comment('a','b') => '/*\<a>\*\/ b /*\</a>*\/'
*/
function tag(t: string, s: string): string {
if(DEBUGGING_TAGS) {
return `/*<${t}>*/ ${s} /*</${t}>*/`;
} else {
return s;
}
}
/** Performs a query and returns results
*
* Limitations:
*
* - Binary Array Operations not currently supported.
*
* The current algorithm is to first create a query, then execute it, returning results.
*
* Method for adding relationship fields:
*
* - JSON aggregation similar to Postgres' approach.
* - 4.13. The json_group_array() and json_group_object() aggregate SQL functions
* - https://www.sqlite.org/json1.html#jgrouparray
*
* Example of a test query:
*
* ```
* query MyQuery {
* Artist(limit: 5, order_by: {ArtistId: asc}, where: {Name: {_neq: "Accept"}, _and: {Name: {_is_null: false}}}, offset: 3) {
* ArtistId
* Name
* Albums(where: {Title: {_is_null: false, _gt: "A", _nin: "foo"}}, limit: 2) {
* AlbumId
* Title
* ArtistId
* Tracks(limit: 1) {
* Name
* TrackId
* }
* Artist {
* ArtistId
* }
* }
* }
* Track(limit: 3) {
* Name
* Album {
* Title
* }
* }
* }
* ```
*
*/
export async function queryData(config: Config, queryRequest: QueryRequest): Promise<QueryResponse> {
const db = connect(config); // TODO: Should this be cached?
const q = query(queryRequest);
const [result, metadata] = await db.query(q);
return output(result);
}

View File

@ -0,0 +1,161 @@
import { SchemaResponse, ScalarType, ColumnInfo, TableInfo } from "./types"
import { Config } from "./config";
import { connect } from './db';
var sqliteParser = require('sqlite-parser');
type TableInfoInternal = {
name: string,
type: string,
tbl_name: string,
rootpage: Number,
sql: string
}
/**
*
* @param ColumnInfoInternalype as per HGE DataConnector IR
* @returns SQLite's corresponding column type
*
* Note: This defaults to "string" when a type is not anticipated
* in order to be as permissive as possible but logs when
* this happens.
*/
function columnCast(ColumnInfoInternalype: string): ScalarType {
switch(ColumnInfoInternalype) {
case "string":
case "number":
case "bool": return ColumnInfoInternalype as ScalarType;
case "boolean": return "bool";
case "numeric": return "number";
case "integer": return "number";
case "double": return "number";
case "float": return "number";
case "text": return "string";
default:
console.log(`Unknown SQLite column type: ${ColumnInfoInternalype}. Interpreting as string.`)
return "string";
}
}
function getColumns(ast : Array<any>) : Array<ColumnInfo> {
return ast.map(c => {
return ({
name: c.name,
type: columnCast(datatypeCast(c.datatype)),
nullable: nullableCast(c.definition)
})
})
}
// Interpret the sqlite-parser datatype as a schema column response type.
function datatypeCast(d: any): any {
switch(d.variant) {
case "datetime": return 'string';
default: return d.affinity;
}
}
function nullableCast(ds: Array<any>): boolean {
for(var d of ds) {
if(d.type === 'constraint' && d.variant == 'not null') {
return false;
}
}
return true;
}
function formatTableInfo(info : TableInfoInternal): TableInfo {
const ast = sqliteParser(info.sql);
const ddl = ddlColumns(ast);
const pks = ddlPKs(ast);
const pk = pks.length > 0 ? { primary_key: pks } : {};
// TODO: Should we include something for the description here?
return {
name: [info.name],
...pk,
description: info.sql,
columns: getColumns(ddl)
}
}
/**
* @param table
* @returns true if the table is an SQLite meta table such as a sequence, index, etc.
*/
function isMeta(table : TableInfoInternal) {
return table.type != 'table';
}
function includeTable(config: Config, table: TableInfoInternal): boolean {
if(config.tables === null) {
if(isMeta(table) && ! config.meta) {
return false;
}
return true;
} else {
return config.tables.indexOf(table.name) >= 0
}
}
/**
* Pulls columns from the output of sqlite-parser.
* Note that this doesn't check if duplicates are present and will emit them as many times as they are present.
* This is done as an easy way to preserve order.
*
* @param ddl - The output of sqlite-parser
* @returns - List of columns as present in the output of sqlite-parser.
*/
function ddlColumns(ddl: any): Array<any> {
if(ddl.type != 'statement' || ddl.variant != 'list') {
throw new Error("Encountered a non-statement or non-list when parsing DDL for table.");
}
return ddl.statement.flatMap((t: any) => {
if(t.type != 'statement' || t.variant != 'create' || t.format != 'table') {
return [];
}
return t.definition.flatMap((c: any) => {
if(c.type != 'definition' || c.variant != 'column') {
return [];
}
return [c];
});
})
}
function ddlPKs(ddl: any): Array<any> {
if(ddl.type != 'statement' || ddl.variant != 'list') {
throw new Error("Encountered a non-statement or non-list when parsing DDL for table.");
}
return ddl.statement.flatMap((t: any) => {
if(t.type != 'statement' || t.variant != 'create' || t.format != 'table') {
return [];
}
return t.definition.flatMap((c: any) => {
if(c.type != 'definition' || c.variant != 'constraint'
|| c.definition.length != 1 || c.definition[0].type != 'constraint' || c.definition[0].variant != 'primary key') {
return [];
}
return c.columns.flatMap((x:any) => {
if(x.type == 'identifier' && x.variant == 'column') {
return [x.name];
} else {
return [];
}
});
});
})
}
export async function getSchema(config: Config): Promise<SchemaResponse> {
const db = connect(config);
const [results, metadata] = await db.query("SELECT * from sqlite_schema");
const resultsT: Array<TableInfoInternal> = results as unknown as Array<TableInfoInternal>;
const filtered: Array<TableInfoInternal> = resultsT.filter(table => includeTable(config,table));
const result: Array<TableInfo> = filtered.map(formatTableInfo);
return {
tables: result
};
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type { Aggregate } from './models/Aggregate';
export type { AndExpression } from './models/AndExpression';
export type { AnotherColumnComparison } from './models/AnotherColumnComparison';
export type { ApplyBinaryArrayComparisonOperator } from './models/ApplyBinaryArrayComparisonOperator';
export type { ApplyBinaryComparisonOperator } from './models/ApplyBinaryComparisonOperator';
export type { ApplyUnaryComparisonOperator } from './models/ApplyUnaryComparisonOperator';
export type { 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 { ColumnCountAggregate } from './models/ColumnCountAggregate';
export type { ColumnField } from './models/ColumnField';
export type { ColumnFieldValue } from './models/ColumnFieldValue';
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 { NullColumnFieldValue } from './models/NullColumnFieldValue';
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 { SingleColumnAggregate } from './models/SingleColumnAggregate';
export type { SingleColumnAggregateFunction } from './models/SingleColumnAggregateFunction';
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

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ColumnCountAggregate } from './ColumnCountAggregate';
import type { SingleColumnAggregate } from './SingleColumnAggregate';
import type { StarCountAggregate } from './StarCountAggregate';
export type Aggregate = (ColumnCountAggregate | SingleColumnAggregate | StarCountAggregate);

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 ApplyBinaryArrayComparisonOperator = {
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,6 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type BinaryArrayComparisonOperator = ('in' | string);

View File

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

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,16 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ColumnCountAggregate = {
/**
* The column to apply the count aggregate function to
*/
column: string;
/**
* Whether or not only distinct items should be counted
*/
distinct: boolean;
type: 'column_count';
};

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,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ColumnFieldValue = {
};

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 = (ScalarValueComparison | AnotherColumnComparison);

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 { ApplyBinaryArrayComparisonOperator } from './ApplyBinaryArrayComparisonOperator';
import type { ApplyBinaryComparisonOperator } from './ApplyBinaryComparisonOperator';
import type { ApplyUnaryComparisonOperator } from './ApplyUnaryComparisonOperator';
import type { NotExpression } from './NotExpression';
import type { OrExpression } from './OrExpression';
export type Expression = (ApplyBinaryArrayComparisonOperator | OrExpression | ApplyUnaryComparisonOperator | ApplyBinaryComparisonOperator | NotExpression | AndExpression);

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 = (RelationshipField | ColumnField);

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,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type NullColumnFieldValue = null;

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,33 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Aggregate } from './Aggregate';
import type { Expression } from './Expression';
import type { Field } from './Field';
import type { OrderBy } from './OrderBy';
export type Query = {
/**
* Aggregate fields of the query
*/
aggregates?: Record<string, Aggregate> | null;
/**
* Fields of the query
*/
fields?: Record<string, Field> | null;
/**
* 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,17 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Query } from './Query';
import type { TableName } from './TableName';
import type { TableRelationships } from './TableRelationships';
export type QueryRequest = {
query: Query;
table: TableName;
/**
* The relationships between tables involved in the entire query request
*/
table_relationships: Array<TableRelationships>;
};

View File

@ -0,0 +1,19 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ColumnFieldValue } from './ColumnFieldValue';
import type { NullColumnFieldValue } from './NullColumnFieldValue';
import type { ScalarValue } from './ScalarValue';
export type QueryResponse = {
/**
* The results of the aggregates returned by the query
*/
aggregates?: Record<string, ScalarValue> | null;
/**
* The rows returned by the query, corresponding to the query's fields
*/
rows?: Array<Record<string, (ColumnFieldValue | QueryResponse | NullColumnFieldValue)>> | null;
};

View File

@ -0,0 +1,16 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RelationshipType } from './RelationshipType';
import type { TableName } from './TableName';
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;
target_table: TableName;
};

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,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { SingleColumnAggregateFunction } from './SingleColumnAggregateFunction';
export type SingleColumnAggregate = {
/**
* The column to apply the aggregation function to
*/
column: string;
function: SingleColumnAggregateFunction;
type: 'single_column';
};

View File

@ -0,0 +1,5 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SingleColumnAggregateFunction = 'avg' | 'max' | 'min' | 'stddev_pop' | 'stddev_samp' | 'sum' | 'var_pop' | 'var_samp';

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type StarCountAggregate = {
type: 'star_count';
};

View File

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

View File

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

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

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* 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>;
source_table: TableName;
};

View File

@ -0,0 +1,6 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type UnaryComparisonOperator = ('is_null' | string);

View File

@ -0,0 +1,60 @@
import { TableName } from "./types";
export const coerceUndefinedToNull = <T>(v: T | undefined): T | null => v === undefined ? null : v;
export const coerceUndefinedOrNullToEmptyArray = <T>(v: Array<T> | undefined | null): Array<T> => v == null ? [] : v;
export const coerceUndefinedOrNullToEmptyRecord = <K extends string | number | symbol, V>(v: Record<K,V> | undefined | null): Record<K,V> => v == null ? {} as Record<K,V> : 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);
const newArray = Array(length);
for (let i = 0; i < length; i++) {
newArray[i] = [arr1[i], arr2[i]];
}
return newArray;
};
export const crossProduct = <T, U>(arr1: T[], arr2: U[]): [T,U][] => {
return arr1.flatMap(a1 => arr2.map(a2 => [a1, a2]) as [T,U][]);
};
export function omap<V,O>(m: { [x: string]: V; },f: (k: string, v: V) => O) {
return Object.keys(m).map(k => f(k, m[k]))
}
export function stringToBool(x: string | null | undefined): boolean {
return (/1|true|t|yes|y/i).test(x || '');
}
export function last<T>(x: Array<T>): T {
return x[x.length - 1];
}
export function logDeep(msg: string, myObject: any): void {
const util = require('util');
console.log(msg, util.inspect(myObject, {showHidden: true, depth: null, colors: true}));
}
export function isEmptyObject(obj: Record<string, any>): boolean {
return Object.keys(obj).length === 0;
}
/**
* Usage: `await delay(5000)`
*
* @param ms
* @returns
*/
export function delay(ms: number): Promise<void> {
return new Promise( resolve => setTimeout(resolve, ms) );
}
export const tableNameEquals = (tableName1: TableName) => (tableName2: TableName): boolean => {
if (tableName1.length !== tableName2.length)
return false;
return zip(tableName1, tableName2).every(([n1, n2]) => n1 === n2);
}

Binary file not shown.

View File

@ -0,0 +1,10 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"resolveJsonModule": true,
},
"include": [
"src/**/*"
]
}

View File

@ -83,6 +83,13 @@ services:
ports:
- "65005:8100"
dc-sqlite-agent:
build: ./dc-agents/sqlite/
ports:
- "65006:8100"
volumes:
- "./dc-agents/sqlite/test/db.chinook.sqlite:/db.chinook.sqlite"
volumes:
citus-data:
mariadb-data: