feat: rename tenant into workspace (#2553)
* feat: rename tenant into workspace * fix: missing some files and reset not working * fix: wrong import * Use link in company seeds * Use link in company seeds --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -0,0 +1,237 @@
|
||||
// import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
// import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface';
|
||||
|
||||
// import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
// import {
|
||||
// PGGraphQLQueryBuilder,
|
||||
// PGGraphQLQueryBuilderOptions,
|
||||
// } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder';
|
||||
|
||||
// const testUUID = '123e4567-e89b-12d3-a456-426614174001';
|
||||
|
||||
// const normalizeWhitespace = (str) => str.replace(/\s+/g, '');
|
||||
|
||||
// // Mocking dependencies
|
||||
// jest.mock('uuid', () => ({
|
||||
// v4: jest.fn(() => testUUID),
|
||||
// }));
|
||||
|
||||
// jest.mock('graphql-fields', () =>
|
||||
// jest.fn(() => ({
|
||||
// name: true,
|
||||
// age: true,
|
||||
// complexField: {
|
||||
// subField1: true,
|
||||
// subField2: true,
|
||||
// },
|
||||
// })),
|
||||
// );
|
||||
|
||||
// describe('PGGraphQLQueryBuilder', () => {
|
||||
// let queryBuilder;
|
||||
// let mockOptions: PGGraphQLQueryBuilderOptions;
|
||||
|
||||
// beforeEach(() => {
|
||||
// const fieldMetadataCollection = [
|
||||
// {
|
||||
// name: 'name',
|
||||
// targetColumnMap: {
|
||||
// value: 'column_name',
|
||||
// } as FieldMetadataTargetColumnMap,
|
||||
// },
|
||||
// {
|
||||
// name: 'age',
|
||||
// targetColumnMap: {
|
||||
// value: 'column_age',
|
||||
// } as FieldMetadataTargetColumnMap,
|
||||
// },
|
||||
// {
|
||||
// name: 'complexField',
|
||||
// targetColumnMap: {
|
||||
// subField1: 'column_subField1',
|
||||
// subField2: 'column_subField2',
|
||||
// } as FieldMetadataTargetColumnMap,
|
||||
// },
|
||||
// ] as FieldMetadata[];
|
||||
|
||||
// mockOptions = {
|
||||
// targetTableName: 'TestTable',
|
||||
// info: {} as GraphQLResolveInfo,
|
||||
// fieldMetadataCollection,
|
||||
// };
|
||||
|
||||
// queryBuilder = new PGGraphQLQueryBuilder(mockOptions);
|
||||
// });
|
||||
|
||||
// test('findMany generates correct query with no arguments', () => {
|
||||
// const query = queryBuilder.findMany();
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// query {
|
||||
// TestTableCollection {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('findMany generates correct query with filter parameters', () => {
|
||||
// const args = {
|
||||
// filter: {
|
||||
// name: { eq: 'Alice' },
|
||||
// age: { gt: 20 },
|
||||
// },
|
||||
// };
|
||||
// const query = queryBuilder.findMany(args);
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// query {
|
||||
// TestTableCollection(filter: { column_name: { eq: "Alice" }, column_age: { gt: 20 } }) {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('findMany generates correct query with combined pagination parameters', () => {
|
||||
// const args = {
|
||||
// first: 5,
|
||||
// after: 'someCursor',
|
||||
// before: 'anotherCursor',
|
||||
// last: 3,
|
||||
// };
|
||||
// const query = queryBuilder.findMany(args);
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// query {
|
||||
// TestTableCollection(
|
||||
// first: 5,
|
||||
// after: "someCursor",
|
||||
// before: "anotherCursor",
|
||||
// last: 3
|
||||
// ) {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('findOne generates correct query with ID filter', () => {
|
||||
// const args = { filter: { id: { eq: testUUID } } };
|
||||
// const query = queryBuilder.findOne(args);
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// query {
|
||||
// TestTableCollection(filter: { id: { eq: "${testUUID}" } }) {
|
||||
// edges {
|
||||
// node {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('createMany generates correct mutation with complex and nested fields', () => {
|
||||
// const args = {
|
||||
// data: [
|
||||
// {
|
||||
// name: 'Alice',
|
||||
// age: 30,
|
||||
// complexField: {
|
||||
// subField1: 'data1',
|
||||
// subField2: 'data2',
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
// const query = queryBuilder.createMany(args);
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// mutation {
|
||||
// insertIntoTestTableCollection(objects: [{
|
||||
// id: "${testUUID}",
|
||||
// column_name: "Alice",
|
||||
// column_age: 30,
|
||||
// column_subField1: "data1",
|
||||
// column_subField2: "data2"
|
||||
// }]) {
|
||||
// affectedCount
|
||||
// records {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
|
||||
// test('updateOne generates correct mutation with complex and nested fields', () => {
|
||||
// const args = {
|
||||
// id: '1',
|
||||
// data: {
|
||||
// name: 'Bob',
|
||||
// age: 40,
|
||||
// complexField: {
|
||||
// subField1: 'newData1',
|
||||
// subField2: 'newData2',
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// const query = queryBuilder.updateOne(args);
|
||||
|
||||
// expect(normalizeWhitespace(query)).toBe(
|
||||
// normalizeWhitespace(`
|
||||
// mutation {
|
||||
// updateTestTableCollection(
|
||||
// set: {
|
||||
// column_name: "Bob",
|
||||
// column_age: 40,
|
||||
// column_subField1: "newData1",
|
||||
// column_subField2: "newData2"
|
||||
// },
|
||||
// filter: { id: { eq: "1" } }
|
||||
// ) {
|
||||
// affectedCount
|
||||
// records {
|
||||
// name: column_name
|
||||
// age: column_age
|
||||
// ___complexField_subField1: column_subField1
|
||||
// ___complexField_subField2: column_subField2
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// `),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should pass', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsAliasFactory {
|
||||
create(
|
||||
args: Record<string, any>,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
): Record<string, any> {
|
||||
const fieldMetadataMap = new Map(
|
||||
fieldMetadataCollection.map((fieldMetadata) => [
|
||||
fieldMetadata.name,
|
||||
fieldMetadata,
|
||||
]),
|
||||
);
|
||||
|
||||
return this.createArgsObjectRecursive(args, fieldMetadataMap);
|
||||
}
|
||||
|
||||
private createArgsObjectRecursive(
|
||||
args: Record<string, any>,
|
||||
fieldMetadataMap: Map<string, FieldMetadataInterface>,
|
||||
) {
|
||||
// If it's not an object, we don't need to do anything
|
||||
if (typeof args !== 'object' || args === null) {
|
||||
return args;
|
||||
}
|
||||
|
||||
// If it's an array, we need to map all items
|
||||
if (Array.isArray(args)) {
|
||||
return args.map((arg) =>
|
||||
this.createArgsObjectRecursive(arg, fieldMetadataMap),
|
||||
);
|
||||
}
|
||||
|
||||
const newArgs = {};
|
||||
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
const fieldMetadata = fieldMetadataMap.get(key);
|
||||
|
||||
// If it's a special complex field, we need to map all columns
|
||||
if (
|
||||
fieldMetadata &&
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
Object.values(fieldMetadata.targetColumnMap).length > 1
|
||||
) {
|
||||
for (const [subKey, subValue] of Object.entries(value)) {
|
||||
const mappedKey = fieldMetadata.targetColumnMap[subKey];
|
||||
|
||||
if (mappedKey) {
|
||||
newArgs[mappedKey] = subValue;
|
||||
}
|
||||
}
|
||||
} else if (fieldMetadata) {
|
||||
// Otherwise we just need to map the value
|
||||
const mappedKey = fieldMetadata.targetColumnMap.value;
|
||||
|
||||
newArgs[mappedKey ?? key] = value;
|
||||
} else {
|
||||
// Recurse if value is a nested object, otherwise append field or alias
|
||||
newArgs[key] = this.createArgsObjectRecursive(value, fieldMetadataMap);
|
||||
}
|
||||
}
|
||||
|
||||
return newArgs;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsStringFactory {
|
||||
constructor(private readonly argsAliasFactory: ArgsAliasFactory) {}
|
||||
create(
|
||||
initialArgs: Record<string, any> | undefined,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
): string | null {
|
||||
if (!initialArgs) {
|
||||
return null;
|
||||
}
|
||||
let argsString = '';
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
initialArgs,
|
||||
fieldMetadataCollection,
|
||||
);
|
||||
|
||||
for (const key in computedArgs) {
|
||||
// Check if the value is not undefined
|
||||
if (computedArgs[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof computedArgs[key] === 'string') {
|
||||
// If it's a string, add quotes
|
||||
argsString += `${key}: "${computedArgs[key]}", `;
|
||||
} else if (
|
||||
typeof computedArgs[key] === 'object' &&
|
||||
computedArgs[key] !== null
|
||||
) {
|
||||
// If it's an object (and not null), stringify it
|
||||
argsString += `${key}: ${stringifyWithoutKeyQuote(
|
||||
computedArgs[key],
|
||||
)}, `;
|
||||
} else {
|
||||
// For other types (number, boolean), add as is
|
||||
argsString += `${key}: ${computedArgs[key]}, `;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing comma and space, if present
|
||||
if (argsString.endsWith(', ')) {
|
||||
argsString = argsString.slice(0, -2);
|
||||
}
|
||||
|
||||
return argsString;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/workspace/utils/deduce-relation-direction.util';
|
||||
import { getFieldArgumentsByKey } from 'src/workspace/workspace-query-builder/utils/get-field-arguments-by-key.util';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class CompositeFieldAliasFactory {
|
||||
private logger = new Logger(CompositeFieldAliasFactory.name);
|
||||
|
||||
constructor(
|
||||
@Inject(forwardRef(() => FieldsStringFactory))
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsStringFactory: ArgsStringFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
info: GraphQLResolveInfo,
|
||||
) {
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
throw new Error(`Field ${fieldMetadata.name} is not a composite field`);
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.RELATION:
|
||||
return this.createRelationAlias(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
fieldMetadata,
|
||||
info,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private createRelationAlias(
|
||||
fieldKey: string,
|
||||
fieldValue: any,
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
info: GraphQLResolveInfo,
|
||||
) {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
throw new Error(
|
||||
`Relation metadata not found for field ${fieldMetadata.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata.objectMetadataId,
|
||||
relationMetadata,
|
||||
);
|
||||
const referencedObjectMetadata =
|
||||
relationDirection == RelationDirection.TO
|
||||
? relationMetadata.fromObjectMetadata
|
||||
: relationMetadata.toObjectMetadata;
|
||||
|
||||
// If it's a relation destination is of kind MANY, we need to add the collection suffix and extract the args
|
||||
if (
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getFieldArgumentsByKey(info, fieldKey);
|
||||
const argsString = this.argsStringFactory.create(
|
||||
args,
|
||||
relationMetadata.toObjectMetadata.fields ?? [],
|
||||
);
|
||||
return `
|
||||
${fieldKey}: ${referencedObjectMetadata.targetTableName}Collection${
|
||||
argsString ? `(${argsString})` : ''
|
||||
} {
|
||||
${this.fieldsStringFactory.createFieldsStringRecursive(
|
||||
info,
|
||||
fieldValue,
|
||||
relationMetadata.toObjectMetadata.fields ?? [],
|
||||
)}
|
||||
}
|
||||
`;
|
||||
}
|
||||
let relationAlias = fieldKey;
|
||||
|
||||
// For one to one relations, pg_graphql use the targetTableName on the side that is not storing the foreign key
|
||||
// so we need to alias it to the field key
|
||||
if (
|
||||
relationMetadata.relationType === RelationMetadataType.ONE_TO_ONE &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
relationAlias = `${fieldKey}: ${referencedObjectMetadata.targetTableName}`;
|
||||
}
|
||||
|
||||
// Otherwise it means it's a relation destination is of kind ONE
|
||||
return `
|
||||
${relationAlias} {
|
||||
${this.fieldsStringFactory.createFieldsStringRecursive(
|
||||
info,
|
||||
fieldValue,
|
||||
referencedObjectMetadata.fields ?? [],
|
||||
)}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
import { CreateManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyQueryFactory {
|
||||
private readonly logger = new Logger(CreateManyQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
||||
) {}
|
||||
|
||||
create<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation {
|
||||
insertInto${
|
||||
options.targetTableName
|
||||
}Collection(objects: ${stringifyWithoutKeyQuote(
|
||||
computedArgs.data.map((datum) => ({
|
||||
id: uuidv4(),
|
||||
...datum,
|
||||
})),
|
||||
)}) {
|
||||
affectedCount
|
||||
records {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { DeleteOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteOneQueryFactory {
|
||||
private readonly logger = new Logger(DeleteOneQueryFactory.name);
|
||||
|
||||
constructor(private readonly fieldsStringFactory: FieldsStringFactory) {}
|
||||
|
||||
create(args: DeleteOneResolverArgs, options: WorkspaceQueryBuilderOptions) {
|
||||
const fieldsString = this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation {
|
||||
deleteFrom${options.targetTableName}Collection(filter: { id: { eq: "${args.id}" } }) {
|
||||
affectedCount
|
||||
records {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
import { CompositeFieldAliasFactory } from './composite-field-alias.factory';
|
||||
import { CreateManyQueryFactory } from './create-many-query.factory';
|
||||
import { DeleteOneQueryFactory } from './delete-one-query.factory';
|
||||
import { FieldAliasFacotry } from './field-alias.factory';
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { FindManyQueryFactory } from './find-many-query.factory';
|
||||
import { FindOneQueryFactory } from './find-one-query.factory';
|
||||
import { UpdateOneQueryFactory } from './update-one-query.factory';
|
||||
|
||||
export const workspaceQueryBuilderFactories = [
|
||||
ArgsAliasFactory,
|
||||
ArgsStringFactory,
|
||||
CompositeFieldAliasFactory,
|
||||
CreateManyQueryFactory,
|
||||
DeleteOneQueryFactory,
|
||||
FieldAliasFacotry,
|
||||
FieldsStringFactory,
|
||||
FindManyQueryFactory,
|
||||
FindOneQueryFactory,
|
||||
UpdateOneQueryFactory,
|
||||
];
|
||||
@ -0,0 +1,30 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
@Injectable()
|
||||
export class FieldAliasFacotry {
|
||||
private readonly logger = new Logger(FieldAliasFacotry.name);
|
||||
|
||||
create(fieldKey: string, fieldMetadata: FieldMetadataInterface) {
|
||||
const entries = Object.entries(fieldMetadata.targetColumnMap);
|
||||
|
||||
if (entries.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (entries.length === 1) {
|
||||
// If there is only one value, use it as the alias
|
||||
const alias = entries[0][1];
|
||||
|
||||
return `${fieldKey}: ${alias}`;
|
||||
}
|
||||
|
||||
// Otherwise it means it's a special type with multiple values, so we need map all columns
|
||||
return `
|
||||
${entries
|
||||
.map(([key, value]) => `___${fieldMetadata.name}_${key}: ${value}`)
|
||||
.join('\n')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
import graphqlFields from 'graphql-fields';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { FieldAliasFacotry } from './field-alias.factory';
|
||||
import { CompositeFieldAliasFactory } from './composite-field-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FieldsStringFactory {
|
||||
private readonly logger = new Logger(FieldsStringFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldAliasFactory: FieldAliasFacotry,
|
||||
private readonly compositeFieldAliasFactory: CompositeFieldAliasFactory,
|
||||
) {}
|
||||
|
||||
create(
|
||||
info: GraphQLResolveInfo,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
) {
|
||||
const selectedFields: Record<string, any> = graphqlFields(info);
|
||||
|
||||
return this.createFieldsStringRecursive(
|
||||
info,
|
||||
selectedFields,
|
||||
fieldMetadataCollection,
|
||||
);
|
||||
}
|
||||
|
||||
createFieldsStringRecursive(
|
||||
info: GraphQLResolveInfo,
|
||||
selectedFields: Record<string, any>,
|
||||
fieldMetadataCollection: FieldMetadataInterface[],
|
||||
accumulator = '',
|
||||
): string {
|
||||
const fieldMetadataMap = new Map(
|
||||
fieldMetadataCollection.map((metadata) => [metadata.name, metadata]),
|
||||
);
|
||||
|
||||
for (const [fieldKey, fieldValue] of Object.entries(selectedFields)) {
|
||||
let fieldAlias: string | null;
|
||||
|
||||
if (fieldMetadataMap.has(fieldKey)) {
|
||||
// We're sure that the field exists in the map after this if condition
|
||||
// ES6 should tackle that more properly
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const fieldMetadata = fieldMetadataMap.get(fieldKey)!;
|
||||
|
||||
// If the field is a composite field, we need to create a special alias
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
const alias = this.compositeFieldAliasFactory.create(
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
fieldMetadata,
|
||||
info,
|
||||
);
|
||||
|
||||
fieldAlias = alias;
|
||||
} else {
|
||||
// Otherwise we just need to create a simple alias
|
||||
const alias = this.fieldAliasFactory.create(fieldKey, fieldMetadata);
|
||||
|
||||
fieldAlias = alias;
|
||||
}
|
||||
}
|
||||
|
||||
fieldAlias ??= fieldKey;
|
||||
|
||||
// Recurse if value is a nested object, otherwise append field or alias
|
||||
if (
|
||||
!fieldMetadataMap.has(fieldKey) &&
|
||||
fieldValue &&
|
||||
typeof fieldValue === 'object' &&
|
||||
!isEmpty(fieldValue)
|
||||
) {
|
||||
accumulator += `${fieldKey} {\n`;
|
||||
accumulator = this.createFieldsStringRecursive(
|
||||
info,
|
||||
fieldValue,
|
||||
fieldMetadataCollection,
|
||||
accumulator,
|
||||
);
|
||||
accumulator += `}\n`;
|
||||
} else {
|
||||
accumulator += `${fieldAlias}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import {
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
import { FindManyResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyQueryFactory {
|
||||
private readonly logger = new Logger(FindManyQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsStringFactory: ArgsStringFactory,
|
||||
) {}
|
||||
|
||||
create<
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
>(
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
const argsString = this.argsStringFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
query {
|
||||
${options.targetTableName}Collection${
|
||||
argsString ? `(${argsString})` : ''
|
||||
} {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { RecordFilter } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
import { FindOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { ArgsStringFactory } from './args-string.factory';
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneQueryFactory {
|
||||
private readonly logger = new Logger(FindOneQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsStringFactory: ArgsStringFactory,
|
||||
) {}
|
||||
|
||||
create<Filter extends RecordFilter = RecordFilter>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
const argsString = this.argsStringFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
query {
|
||||
${options.targetTableName}Collection${
|
||||
argsString ? `(${argsString})` : ''
|
||||
} {
|
||||
edges {
|
||||
node {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
import { UpdateOneResolverArgs } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||
|
||||
import { FieldsStringFactory } from './fields-string.factory';
|
||||
import { ArgsAliasFactory } from './args-alias.factory';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneQueryFactory {
|
||||
private readonly logger = new Logger(UpdateOneQueryFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly fieldsStringFactory: FieldsStringFactory,
|
||||
private readonly argsAliasFactory: ArgsAliasFactory,
|
||||
) {}
|
||||
|
||||
create<Record extends IRecord = IRecord>(
|
||||
args: UpdateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
) {
|
||||
const fieldsString = this.fieldsStringFactory.create(
|
||||
options.info,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
const computedArgs = this.argsAliasFactory.create(
|
||||
args,
|
||||
options.fieldMetadataCollection,
|
||||
);
|
||||
|
||||
return `
|
||||
mutation {
|
||||
update${
|
||||
options.targetTableName
|
||||
}Collection(set: ${stringifyWithoutKeyQuote(
|
||||
computedArgs.data,
|
||||
)}, filter: { id: { eq: "${computedArgs.id}" } }) {
|
||||
affectedCount
|
||||
records {
|
||||
${fieldsString}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
export interface Record {
|
||||
id: string;
|
||||
[key: string]: any;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type RecordFilter = {
|
||||
[Property in keyof Record]: any;
|
||||
};
|
||||
|
||||
export enum OrderByDirection {
|
||||
AscNullsFirst = 'AscNullsFirst',
|
||||
AscNullsLast = 'AscNullsLast',
|
||||
DescNullsFirst = 'DescNullsFirst',
|
||||
DescNullsLast = 'DescNullsLast',
|
||||
}
|
||||
|
||||
export type RecordOrderBy = {
|
||||
[Property in keyof Record]: OrderByDirection;
|
||||
};
|
||||
@ -0,0 +1,9 @@
|
||||
import { GraphQLResolveInfo } from 'graphql';
|
||||
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
export interface WorkspaceQueryBuilderOptions {
|
||||
targetTableName: string;
|
||||
info: GraphQLResolveInfo;
|
||||
fieldMetadataCollection: FieldMetadataInterface[];
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { stringifyWithoutKeyQuote } from 'src/workspace/workspace-query-builder/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,95 @@
|
||||
import {
|
||||
GraphQLResolveInfo,
|
||||
SelectionSetNode,
|
||||
Kind,
|
||||
SelectionNode,
|
||||
FieldNode,
|
||||
InlineFragmentNode,
|
||||
ValueNode,
|
||||
} from 'graphql';
|
||||
|
||||
const isFieldNode = (node: SelectionNode): node is FieldNode =>
|
||||
node.kind === Kind.FIELD;
|
||||
|
||||
const isInlineFragmentNode = (
|
||||
node: SelectionNode,
|
||||
): node is InlineFragmentNode => node.kind === Kind.INLINE_FRAGMENT;
|
||||
|
||||
const findFieldNode = (
|
||||
selectionSet: SelectionSetNode | undefined,
|
||||
key: string,
|
||||
): FieldNode | null => {
|
||||
if (!selectionSet) return null;
|
||||
|
||||
let field: FieldNode | null = null;
|
||||
|
||||
for (const selection of selectionSet.selections) {
|
||||
// We've found the field
|
||||
if (isFieldNode(selection) && selection.name.value === key) {
|
||||
return selection;
|
||||
}
|
||||
|
||||
// Recursively search for the field in nested selections
|
||||
if (
|
||||
(isFieldNode(selection) || isInlineFragmentNode(selection)) &&
|
||||
selection.selectionSet
|
||||
) {
|
||||
field = findFieldNode(selection.selectionSet, key);
|
||||
|
||||
// If we find the field in a nested selection, stop searching
|
||||
if (field) break;
|
||||
}
|
||||
}
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const parseValueNode = (
|
||||
valueNode: ValueNode,
|
||||
variables: GraphQLResolveInfo['variableValues'],
|
||||
) => {
|
||||
switch (valueNode.kind) {
|
||||
case Kind.VARIABLE:
|
||||
return variables[valueNode.name.value];
|
||||
case Kind.INT:
|
||||
case Kind.FLOAT:
|
||||
return Number(valueNode.value);
|
||||
case Kind.STRING:
|
||||
case Kind.BOOLEAN:
|
||||
case Kind.ENUM:
|
||||
return valueNode.value;
|
||||
case Kind.LIST:
|
||||
return valueNode.values.map((value) => parseValueNode(value, variables));
|
||||
case Kind.OBJECT:
|
||||
return valueNode.fields.reduce((obj, field) => {
|
||||
obj[field.name.value] = parseValueNode(field.value, variables);
|
||||
return obj;
|
||||
}, {});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFieldArgumentsByKey = (
|
||||
info: GraphQLResolveInfo,
|
||||
fieldKey: string,
|
||||
): Record<string, any> => {
|
||||
// Start from the first top-level field node and search recursively
|
||||
const targetField = findFieldNode(info.fieldNodes[0].selectionSet, fieldKey);
|
||||
|
||||
// If the field is not found, throw an error
|
||||
if (!targetField) {
|
||||
throw new Error(`Field "${fieldKey}" not found.`);
|
||||
}
|
||||
|
||||
// Extract the arguments from the field we've found
|
||||
const args: Record<string, any> = {};
|
||||
|
||||
if (targetField.arguments && targetField.arguments.length) {
|
||||
for (const arg of targetField.arguments) {
|
||||
args[arg.name.value] = parseValueNode(arg.value, info.variableValues);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
@ -0,0 +1,6 @@
|
||||
export const stringifyWithoutKeyQuote = (obj: any) => {
|
||||
const jsonString = JSON.stringify(obj);
|
||||
const jsonWithoutQuotes = jsonString?.replace(/"(\w+)"\s*:/g, '$1:');
|
||||
|
||||
return jsonWithoutQuotes;
|
||||
};
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderOptions } from 'src/workspace/workspace-query-builder/interfaces/workspace-query-builder-options.interface';
|
||||
import {
|
||||
Record as IRecord,
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
import {
|
||||
FindManyResolverArgs,
|
||||
FindOneResolverArgs,
|
||||
CreateManyResolverArgs,
|
||||
UpdateOneResolverArgs,
|
||||
DeleteOneResolverArgs,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { FindManyQueryFactory } from './factories/find-many-query.factory';
|
||||
import { FindOneQueryFactory } from './factories/find-one-query.factory';
|
||||
import { CreateManyQueryFactory } from './factories/create-many-query.factory';
|
||||
import { UpdateOneQueryFactory } from './factories/update-one-query.factory';
|
||||
import { DeleteOneQueryFactory } from './factories/delete-one-query.factory';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceQueryBuilderFactory {
|
||||
private readonly logger = new Logger(WorkspaceQueryBuilderFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly findManyQueryFactory: FindManyQueryFactory,
|
||||
private readonly findOneQueryFactory: FindOneQueryFactory,
|
||||
private readonly createManyQueryFactory: CreateManyQueryFactory,
|
||||
private readonly updateOneQueryFactory: UpdateOneQueryFactory,
|
||||
private readonly deleteOneQueryFactory: DeleteOneQueryFactory,
|
||||
) {}
|
||||
|
||||
findMany<
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
>(
|
||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.findManyQueryFactory.create<Filter, OrderBy>(args, options);
|
||||
}
|
||||
|
||||
findOne<Filter extends RecordFilter = RecordFilter>(
|
||||
args: FindOneResolverArgs<Filter>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.findOneQueryFactory.create<Filter>(args, options);
|
||||
}
|
||||
|
||||
createMany<Record extends IRecord = IRecord>(
|
||||
args: CreateManyResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.createManyQueryFactory.create<Record>(args, options);
|
||||
}
|
||||
|
||||
updateOne<Record extends IRecord = IRecord>(
|
||||
initialArgs: UpdateOneResolverArgs<Record>,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.updateOneQueryFactory.create<Record>(initialArgs, options);
|
||||
}
|
||||
|
||||
deleteOne(
|
||||
args: DeleteOneResolverArgs,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.deleteOneQueryFactory.create(args, options);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryBuilderFactory } from './workspace-query-builder.factory';
|
||||
|
||||
import { workspaceQueryBuilderFactories } from './factories/factories';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [...workspaceQueryBuilderFactories, WorkspaceQueryBuilderFactory],
|
||||
exports: [WorkspaceQueryBuilderFactory],
|
||||
})
|
||||
export class WorkspaceQueryBuilderModule {}
|
||||
Reference in New Issue
Block a user