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:
Jérémy M
2023-11-17 11:26:33 +01:00
committed by GitHub
parent bc579d64a6
commit b86ada6d2b
239 changed files with 1603 additions and 1618 deletions

View File

@ -0,0 +1,15 @@
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
export interface PGGraphQLResponse<Data = any> {
resolve: {
data: Data;
errors: any[];
};
}
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
export interface PGGraphQLMutation<Record = IRecord> {
affectedRows: number;
records: Record[];
}

View File

@ -0,0 +1,10 @@
import { GraphQLResolveInfo } from 'graphql';
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
export interface WorkspaceQueryRunnerOptions {
targetTableName: string;
workspaceId: string;
info: GraphQLResolveInfo;
fieldMetadataCollection: FieldMetadataInterface[];
}

View File

@ -0,0 +1,105 @@
import {
isSpecialKey,
handleSpecialKey,
parseResult,
} from 'src/workspace/workspace-query-runner/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);
});
});

View File

@ -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;
};

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { WorkspaceQueryBuilderModule } from 'src/workspace/workspace-query-builder/workspace-query-builder.module';
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
import { WorkspaceQueryRunnerService } from './workspace-query-runner.service';
@Module({
imports: [WorkspaceQueryBuilderModule, WorkspaceDataSourceModule],
providers: [WorkspaceQueryRunnerService],
exports: [WorkspaceQueryRunnerService],
})
export class WorkspaceQueryRunnerModule {}

View File

@ -0,0 +1,189 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { IConnection } from 'src/utils/pagination/interfaces/connection.interface';
import {
Record as IRecord,
RecordFilter,
RecordOrderBy,
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
import {
CreateManyResolverArgs,
CreateOneResolverArgs,
DeleteOneResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
UpdateOneResolverArgs,
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryBuilderFactory } from 'src/workspace/workspace-query-builder/workspace-query-builder.factory';
import { parseResult } from 'src/workspace/workspace-query-runner/utils/parse-result.util';
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-optionts.interface';
import {
PGGraphQLMutation,
PGGraphQLResult,
} from './interfaces/pg-graphql.interface';
@Injectable()
export class WorkspaceQueryRunnerService {
private readonly logger = new Logger(WorkspaceQueryRunnerService.name);
constructor(
private readonly workspaceQueryBuilderFactory: WorkspaceQueryBuilderFactory,
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
async findMany<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,
>(
args: FindManyResolverArgs<Filter, OrderBy>,
options: WorkspaceQueryRunnerOptions,
): Promise<IConnection<Record> | undefined> {
const { workspaceId, targetTableName } = options;
const query = this.workspaceQueryBuilderFactory.findMany(args, options);
const result = await this.execute(query, workspaceId);
return this.parseResult<IConnection<Record>>(result, targetTableName, '');
}
async findOne<
Record extends IRecord = IRecord,
Filter extends RecordFilter = RecordFilter,
>(
args: FindOneResolverArgs<Filter>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
if (!args.filter || Object.keys(args.filter).length === 0) {
throw new BadRequestException('Missing filter argument');
}
const { workspaceId, targetTableName } = options;
const query = this.workspaceQueryBuilderFactory.findOne(args, options);
const result = await this.execute(query, workspaceId);
const parsedResult = this.parseResult<IConnection<Record>>(
result,
targetTableName,
'',
);
return parsedResult?.edges?.[0]?.node;
}
async createMany<Record extends IRecord = IRecord>(
args: CreateManyResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record[] | undefined> {
const { workspaceId, targetTableName } = options;
const query = this.workspaceQueryBuilderFactory.createMany(args, options);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'insertInto',
)?.records;
}
async createOne<Record extends IRecord = IRecord>(
args: CreateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const records = await this.createMany({ data: [args.data] }, options);
return records?.[0];
}
async updateOne<Record extends IRecord = IRecord>(
args: UpdateOneResolverArgs<Record>,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options;
console.log({
workspaceId,
targetTableName,
});
const query = this.workspaceQueryBuilderFactory.updateOne(args, options);
console.log({ query });
const result = await this.execute(query, workspaceId);
console.log('HEY');
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'update',
)?.records?.[0];
}
async deleteOne<Record extends IRecord = IRecord>(
args: DeleteOneResolverArgs,
options: WorkspaceQueryRunnerOptions,
): Promise<Record | undefined> {
const { workspaceId, targetTableName } = options;
const query = this.workspaceQueryBuilderFactory.deleteOne(args, options);
const result = await this.execute(query, workspaceId);
return this.parseResult<PGGraphQLMutation<Record>>(
result,
targetTableName,
'deleteFrom',
)?.records?.[0];
}
private async execute(
query: string,
workspaceId: string,
): Promise<PGGraphQLResult | undefined> {
const workspaceDataSource =
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
workspaceId,
);
await workspaceDataSource?.query(`
SET search_path TO ${this.workspaceDataSourceService.getSchemaName(
workspaceId,
)};
`);
const results = await workspaceDataSource?.query<PGGraphQLResult>(`
SELECT graphql.resolve($$
${query}
$$);
`);
console.log(
JSON.stringify({
results,
}),
);
return results;
}
private parseResult<Result>(
graphqlResult: PGGraphQLResult | undefined,
targetTableName: string,
command: string,
): Result {
const entityKey = `${command}${targetTableName}Collection`;
const result = graphqlResult?.[0]?.resolve?.data?.[entityKey];
const errors = graphqlResult?.[0]?.resolve?.errors;
console.log('Result : ', graphqlResult?.[0]?.resolve);
if (Array.isArray(errors) && errors.length > 0) {
console.error('GraphQL errors', errors);
}
if (!result) {
throw new BadRequestException('Malformed result from GraphQL query');
}
return parseResult(result);
}
}