diff --git a/src/graphql/hasura-schema-loader.ts b/src/graphql/hasura-schema-loader.ts index 01eecbd..5bf4bd3 100644 --- a/src/graphql/hasura-schema-loader.ts +++ b/src/graphql/hasura-schema-loader.ts @@ -1,7 +1,7 @@ import {ok} from 'assert'; import axios, {AxiosInstance} from 'axios'; import fs from 'fs-extra'; -import {camelCase, find} from 'lodash'; +import {camelCase, find, snakeCase} from 'lodash'; import path from 'path'; import toposort from 'toposort'; import {Dictionary} from 'ts-essentials'; @@ -11,11 +11,14 @@ import { foreignKeyForArray, foreignKeyForObj, isManualConfiguration, + MULTI_TENANT_COLUMNS, + parsePrimaryKeys, SchemaLoader, } from './schema'; import { ArrayForeignKey, ArrayRelationship, + BackReference, ObjectRelationship, Reference, Schema, @@ -75,13 +78,7 @@ export class HasuraSchemaLoader implements SchemaLoader { result .filter((row) => row[0] !== 'table_name') .forEach(([table, exp]) => { - // TODO: better way to do this? - primaryKeys[table] = exp - .replace('pkey(VARIADIC ARRAY[', '') - .replace('])', '') - .split(', ') - .map((col) => col.replace(/"/g, '')) - .map((col) => (this.camelCaseFieldNames ? camelCase(col) : col)); + primaryKeys[table] = parsePrimaryKeys(exp, this.camelCaseFieldNames); }); return primaryKeys; } @@ -95,7 +92,7 @@ export class HasuraSchemaLoader implements SchemaLoader { * targetTable (e.g. cicd_Pipeline): table.table.name * * The output, res, can be used as: - * res['cicd_Build']['pipeline_id'] => 'cicd_Pipeline + * res['cicd_Build']['pipeline_id'] => 'cicd_Pipeline' */ static indexFkTargetModels(source: Source): Dictionary> { const res: Dictionary> = {}; @@ -129,7 +126,7 @@ export class HasuraSchemaLoader implements SchemaLoader { const tableNames = []; const scalars: Dictionary> = {}; const references: Dictionary> = {}; - const backReferences: Dictionary = {}; + const backReferences: Dictionary = {}; for (const table of source.tables) { const tableName = table.table.name; tableNames.push(tableName); @@ -148,8 +145,10 @@ export class HasuraSchemaLoader implements SchemaLoader { ); const tableScalars: Dictionary = {}; for (const scalar of scalarTypes) { - tableScalars[scalar.name] = - scalar.type.ofType?.name ?? scalar.type.name; + if (!MULTI_TENANT_COLUMNS.has(snakeCase(scalar.name))) { + tableScalars[scalar.name] = + scalar.type.ofType?.name ?? scalar.type.name; + } } scalars[tableName] = tableScalars; const tableReferences: Dictionary = {}; @@ -159,6 +158,7 @@ export class HasuraSchemaLoader implements SchemaLoader { const relMetadata = { field: rel.name, model: targetTableByFk[table.table.name][fk], + foreignKey: relFldName }; // index relation metadata using both FK column and rel.name // this is needed for cross-compatibility with CE and SaaS diff --git a/src/graphql/schema.ts b/src/graphql/schema.ts index c2c087c..1774c11 100644 --- a/src/graphql/schema.ts +++ b/src/graphql/schema.ts @@ -1,5 +1,5 @@ import {ok} from 'assert'; -import _ from 'lodash'; +import _, {camelCase} from 'lodash'; import { ArrayForeignKey, @@ -10,6 +10,9 @@ import { Schema, } from './types'; +export const MULTI_TENANT_COLUMNS = new Set(['tenant_id', 'graph_name']); +const PG_TYPE_REGEX = /\((.*)\)::.*/; + export function foreignKeyForObj(rel: ObjectRelationship): string { if (isManualConfiguration(rel.using)) { // for object rel, column_mapping contains one entry @@ -46,6 +49,31 @@ export function foreignKeyForArray(rel: ArrayRelationship): string { return fk; } +/** + * Parse elements of primary key from pkey function definition. + * e.g. pkey(VARIADIC ARRAY[tenant_id, graph_name, source, uid]) + */ +export function parsePrimaryKeys( + exp: string, + camelCaseFieldNames: boolean, + includeMultiTenantColumns = false +): string [] { + return exp + .replace('pkey(VARIADIC ARRAY[', '') + .replace('])', '') + .split(', ') + .map((col) => col.replace(/"/g, '')) + .map((col) => { + // extract col from types e.g. foo::text => foo + const matches = col.match(PG_TYPE_REGEX); + return matches ? matches[1] : col; + }) + .filter((col) => + // conditionally filter multi-tenant columns + includeMultiTenantColumns || !MULTI_TENANT_COLUMNS.has(col)) + .map((col) => (camelCaseFieldNames ? camelCase(col) : col)); +} + export function isManualConfiguration( using: ManualConfiguration | any ): using is ManualConfiguration { diff --git a/src/graphql/types.ts b/src/graphql/types.ts index 62c863a..e798102 100644 --- a/src/graphql/types.ts +++ b/src/graphql/types.ts @@ -36,16 +36,20 @@ export interface ArrayRelationship { using: ArrayForeignKey | ManualConfiguration; } -export interface Reference { +export interface BackReference { field: string; model: string; } +export interface Reference extends BackReference { + foreignKey: string; +} + export interface Schema { primaryKeys: Dictionary; scalars: Dictionary>; references: Dictionary>; - backReferences: Dictionary; + backReferences: Dictionary; sortedModelDependencies: ReadonlyArray; tableNames: ReadonlyArray; } diff --git a/src/index.ts b/src/index.ts index 32992a0..33c7287 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ export { ObjectForeignKey, ObjectRelationship, Reference, + BackReference, Schema, PathToModel, Query, diff --git a/test/client.test.ts b/test/client.test.ts index 5408513..bf147ff 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -131,6 +131,7 @@ describe('client', () => { owner: { field: 'cal_User', model: 'cal_User', + foreignKey: 'user' }, }, }, diff --git a/test/utils.test.ts b/test/utils.test.ts index 1311eaf..48cf6f2 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -1,3 +1,4 @@ +import {parsePrimaryKeys} from '../src/graphql/schema'; import * as sut from '../src/utils'; describe('utils', () => { @@ -10,3 +11,42 @@ describe('utils', () => { expect(f('https://example.com///')).toEqual('https://example.com'); }); }); + +describe('parse primary keys', () => { + test('multi-tenant columns', () => { + expect(parsePrimaryKeys( + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, source, uid])', + true, + false) + ).toEqual(['source', 'uid']); + expect(parsePrimaryKeys( + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, source, uid])', + true, + true) + ).toEqual(['tenantId', 'graphName', 'source', 'uid']); + }); + test('camel case', () => { + expect(parsePrimaryKeys( + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, source, uid])', + false, + true) + ).toEqual(['tenant_id', 'graph_name', 'source', 'uid']); + }); + test('remove PG type info', () => { + expect(parsePrimaryKeys( + // eslint-disable-next-line max-len + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, (number)::text, repository_id])', + true) + ).toEqual(['number', 'repositoryId']); + expect(parsePrimaryKeys( + // eslint-disable-next-line max-len + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, (number)::text, repository_id])', + false) + ).toEqual(['number', 'repository_id']); + expect(parsePrimaryKeys( + // eslint-disable-next-line max-len + 'pkey(VARIADIC ARRAY[tenant_id, graph_name, (lat)::text, (lon)::text])', + true) + ).toEqual(['lat', 'lon']); + }); +});