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,69 @@
|
||||
import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface';
|
||||
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
import {
|
||||
deduceRelationDirection,
|
||||
RelationDirection,
|
||||
} from 'src/workspace/utils/deduce-relation-direction.util';
|
||||
|
||||
describe('deduceRelationDirection', () => {
|
||||
it('should return FROM when the current object Metadata ID matches fromObjectMetadataId', () => {
|
||||
const currentObjectId = 'from_object_id';
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: currentObjectId,
|
||||
toObjectMetadataId: 'to_object_id',
|
||||
fromFieldMetadataId: 'from_field_id',
|
||||
toFieldMetadataId: 'to_field_id',
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
const result = deduceRelationDirection(
|
||||
currentObjectId,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
);
|
||||
|
||||
expect(result).toBe(RelationDirection.FROM);
|
||||
});
|
||||
|
||||
it('should return TO when the current object Metadata ID matches toObjectMetadataId', () => {
|
||||
// Arrange
|
||||
const currentObjectId = 'to_object_id';
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: 'from_object_id',
|
||||
toObjectMetadataId: currentObjectId,
|
||||
fromFieldMetadataId: 'from_field_id',
|
||||
toFieldMetadataId: 'to_field_id',
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
const result = deduceRelationDirection(
|
||||
currentObjectId,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
);
|
||||
|
||||
expect(result).toBe(RelationDirection.TO);
|
||||
});
|
||||
|
||||
it('should throw an error when the current object Metadata ID does not match any object metadata ID', () => {
|
||||
const currentObjectId = 'unrelated_object_id';
|
||||
const relationMetadata = {
|
||||
id: 'relation_id',
|
||||
fromObjectMetadataId: 'from_object_id',
|
||||
toObjectMetadataId: 'to_object_id',
|
||||
fromFieldMetadataId: 'from_field_id',
|
||||
toFieldMetadataId: 'to_field_id',
|
||||
relationType: RelationMetadataType.ONE_TO_ONE,
|
||||
};
|
||||
|
||||
expect(() =>
|
||||
deduceRelationDirection(
|
||||
currentObjectId,
|
||||
relationMetadata as RelationMetadataInterface,
|
||||
),
|
||||
).toThrow(
|
||||
`Relation metadata ${relationMetadata.id} is not related to object ${currentObjectId}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
|
||||
|
||||
describe('getResolverName', () => {
|
||||
const metadata = {
|
||||
nameSingular: 'entity',
|
||||
namePlural: 'entities',
|
||||
};
|
||||
|
||||
it.each([
|
||||
['findMany', 'entities'],
|
||||
['findOne', 'entity'],
|
||||
['createMany', 'createEntities'],
|
||||
['createOne', 'createEntity'],
|
||||
['updateOne', 'updateEntity'],
|
||||
['deleteOne', 'deleteEntity'],
|
||||
])('should return correct name for %s resolver', (type, expectedResult) => {
|
||||
expect(
|
||||
getResolverName(metadata, type as WorkspaceResolverBuilderMethodNames),
|
||||
).toBe(expectedResult);
|
||||
});
|
||||
|
||||
it('should throw an error for an unknown resolver type', () => {
|
||||
const unknownType = 'unknownType';
|
||||
expect(() =>
|
||||
getResolverName(
|
||||
metadata,
|
||||
unknownType as WorkspaceResolverBuilderMethodNames,
|
||||
),
|
||||
).toThrow(`Unknown resolver type: ${unknownType}`);
|
||||
});
|
||||
});
|
||||
23
server/src/workspace/utils/deduce-relation-direction.util.ts
Normal file
23
server/src/workspace/utils/deduce-relation-direction.util.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { RelationMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/relation-metadata.interface';
|
||||
|
||||
export enum RelationDirection {
|
||||
FROM = 'from',
|
||||
TO = 'to',
|
||||
}
|
||||
|
||||
export const deduceRelationDirection = (
|
||||
currentObjectId: string,
|
||||
relationMetadata: RelationMetadataInterface,
|
||||
): RelationDirection => {
|
||||
if (relationMetadata.fromObjectMetadataId === currentObjectId) {
|
||||
return RelationDirection.FROM;
|
||||
}
|
||||
|
||||
if (relationMetadata.toObjectMetadataId === currentObjectId) {
|
||||
return RelationDirection.TO;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Relation metadata ${relationMetadata.id} is not related to object ${currentObjectId}`,
|
||||
);
|
||||
};
|
||||
27
server/src/workspace/utils/get-resolver-name.util.ts
Normal file
27
server/src/workspace/utils/get-resolver-name.util.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { WorkspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { camelCase } from 'src/utils/camel-case';
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
export const getResolverName = (
|
||||
objectMetadata: Pick<ObjectMetadataInterface, 'namePlural' | 'nameSingular'>,
|
||||
type: WorkspaceResolverBuilderMethodNames,
|
||||
) => {
|
||||
switch (type) {
|
||||
case 'findMany':
|
||||
return `${camelCase(objectMetadata.namePlural)}`;
|
||||
case 'findOne':
|
||||
return `${camelCase(objectMetadata.nameSingular)}`;
|
||||
case 'createMany':
|
||||
return `create${pascalCase(objectMetadata.namePlural)}`;
|
||||
case 'createOne':
|
||||
return `create${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'updateOne':
|
||||
return `update${pascalCase(objectMetadata.nameSingular)}`;
|
||||
case 'deleteOne':
|
||||
return `delete${pascalCase(objectMetadata.nameSingular)}`;
|
||||
default:
|
||||
throw new Error(`Unknown resolver type: ${type}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,5 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const isCompositeFieldMetadataType = (type: FieldMetadataType) => {
|
||||
return type === FieldMetadataType.RELATION;
|
||||
};
|
||||
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
|
||||
import { WorkspaceDataSourceService } from './workspace-datasource.service';
|
||||
|
||||
@Module({
|
||||
imports: [DataSourceModule, TypeORMModule],
|
||||
exports: [WorkspaceDataSourceService],
|
||||
providers: [WorkspaceDataSourceService],
|
||||
})
|
||||
export class WorkspaceDataSourceModule {}
|
||||
@ -0,0 +1,101 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceDataSourceService {
|
||||
constructor(
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly typeormService: TypeORMService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
*
|
||||
* Connect to the workspace data source
|
||||
*
|
||||
* @param workspaceId
|
||||
* @returns
|
||||
*/
|
||||
public async connectToWorkspaceDataSource(
|
||||
workspaceId: string,
|
||||
): Promise<DataSource> {
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const dataSource = await this.typeormService.connectToDataSource(
|
||||
dataSourceMetadata,
|
||||
);
|
||||
|
||||
if (!dataSource) {
|
||||
throw new Error(
|
||||
`Could not connect to workspace data source for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Create a new DB schema for a workspace
|
||||
*
|
||||
* @param workspaceId
|
||||
* @returns
|
||||
*/
|
||||
public async createWorkspaceDBSchema(workspaceId: string): Promise<string> {
|
||||
const schemaName = this.getSchemaName(workspaceId);
|
||||
|
||||
return await this.typeormService.createSchema(schemaName);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Delete a DB schema for a workspace
|
||||
*
|
||||
* @param workspaceId
|
||||
* @returns
|
||||
*/
|
||||
public async deleteWorkspaceDBSchema(workspaceId: string): Promise<void> {
|
||||
const schemaName = this.getSchemaName(workspaceId);
|
||||
|
||||
return await this.typeormService.deleteSchema(schemaName);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the schema name for a workspace
|
||||
*
|
||||
* @param workspaceId
|
||||
* @returns string
|
||||
*/
|
||||
public getSchemaName(workspaceId: string): string {
|
||||
return `workspace_${this.uuidToBase36(workspaceId)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Convert a uuid to base36
|
||||
*
|
||||
* @param uuid
|
||||
* @returns string
|
||||
*/
|
||||
private uuidToBase36(uuid: string): string {
|
||||
let devId = false;
|
||||
|
||||
if (uuid.startsWith('twenty-')) {
|
||||
devId = true;
|
||||
// Clean dev uuids (twenty-)
|
||||
uuid = uuid.replace('twenty-', '');
|
||||
}
|
||||
const hexString = uuid.replace(/-/g, '');
|
||||
const base10Number = BigInt('0x' + hexString);
|
||||
const base36String = base10Number.toString(36);
|
||||
|
||||
return `${devId ? 'twenty_' : ''}${base36String}`;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { WorkspaceManagerService } from 'src/workspace/workspace-manager/workspace-manager.service';
|
||||
|
||||
// TODO: implement dry-run
|
||||
interface RunWorkspaceMigrationsOptions {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'workspace:sync-metadata',
|
||||
description: 'Sync metadata',
|
||||
})
|
||||
export class SyncWorkspaceMetadataCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: RunWorkspaceMigrationsOptions,
|
||||
): Promise<void> {
|
||||
// TODO: run in a dedicated job + run queries in a transaction.
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||
options.workspaceId,
|
||||
);
|
||||
|
||||
// TODO: This solution could be improved, using a diff for example, we should not have to delete all metadata and recreate them.
|
||||
await this.workspaceManagerService.resetStandardObjectsAndFieldsMetadata(
|
||||
dataSourceMetadata.id,
|
||||
options.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module';
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
|
||||
import { SyncWorkspaceMetadataCommand } from './sync-workspace-metadata.command';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceManagerModule, DataSourceModule],
|
||||
providers: [SyncWorkspaceMetadataCommand],
|
||||
})
|
||||
export class WorkspaceManagerCommandsModule {}
|
||||
@ -0,0 +1,51 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
export const companyPrefillData = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
) => {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.company`, [
|
||||
'name',
|
||||
'domainName',
|
||||
'address',
|
||||
'employees',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
name: 'Airbnb',
|
||||
domainName: 'airbnb.com',
|
||||
address: 'San Francisco',
|
||||
employees: 5000,
|
||||
},
|
||||
{
|
||||
name: 'Qonto',
|
||||
domainName: 'qonto.com',
|
||||
address: 'San Francisco',
|
||||
employees: 800,
|
||||
},
|
||||
{
|
||||
name: 'Stripe',
|
||||
domainName: 'stripe.com',
|
||||
address: 'San Francisco',
|
||||
employees: 8000,
|
||||
},
|
||||
{
|
||||
name: 'Figma',
|
||||
domainName: 'figma.com',
|
||||
address: 'San Francisco',
|
||||
employees: 800,
|
||||
},
|
||||
{
|
||||
name: 'Notion',
|
||||
domainName: 'notion.com',
|
||||
address: 'San Francisco',
|
||||
employees: 400,
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,41 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
export const pipelineStepPrefillData = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
) => {
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.pipelineStep`, ['name', 'color', 'position'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
name: 'New',
|
||||
color: 'red',
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
name: 'Screening',
|
||||
color: 'purple',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
name: 'Meeting',
|
||||
color: 'sky',
|
||||
position: 2,
|
||||
},
|
||||
{
|
||||
name: 'Proposal',
|
||||
color: 'turquoise',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
name: 'Customer',
|
||||
color: 'yellow',
|
||||
position: 4,
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import { viewPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/view';
|
||||
import { companyPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/company';
|
||||
import { personPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/person';
|
||||
import { pipelineStepPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/pipeline-step';
|
||||
|
||||
export const standardObjectsPrefillData = async (
|
||||
workspaceDataSource: DataSource,
|
||||
schemaName: string,
|
||||
objectMetadata: ObjectMetadataEntity[],
|
||||
) => {
|
||||
const objectMetadataMap = objectMetadata.reduce((acc, object) => {
|
||||
acc[object.nameSingular] = {
|
||||
id: object.id,
|
||||
fields: object.fields.reduce((acc, field) => {
|
||||
acc[field.name] = field.id;
|
||||
return acc;
|
||||
}, {}),
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
workspaceDataSource.transaction(async (entityManager: EntityManager) => {
|
||||
await companyPrefillData(entityManager, schemaName);
|
||||
await personPrefillData(entityManager, schemaName);
|
||||
await viewPrefillData(entityManager, schemaName, objectMetadataMap);
|
||||
await pipelineStepPrefillData(entityManager, schemaName);
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,278 @@
|
||||
import { EntityManager } from 'typeorm';
|
||||
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
|
||||
export const viewPrefillData = async (
|
||||
entityManager: EntityManager,
|
||||
schemaName: string,
|
||||
objectMetadataMap: Record<string, ObjectMetadataEntity>,
|
||||
) => {
|
||||
// Creating views
|
||||
const createdViews = await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.view`, ['name', 'objectMetadataId', 'type'])
|
||||
.orIgnore()
|
||||
.values([
|
||||
{
|
||||
name: 'All companies',
|
||||
objectMetadataId: 'company',
|
||||
type: 'table',
|
||||
},
|
||||
{
|
||||
name: 'All people',
|
||||
objectMetadataId: 'person',
|
||||
type: 'table',
|
||||
},
|
||||
{
|
||||
name: 'All opportunities',
|
||||
objectMetadataId: 'company',
|
||||
type: 'kanban',
|
||||
},
|
||||
{
|
||||
name: 'All Companies (V2)',
|
||||
objectMetadataId: objectMetadataMap['companyV2'].id,
|
||||
type: 'table',
|
||||
},
|
||||
{
|
||||
name: 'All People (V2)',
|
||||
objectMetadataId: objectMetadataMap['personV2'].id,
|
||||
type: 'table',
|
||||
},
|
||||
{
|
||||
name: 'All Opportunities (V2)',
|
||||
objectMetadataId: objectMetadataMap['companyV2'].id,
|
||||
type: 'kanban',
|
||||
},
|
||||
])
|
||||
.returning('*')
|
||||
.execute();
|
||||
|
||||
const viewIdMap = createdViews.raw.reduce((acc, view) => {
|
||||
acc[view.name] = view.id;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Creating viewFields
|
||||
await entityManager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.viewField`, [
|
||||
'fieldMetadataId',
|
||||
'viewId',
|
||||
'position',
|
||||
'isVisible',
|
||||
'size',
|
||||
])
|
||||
.orIgnore()
|
||||
.values([
|
||||
// CompanyV2
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['companyV2'].fields['name'],
|
||||
viewId: viewIdMap['All Companies (V2)'],
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['companyV2'].fields['domainName'],
|
||||
viewId: viewIdMap['All Companies (V2)'],
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
},
|
||||
// {
|
||||
// fieldMetadataId: objectMetadataMap['companyV2'].fields['accountOwner'],
|
||||
// viewId: viewIdMap['All Companies (V2)'],
|
||||
// position: 2,
|
||||
// isVisible: true,
|
||||
// size: 150,
|
||||
// },
|
||||
// {
|
||||
// fieldMetadataId: 'createdAt',
|
||||
// viewId: viewIdMap['All Companies (V2)'],
|
||||
// position: 3,
|
||||
// isVisible: true,
|
||||
// size: 150,
|
||||
// },
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['companyV2'].fields['employees'],
|
||||
viewId: viewIdMap['All Companies (V2)'],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['companyV2'].fields['linkedinUrl'],
|
||||
viewId: viewIdMap['All Companies (V2)'],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['companyV2'].fields['address'],
|
||||
viewId: viewIdMap['All Companies (V2)'],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
// PeopleV2
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['firstName'], // TODO: change to displayName once we have name field type
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 210,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['email'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
// {
|
||||
// fieldMetadataId: objectMetadataMap['personV2'].fields['company'],
|
||||
// viewId: viewIdMap['All People (V2)'],
|
||||
// position: 2,
|
||||
// isVisible: true,
|
||||
// size: 150,
|
||||
// },
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['phone'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
// {
|
||||
// fieldMetadataId: 'createdAt',
|
||||
// viewId: viewIdMap['All People (V2)'],
|
||||
// position: 4,
|
||||
// isVisible: true,
|
||||
// size: 150,
|
||||
// },
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['city'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['jobTitle'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['linkedinUrl'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: objectMetadataMap['personV2'].fields['xUrl'],
|
||||
viewId: viewIdMap['All People (V2)'],
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
// Companies
|
||||
{
|
||||
fieldMetadataId: 'name',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'domainName',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'accountOwner',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'createdAt',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'employees',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'linkedin',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'address',
|
||||
viewId: viewIdMap['All companies'],
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 170,
|
||||
},
|
||||
// Opportunities
|
||||
{
|
||||
fieldMetadataId: 'amount',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 0,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'probability',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 1,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'closeDate',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'company',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'createdAt',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
{
|
||||
fieldMetadataId: 'pointOfContact',
|
||||
viewId: viewIdMap['All opportunities'],
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 150,
|
||||
},
|
||||
])
|
||||
.execute();
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const activityTargetMetadata = {
|
||||
nameSingular: 'activityTargetV2',
|
||||
namePlural: 'activityTargetsV2',
|
||||
labelSingular: 'Activity Target',
|
||||
labelPlural: 'Activity Targets',
|
||||
targetTableName: 'activityTarget',
|
||||
description: 'An activity target',
|
||||
icon: 'IconCheckbox',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
// Relations
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activity',
|
||||
label: 'Activity',
|
||||
targetColumnMap: {
|
||||
value: 'activityId',
|
||||
},
|
||||
description: 'ActivityTarget activity',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
targetColumnMap: {
|
||||
value: 'personId',
|
||||
},
|
||||
description: 'ActivityTarget person',
|
||||
icon: 'IconUser',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
targetColumnMap: {
|
||||
value: 'companyId',
|
||||
},
|
||||
description: 'ActivityTarget company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default activityTargetMetadata;
|
||||
@ -0,0 +1,157 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const activityMetadata = {
|
||||
nameSingular: 'activityV2',
|
||||
namePlural: 'activitiesV2',
|
||||
labelSingular: 'Activity',
|
||||
labelPlural: 'Activities',
|
||||
targetTableName: 'activity',
|
||||
description: 'An activity',
|
||||
icon: 'IconCheckbox',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
targetColumnMap: {
|
||||
value: 'title',
|
||||
},
|
||||
description: 'Activity title',
|
||||
icon: 'IconNotes',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'body',
|
||||
label: 'Body',
|
||||
targetColumnMap: {
|
||||
value: 'body',
|
||||
},
|
||||
description: 'Activity body',
|
||||
icon: 'IconList',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
targetColumnMap: {
|
||||
value: 'type',
|
||||
},
|
||||
description: 'Activity type',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'reminderAt',
|
||||
label: 'Reminder Date',
|
||||
targetColumnMap: {
|
||||
value: 'reminderAt',
|
||||
},
|
||||
description: 'Activity reminder date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'dueAt',
|
||||
label: 'Due Date',
|
||||
targetColumnMap: {
|
||||
value: 'dueAt',
|
||||
},
|
||||
description: 'Activity due date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'completedAt',
|
||||
label: 'Completion Date',
|
||||
targetColumnMap: {
|
||||
value: 'completedAt',
|
||||
},
|
||||
description: 'Activity completion date',
|
||||
icon: 'IconCheck',
|
||||
isNullable: true,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activityTargets',
|
||||
label: 'Targets',
|
||||
targetColumnMap: {},
|
||||
description: 'Activity targets',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
targetColumnMap: {},
|
||||
description: 'Activity attachments',
|
||||
icon: 'IconFileImport',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'comments',
|
||||
label: 'Comments',
|
||||
targetColumnMap: {},
|
||||
description: 'Activity comments',
|
||||
icon: 'IconComment',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
targetColumnMap: {
|
||||
value: 'authorId',
|
||||
},
|
||||
description:
|
||||
'Activity author. This is the person who created the activity',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'assignee',
|
||||
label: 'Assignee',
|
||||
targetColumnMap: {
|
||||
value: 'assigneeId',
|
||||
},
|
||||
description:
|
||||
'Acitivity assignee. This is the workspace member assigned to the activity ',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default activityMetadata;
|
||||
@ -0,0 +1,56 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const apiKeyMetadata = {
|
||||
nameSingular: 'apiKeyV2',
|
||||
namePlural: 'apiKeysV2',
|
||||
labelSingular: 'Api Key',
|
||||
labelPlural: 'Api Keys',
|
||||
targetTableName: 'apiKey',
|
||||
description: 'An api key',
|
||||
icon: 'IconRobot',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
description: 'ApiKey name',
|
||||
icon: 'IconLink',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'expiresAt',
|
||||
label: 'Expiration date',
|
||||
targetColumnMap: {
|
||||
value: 'expiresAt',
|
||||
},
|
||||
description: 'ApiKey expiration date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'revokedAt',
|
||||
label: 'Revocation date',
|
||||
targetColumnMap: {
|
||||
value: 'revokedAt',
|
||||
},
|
||||
description: 'ApiKey revocation date',
|
||||
icon: 'IconCalendar',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default apiKeyMetadata;
|
||||
@ -0,0 +1,109 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const attachmentMetadata = {
|
||||
nameSingular: 'attachmentV2',
|
||||
namePlural: 'attachmentsV2',
|
||||
labelSingular: 'Attachment',
|
||||
labelPlural: 'Attachments',
|
||||
targetTableName: 'attachment',
|
||||
description: 'An attachment',
|
||||
icon: 'IconFileImport',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
description: 'Attachment name',
|
||||
icon: 'IconFileUpload',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'fullPath',
|
||||
label: 'Full path',
|
||||
targetColumnMap: {
|
||||
value: 'fullPath',
|
||||
},
|
||||
description: 'Attachment full path',
|
||||
icon: 'IconLink',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
targetColumnMap: {
|
||||
value: 'type',
|
||||
},
|
||||
description: 'Attachment type',
|
||||
icon: 'IconList',
|
||||
isNullable: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
targetColumnMap: {
|
||||
value: 'authorId',
|
||||
},
|
||||
description: 'Attachment author',
|
||||
icon: 'IconCircleUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activity',
|
||||
label: 'Activity',
|
||||
targetColumnMap: {
|
||||
value: 'activityId',
|
||||
},
|
||||
description: 'Attachment activity',
|
||||
icon: 'IconNotes',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
targetColumnMap: {
|
||||
value: 'personId',
|
||||
},
|
||||
description: 'Attachment person',
|
||||
icon: 'IconUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
targetColumnMap: {
|
||||
value: 'companyId',
|
||||
},
|
||||
description: 'Attachment company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default attachmentMetadata;
|
||||
@ -0,0 +1,57 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const commentMetadata = {
|
||||
nameSingular: 'commentV2',
|
||||
namePlural: 'commentsV2',
|
||||
labelSingular: 'Comment',
|
||||
labelPlural: 'Comments',
|
||||
targetTableName: 'comment',
|
||||
description: 'A comment',
|
||||
icon: 'IconMessageCircle',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'body',
|
||||
label: 'Body',
|
||||
targetColumnMap: {
|
||||
value: 'body',
|
||||
},
|
||||
description: 'Comment body',
|
||||
icon: 'IconLink',
|
||||
isNullable: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'author',
|
||||
label: 'Author',
|
||||
targetColumnMap: {
|
||||
value: 'authorId',
|
||||
},
|
||||
description: 'Comment author',
|
||||
icon: 'IconCircleUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activity',
|
||||
label: 'Activity',
|
||||
targetColumnMap: {
|
||||
value: 'activityId',
|
||||
},
|
||||
description: 'Comment activity',
|
||||
icon: 'IconNotes',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default commentMetadata;
|
||||
@ -0,0 +1,194 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const companyMetadata = {
|
||||
nameSingular: 'companyV2',
|
||||
namePlural: 'companiesV2',
|
||||
labelSingular: 'Company',
|
||||
labelPlural: 'Companies',
|
||||
targetTableName: 'company',
|
||||
description: 'A company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
description: 'The company name',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'domainName',
|
||||
label: 'Domain Name',
|
||||
targetColumnMap: {
|
||||
value: 'domainName',
|
||||
},
|
||||
description:
|
||||
'The company website URL. We use this url to fetch the company icon',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'address',
|
||||
label: 'Address',
|
||||
targetColumnMap: {
|
||||
value: 'address',
|
||||
},
|
||||
description: 'The company address',
|
||||
icon: 'IconMap',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'employees',
|
||||
label: 'Employees',
|
||||
targetColumnMap: {
|
||||
value: 'employees',
|
||||
},
|
||||
description: 'Number of employees in the company',
|
||||
icon: 'IconUsers',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.LINK,
|
||||
name: 'linkedinLink',
|
||||
label: 'Linkedin',
|
||||
targetColumnMap: {
|
||||
value: 'linkedinLink',
|
||||
},
|
||||
description: 'The company Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.LINK,
|
||||
name: 'xLink',
|
||||
label: 'X',
|
||||
targetColumnMap: {
|
||||
value: 'xLink',
|
||||
},
|
||||
description: 'The company Twitter/X account',
|
||||
icon: 'IconBrandX',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'annualRecurringRevenue',
|
||||
label: 'ARR',
|
||||
targetColumnMap: {
|
||||
value: 'annualRecurringRevenue',
|
||||
},
|
||||
description:
|
||||
'Annual Recurring Revenue: The actual or estimated annual revenue of the company',
|
||||
icon: 'IconMoneybag',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
name: 'idealCustomerProfile',
|
||||
label: 'ICP',
|
||||
targetColumnMap: {
|
||||
value: 'idealCustomerProfile',
|
||||
},
|
||||
description:
|
||||
'Ideal Customer Profile: Indicates whether the company is the most suitable and valuable customer for you',
|
||||
icon: 'IconTarget',
|
||||
isNullable: true,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'people',
|
||||
label: 'People',
|
||||
targetColumnMap: {},
|
||||
description: 'People linked to the company.',
|
||||
icon: 'IconUsers',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'accountOwner',
|
||||
label: 'Account Owner',
|
||||
targetColumnMap: {
|
||||
value: 'accountOwnerId',
|
||||
},
|
||||
description:
|
||||
'Your team member responsible for managing the company account',
|
||||
icon: 'IconUserCircle',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activityTargets',
|
||||
label: 'Activities',
|
||||
targetColumnMap: {},
|
||||
description: 'Activities tied to the company',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
targetColumnMap: {},
|
||||
description: 'Opportunities linked to the company.',
|
||||
icon: 'IconTargetArrow',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
targetColumnMap: {},
|
||||
description: 'Favorites linked to the company',
|
||||
icon: 'IconHeart',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
targetColumnMap: {},
|
||||
description: 'Attachments linked to the company.',
|
||||
icon: 'IconFileImport',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default companyMetadata;
|
||||
@ -0,0 +1,70 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const favoriteMetadata = {
|
||||
nameSingular: 'favoriteV2',
|
||||
namePlural: 'favoritesV2',
|
||||
labelSingular: 'Favorite',
|
||||
labelPlural: 'Favorites',
|
||||
targetTableName: 'favorite',
|
||||
description: 'A favorite',
|
||||
icon: 'IconHeart',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
targetColumnMap: {
|
||||
value: 'position',
|
||||
},
|
||||
description: 'Favorite position',
|
||||
icon: 'IconList',
|
||||
isNullable: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'workspaceMember',
|
||||
label: 'Workspace Member',
|
||||
targetColumnMap: {
|
||||
value: 'workspaceMemberId',
|
||||
},
|
||||
description: 'Favorite workspace member',
|
||||
icon: 'IconCircleUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
targetColumnMap: {
|
||||
value: 'personId',
|
||||
},
|
||||
description: 'Favorite person',
|
||||
icon: 'IconUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
targetColumnMap: {
|
||||
value: 'companyId',
|
||||
},
|
||||
description: 'Favorite company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default favoriteMetadata;
|
||||
@ -0,0 +1,109 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const opportunityMetadata = {
|
||||
nameSingular: 'opportunityV2',
|
||||
namePlural: 'opportunitiesV2',
|
||||
labelSingular: 'Opportunity',
|
||||
labelPlural: 'Opportunities',
|
||||
targetTableName: 'opportunity',
|
||||
description: 'An opportunity',
|
||||
icon: 'IconTargetArrow',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'amount',
|
||||
label: 'Amount',
|
||||
targetColumnMap: {
|
||||
value: 'amount',
|
||||
},
|
||||
description: 'Opportunity amount',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.DATE,
|
||||
name: 'closeDate',
|
||||
label: 'Close date',
|
||||
targetColumnMap: {
|
||||
value: 'closeDate',
|
||||
},
|
||||
description: 'Opportunity close date',
|
||||
icon: 'IconCalendarEvent',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'probability',
|
||||
label: 'Probability',
|
||||
targetColumnMap: {
|
||||
value: 'probability',
|
||||
},
|
||||
description: 'Opportunity amount',
|
||||
icon: 'IconProgressCheck',
|
||||
isNullable: true,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'pipelineStep',
|
||||
label: 'Pipeline Step',
|
||||
targetColumnMap: {
|
||||
value: 'pipelineStepId',
|
||||
},
|
||||
description: 'Opportunity pipeline step',
|
||||
icon: 'IconKanban',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'pointOfContact',
|
||||
label: 'Point of Contact',
|
||||
targetColumnMap: {
|
||||
value: 'pointOfContactId',
|
||||
},
|
||||
description: 'Opportunity point of contact',
|
||||
icon: 'IconUser',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'person',
|
||||
label: 'Person',
|
||||
targetColumnMap: {
|
||||
value: 'personId',
|
||||
},
|
||||
description: 'Opportunity person',
|
||||
icon: 'IconUser',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
targetColumnMap: {
|
||||
value: 'companyId',
|
||||
},
|
||||
description: 'Opportunity company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default opportunityMetadata;
|
||||
@ -0,0 +1,203 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const personMetadata = {
|
||||
nameSingular: 'personV2',
|
||||
namePlural: 'peopleV2',
|
||||
labelSingular: 'Person',
|
||||
labelPlural: 'People',
|
||||
targetTableName: 'person',
|
||||
description: 'A person',
|
||||
icon: 'IconUser',
|
||||
isActive: true,
|
||||
isSystem: false,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
targetColumnMap: {
|
||||
value: 'firstName',
|
||||
},
|
||||
description: 'Contact’s first name',
|
||||
icon: 'IconUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
targetColumnMap: {
|
||||
value: 'lastName',
|
||||
},
|
||||
description: 'Contact’s last name',
|
||||
icon: 'IconUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.EMAIL,
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
targetColumnMap: {
|
||||
value: 'email',
|
||||
},
|
||||
description: 'Contact’s Email',
|
||||
icon: 'IconMail',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.LINK,
|
||||
name: 'linkedinUrl',
|
||||
label: 'Linkedin',
|
||||
targetColumnMap: {
|
||||
value: 'linkedinUrl',
|
||||
},
|
||||
description: 'Contact’s Linkedin account',
|
||||
icon: 'IconBrandLinkedin',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.LINK,
|
||||
name: 'xUrl',
|
||||
label: 'X',
|
||||
targetColumnMap: {
|
||||
value: 'xUrl',
|
||||
},
|
||||
description: 'Contact’s X/Twitter account',
|
||||
icon: 'IconUser',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'jobTitle',
|
||||
label: 'Job Title',
|
||||
targetColumnMap: {
|
||||
value: 'jobTitle',
|
||||
},
|
||||
description: 'Contact’s job title',
|
||||
icon: 'IconBriefcase',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'phone',
|
||||
label: 'Phone',
|
||||
targetColumnMap: {
|
||||
value: 'phone',
|
||||
},
|
||||
description: 'Contact’s phone number',
|
||||
icon: 'IconPhone',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'city',
|
||||
label: 'City',
|
||||
targetColumnMap: {
|
||||
value: 'city',
|
||||
},
|
||||
description: 'Contact’s city',
|
||||
icon: 'IconMap',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'avatarUrl',
|
||||
label: 'Avatar',
|
||||
targetColumnMap: {
|
||||
value: 'avatarUrl',
|
||||
},
|
||||
description: 'Contact’s avatar',
|
||||
icon: 'IconFileUpload',
|
||||
isNullable: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'company',
|
||||
label: 'Company',
|
||||
targetColumnMap: {
|
||||
value: 'companyId',
|
||||
},
|
||||
description: 'Contact’s company',
|
||||
icon: 'IconBuildingSkyscraper',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'pointOfContactForOpportunities',
|
||||
label: 'POC for Opportunities',
|
||||
targetColumnMap: {},
|
||||
description: 'Point of Contact for Opportunities',
|
||||
icon: 'IconArrowTarget',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'activityTargets',
|
||||
label: 'Activities',
|
||||
targetColumnMap: {},
|
||||
description: 'Activities tied to the contact',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
targetColumnMap: {},
|
||||
description: 'Opportunities linked to the contact.',
|
||||
icon: 'IconTargetArrow',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
targetColumnMap: {},
|
||||
description: 'Favorites linked to the contact',
|
||||
icon: 'IconHeart',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'attachments',
|
||||
label: 'Attachments',
|
||||
targetColumnMap: {},
|
||||
description: 'Attachments linked to the contact.',
|
||||
icon: 'IconFileImport',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default personMetadata;
|
||||
@ -0,0 +1,68 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const pipelineStepMetadata = {
|
||||
nameSingular: 'pipelineStepV2',
|
||||
namePlural: 'pipelineStepsV2',
|
||||
labelSingular: 'Pipeline Step',
|
||||
labelPlural: 'Pipeline Steps',
|
||||
targetTableName: 'pipelineStep',
|
||||
description: 'A pipeline step',
|
||||
icon: 'IconLayoutKanban',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
description: 'Pipeline Step name',
|
||||
icon: 'IconCurrencyDollar',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'color',
|
||||
label: 'Color',
|
||||
targetColumnMap: {
|
||||
value: 'color',
|
||||
},
|
||||
description: 'Pipeline Step color',
|
||||
icon: 'IconColorSwatch',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
targetColumnMap: {
|
||||
value: 'position',
|
||||
},
|
||||
description: 'Pipeline Step position',
|
||||
icon: 'IconHierarchy2',
|
||||
isNullable: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'opportunities',
|
||||
label: 'Opportunities',
|
||||
targetColumnMap: {},
|
||||
description: 'Opportunities linked to the step.',
|
||||
icon: 'IconTargetArrow',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default pipelineStepMetadata;
|
||||
@ -0,0 +1,27 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const activityRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'activityV2',
|
||||
toObjectNameSingular: 'activityTargetV2',
|
||||
fromFieldMetadataName: 'activityTargets',
|
||||
toFieldMetadataName: 'activity',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'activityV2',
|
||||
toObjectNameSingular: 'attachmentV2',
|
||||
fromFieldMetadataName: 'attachments',
|
||||
toFieldMetadataName: 'activity',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'activityV2',
|
||||
toObjectNameSingular: 'commentV2',
|
||||
fromFieldMetadataName: 'comments',
|
||||
toFieldMetadataName: 'activity',
|
||||
},
|
||||
];
|
||||
|
||||
export default activityRelationMetadata;
|
||||
@ -0,0 +1,41 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const companyRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'companyV2',
|
||||
toObjectNameSingular: 'personV2',
|
||||
fromFieldMetadataName: 'people',
|
||||
toFieldMetadataName: 'company',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'companyV2',
|
||||
toObjectNameSingular: 'favoriteV2',
|
||||
fromFieldMetadataName: 'favorites',
|
||||
toFieldMetadataName: 'company',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'companyV2',
|
||||
toObjectNameSingular: 'attachmentV2',
|
||||
fromFieldMetadataName: 'attachments',
|
||||
toFieldMetadataName: 'company',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'companyV2',
|
||||
toObjectNameSingular: 'opportunityV2',
|
||||
fromFieldMetadataName: 'opportunities',
|
||||
toFieldMetadataName: 'company',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'companyV2',
|
||||
toObjectNameSingular: 'activityTargetV2',
|
||||
fromFieldMetadataName: 'activityTargets',
|
||||
toFieldMetadataName: 'company',
|
||||
},
|
||||
];
|
||||
|
||||
export default companyRelationMetadata;
|
||||
@ -0,0 +1,41 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const personRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'personV2',
|
||||
toObjectNameSingular: 'favoriteV2',
|
||||
fromFieldMetadataName: 'favorites',
|
||||
toFieldMetadataName: 'person',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'personV2',
|
||||
toObjectNameSingular: 'attachmentV2',
|
||||
fromFieldMetadataName: 'attachments',
|
||||
toFieldMetadataName: 'person',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'personV2',
|
||||
toObjectNameSingular: 'opportunityV2',
|
||||
fromFieldMetadataName: 'opportunities',
|
||||
toFieldMetadataName: 'person',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'personV2',
|
||||
toObjectNameSingular: 'opportunityV2',
|
||||
fromFieldMetadataName: 'pointOfContactForOpportunities',
|
||||
toFieldMetadataName: 'pointOfContact',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'personV2',
|
||||
toObjectNameSingular: 'activityTargetV2',
|
||||
fromFieldMetadataName: 'activityTargets',
|
||||
toFieldMetadataName: 'person',
|
||||
},
|
||||
];
|
||||
|
||||
export default personRelationMetadata;
|
||||
@ -0,0 +1,13 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const pipelineStepRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'pipelineStepV2',
|
||||
toObjectNameSingular: 'opportunityV2',
|
||||
fromFieldMetadataName: 'opportunities',
|
||||
toFieldMetadataName: 'pipelineStep',
|
||||
},
|
||||
];
|
||||
|
||||
export default pipelineStepRelationMetadata;
|
||||
@ -0,0 +1,27 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const viewRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'viewV2',
|
||||
toObjectNameSingular: 'viewFieldV2',
|
||||
fromFieldMetadataName: 'viewFields',
|
||||
toFieldMetadataName: 'view',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'viewV2',
|
||||
toObjectNameSingular: 'viewFilterV2',
|
||||
fromFieldMetadataName: 'viewFilters',
|
||||
toFieldMetadataName: 'view',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'viewV2',
|
||||
toObjectNameSingular: 'viewSortV2',
|
||||
fromFieldMetadataName: 'viewSorts',
|
||||
toFieldMetadataName: 'view',
|
||||
},
|
||||
];
|
||||
|
||||
export default viewRelationMetadata;
|
||||
@ -0,0 +1,48 @@
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
const workspaceMemberRelationMetadata = [
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'companyV2',
|
||||
fromFieldMetadataName: 'accountOwnerForCompanies',
|
||||
toFieldMetadataName: 'accountOwner',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'favoriteV2',
|
||||
fromFieldMetadataName: 'favorites',
|
||||
toFieldMetadataName: 'workspaceMember',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'activityV2',
|
||||
fromFieldMetadataName: 'authoredActivities',
|
||||
toFieldMetadataName: 'author',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'activityV2',
|
||||
fromFieldMetadataName: 'assignedActivities',
|
||||
toFieldMetadataName: 'assignee',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'commentV2',
|
||||
fromFieldMetadataName: 'authoredComments',
|
||||
toFieldMetadataName: 'author',
|
||||
},
|
||||
{
|
||||
type: RelationMetadataType.ONE_TO_MANY,
|
||||
fromObjectNameSingular: 'workspaceMemberV2',
|
||||
toObjectNameSingular: 'attachmentV2',
|
||||
fromFieldMetadataName: 'authoredAttachments',
|
||||
toFieldMetadataName: 'author',
|
||||
},
|
||||
];
|
||||
|
||||
export default workspaceMemberRelationMetadata;
|
||||
@ -0,0 +1,78 @@
|
||||
import activityTargetMetadata from 'src/workspace/workspace-manager/standard-objects/activity-target';
|
||||
import activityMetadata from 'src/workspace/workspace-manager/standard-objects/activity';
|
||||
import apiKeyMetadata from 'src/workspace/workspace-manager/standard-objects/api-key';
|
||||
import attachmentMetadata from 'src/workspace/workspace-manager/standard-objects/attachment';
|
||||
import commentMetadata from 'src/workspace/workspace-manager/standard-objects/comment';
|
||||
import favoriteMetadata from 'src/workspace/workspace-manager/standard-objects/favorite';
|
||||
import opportunityMetadata from 'src/workspace/workspace-manager/standard-objects/opportunity';
|
||||
import personMetadata from 'src/workspace/workspace-manager/standard-objects/person';
|
||||
import viewMetadata from 'src/workspace/workspace-manager/standard-objects/view';
|
||||
import viewFieldMetadata from 'src/workspace/workspace-manager/standard-objects/view-field';
|
||||
import viewFilterMetadata from 'src/workspace/workspace-manager/standard-objects/view-filter';
|
||||
import viewSortMetadata from 'src/workspace/workspace-manager/standard-objects/view-sort';
|
||||
import webhookMetadata from 'src/workspace/workspace-manager/standard-objects/webhook';
|
||||
import pipelineStepMetadata from 'src/workspace/workspace-manager/standard-objects/pipeline-step';
|
||||
import companyMetadata from 'src/workspace/workspace-manager/standard-objects/company';
|
||||
import workspaceMemberMetadata from 'src/workspace/workspace-manager/standard-objects/workspace-member';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
export const standardObjectsMetadata = {
|
||||
activityTargetV2: activityTargetMetadata,
|
||||
activityV2: activityMetadata,
|
||||
apiKeyV2: apiKeyMetadata,
|
||||
attachmentV2: attachmentMetadata,
|
||||
commentV2: commentMetadata,
|
||||
companyV2: companyMetadata,
|
||||
favoriteV2: favoriteMetadata,
|
||||
opportunityV2: opportunityMetadata,
|
||||
personV2: personMetadata,
|
||||
pipelineStepV2: pipelineStepMetadata,
|
||||
viewFieldV2: viewFieldMetadata,
|
||||
viewFilterV2: viewFilterMetadata,
|
||||
viewSortV2: viewSortMetadata,
|
||||
viewV2: viewMetadata,
|
||||
webhookV2: webhookMetadata,
|
||||
workspaceMemberV2: workspaceMemberMetadata,
|
||||
};
|
||||
|
||||
export const basicFieldsMetadata: Partial<FieldMetadataEntity>[] = [
|
||||
{
|
||||
name: 'id',
|
||||
label: 'Id',
|
||||
type: FieldMetadataType.UUID,
|
||||
targetColumnMap: {
|
||||
value: 'id',
|
||||
},
|
||||
isNullable: true,
|
||||
// isSystem: true,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
label: 'Creation date',
|
||||
type: FieldMetadataType.DATE,
|
||||
targetColumnMap: {
|
||||
value: 'createdAt',
|
||||
},
|
||||
icon: 'IconCalendar',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
label: 'Update date',
|
||||
type: FieldMetadataType.DATE,
|
||||
targetColumnMap: {
|
||||
value: 'updatedAt',
|
||||
},
|
||||
icon: 'IconCalendar',
|
||||
isNullable: true,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,15 @@
|
||||
import activityRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/activity';
|
||||
import companyRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/company';
|
||||
import personRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/person';
|
||||
import pipelineStepRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/pipeline-step';
|
||||
import viewRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/view';
|
||||
import workspaceMemberRelationMetadata from 'src/workspace/workspace-manager/standard-objects/relations/workspace-member';
|
||||
|
||||
export const standardObjectRelationMetadata = [
|
||||
...activityRelationMetadata,
|
||||
...companyRelationMetadata,
|
||||
...personRelationMetadata,
|
||||
...pipelineStepRelationMetadata,
|
||||
...viewRelationMetadata,
|
||||
...workspaceMemberRelationMetadata,
|
||||
];
|
||||
@ -0,0 +1,94 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const viewFieldMetadata = {
|
||||
nameSingular: 'viewFieldV2',
|
||||
namePlural: 'viewFieldsV2',
|
||||
labelSingular: 'View Field',
|
||||
labelPlural: 'View Fields',
|
||||
targetTableName: 'viewField',
|
||||
description: '(System) View Fields',
|
||||
icon: 'IconTag',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'fieldMetadataId',
|
||||
label: 'Field Metadata Id',
|
||||
targetColumnMap: {
|
||||
value: 'fieldMetadataId',
|
||||
},
|
||||
description: 'View Field target field',
|
||||
icon: 'IconTag',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
name: 'isVisible',
|
||||
label: 'Visible',
|
||||
targetColumnMap: {
|
||||
value: 'isVisible',
|
||||
},
|
||||
description: 'View Field visibility',
|
||||
icon: 'IconEye',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
targetColumnMap: {
|
||||
value: 'size',
|
||||
},
|
||||
description: 'View Field size',
|
||||
icon: 'IconEye',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.NUMBER,
|
||||
name: 'position',
|
||||
label: 'Position',
|
||||
targetColumnMap: {
|
||||
value: 'position',
|
||||
},
|
||||
description: 'View Field position',
|
||||
icon: 'IconList',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'view',
|
||||
label: 'View',
|
||||
targetColumnMap: { value: 'viewId' },
|
||||
description: 'View Field related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
// Temporary hack?
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
value: 'viewId',
|
||||
},
|
||||
description: 'View field related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default viewFieldMetadata;
|
||||
@ -0,0 +1,94 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const viewFilterMetadata = {
|
||||
nameSingular: 'viewFilterV2',
|
||||
namePlural: 'viewFiltersV2',
|
||||
labelSingular: 'View Filter',
|
||||
labelPlural: 'View Filters',
|
||||
targetTableName: 'viewFilter',
|
||||
description: '(System) View Filters',
|
||||
icon: 'IconFilterBolt',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'fieldMetadataId',
|
||||
label: 'Field Metadata Id',
|
||||
targetColumnMap: {
|
||||
value: 'fieldMetadataId',
|
||||
},
|
||||
description: 'View Filter target field',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'operand',
|
||||
label: 'Operand',
|
||||
targetColumnMap: {
|
||||
value: 'operand',
|
||||
},
|
||||
description: 'View Filter operand',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'value',
|
||||
label: 'Value',
|
||||
targetColumnMap: {
|
||||
value: 'value',
|
||||
},
|
||||
description: 'View Filter value',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'displayValue',
|
||||
label: 'Display Value',
|
||||
targetColumnMap: {
|
||||
value: 'displayValue',
|
||||
},
|
||||
description: 'View Filter Display Value',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'view',
|
||||
label: 'View',
|
||||
targetColumnMap: { value: 'viewId' },
|
||||
description: 'View Filter related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
// Temporary hack?
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
value: 'viewId',
|
||||
},
|
||||
description: 'View Filter related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default viewFilterMetadata;
|
||||
@ -0,0 +1,70 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const viewSortMetadata = {
|
||||
nameSingular: 'viewSortV2',
|
||||
namePlural: 'viewSortsV2',
|
||||
labelSingular: 'View Sort',
|
||||
labelPlural: 'View Sorts',
|
||||
targetTableName: 'viewSort',
|
||||
description: '(System) View Sorts',
|
||||
icon: 'IconArrowsSort',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'fieldMetadataId',
|
||||
label: 'Field Metadata Id',
|
||||
targetColumnMap: {
|
||||
value: 'fieldMetadataId',
|
||||
},
|
||||
description: 'View Sort target field',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'direction',
|
||||
label: 'Direction',
|
||||
targetColumnMap: {
|
||||
value: 'direction',
|
||||
},
|
||||
description: 'View Sort direction',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'view',
|
||||
label: 'View',
|
||||
targetColumnMap: {
|
||||
value: 'viewId',
|
||||
},
|
||||
description: 'View Sort related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
// Temporary Hack?
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'viewId',
|
||||
label: 'View Id',
|
||||
targetColumnMap: {
|
||||
value: 'viewId',
|
||||
},
|
||||
description: 'View Sort related view',
|
||||
icon: 'IconLayoutCollage',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default viewSortMetadata;
|
||||
@ -0,0 +1,83 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const viewMetadata = {
|
||||
nameSingular: 'viewV2',
|
||||
namePlural: 'viewsV2',
|
||||
labelSingular: 'View',
|
||||
labelPlural: 'Views',
|
||||
targetTableName: 'view',
|
||||
description: '(System) Views',
|
||||
icon: 'IconLayoutCollage',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
targetColumnMap: {
|
||||
value: 'name',
|
||||
},
|
||||
description: 'View name',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'objectMetadataId',
|
||||
label: 'Object Metadata Id',
|
||||
targetColumnMap: {
|
||||
value: 'objectMetadataId',
|
||||
},
|
||||
description: 'View target object',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'type',
|
||||
label: 'Type',
|
||||
targetColumnMap: {
|
||||
value: 'type',
|
||||
},
|
||||
description: 'View type',
|
||||
icon: null,
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'viewFields',
|
||||
label: 'View Fields',
|
||||
targetColumnMap: {},
|
||||
description: 'View Fields',
|
||||
icon: 'IconTag',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'viewSorts',
|
||||
label: 'View Sorts',
|
||||
targetColumnMap: {},
|
||||
description: 'View Sorts',
|
||||
icon: 'IconArrowsSort',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'viewFilters',
|
||||
label: 'View Filters',
|
||||
targetColumnMap: {},
|
||||
description: 'View Filters',
|
||||
icon: 'IconFilterBolt',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default viewMetadata;
|
||||
@ -0,0 +1,43 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const webhookMetadata = {
|
||||
nameSingular: 'webhookV2',
|
||||
namePlural: 'webhooksV2',
|
||||
labelSingular: 'Webhook',
|
||||
labelPlural: 'Webhooks',
|
||||
targetTableName: 'webhook',
|
||||
description: 'A webhook',
|
||||
icon: 'IconRobot',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'targetUrl',
|
||||
label: 'Target Url',
|
||||
targetColumnMap: {
|
||||
value: 'targetUrl',
|
||||
},
|
||||
description: 'Webhook target url',
|
||||
icon: 'IconLink',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'operation',
|
||||
label: 'Operation',
|
||||
targetColumnMap: {
|
||||
value: 'operation',
|
||||
},
|
||||
description: 'Webhook operation',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default webhookMetadata;
|
||||
@ -0,0 +1,176 @@
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
const workspaceMemberMetadata = {
|
||||
nameSingular: 'workspaceMemberV2',
|
||||
namePlural: 'workspaceMembersV2',
|
||||
labelSingular: 'Workspace Member',
|
||||
labelPlural: 'Workspace Members',
|
||||
targetTableName: 'workspaceMember',
|
||||
description: 'A workspace member',
|
||||
icon: 'IconUserCircle',
|
||||
isActive: true,
|
||||
isSystem: true,
|
||||
fields: [
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'firstName',
|
||||
label: 'First name',
|
||||
targetColumnMap: {
|
||||
value: 'firstName',
|
||||
},
|
||||
description: 'Workspace member first name',
|
||||
icon: 'IconCircleUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'lastName',
|
||||
label: 'Last name',
|
||||
targetColumnMap: {
|
||||
value: 'lastName',
|
||||
},
|
||||
description: 'Workspace member last name',
|
||||
icon: 'IconCircleUser',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.UUID,
|
||||
name: 'userId',
|
||||
label: 'User Id',
|
||||
targetColumnMap: {
|
||||
value: 'userId',
|
||||
},
|
||||
description: 'Associated User Id',
|
||||
icon: 'IconCircleUsers',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.BOOLEAN,
|
||||
name: 'allowImpersonation',
|
||||
label: 'Admin Access',
|
||||
targetColumnMap: {
|
||||
value: 'allowImpersonation',
|
||||
},
|
||||
description: 'Allow Admin Access',
|
||||
icon: 'IconEye',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'colorScheme',
|
||||
label: 'Color Scheme',
|
||||
targetColumnMap: {
|
||||
value: 'colorScheme',
|
||||
},
|
||||
description: 'Preferred color scheme',
|
||||
icon: 'IconColorSwatch',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'locale',
|
||||
label: 'Language',
|
||||
targetColumnMap: {
|
||||
value: 'locale',
|
||||
},
|
||||
description: 'Preferred language',
|
||||
icon: 'IconLanguage',
|
||||
isNullable: false,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: 'avatarUrl',
|
||||
label: 'Avatar Url',
|
||||
targetColumnMap: {
|
||||
value: 'avatarUrl',
|
||||
},
|
||||
description: 'Workspace member avatar',
|
||||
icon: 'IconFileUpload',
|
||||
isNullable: true,
|
||||
isSystem: false,
|
||||
},
|
||||
// Relations
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'authoredActivities',
|
||||
label: 'Authored activities',
|
||||
targetColumnMap: {},
|
||||
description: 'Activities created by the workspace member',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'assignedActivities',
|
||||
label: 'Assigned activities',
|
||||
targetColumnMap: {},
|
||||
description: 'Activities assigned to the workspace member',
|
||||
icon: 'IconCheckbox',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'favorites',
|
||||
label: 'Favorites',
|
||||
targetColumnMap: {},
|
||||
description: 'Favorites linked to the workspace member',
|
||||
icon: 'IconHeart',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'accountOwnerForCompanies',
|
||||
label: 'Account Owner For Companies',
|
||||
targetColumnMap: {},
|
||||
description: 'Account owner for companies',
|
||||
icon: 'IconBriefcase',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'authoredAttachments',
|
||||
label: 'Authored attachments',
|
||||
targetColumnMap: {},
|
||||
description: 'Attachments created by the workspace member',
|
||||
icon: 'IconFileImport',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
type: FieldMetadataType.RELATION,
|
||||
name: 'authoredComments',
|
||||
label: 'Authored comments',
|
||||
targetColumnMap: {},
|
||||
description: 'Authored comments',
|
||||
icon: 'IconComment',
|
||||
isNullable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default workspaceMemberMetadata;
|
||||
@ -0,0 +1,26 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { DataSourceModule } from 'src/metadata/data-source/data-source.module';
|
||||
import { FieldMetadataModule } from 'src/metadata/field-metadata/field-metadata.module';
|
||||
import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module';
|
||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||
import { RelationMetadataModule } from 'src/metadata/relation-metadata/relation-metadata.module';
|
||||
|
||||
import { WorkspaceManagerService } from './workspace-manager.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceDataSourceModule,
|
||||
WorkspaceMigrationModule,
|
||||
WorkspaceMigrationRunnerModule,
|
||||
ObjectMetadataModule,
|
||||
FieldMetadataModule,
|
||||
DataSourceModule,
|
||||
RelationMetadataModule,
|
||||
],
|
||||
exports: [WorkspaceManagerService],
|
||||
providers: [WorkspaceManagerService],
|
||||
})
|
||||
export class WorkspaceManagerModule {}
|
||||
@ -0,0 +1,265 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { DataSourceService } from 'src/metadata/data-source/data-source.service';
|
||||
import { FieldMetadataService } from 'src/metadata/field-metadata/field-metadata.service';
|
||||
import { ObjectMetadataService } from 'src/metadata/object-metadata/object-metadata.service';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
|
||||
import { standardObjectsPrefillData } from 'src/workspace/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data';
|
||||
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
|
||||
import { DataSourceEntity } from 'src/metadata/data-source/data-source.entity';
|
||||
import { RelationMetadataService } from 'src/metadata/relation-metadata/relation-metadata.service';
|
||||
import { standardObjectRelationMetadata } from 'src/workspace/workspace-manager/standard-objects/standard-object-relation-metadata';
|
||||
import { ObjectMetadataEntity } from 'src/metadata/object-metadata/object-metadata.entity';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
|
||||
import {
|
||||
basicFieldsMetadata,
|
||||
standardObjectsMetadata,
|
||||
} from './standard-objects/standard-object-metadata';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceManagerService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly objectMetadataService: ObjectMetadataService,
|
||||
private readonly fieldMetadataService: FieldMetadataService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly relationMetadataService: RelationMetadataService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Init a workspace by creating a new data source and running all migrations
|
||||
* @param workspaceId
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
public async init(workspaceId: string): Promise<void> {
|
||||
const schemaName =
|
||||
await this.workspaceDataSourceService.createWorkspaceDBSchema(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const dataSourceMetadata =
|
||||
await this.dataSourceService.createDataSourceMetadata(
|
||||
workspaceId,
|
||||
schemaName,
|
||||
);
|
||||
|
||||
await this.workspaceMigrationService.insertStandardMigrations(workspaceId);
|
||||
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const createdObjectMetadata =
|
||||
await this.createStandardObjectsAndFieldsMetadata(
|
||||
dataSourceMetadata.id,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.prefillWorkspaceWithStandardObjects(
|
||||
dataSourceMetadata,
|
||||
workspaceId,
|
||||
createdObjectMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Create all standard objects and fields metadata for a given workspace
|
||||
*
|
||||
* @param dataSourceId
|
||||
* @param workspaceId
|
||||
*/
|
||||
public async createStandardObjectsAndFieldsMetadata(
|
||||
dataSourceId: string,
|
||||
workspaceId: string,
|
||||
): Promise<ObjectMetadataEntity[]> {
|
||||
const createdObjectMetadata = await this.objectMetadataService.createMany(
|
||||
Object.values(standardObjectsMetadata).map(
|
||||
(objectMetadata: ObjectMetadataEntity) => ({
|
||||
...objectMetadata,
|
||||
dataSourceId,
|
||||
workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
fields: [...basicFieldsMetadata, ...objectMetadata.fields].map(
|
||||
(field) => ({
|
||||
...field,
|
||||
workspaceId,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await this.relationMetadataService.createMany(
|
||||
Object.values(standardObjectRelationMetadata).map((relationMetadata) =>
|
||||
this.createStandardObjectRelations(
|
||||
workspaceId,
|
||||
createdObjectMetadata,
|
||||
relationMetadata,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return createdObjectMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param workspaceId
|
||||
* @param createdObjectMetadata
|
||||
* @param relationMetadata
|
||||
* @returns Partial<RelationMetadataEntity>
|
||||
*/
|
||||
private createStandardObjectRelations(
|
||||
workspaceId: string,
|
||||
createdObjectMetadata: ObjectMetadataEntity[],
|
||||
relationMetadata: any,
|
||||
) {
|
||||
const createdObjectMetadataByNameSingular = createdObjectMetadata.reduce(
|
||||
(acc, curr) => {
|
||||
acc[curr.nameSingular] = curr;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const fromObjectMetadata =
|
||||
createdObjectMetadataByNameSingular[
|
||||
relationMetadata.fromObjectNameSingular
|
||||
];
|
||||
const toObjectMetadata =
|
||||
createdObjectMetadataByNameSingular[
|
||||
relationMetadata.toObjectNameSingular
|
||||
];
|
||||
|
||||
if (!fromObjectMetadata) {
|
||||
throw new Error(
|
||||
`Could not find created object metadata with
|
||||
fromObjectNameSingular: ${relationMetadata.fromObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!toObjectMetadata) {
|
||||
throw new Error(
|
||||
`Could not find created object metadata with
|
||||
toObjectNameSingular: ${relationMetadata.toObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
const fromFieldMetadata = createdObjectMetadataByNameSingular[
|
||||
relationMetadata.fromObjectNameSingular
|
||||
]?.fields.find(
|
||||
(field: FieldMetadataEntity) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.name === relationMetadata.fromFieldMetadataName,
|
||||
);
|
||||
|
||||
const toFieldMetadata = createdObjectMetadataByNameSingular[
|
||||
relationMetadata.toObjectNameSingular
|
||||
]?.fields.find(
|
||||
(field: FieldMetadataEntity) =>
|
||||
field.type === FieldMetadataType.RELATION &&
|
||||
field.name === relationMetadata.toFieldMetadataName,
|
||||
);
|
||||
|
||||
if (!fromFieldMetadata) {
|
||||
throw new Error(
|
||||
`Could not find created field metadata with
|
||||
fromFieldMetadataName: ${relationMetadata.fromFieldMetadataName}
|
||||
for object: ${relationMetadata.fromObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!toFieldMetadata) {
|
||||
throw new Error(
|
||||
`Could not find created field metadata with
|
||||
toFieldMetadataName: ${relationMetadata.toFieldMetadataName}
|
||||
for object: ${relationMetadata.toObjectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
fromObjectMetadataId: fromObjectMetadata.id,
|
||||
toObjectMetadataId: toObjectMetadata.id,
|
||||
workspaceId,
|
||||
relationType: relationMetadata.type,
|
||||
fromFieldMetadataId: fromFieldMetadata.id,
|
||||
toFieldMetadataId: toFieldMetadata.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Reset all standard objects and fields metadata for a given workspace
|
||||
*
|
||||
* @param dataSourceId
|
||||
* @param workspaceId
|
||||
*/
|
||||
public async resetStandardObjectsAndFieldsMetadata(
|
||||
dataSourceId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
await this.objectMetadataService.deleteMany({
|
||||
workspaceId: { eq: workspaceId },
|
||||
});
|
||||
|
||||
await this.createStandardObjectsAndFieldsMetadata(
|
||||
dataSourceId,
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* We are prefilling a few standard objects with data to make it easier for the user to get started.
|
||||
*
|
||||
* @param dataSourceMetadata
|
||||
* @param workspaceId
|
||||
*/
|
||||
private async prefillWorkspaceWithStandardObjects(
|
||||
dataSourceMetadata: DataSourceEntity,
|
||||
workspaceId: string,
|
||||
createdObjectMetadata: ObjectMetadataEntity[],
|
||||
) {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('Could not connect to workspace data source');
|
||||
}
|
||||
|
||||
standardObjectsPrefillData(
|
||||
workspaceDataSource,
|
||||
dataSourceMetadata.schema,
|
||||
createdObjectMetadata,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Delete a workspace by deleting all metadata and the schema
|
||||
*
|
||||
* @param workspaceId
|
||||
*/
|
||||
public async delete(workspaceId: string): Promise<void> {
|
||||
// Delete data from metadata tables
|
||||
await this.fieldMetadataService.deleteFieldsMetadata(workspaceId);
|
||||
await this.objectMetadataService.deleteObjectsMetadata(workspaceId);
|
||||
await this.workspaceMigrationService.delete(workspaceId);
|
||||
await this.dataSourceService.delete(workspaceId);
|
||||
// Delete schema
|
||||
await this.workspaceDataSourceService.deleteWorkspaceDBSchema(workspaceId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.service';
|
||||
|
||||
// TODO: implement dry-run
|
||||
interface RunWorkspaceMigrationsOptions {
|
||||
workspaceId: string;
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'workspace:migrate',
|
||||
description: 'Run workspace migrations',
|
||||
})
|
||||
export class RunWorkspaceMigrationsCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(
|
||||
_passedParam: string[],
|
||||
options: RunWorkspaceMigrationsOptions,
|
||||
): Promise<void> {
|
||||
// TODO: run in a dedicated job + run queries in a transaction.
|
||||
await this.workspaceMigrationService.insertStandardMigrations(
|
||||
options.workspaceId,
|
||||
);
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
options.workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: workspaceId should be optional and we should run migrations for all workspaces
|
||||
@Option({
|
||||
flags: '-w, --workspace-id [workspace_id]',
|
||||
description: 'workspace id',
|
||||
required: true,
|
||||
})
|
||||
parseWorkspaceId(value: string): string {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/workspace/workspace-migration-runner/workspace-migration-runner.module';
|
||||
|
||||
import { RunWorkspaceMigrationsCommand } from './run-workspace-migrations.command';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceMigrationModule, WorkspaceMigrationRunnerModule],
|
||||
providers: [RunWorkspaceMigrationsCommand],
|
||||
})
|
||||
export class WorkspaceMigrationRunnerCommandsModule {}
|
||||
@ -0,0 +1,25 @@
|
||||
import { TableColumnOptions } from 'typeorm';
|
||||
|
||||
export const customTableDefaultColumns: TableColumnOptions[] = [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'uuid',
|
||||
isPrimary: true,
|
||||
default: 'public.uuid_generate_v4()',
|
||||
},
|
||||
{
|
||||
name: 'createdAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'updatedAt',
|
||||
type: 'timestamp',
|
||||
default: 'now()',
|
||||
},
|
||||
{
|
||||
name: 'deletedAt',
|
||||
type: 'timestamp',
|
||||
isNullable: true,
|
||||
},
|
||||
];
|
||||
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceMigrationModule } from 'src/metadata/workspace-migration/workspace-migration.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/workspace/workspace-datasource/workspace-datasource.module';
|
||||
|
||||
import { WorkspaceMigrationRunnerService } from './workspace-migration-runner.service';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceDataSourceModule, WorkspaceMigrationModule],
|
||||
exports: [WorkspaceMigrationRunnerService],
|
||||
providers: [WorkspaceMigrationRunnerService],
|
||||
})
|
||||
export class WorkspaceMigrationRunnerModule {}
|
||||
@ -0,0 +1,240 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableColumn,
|
||||
TableForeignKey,
|
||||
TableUnique,
|
||||
} from 'typeorm';
|
||||
|
||||
import { WorkspaceMigrationService } from 'src/metadata/workspace-migration/workspace-migration.service';
|
||||
import { WorkspaceDataSourceService } from 'src/workspace/workspace-datasource/workspace-datasource.service';
|
||||
import {
|
||||
WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationColumnAction,
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationColumnCreate,
|
||||
WorkspaceMigrationColumnRelation,
|
||||
} from 'src/metadata/workspace-migration/workspace-migration.entity';
|
||||
|
||||
import { customTableDefaultColumns } from './utils/custom-table-default-column.util';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationRunnerService {
|
||||
constructor(
|
||||
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Executes pending migrations for a given workspace
|
||||
*
|
||||
* @param workspaceId string
|
||||
* @returns Promise<WorkspaceMigrationTableAction[]>
|
||||
*/
|
||||
public async executeMigrationFromPendingMigrations(
|
||||
workspaceId: string,
|
||||
): Promise<WorkspaceMigrationTableAction[]> {
|
||||
const workspaceDataSource =
|
||||
await this.workspaceDataSourceService.connectToWorkspaceDataSource(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!workspaceDataSource) {
|
||||
throw new Error('Workspace data source not found');
|
||||
}
|
||||
|
||||
const pendingMigrations =
|
||||
await this.workspaceMigrationService.getPendingMigrations(workspaceId);
|
||||
|
||||
if (pendingMigrations.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flattenedPendingMigrations: WorkspaceMigrationTableAction[] =
|
||||
pendingMigrations.reduce((acc, pendingMigration) => {
|
||||
return [...acc, ...pendingMigration.migrations];
|
||||
}, []);
|
||||
|
||||
const queryRunner = workspaceDataSource?.createQueryRunner();
|
||||
const schemaName =
|
||||
this.workspaceDataSourceService.getSchemaName(workspaceId);
|
||||
|
||||
// Loop over each migration and create or update the table
|
||||
// TODO: Should be done in a transaction
|
||||
for (const migration of flattenedPendingMigrations) {
|
||||
await this.handleTableChanges(queryRunner, schemaName, migration);
|
||||
}
|
||||
|
||||
// Update appliedAt date for each migration
|
||||
// TODO: Should be done after the migration is successful
|
||||
for (const pendingMigration of pendingMigrations) {
|
||||
await this.workspaceMigrationService.setAppliedAtForMigration(
|
||||
workspaceId,
|
||||
pendingMigration,
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.release();
|
||||
|
||||
return flattenedPendingMigrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles table changes for a given migration
|
||||
*
|
||||
* @param queryRunner QueryRunner
|
||||
* @param schemaName string
|
||||
* @param tableMigration WorkspaceMigrationTableChange
|
||||
*/
|
||||
private async handleTableChanges(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableMigration: WorkspaceMigrationTableAction,
|
||||
) {
|
||||
switch (tableMigration.action) {
|
||||
case 'create':
|
||||
await this.createTable(queryRunner, schemaName, tableMigration.name);
|
||||
break;
|
||||
case 'alter':
|
||||
await this.handleColumnChanges(
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableMigration.name,
|
||||
tableMigration?.columns,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
`Migration table action ${tableMigration.action} not supported`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table for a given schema and table name
|
||||
*
|
||||
* @param queryRunner QueryRunner
|
||||
* @param schemaName string
|
||||
* @param tableName string
|
||||
*/
|
||||
private async createTable(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
) {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: tableName,
|
||||
schema: schemaName,
|
||||
columns: customTableDefaultColumns,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles column changes for a given migration
|
||||
*
|
||||
* @param queryRunner QueryRunner
|
||||
* @param schemaName string
|
||||
* @param tableName string
|
||||
* @param columnMigrations WorkspaceMigrationColumnAction[]
|
||||
* @returns
|
||||
*/
|
||||
private async handleColumnChanges(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
columnMigrations?: WorkspaceMigrationColumnAction[],
|
||||
) {
|
||||
if (!columnMigrations || columnMigrations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const columnMigration of columnMigrations) {
|
||||
switch (columnMigration.action) {
|
||||
case WorkspaceMigrationColumnActionType.CREATE:
|
||||
await this.createColumn(
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnMigration,
|
||||
);
|
||||
break;
|
||||
case WorkspaceMigrationColumnActionType.RELATION:
|
||||
await this.createForeignKey(
|
||||
queryRunner,
|
||||
schemaName,
|
||||
tableName,
|
||||
columnMigration,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Migration column action not supported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a column for a given schema, table name, and column migration
|
||||
*
|
||||
* @param queryRunner QueryRunner
|
||||
* @param schemaName string
|
||||
* @param tableName string
|
||||
* @param migrationColumn WorkspaceMigrationColumnAction
|
||||
*/
|
||||
private async createColumn(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
migrationColumn: WorkspaceMigrationColumnCreate,
|
||||
) {
|
||||
const hasColumn = await queryRunner.hasColumn(
|
||||
`${schemaName}.${tableName}`,
|
||||
migrationColumn.columnName,
|
||||
);
|
||||
if (hasColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryRunner.addColumn(
|
||||
`${schemaName}.${tableName}`,
|
||||
new TableColumn({
|
||||
name: migrationColumn.columnName,
|
||||
type: migrationColumn.columnType,
|
||||
default: migrationColumn.defaultValue,
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async createForeignKey(
|
||||
queryRunner: QueryRunner,
|
||||
schemaName: string,
|
||||
tableName: string,
|
||||
migrationColumn: WorkspaceMigrationColumnRelation,
|
||||
) {
|
||||
await queryRunner.createForeignKey(
|
||||
`${schemaName}.${tableName}`,
|
||||
new TableForeignKey({
|
||||
columnNames: [migrationColumn.columnName],
|
||||
referencedColumnNames: [migrationColumn.referencedTableColumnName],
|
||||
referencedTableName: migrationColumn.referencedTableName,
|
||||
onDelete: 'CASCADE',
|
||||
}),
|
||||
);
|
||||
|
||||
// Create unique constraint if for one to one relation
|
||||
if (migrationColumn.isUnique) {
|
||||
await queryRunner.createUniqueConstraint(
|
||||
`${schemaName}.${tableName}`,
|
||||
new TableUnique({
|
||||
name: `UNIQUE_${tableName}_${migrationColumn.columnName}`,
|
||||
columnNames: [migrationColumn.columnName],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {}
|
||||
@ -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[];
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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 {}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateManyResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<CreateManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.createMany(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class CreateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'createOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<CreateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.createOne(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
DeleteOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'deleteOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<DeleteOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.deleteOne(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { FindManyResolverFactory } from './find-many-resolver.factory';
|
||||
import { FindOneResolverFactory } from './find-one-resolver.factory';
|
||||
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
||||
import { CreateOneResolverFactory } from './create-one-resolver.factory';
|
||||
import { UpdateOneResolverFactory } from './update-one-resolver.factory';
|
||||
import { DeleteOneResolverFactory } from './delete-one-resolver.factory';
|
||||
|
||||
export const workspaceResolverBuilderFactories = [
|
||||
FindManyResolverFactory,
|
||||
FindOneResolverFactory,
|
||||
CreateManyResolverFactory,
|
||||
CreateOneResolverFactory,
|
||||
UpdateOneResolverFactory,
|
||||
DeleteOneResolverFactory,
|
||||
];
|
||||
|
||||
export const workspaceResolverBuilderMethodNames = {
|
||||
queries: [
|
||||
FindManyResolverFactory.methodName,
|
||||
FindOneResolverFactory.methodName,
|
||||
],
|
||||
mutations: [
|
||||
CreateManyResolverFactory.methodName,
|
||||
CreateOneResolverFactory.methodName,
|
||||
UpdateOneResolverFactory.methodName,
|
||||
DeleteOneResolverFactory.methodName,
|
||||
],
|
||||
} as const;
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FindManyResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindManyResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findMany' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<FindManyResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.findMany(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
FindOneResolverArgs,
|
||||
Resolver,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class FindOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'findOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<FindOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.findOne(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Resolver,
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
import { WorkspaceQueryRunnerService } from 'src/workspace/workspace-query-runner/workspace-query-runner.service';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateOneResolverFactory
|
||||
implements WorkspaceResolverBuilderFactoryInterface
|
||||
{
|
||||
public static methodName = 'updateOne' as const;
|
||||
|
||||
constructor(
|
||||
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||
) {}
|
||||
|
||||
create(
|
||||
context: WorkspaceSchemaBuilderContext,
|
||||
): Resolver<UpdateOneResolverArgs> {
|
||||
const internalContext = context;
|
||||
|
||||
return (_source, args, context, info) => {
|
||||
return this.workspaceQueryRunnerService.updateOne(args, {
|
||||
targetTableName: internalContext.targetTableName,
|
||||
workspaceId: internalContext.workspaceId,
|
||||
info,
|
||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { Record as IRecord } from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
export interface PGGraphQLResponse<Data = any> {
|
||||
resolve: {
|
||||
data: Data;
|
||||
};
|
||||
}
|
||||
|
||||
export type PGGraphQLResult<Data = any> = [PGGraphQLResponse<Data>];
|
||||
|
||||
export interface PGGraphQLMutation<Record = IRecord> {
|
||||
affectedRows: number;
|
||||
records: Record[];
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { WorkspaceSchemaBuilderContext } from 'src/workspace/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||
|
||||
import { Resolver } from './workspace-resolvers-builder.interface';
|
||||
|
||||
export interface WorkspaceResolverBuilderFactoryInterface {
|
||||
create(context: WorkspaceSchemaBuilderContext): Resolver;
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { GraphQLFieldResolver } from 'graphql';
|
||||
|
||||
import {
|
||||
Record,
|
||||
RecordFilter,
|
||||
RecordOrderBy,
|
||||
} from 'src/workspace/workspace-query-builder/interfaces/record.interface';
|
||||
|
||||
import { workspaceResolverBuilderMethodNames } from 'src/workspace/workspace-resolver-builder/factories/factories';
|
||||
|
||||
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
|
||||
|
||||
export interface FindManyResolverArgs<
|
||||
Filter extends RecordFilter = RecordFilter,
|
||||
OrderBy extends RecordOrderBy = RecordOrderBy,
|
||||
> {
|
||||
first?: number;
|
||||
last?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
filter?: Filter;
|
||||
orderBy?: OrderBy;
|
||||
}
|
||||
|
||||
export interface FindOneResolverArgs<Filter = any> {
|
||||
filter?: Filter;
|
||||
}
|
||||
|
||||
export interface CreateOneResolverArgs<Data extends Record = Record> {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface CreateManyResolverArgs<Data extends Record = Record> {
|
||||
data: Data[];
|
||||
}
|
||||
|
||||
export interface UpdateOneResolverArgs<Data extends Record = Record> {
|
||||
id: string;
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface DeleteOneResolverArgs {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type WorkspaceResolverBuilderQueryMethodNames =
|
||||
(typeof workspaceResolverBuilderMethodNames.queries)[number];
|
||||
|
||||
export type WorkspaceResolverBuilderMutationMethodNames =
|
||||
(typeof workspaceResolverBuilderMethodNames.mutations)[number];
|
||||
|
||||
export type WorkspaceResolverBuilderMethodNames =
|
||||
| WorkspaceResolverBuilderQueryMethodNames
|
||||
| WorkspaceResolverBuilderMutationMethodNames;
|
||||
|
||||
export interface WorkspaceResolverBuilderMethods {
|
||||
readonly queries: readonly WorkspaceResolverBuilderQueryMethodNames[];
|
||||
readonly mutations: readonly WorkspaceResolverBuilderMutationMethodNames[];
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceQueryRunnerModule } from 'src/workspace/workspace-query-runner/workspace-query-runner.module';
|
||||
|
||||
import { WorkspaceResolverFactory } from './workspace-resolver.factory';
|
||||
|
||||
import { workspaceResolverBuilderFactories } from './factories/factories';
|
||||
|
||||
@Module({
|
||||
imports: [WorkspaceQueryRunnerModule],
|
||||
providers: [...workspaceResolverBuilderFactories, WorkspaceResolverFactory],
|
||||
exports: [WorkspaceResolverFactory],
|
||||
})
|
||||
export class WorkspaceResolverBuilderModule {}
|
||||
@ -0,0 +1,103 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { IResolvers } from '@graphql-tools/utils';
|
||||
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { getResolverName } from 'src/workspace/utils/get-resolver-name.util';
|
||||
|
||||
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
||||
import { FindOneResolverFactory } from './factories/find-one-resolver.factory';
|
||||
import { CreateManyResolverFactory } from './factories/create-many-resolver.factory';
|
||||
import { CreateOneResolverFactory } from './factories/create-one-resolver.factory';
|
||||
import { UpdateOneResolverFactory } from './factories/update-one-resolver.factory';
|
||||
import { DeleteOneResolverFactory } from './factories/delete-one-resolver.factory';
|
||||
import {
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
WorkspaceResolverBuilderMethods,
|
||||
} from './interfaces/workspace-resolvers-builder.interface';
|
||||
import { WorkspaceResolverBuilderFactoryInterface } from './interfaces/workspace-resolver-builder-factory.interface';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceResolverFactory {
|
||||
private readonly logger = new Logger(WorkspaceResolverFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly findManyResolverFactory: FindManyResolverFactory,
|
||||
private readonly findOneResolverFactory: FindOneResolverFactory,
|
||||
private readonly createManyResolverFactory: CreateManyResolverFactory,
|
||||
private readonly createOneResolverFactory: CreateOneResolverFactory,
|
||||
private readonly updateOneResolverFactory: UpdateOneResolverFactory,
|
||||
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
workspaceId: string,
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||
): Promise<IResolvers> {
|
||||
const factories = new Map<
|
||||
WorkspaceResolverBuilderMethodNames,
|
||||
WorkspaceResolverBuilderFactoryInterface
|
||||
>([
|
||||
['findMany', this.findManyResolverFactory],
|
||||
['findOne', this.findOneResolverFactory],
|
||||
['createMany', this.createManyResolverFactory],
|
||||
['createOne', this.createOneResolverFactory],
|
||||
['updateOne', this.updateOneResolverFactory],
|
||||
['deleteOne', this.deleteOneResolverFactory],
|
||||
]);
|
||||
const resolvers: IResolvers = {
|
||||
Query: {},
|
||||
Mutation: {},
|
||||
};
|
||||
|
||||
for (const objectMetadata of objectMetadataCollection) {
|
||||
// Generate query resolvers
|
||||
for (const methodName of workspaceResolverBuilderMethods.queries) {
|
||||
const resolverName = getResolverName(objectMetadata, methodName);
|
||||
const resolverFactory = factories.get(methodName);
|
||||
|
||||
if (!resolverFactory) {
|
||||
this.logger.error(`Unknown query resolver type: ${methodName}`, {
|
||||
objectMetadata,
|
||||
methodName,
|
||||
resolverName,
|
||||
});
|
||||
|
||||
throw new Error(`Unknown query resolver type: ${methodName}`);
|
||||
}
|
||||
|
||||
resolvers.Query[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
targetTableName: objectMetadata.targetTableName,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
});
|
||||
}
|
||||
|
||||
// Generate mutation resolvers
|
||||
for (const methodName of workspaceResolverBuilderMethods.mutations) {
|
||||
const resolverName = getResolverName(objectMetadata, methodName);
|
||||
const resolverFactory = factories.get(methodName);
|
||||
|
||||
if (!resolverFactory) {
|
||||
this.logger.error(`Unknown mutation resolver type: ${methodName}`, {
|
||||
objectMetadata,
|
||||
methodName,
|
||||
resolverName,
|
||||
});
|
||||
|
||||
throw new Error(`Unknown mutation resolver type: ${methodName}`);
|
||||
}
|
||||
|
||||
resolvers.Mutation[resolverName] = resolverFactory.create({
|
||||
workspaceId,
|
||||
targetTableName: objectMetadata.targetTableName,
|
||||
fieldMetadataCollection: objectMetadata.fields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return resolvers;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigArgumentMap } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ArgsMetadata } from 'src/workspace/workspace-schema-builder/interfaces/param-metadata.interface';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
|
||||
@Injectable()
|
||||
export class ArgsFactory {
|
||||
private readonly logger = new Logger(ArgsFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
{ args, objectMetadata }: ArgsMetadata,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigArgumentMap {
|
||||
const fieldConfigMap: GraphQLFieldConfigArgumentMap = {};
|
||||
|
||||
for (const key in args) {
|
||||
if (!args.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
const arg = args[key];
|
||||
|
||||
// Argument is a scalar type
|
||||
if (arg.type) {
|
||||
const fieldType = this.typeMapperService.mapToScalarType(
|
||||
arg.type,
|
||||
options.dateScalarMode,
|
||||
options.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!fieldType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${arg.type.toString()}`,
|
||||
{
|
||||
arg,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${arg.type.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gqlType = this.typeMapperService.mapToGqlType(fieldType, {
|
||||
defaultValue: arg.defaultValue,
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
|
||||
// Argument is an input type
|
||||
if (arg.kind) {
|
||||
const inputType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
objectMetadata.id,
|
||||
arg.kind,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL input type for ${objectMetadata.id}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL input type for ${objectMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const gqlType = this.typeMapperService.mapToGqlType(inputType, {
|
||||
nullable: arg.isNullable,
|
||||
isArray: arg.isArray,
|
||||
});
|
||||
|
||||
fieldConfigMap[key] = {
|
||||
type: gqlType,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return fieldConfigMap;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
|
||||
export enum ConnectionTypeDefinitionKind {
|
||||
Edge = 'Edge',
|
||||
PageInfo = 'PageInfo',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Connection;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.edges = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.Edge,
|
||||
options,
|
||||
{
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.pageInfo = {
|
||||
type: this.connectionTypeFactory.create(
|
||||
objectMetadata,
|
||||
ConnectionTypeDefinitionKind.PageInfo,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { PageInfoType } from 'src/workspace/workspace-schema-builder/graphql-types/object';
|
||||
|
||||
import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory';
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectionTypeFactory {
|
||||
private readonly logger = new Logger(ConnectionTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ConnectionTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === ConnectionTypeDefinitionKind.PageInfo) {
|
||||
return this.typeMapperService.mapToGqlType(PageInfoType, typeOptions);
|
||||
}
|
||||
|
||||
const edgeType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind as unknown as ObjectTypeDefinitionKind,
|
||||
);
|
||||
|
||||
if (!edgeType) {
|
||||
this.logger.error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(edgeType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
|
||||
import {
|
||||
ObjectTypeDefinition,
|
||||
ObjectTypeDefinitionKind,
|
||||
} from './object-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
|
||||
export enum EdgeTypeDefinitionKind {
|
||||
Node = 'Node',
|
||||
Cursor = 'Cursor',
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(EdgeTypeDefinitionFactory.name);
|
||||
|
||||
constructor(private readonly edgeTypeFactory: EdgeTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Edge;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
fields.node = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Node,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
fields.cursor = {
|
||||
type: this.edgeTypeFactory.create(
|
||||
objectMetadata,
|
||||
EdgeTypeDefinitionKind.Cursor,
|
||||
options,
|
||||
{
|
||||
nullable: false,
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { CursorScalarType } from 'src/workspace/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
import { EdgeTypeDefinitionKind } from './edge-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class EdgeTypeFactory {
|
||||
private readonly logger = new Logger(EdgeTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: EdgeTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
if (kind === EdgeTypeDefinitionKind.Cursor) {
|
||||
return this.typeMapperService.mapToGqlType(CursorScalarType, typeOptions);
|
||||
}
|
||||
|
||||
const objectType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
ObjectTypeDefinitionKind.Plain,
|
||||
);
|
||||
|
||||
if (!objectType) {
|
||||
this.logger.error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
{
|
||||
objectMetadata,
|
||||
buildOtions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`,
|
||||
);
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(objectType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,176 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
GraphQLFieldConfigArgumentMap,
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLObjectType,
|
||||
} from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
import { objectContainsCompositeField } from 'src/workspace/workspace-schema-builder/utils/object-contains-composite-field';
|
||||
import { getResolverArgs } from 'src/workspace/workspace-schema-builder/utils/get-resolver-args.util';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
import {
|
||||
RelationDirection,
|
||||
deduceRelationDirection,
|
||||
} from 'src/workspace/utils/deduce-relation-direction.util';
|
||||
import { RelationMetadataType } from 'src/metadata/relation-metadata/relation-metadata.entity';
|
||||
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ArgsFactory } from './args.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ExtendObjectTypeDefinitionFactory {
|
||||
private readonly logger = new Logger(ExtendObjectTypeDefinitionFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly relationTypeFactory: RelationTypeFactory,
|
||||
private readonly argsFactory: ArgsFactory,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
const kind = ObjectTypeDefinitionKind.Plain;
|
||||
const gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
objectMetadata.id,
|
||||
kind,
|
||||
);
|
||||
const containsCompositeField = objectContainsCompositeField(objectMetadata);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Security check to avoid extending an object that does not need to be extended
|
||||
if (!containsCompositeField) {
|
||||
this.logger.error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
{
|
||||
objectMetadata,
|
||||
options,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`This object does not need to be extended: ${objectMetadata.id.toString()}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract current object config to extend it
|
||||
const config = gqlType.toConfig();
|
||||
|
||||
// Recreate the same object type with the new fields
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
...config,
|
||||
fields: () => ({
|
||||
...config.fields,
|
||||
...this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Ignore non composite fields as they are already defined
|
||||
if (!isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (fieldMetadata.type) {
|
||||
case FieldMetadataType.RELATION: {
|
||||
const relationMetadata =
|
||||
fieldMetadata.fromRelationMetadata ??
|
||||
fieldMetadata.toRelationMetadata;
|
||||
|
||||
if (!relationMetadata) {
|
||||
this.logger.error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a relation metadata for ${fieldMetadata.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relationDirection = deduceRelationDirection(
|
||||
fieldMetadata.objectMetadataId,
|
||||
relationMetadata,
|
||||
);
|
||||
const relationType = this.relationTypeFactory.create(
|
||||
fieldMetadata,
|
||||
relationMetadata,
|
||||
relationDirection,
|
||||
);
|
||||
let argsType: GraphQLFieldConfigArgumentMap | undefined = undefined;
|
||||
|
||||
// Args are only needed when relation is of kind `oneToMany` and the relation direction is `from`
|
||||
if (
|
||||
relationMetadata.relationType ===
|
||||
RelationMetadataType.ONE_TO_MANY &&
|
||||
relationDirection === RelationDirection.FROM
|
||||
) {
|
||||
const args = getResolverArgs('findMany');
|
||||
|
||||
argsType = this.argsFactory.create(
|
||||
{
|
||||
args,
|
||||
objectMetadata: relationMetadata.toObjectMetadata,
|
||||
},
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type: relationType,
|
||||
args: argsType,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { ArgsFactory } from './args.factory';
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
import { InputTypeDefinitionFactory } from './input-type-definition.factory';
|
||||
import { ObjectTypeDefinitionFactory } from './object-type-definition.factory';
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
import { QueryTypeFactory } from './query-type.factory';
|
||||
import { RootTypeFactory } from './root-type.factory';
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import { FilterTypeDefinitionFactory } from './filter-type-definition.factory';
|
||||
import { ConnectionTypeFactory } from './connection-type.factory';
|
||||
import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory';
|
||||
import { EdgeTypeFactory } from './edge-type.factory';
|
||||
import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory';
|
||||
import { MutationTypeFactory } from './mutation-type.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory';
|
||||
import { RelationTypeFactory } from './relation-type.factory';
|
||||
import { ExtendObjectTypeDefinitionFactory } from './extend-object-type-definition.factory';
|
||||
import { OrphanedTypesFactory } from './orphaned-types.factory';
|
||||
|
||||
export const workspaceSchemaBuilderFactories = [
|
||||
ArgsFactory,
|
||||
InputTypeFactory,
|
||||
InputTypeDefinitionFactory,
|
||||
OutputTypeFactory,
|
||||
ObjectTypeDefinitionFactory,
|
||||
RelationTypeFactory,
|
||||
ExtendObjectTypeDefinitionFactory,
|
||||
FilterTypeFactory,
|
||||
FilterTypeDefinitionFactory,
|
||||
OrderByTypeFactory,
|
||||
OrderByTypeDefinitionFactory,
|
||||
ConnectionTypeFactory,
|
||||
ConnectionTypeDefinitionFactory,
|
||||
EdgeTypeFactory,
|
||||
EdgeTypeDefinitionFactory,
|
||||
RootTypeFactory,
|
||||
QueryTypeFactory,
|
||||
MutationTypeFactory,
|
||||
OrphanedTypesFactory,
|
||||
];
|
||||
@ -0,0 +1,91 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { TypeMapperService } from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { FilterTypeFactory } from './filter-type.factory';
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeDefinitionFactory {
|
||||
constructor(
|
||||
private readonly filterTypeFactory: FilterTypeFactory,
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.Filter;
|
||||
const filterInputType = new GraphQLInputObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: () => {
|
||||
const andOrType = this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
isArray: true,
|
||||
arrayDepth: 1,
|
||||
nullable: true,
|
||||
});
|
||||
|
||||
return {
|
||||
...this.generateFields(objectMetadata, options),
|
||||
and: {
|
||||
type: andOrType,
|
||||
},
|
||||
or: {
|
||||
type: andOrType,
|
||||
},
|
||||
not: {
|
||||
type: this.typeMapperService.mapToGqlType(filterInputType, {
|
||||
nullable: true,
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: filterInputType,
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
//continue;
|
||||
}
|
||||
|
||||
const type = this.filterTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class FilterTypeFactory {
|
||||
private readonly logger = new Logger(FilterTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let filterType = this.typeMapperService.mapToFilterType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!filterType) {
|
||||
filterType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
InputTypeDefinitionKind.Filter,
|
||||
);
|
||||
|
||||
if (!filterType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(filterType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { InputTypeFactory } from './input-type.factory';
|
||||
|
||||
export enum InputTypeDefinitionKind {
|
||||
Create = 'Create',
|
||||
Update = 'Update',
|
||||
Filter = 'Filter',
|
||||
OrderBy = 'OrderBy',
|
||||
}
|
||||
|
||||
export interface InputTypeDefinition {
|
||||
target: string;
|
||||
kind: InputTypeDefinitionKind;
|
||||
type: GraphQLInputObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeDefinitionFactory {
|
||||
constructor(private readonly inputTypeFactory: InputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
//continue;
|
||||
}
|
||||
|
||||
const type = this.inputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
defaultValue: fieldMetadata.defaultValue,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class InputTypeFactory {
|
||||
private readonly logger = new Logger(InputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: InputTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let inputType: GraphQLInputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
inputType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
kind,
|
||||
);
|
||||
|
||||
if (!inputType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
kind,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(inputType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderMutationMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class MutationTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderMutationMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Mutation,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import { OutputTypeFactory } from './output-type.factory';
|
||||
|
||||
export enum ObjectTypeDefinitionKind {
|
||||
Connection = 'Connection',
|
||||
Edge = 'Edge',
|
||||
Plain = '',
|
||||
}
|
||||
|
||||
export interface ObjectTypeDefinition {
|
||||
target: string;
|
||||
kind: ObjectTypeDefinitionKind;
|
||||
type: GraphQLObjectType;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ObjectTypeDefinitionFactory {
|
||||
constructor(private readonly outputTypeFactory: OutputTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): ObjectTypeDefinition {
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLObjectType({
|
||||
name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, kind, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLFieldConfigMap<any, any> {
|
||||
const fields: GraphQLFieldConfigMap<any, any> = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.outputTypeFactory.create(fieldMetadata, kind, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { pascalCase } from 'src/utils/pascal-case';
|
||||
import { isCompositeFieldMetadataType } from 'src/workspace/utils/is-composite-field-metadata-type.util';
|
||||
|
||||
import {
|
||||
InputTypeDefinition,
|
||||
InputTypeDefinitionKind,
|
||||
} from './input-type-definition.factory';
|
||||
import { OrderByTypeFactory } from './order-by-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeDefinitionFactory {
|
||||
constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {}
|
||||
|
||||
public create(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): InputTypeDefinition {
|
||||
const kind = InputTypeDefinitionKind.OrderBy;
|
||||
|
||||
return {
|
||||
target: objectMetadata.id,
|
||||
kind,
|
||||
type: new GraphQLInputObjectType({
|
||||
name: `${pascalCase(
|
||||
objectMetadata.nameSingular,
|
||||
)}${kind.toString()}Input`,
|
||||
description: objectMetadata.description,
|
||||
fields: this.generateFields(objectMetadata, options),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private generateFields(
|
||||
objectMetadata: ObjectMetadataInterface,
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLInputFieldConfigMap {
|
||||
const fields: GraphQLInputFieldConfigMap = {};
|
||||
|
||||
for (const fieldMetadata of objectMetadata.fields) {
|
||||
// Composite field types are generated during extension of object type definition
|
||||
if (isCompositeFieldMetadataType(fieldMetadata.type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = this.orderByTypeFactory.create(fieldMetadata, options, {
|
||||
nullable: fieldMetadata.isNullable,
|
||||
});
|
||||
|
||||
fields[fieldMetadata.name] = {
|
||||
type,
|
||||
description: fieldMetadata.description,
|
||||
// TODO: Add default value
|
||||
defaultValue: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLInputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { InputTypeDefinitionKind } from './input-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OrderByTypeFactory {
|
||||
private readonly logger = new Logger(OrderByTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLInputType {
|
||||
let orderByType = this.typeMapperService.mapToOrderByType(
|
||||
fieldMetadata.type,
|
||||
);
|
||||
|
||||
if (!orderByType) {
|
||||
orderByType = this.typeDefinitionsStorage.getInputTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
InputTypeDefinitionKind.OrderBy,
|
||||
);
|
||||
|
||||
if (!orderByType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(orderByType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLNamedType } from 'graphql';
|
||||
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
@Injectable()
|
||||
export class OrphanedTypesFactory {
|
||||
constructor(
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(): GraphQLNamedType[] {
|
||||
const objectTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllObjectTypeDefinitions();
|
||||
const inputTypeDefs =
|
||||
this.typeDefinitionsStorage.getAllInputTypeDefinitions();
|
||||
const classTypeDefs = [...objectTypeDefs, ...inputTypeDefs];
|
||||
|
||||
return [...classTypeDefs.map(({ type }) => type)];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { GraphQLOutputType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { FieldMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/field-metadata.interface';
|
||||
|
||||
import {
|
||||
TypeMapperService,
|
||||
TypeOptions,
|
||||
} from 'src/workspace/workspace-schema-builder/services/type-mapper.service';
|
||||
import { TypeDefinitionsStorage } from 'src/workspace/workspace-schema-builder/storages/type-definitions.storage';
|
||||
|
||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||
|
||||
@Injectable()
|
||||
export class OutputTypeFactory {
|
||||
private readonly logger = new Logger(OutputTypeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly typeMapperService: TypeMapperService,
|
||||
private readonly typeDefinitionsStorage: TypeDefinitionsStorage,
|
||||
) {}
|
||||
|
||||
public create(
|
||||
fieldMetadata: FieldMetadataInterface,
|
||||
kind: ObjectTypeDefinitionKind,
|
||||
buildOtions: WorkspaceBuildSchemaOptions,
|
||||
typeOptions: TypeOptions,
|
||||
): GraphQLOutputType {
|
||||
let gqlType: GraphQLOutputType | undefined =
|
||||
this.typeMapperService.mapToScalarType(
|
||||
fieldMetadata.type,
|
||||
buildOtions.dateScalarMode,
|
||||
buildOtions.numberScalarMode,
|
||||
);
|
||||
|
||||
if (!gqlType) {
|
||||
gqlType = this.typeDefinitionsStorage.getObjectTypeByKey(
|
||||
fieldMetadata.type.toString(),
|
||||
kind,
|
||||
);
|
||||
|
||||
if (!gqlType) {
|
||||
this.logger.error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
{
|
||||
fieldMetadata,
|
||||
buildOtions,
|
||||
typeOptions,
|
||||
},
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Could not find a GraphQL type for ${fieldMetadata.type.toString()}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.typeMapperService.mapToGqlType(gqlType, typeOptions);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { GraphQLObjectType } from 'graphql';
|
||||
|
||||
import { WorkspaceBuildSchemaOptions } from 'src/workspace/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||
import { WorkspaceResolverBuilderQueryMethodNames } from 'src/workspace/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
import { ObjectMetadataInterface } from 'src/workspace/workspace-schema-builder/interfaces/object-metadata.interface';
|
||||
|
||||
import { ObjectTypeName, RootTypeFactory } from './root-type.factory';
|
||||
|
||||
@Injectable()
|
||||
export class QueryTypeFactory {
|
||||
constructor(private readonly rootTypeFactory: RootTypeFactory) {}
|
||||
create(
|
||||
objectMetadataCollection: ObjectMetadataInterface[],
|
||||
workspaceResolverMethodNames: WorkspaceResolverBuilderQueryMethodNames[],
|
||||
options: WorkspaceBuildSchemaOptions,
|
||||
): GraphQLObjectType {
|
||||
return this.rootTypeFactory.create(
|
||||
objectMetadataCollection,
|
||||
workspaceResolverMethodNames,
|
||||
ObjectTypeName.Query,
|
||||
options,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user