feat: conditional schema based on column map instead of column field (#1978)
* feat: wip conditional schema based on column map instead of column field * feat: conditionalSchema columnMap and singular plural * fix: remove uuid fix * feat: add name and label (singular/plural) drop old tableColumnName
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldMetadataTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { convertArguments } from 'src/tenant/entity-resolver/utils/convert-arguments.util';
|
||||
|
||||
describe('convertArguments', () => {
|
||||
let fields;
|
||||
|
||||
beforeEach(() => {
|
||||
fields = [
|
||||
{
|
||||
nameSingular: 'firstName',
|
||||
targetColumnMap: {
|
||||
value: 'column_1randomFirstNameKey',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
nameSingular: 'age',
|
||||
targetColumnMap: {
|
||||
value: 'column_randomAgeKey',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
nameSingular: 'website',
|
||||
targetColumnMap: {
|
||||
link: 'column_randomLinkKey',
|
||||
text: 'column_randomTex7Key',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
type: 'url',
|
||||
},
|
||||
] as FieldMetadata[];
|
||||
});
|
||||
|
||||
test('should handle non-array arguments', () => {
|
||||
const args = { firstName: 'John', age: 30 };
|
||||
const expected = {
|
||||
column_1randomFirstNameKey: 'John',
|
||||
column_randomAgeKey: 30,
|
||||
};
|
||||
expect(convertArguments(args, fields)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should handle array arguments', () => {
|
||||
const args = [{ firstName: 'John' }, { firstName: 'Jane' }];
|
||||
const expected = [
|
||||
{ column_1randomFirstNameKey: 'John' },
|
||||
{ column_1randomFirstNameKey: 'Jane' },
|
||||
];
|
||||
expect(convertArguments(args, fields)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should handle nested object arguments', () => {
|
||||
const args = { website: { link: 'https://www.google.fr', text: 'google' } };
|
||||
const expected = {
|
||||
column_randomLinkKey: 'https://www.google.fr',
|
||||
column_randomTex7Key: 'google',
|
||||
};
|
||||
expect(convertArguments(args, fields)).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should ignore fields not in the field metadata', () => {
|
||||
const args = { firstName: 'John', lastName: 'Doe' };
|
||||
const expected = { column_1randomFirstNameKey: 'John', lastName: 'Doe' };
|
||||
expect(convertArguments(args, fields)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldMetadataTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { convertFieldsToGraphQL } from 'src/tenant/entity-resolver/utils/convert-fields-to-graphql.util';
|
||||
|
||||
const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim();
|
||||
|
||||
describe('convertFieldsToGraphQL', () => {
|
||||
let fields;
|
||||
|
||||
beforeEach(() => {
|
||||
fields = [
|
||||
{
|
||||
nameSingular: 'simpleField',
|
||||
targetColumnMap: {
|
||||
value: 'column_RANDOMSTRING1',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
},
|
||||
{
|
||||
nameSingular: 'complexField',
|
||||
targetColumnMap: {
|
||||
link: 'column_RANDOMSTRING2',
|
||||
text: 'column_RANDOMSTRING3',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
},
|
||||
] as FieldMetadata[];
|
||||
});
|
||||
|
||||
test('should handle simple fields correctly', () => {
|
||||
const select = { simpleField: true };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = 'simpleField: column_RANDOMSTRING1\n';
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should handle complex fields with multiple values correctly', () => {
|
||||
const select = { complexField: true };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = `
|
||||
___complexField_link: column_RANDOMSTRING2
|
||||
___complexField_text: column_RANDOMSTRING3
|
||||
`;
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should handle fields not in the field metadata correctly', () => {
|
||||
const select = { unknownField: true };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = 'unknownField\n';
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should handle nested object fields correctly', () => {
|
||||
const select = { parentField: { childField: true } };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = 'parentField {\nchildField\n}\n';
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should handle nested selections with multiple levels correctly', () => {
|
||||
const select = {
|
||||
level1: {
|
||||
level2: {
|
||||
simpleField: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected =
|
||||
'level1 {\nlevel2 {\nsimpleField: column_RANDOMSTRING1\n}\n}\n';
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should handle empty targetColumnMap gracefully', () => {
|
||||
const emptyField = {
|
||||
nameSingular: 'emptyField',
|
||||
targetColumnMap: {},
|
||||
} as FieldMetadata;
|
||||
|
||||
fields.push(emptyField);
|
||||
|
||||
const select = { emptyField: true };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = 'emptyField\n';
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
|
||||
test('should use formatted targetColumnMap values with unique random parts', () => {
|
||||
const select = { simpleField: true, complexField: true };
|
||||
const result = convertFieldsToGraphQL(select, fields);
|
||||
const expected = `
|
||||
simpleField: column_RANDOMSTRING1
|
||||
___complexField_link: column_RANDOMSTRING2
|
||||
___complexField_text: column_RANDOMSTRING3
|
||||
`;
|
||||
expect(normalizeWhitespace(result)).toBe(normalizeWhitespace(expected));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
FieldMetadata,
|
||||
FieldMetadataTargetColumnMap,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { getFieldAliases } from 'src/tenant/entity-resolver/utils/get-fields-aliases.util';
|
||||
|
||||
describe('getFieldAliases', () => {
|
||||
let fields: FieldMetadata[];
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup sample field metadata
|
||||
fields = [
|
||||
{
|
||||
nameSingular: 'singleValueField',
|
||||
namePlural: 'singleValueFields',
|
||||
targetColumnMap: {
|
||||
value: 'column_singleValue',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
},
|
||||
{
|
||||
nameSingular: 'multipleValuesField',
|
||||
namePlural: 'multipleValuesFields',
|
||||
targetColumnMap: {
|
||||
link: 'column_value1',
|
||||
text: 'column_value2',
|
||||
} as FieldMetadataTargetColumnMap,
|
||||
},
|
||||
] as FieldMetadata[];
|
||||
});
|
||||
|
||||
test('should return correct aliases for fields with a single value in targetColumnMap', () => {
|
||||
const aliases = getFieldAliases(fields);
|
||||
expect(aliases).toHaveProperty('singleValueField', 'column_singleValue');
|
||||
});
|
||||
|
||||
test('should return correct aliases for fields with multiple values in targetColumnMap', () => {
|
||||
const aliases = getFieldAliases(fields);
|
||||
expect(aliases).toHaveProperty('column_value1', 'column_value1');
|
||||
});
|
||||
|
||||
test('should handle empty fields array', () => {
|
||||
const aliases = getFieldAliases([]);
|
||||
expect(aliases).toEqual({});
|
||||
});
|
||||
|
||||
test('should not create aliases for fields without targetColumnMap values', () => {
|
||||
const fieldsWithEmptyMap = [
|
||||
...fields,
|
||||
{
|
||||
nameSingular: 'emptyField',
|
||||
namePlural: 'emptyFields',
|
||||
targetColumnMap: {} as FieldMetadataTargetColumnMap,
|
||||
},
|
||||
] as FieldMetadata[];
|
||||
const aliases = getFieldAliases(fieldsWithEmptyMap);
|
||||
expect(aliases).not.toHaveProperty('emptyField');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
isSpecialKey,
|
||||
handleSpecialKey,
|
||||
parseResult,
|
||||
} from 'src/tenant/entity-resolver/utils/parse-result.util';
|
||||
|
||||
describe('isSpecialKey', () => {
|
||||
test('should return true if the key starts with "___"', () => {
|
||||
expect(isSpecialKey('___specialKey')).toBe(true);
|
||||
});
|
||||
|
||||
test('should return false if the key does not start with "___"', () => {
|
||||
expect(isSpecialKey('normalKey')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleSpecialKey', () => {
|
||||
let result;
|
||||
|
||||
beforeEach(() => {
|
||||
result = {};
|
||||
});
|
||||
|
||||
test('should correctly process a special key and add it to the result object', () => {
|
||||
handleSpecialKey(result, '___complexField_link', 'value1');
|
||||
expect(result).toEqual({
|
||||
complexField: {
|
||||
link: 'value1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should add values under the same newKey if called multiple times', () => {
|
||||
handleSpecialKey(result, '___complexField_link', 'value1');
|
||||
handleSpecialKey(result, '___complexField_text', 'value2');
|
||||
expect(result).toEqual({
|
||||
complexField: {
|
||||
link: 'value1',
|
||||
text: 'value2',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should not create a new field if the special key is not correctly formed', () => {
|
||||
handleSpecialKey(result, '___complexField', 'value1');
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResult', () => {
|
||||
test('should recursively parse an object and handle special keys', () => {
|
||||
const obj = {
|
||||
normalField: 'value1',
|
||||
___specialField_part1: 'value2',
|
||||
nested: {
|
||||
___specialFieldNested_part2: 'value3',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedResult = {
|
||||
normalField: 'value1',
|
||||
specialField: {
|
||||
part1: 'value2',
|
||||
},
|
||||
nested: {
|
||||
specialFieldNested: {
|
||||
part2: 'value3',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(parseResult(obj)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should handle arrays and parse each element', () => {
|
||||
const objArray = [
|
||||
{
|
||||
___specialField_part1: 'value1',
|
||||
},
|
||||
{
|
||||
___specialField_part2: 'value2',
|
||||
},
|
||||
];
|
||||
|
||||
const expectedResult = [
|
||||
{
|
||||
specialField: {
|
||||
part1: 'value1',
|
||||
},
|
||||
},
|
||||
{
|
||||
specialField: {
|
||||
part2: 'value2',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(parseResult(objArray)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
test('should return the original value if it is not an object or array', () => {
|
||||
expect(parseResult('stringValue')).toBe('stringValue');
|
||||
expect(parseResult(12345)).toBe(12345);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { stringifyWithoutKeyQuote } from 'src/tenant/entity-resolver/utils/stringify-without-key-quote.util';
|
||||
|
||||
describe('stringifyWithoutKeyQuote', () => {
|
||||
test('should stringify object correctly without quotes around keys', () => {
|
||||
const obj = { name: 'John', age: 30, isAdmin: false };
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
expect(result).toBe('{name:"John",age:30,isAdmin:false}');
|
||||
});
|
||||
|
||||
test('should handle nested objects', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
address: { city: 'New York', zipCode: 10001 },
|
||||
};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
expect(result).toBe(
|
||||
'{name:"John",age:30,address:{city:"New York",zipCode:10001}}',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle arrays', () => {
|
||||
const obj = {
|
||||
name: 'John',
|
||||
age: 30,
|
||||
hobbies: ['reading', 'movies', 'hiking'],
|
||||
};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
expect(result).toBe(
|
||||
'{name:"John",age:30,hobbies:["reading","movies","hiking"]}',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle empty objects', () => {
|
||||
const obj = {};
|
||||
const result = stringifyWithoutKeyQuote(obj);
|
||||
expect(result).toBe('{}');
|
||||
});
|
||||
|
||||
test('should handle numbers, strings, and booleans', () => {
|
||||
const num = 10;
|
||||
const str = 'Hello';
|
||||
const bool = false;
|
||||
expect(stringifyWithoutKeyQuote(num)).toBe('10');
|
||||
expect(stringifyWithoutKeyQuote(str)).toBe('"Hello"');
|
||||
expect(stringifyWithoutKeyQuote(bool)).toBe('false');
|
||||
});
|
||||
|
||||
test('should handle null and undefined', () => {
|
||||
expect(stringifyWithoutKeyQuote(null)).toBe('null');
|
||||
expect(stringifyWithoutKeyQuote(undefined)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const convertArguments = (args: any, fields: FieldMetadata[]): any => {
|
||||
const fieldsMap = new Map(
|
||||
// TODO: Handle plural for fields when we add relations
|
||||
fields.map((metadata) => [metadata.nameSingular, metadata]),
|
||||
);
|
||||
|
||||
if (Array.isArray(args)) {
|
||||
return args.map((arg) => convertArguments(arg, fields));
|
||||
}
|
||||
|
||||
const newArgs = {};
|
||||
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
if (fieldsMap.has(key)) {
|
||||
const fieldMetadata = fieldsMap.get(key)!;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !isEmpty(value)) {
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
if (fieldMetadata.targetColumnMap[subKey]) {
|
||||
newArgs[fieldMetadata.targetColumnMap[subKey]] = subValue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (fieldMetadata.targetColumnMap.value) {
|
||||
newArgs[fieldMetadata.targetColumnMap.value] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newArgs[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return newArgs;
|
||||
};
|
||||
@@ -1,21 +1,55 @@
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const convertFieldsToGraphQL = (
|
||||
fields: any,
|
||||
fieldAliases: Record<string, string>,
|
||||
select: any,
|
||||
fields: FieldMetadata[],
|
||||
acc = '',
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (value && !isEmpty(value)) {
|
||||
const fieldsMap = new Map(
|
||||
// TODO: Handle plural for fields when we add relations
|
||||
fields.map((metadata) => [metadata.nameSingular, metadata]),
|
||||
);
|
||||
|
||||
for (const [key, value] of Object.entries(select)) {
|
||||
let fieldAlias = key;
|
||||
|
||||
if (fieldsMap.has(key)) {
|
||||
const metadata = fieldsMap.get(key)!;
|
||||
const entries = Object.entries(metadata.targetColumnMap);
|
||||
|
||||
if (entries.length > 0) {
|
||||
// If there is only one value, use it as the alias
|
||||
if (entries.length === 1) {
|
||||
const alias = entries[0][1];
|
||||
|
||||
fieldAlias = `${key}: ${alias}`;
|
||||
} else {
|
||||
// Otherwise it means it's a special type with multiple values, so we need fetch all fields
|
||||
fieldAlias = `
|
||||
${entries
|
||||
.map(
|
||||
([key, value]) => `___${metadata.nameSingular}_${key}: ${value}`,
|
||||
)
|
||||
.join('\n')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse if value is a nested object, otherwise append field or alias
|
||||
if (
|
||||
!fieldsMap.has(key) &&
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
!isEmpty(value)
|
||||
) {
|
||||
acc += `${key} {\n`;
|
||||
acc = convertFieldsToGraphQL(value, fieldAliases, acc);
|
||||
acc = convertFieldsToGraphQL(value, fields, acc); // recursive call with updated accumulator
|
||||
acc += `}\n`;
|
||||
} else {
|
||||
if (fieldAliases[key]) {
|
||||
acc += `${key}: ${fieldAliases[key]}\n`;
|
||||
} else {
|
||||
acc += `${key}\n`;
|
||||
}
|
||||
acc += `${fieldAlias}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const getFieldAliases = (fields: FieldMetadata[]) => {
|
||||
const fieldAliases = fields.reduce((acc, column) => {
|
||||
const values = Object.values(column.targetColumnMap);
|
||||
|
||||
if (values.length === 1) {
|
||||
return {
|
||||
...acc,
|
||||
// TODO: Handle plural for fields when we add relations
|
||||
[column.nameSingular]: values[0],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...acc,
|
||||
[values[0]]: values[0],
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
|
||||
return fieldAliases;
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
export const isSpecialKey = (key: string): boolean => {
|
||||
return key.startsWith('___');
|
||||
};
|
||||
|
||||
export const handleSpecialKey = (
|
||||
result: any,
|
||||
key: string,
|
||||
value: any,
|
||||
): void => {
|
||||
const parts = key.split('_').filter((part) => part);
|
||||
|
||||
// If parts don't contain enough information, return without altering result
|
||||
if (parts.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newKey = parts.slice(0, -1).join('');
|
||||
const subKey = parts[parts.length - 1];
|
||||
|
||||
if (!result[newKey]) {
|
||||
result[newKey] = {};
|
||||
}
|
||||
|
||||
result[newKey][subKey] = value;
|
||||
};
|
||||
|
||||
export const parseResult = (obj: any): any => {
|
||||
if (obj === null || typeof obj !== 'object' || typeof obj === 'function') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => parseResult(item));
|
||||
}
|
||||
|
||||
const result: any = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
result[key] = parseResult(obj[key]);
|
||||
} else if (isSpecialKey(key)) {
|
||||
handleSpecialKey(result, key, obj[key]);
|
||||
} else {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -1,128 +0,0 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import { stringifyWithoutKeyQuote } from './stringify-without-key-quote.util';
|
||||
import { convertFieldsToGraphQL } from './convert-fields-to-graphql.util';
|
||||
|
||||
type Command = 'findMany' | 'findOne' | 'createMany' | 'updateOne';
|
||||
|
||||
type CommandArgs = {
|
||||
findMany: null;
|
||||
findOne: { id: string };
|
||||
createMany: { data: any[] };
|
||||
updateOne: { id: string; data: any };
|
||||
};
|
||||
|
||||
export interface PGGraphQLQueryBuilderOptions {
|
||||
entityName: string;
|
||||
tableName: string;
|
||||
info: GraphQLResolveInfo;
|
||||
fieldAliases: Record<string, string>;
|
||||
}
|
||||
|
||||
export class PGGraphQLQueryBuilder {
|
||||
private options: PGGraphQLQueryBuilderOptions;
|
||||
private command: Command;
|
||||
private commandArgs: any;
|
||||
|
||||
constructor(options: PGGraphQLQueryBuilderOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
private getFields(): string {
|
||||
const fields = graphqlFields(this.options.info);
|
||||
|
||||
return convertFieldsToGraphQL(fields, this.options.fieldAliases);
|
||||
}
|
||||
|
||||
// Define command setters
|
||||
findMany() {
|
||||
this.command = 'findMany';
|
||||
this.commandArgs = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
findOne(args: CommandArgs['findOne']) {
|
||||
this.command = 'findOne';
|
||||
this.commandArgs = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
createMany(args: CommandArgs['createMany']) {
|
||||
this.command = 'createMany';
|
||||
this.commandArgs = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
updateOne(args: CommandArgs['updateOne']) {
|
||||
this.command = 'updateOne';
|
||||
this.commandArgs = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
build() {
|
||||
const { entityName, tableName } = this.options;
|
||||
const fields = this.getFields();
|
||||
|
||||
switch (this.command) {
|
||||
case 'findMany':
|
||||
return `
|
||||
query FindMany${pascalCase(entityName)} {
|
||||
findMany${pascalCase(entityName)}: ${tableName}Collection {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
case 'findOne':
|
||||
return `
|
||||
query FindOne${pascalCase(entityName)} {
|
||||
findOne${pascalCase(
|
||||
entityName,
|
||||
)}: ${tableName}Collection(filter: { id: { eq: "${
|
||||
this.commandArgs.id
|
||||
}" } }) {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
`;
|
||||
case 'createMany':
|
||||
return `
|
||||
mutation CreateMany${pascalCase(entityName)} {
|
||||
createMany${pascalCase(
|
||||
entityName,
|
||||
)}: insertInto${tableName}Collection(objects: ${stringifyWithoutKeyQuote(
|
||||
this.commandArgs.data.map((datum) => ({
|
||||
id: uuidv4(),
|
||||
...datum,
|
||||
})),
|
||||
)}) {
|
||||
affectedCount
|
||||
records {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
case 'updateOne':
|
||||
return `
|
||||
mutation UpdateOne${pascalCase(entityName)} {
|
||||
updateOne${pascalCase(
|
||||
entityName,
|
||||
)}: update${tableName}Collection(set: ${stringifyWithoutKeyQuote(
|
||||
this.commandArgs.data,
|
||||
)}, filter: { id: { eq: "${this.commandArgs.id}" } }) {
|
||||
affectedCount
|
||||
records {
|
||||
${fields}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
default:
|
||||
throw new Error('Invalid command');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder.util';
|
||||
|
||||
interface QueryRunnerOptions {
|
||||
entityName: string;
|
||||
tableName: string;
|
||||
workspaceId: string;
|
||||
info: GraphQLResolveInfo;
|
||||
fieldAliases: Record<string, string>;
|
||||
}
|
||||
|
||||
export class PGGraphQLQueryRunner {
|
||||
private queryBuilder: PGGraphQLQueryBuilder;
|
||||
private options: QueryRunnerOptions;
|
||||
|
||||
constructor(
|
||||
private dataSourceService: DataSourceService,
|
||||
options: QueryRunnerOptions,
|
||||
) {
|
||||
this.queryBuilder = new PGGraphQLQueryBuilder({
|
||||
entityName: options.entityName,
|
||||
tableName: options.tableName,
|
||||
info: options.info,
|
||||
fieldAliases: options.fieldAliases,
|
||||
});
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
private async execute(query: string, workspaceId: string): Promise<any> {
|
||||
const workspaceDataSource =
|
||||
await this.dataSourceService.connectToWorkspaceDataSource(workspaceId);
|
||||
|
||||
await workspaceDataSource?.query(`
|
||||
SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)};
|
||||
`);
|
||||
|
||||
return workspaceDataSource?.query(`
|
||||
SELECT graphql.resolve($$
|
||||
${query}
|
||||
$$);
|
||||
`);
|
||||
}
|
||||
|
||||
private parseResults(graphqlResult: any, command: string): any {
|
||||
const entityKey = `${command}${pascalCase(this.options.entityName)}`;
|
||||
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
|
||||
|
||||
if (!result) {
|
||||
throw new BadRequestException('Malformed result from GraphQL query');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMany(): Promise<any[]> {
|
||||
const query = this.queryBuilder.findMany().build();
|
||||
const result = await this.execute(query, this.options.workspaceId);
|
||||
|
||||
return this.parseResults(result, 'findMany');
|
||||
}
|
||||
|
||||
async findOne(args: { id: string }): Promise<any> {
|
||||
const query = this.queryBuilder.findOne(args).build();
|
||||
const result = await this.execute(query, this.options.workspaceId);
|
||||
|
||||
return this.parseResults(result, 'findOne');
|
||||
}
|
||||
|
||||
async createMany(args: { data: any[] }): Promise<any[]> {
|
||||
const query = this.queryBuilder.createMany(args).build();
|
||||
const result = await this.execute(query, this.options.workspaceId);
|
||||
|
||||
return this.parseResults(result, 'createMany')?.records;
|
||||
}
|
||||
|
||||
async createOne(args: { data: any }): Promise<any> {
|
||||
const records = await this.createMany({ data: [args.data] });
|
||||
|
||||
return records?.[0];
|
||||
}
|
||||
|
||||
async updateOne(args: { id: string; data: any }): Promise<any> {
|
||||
const query = this.queryBuilder.updateOne(args).build();
|
||||
const result = await this.execute(query, this.options.workspaceId);
|
||||
|
||||
return this.parseResults(result, 'updateOne')?.records?.[0];
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export const stringifyWithoutKeyQuote = (obj: any) => {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
const jsonWithoutQuotes = jsonString.replace(/"(\w+)"\s*:/g, '$1:');
|
||||
const jsonWithoutQuotes = jsonString?.replace(/"(\w+)"\s*:/g, '$1:');
|
||||
|
||||
return jsonWithoutQuotes;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user