Setup relations for remote objects (#5149)

New strategy:
- add settings field on FieldMetadata. Contains a boolean isIdField and
for numbers, a precision
- if idField, the graphql scalar returned will be a GraphQL id. This
will allow the app to work even for ids that are not uuid
- remove globals dateScalar and numberScalar modes. These were not used
- set limit as Integer
- check manually in query runner mutations that we send a valid id

Todo left:
- remove WorkspaceBuildSchemaOptions since this is not used anymore.
Will do in another PR

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
Thomas Trompette
2024-04-26 14:37:34 +02:00
committed by GitHub
parent dc576d0818
commit 224c8d361b
71 changed files with 616 additions and 223 deletions

View File

@ -78,14 +78,16 @@ export class FindDuplicatesQueryFactory {
}
buildQueryForExistingRecord(
id: string,
id: string | number,
options: WorkspaceQueryBuilderOptions,
) {
const idQueryField = typeof id === 'string' ? `"${id}"` : id;
return `
query {
${computeObjectTargetTable(
options.objectMetadataItem,
)}Collection(filter: { id: { eq: "${id}" }}){
)}Collection(filter: { id: { eq: ${idQueryField} }}){
edges {
node {
__typename

View File

@ -77,7 +77,7 @@ export class WorkspaceQueryBuilderFactory {
}
findDuplicatesExistingRecord(
id: string,
id: string | number,
options: WorkspaceQueryBuilderOptions,
): string {
return this.findDuplicatesQueryFactory.buildQueryForExistingRecord(

View File

@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { ResolverArgsType } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -14,6 +15,7 @@ describe('QueryRunnerArgsFactory', () => {
const options = {
fieldMetadataCollection: [
{ name: 'position', type: FieldMetadataType.POSITION },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
} as WorkspaceQueryRunnerOptions;
@ -45,18 +47,92 @@ describe('QueryRunnerArgsFactory', () => {
const args = {
data: [],
};
const result = await factory.create(args, options);
const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
);
expect(result).toEqual(args);
});
it('should override args when of type array', async () => {
const args = { data: [{ id: 1 }, { position: 'last' }] };
it('createMany type should override data position and number', async () => {
const args = {
id: 'uuid',
data: [{ position: 'last', testNumber: '1' }],
};
const result = await factory.create(args, options);
const result = await factory.create(
args,
options,
ResolverArgsType.CreateMany,
);
expect(result).toEqual({
data: [{ id: 1 }, { position: 2 }],
id: 'uuid',
data: [{ position: 2, testNumber: 1 }],
});
});
it('findMany type should override data position and number', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FindMany,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findOne type should override number in filter', async () => {
const args = {
id: 'uuid',
filter: { testNumber: { eq: '1' }, otherField: { eq: 'test' } },
};
const result = await factory.create(
args,
options,
ResolverArgsType.FindOne,
);
expect(result).toEqual({
id: 'uuid',
filter: { testNumber: { eq: 1 }, otherField: { eq: 'test' } },
});
});
it('findDuplicates type should override number in data and id', async () => {
const optionsDuplicate = {
fieldMetadataCollection: [
{ name: 'id', type: FieldMetadataType.NUMBER },
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
] as FieldMetadataInterface[],
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
} as WorkspaceQueryRunnerOptions;
const args = {
id: '123',
data: { testNumber: '1', otherField: 'test' },
};
const result = await factory.create(
args,
optionsDuplicate,
ResolverArgsType.FindDuplicates,
);
expect(result).toEqual({
id: 123,
data: { testNumber: 1, otherField: 'test' },
});
});
});

View File

@ -2,6 +2,15 @@ import { Injectable } from '@nestjs/common';
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface';
import {
CreateManyResolverArgs,
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgs,
ResolverArgsType,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -12,8 +21,9 @@ export class QueryRunnerArgsFactory {
constructor(private readonly recordPositionFactory: RecordPositionFactory) {}
async create(
args: Record<string, any>,
args: ResolverArgs,
options: WorkspaceQueryRunnerOptions,
resolverArgsType: ResolverArgsType,
) {
const fieldMetadataCollection = options.fieldMetadataCollection;
@ -24,21 +34,62 @@ export class QueryRunnerArgsFactory {
]),
);
return {
data: await Promise.all(
args.data.map((arg) =>
this.overrideArgByFieldMetadata(arg, options, fieldMetadataMap),
),
),
};
switch (resolverArgsType) {
case ResolverArgsType.CreateMany:
return {
...args,
data: await Promise.all(
(args as CreateManyResolverArgs).data.map((arg) =>
this.overrideDataByFieldMetadata(arg, options, fieldMetadataMap),
),
),
} satisfies CreateManyResolverArgs;
case ResolverArgsType.FindOne:
return {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindOneResolverArgs).filter,
fieldMetadataMap,
),
};
case ResolverArgsType.FindMany:
return {
...args,
filter: await this.overrideFilterByFieldMetadata(
(args as FindManyResolverArgs).filter,
fieldMetadataMap,
),
};
case ResolverArgsType.FindDuplicates:
return {
...args,
id: await this.overrideValueByFieldMetadata(
'id',
(args as FindDuplicatesResolverArgs).id,
fieldMetadataMap,
),
data: await this.overrideDataByFieldMetadata(
(args as FindDuplicatesResolverArgs).data,
options,
fieldMetadataMap,
),
};
default:
return args;
}
}
private async overrideArgByFieldMetadata(
arg: Record<string, any>,
private async overrideDataByFieldMetadata(
data: Record<string, any> | undefined,
options: WorkspaceQueryRunnerOptions,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
const createArgPromiseByArgKey = Object.entries(arg).map(
if (!data) {
return;
}
const createArgPromiseByArgKey = Object.entries(data).map(
async ([key, value]) => {
const fieldMetadata = fieldMetadataMap.get(key);
@ -59,6 +110,8 @@ export class QueryRunnerArgsFactory {
options.workspaceId,
),
];
case FieldMetadataType.NUMBER:
return [key, await Promise.resolve(Number(value))];
default:
return [key, await Promise.resolve(value)];
}
@ -69,4 +122,57 @@ export class QueryRunnerArgsFactory {
return Object.fromEntries(newArgEntries);
}
private overrideFilterByFieldMetadata(
filter: RecordFilter | undefined,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
if (!filter) {
return;
}
const createArgPromiseByArgKey = Object.entries(filter).map(
([key, value]) => {
const fieldMetadata = fieldMetadataMap.get(key);
if (!fieldMetadata) {
return [key, value];
}
const createFilterByKey = Object.entries(value).map(
([filterKey, filterValue]) => {
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
return [filterKey, Number(filterValue)];
default:
return [filterKey, filterValue];
}
},
);
return [key, Object.fromEntries(createFilterByKey)];
},
);
return Object.fromEntries(createArgPromiseByArgKey);
}
private async overrideValueByFieldMetadata(
key: string,
value: any,
fieldMetadataMap: Map<string, FieldMetadataInterface>,
) {
const fieldMetadata = fieldMetadataMap.get(key);
if (!fieldMetadata) {
return value;
}
switch (fieldMetadata.type) {
case FieldMetadataType.NUMBER:
return Number(value);
default:
return value;
}
}
}

View File

@ -0,0 +1,12 @@
import { BadRequestException } from '@nestjs/common';
export const assertIsValidUuid = (value: string) => {
const isValid =
/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
value,
);
if (!isValid) {
throw new BadRequestException(`Value "${value}" is not a valid UUID`);
}
};

View File

@ -22,6 +22,7 @@ import {
FindDuplicatesResolverArgs,
FindManyResolverArgs,
FindOneResolverArgs,
ResolverArgsType,
UpdateManyResolverArgs,
UpdateOneResolverArgs,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
@ -48,6 +49,7 @@ import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-r
import { QueryResultGettersFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-result-getters.factory';
import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assertIsValidUuid.util';
import { WorkspaceQueryRunnerOptions } from './interfaces/query-runner-option.interface';
import {
@ -83,9 +85,15 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options;
const start = performance.now();
const query = await this.workspaceQueryBuilderFactory.findMany(
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
options,
ResolverArgsType.FindMany,
)) as FindManyResolverArgs<Filter, OrderBy>;
const query = await this.workspaceQueryBuilderFactory.findMany(
computedArgs,
options,
);
await this.workspacePreQueryHookService.executePreHooks(
@ -123,9 +131,16 @@ export class WorkspaceQueryRunnerService {
throw new BadRequestException('Missing filter argument');
}
const { workspaceId, userId, objectMetadataItem } = options;
const query = await this.workspaceQueryBuilderFactory.findOne(
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
options,
ResolverArgsType.FindOne,
)) as FindOneResolverArgs<Filter>;
const query = await this.workspaceQueryBuilderFactory.findOne(
computedArgs,
options,
);
await this.workspacePreQueryHookService.executePreHooks(
@ -164,12 +179,18 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options;
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
options,
ResolverArgsType.FindDuplicates,
)) as FindDuplicatesResolverArgs<TRecord>;
let existingRecord: Record<string, unknown> | undefined;
if (args.id) {
if (computedArgs.id) {
const existingRecordQuery =
this.workspaceQueryBuilderFactory.findDuplicatesExistingRecord(
args.id,
computedArgs.id,
options,
);
@ -192,7 +213,7 @@ export class WorkspaceQueryRunnerService {
}
const query = await this.workspaceQueryBuilderFactory.findDuplicates(
args,
computedArgs,
options,
existingRecord,
);
@ -202,7 +223,7 @@ export class WorkspaceQueryRunnerService {
workspaceId,
objectMetadataItem.nameSingular,
'findDuplicates',
args,
computedArgs,
);
const result = await this.execute(query, workspaceId);
@ -222,10 +243,17 @@ export class WorkspaceQueryRunnerService {
assertMutationNotOnRemoteObject(objectMetadataItem);
const computedArgs = await this.queryRunnerArgsFactory.create(
args.data.forEach((record) => {
if (record.id) {
assertIsValidUuid(record.id);
}
});
const computedArgs = (await this.queryRunnerArgsFactory.create(
args,
options,
);
ResolverArgsType.CreateMany,
)) as CreateManyResolverArgs<Record>;
await this.workspacePreQueryHookService.executePreHooks(
userId,
@ -288,6 +316,7 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, userId, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.id);
const existingRecord = await this.findOne(
{ filter: { id: { eq: args.id } } } as FindOneResolverArgs,
@ -337,6 +366,7 @@ export class WorkspaceQueryRunnerService {
const { workspaceId, objectMetadataItem } = options;
assertMutationNotOnRemoteObject(objectMetadataItem);
assertIsValidUuid(args.data.id);
const maximumRecordAffected = this.environmentService.get(
'MUTATION_MAXIMUM_RECORD_AFFECTED',

View File

@ -10,6 +10,19 @@ import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/work
export type Resolver<Args = any> = GraphQLFieldResolver<any, any, Args>;
export enum ResolverArgsType {
FindMany = 'FindMany',
FindOne = 'FindOne',
FindDuplicates = 'FindDuplicates',
CreateOne = 'CreateOne',
CreateMany = 'CreateMany',
UpdateOne = 'UpdateOne',
UpdateMany = 'UpdateMany',
DeleteOne = 'DeleteOne',
DeleteMany = 'DeleteMany',
ExecuteQuickActionOnOne = 'ExecuteQuickActionOnOne',
}
export interface FindManyResolverArgs<
Filter extends RecordFilter = RecordFilter,
OrderBy extends RecordOrderBy = RecordOrderBy,

View File

@ -32,27 +32,7 @@ export class ArgsFactory {
// 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, {
const gqlType = this.typeMapperService.mapToGqlType(arg.type, {
defaultValue: arg.defaultValue,
nullable: arg.isNullable,
isArray: arg.isArray,

View File

@ -102,6 +102,8 @@ export class InputTypeDefinitionFactory {
? fieldMetadata.type.toString()
: fieldMetadata.id;
const isIdField = fieldMetadata.name === 'id';
const type = this.inputTypeFactory.create(
target,
fieldMetadata.type,
@ -111,6 +113,8 @@ export class InputTypeDefinitionFactory {
nullable: fieldMetadata.isNullable,
defaultValue: fieldMetadata.defaultValue,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
isIdField,
},
);

View File

@ -41,8 +41,8 @@ export class InputTypeFactory {
case InputTypeDefinitionKind.Update:
inputType = this.typeMapperService.mapToScalarType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
break;
/**
@ -54,8 +54,8 @@ export class InputTypeFactory {
} else {
inputType = this.typeMapperService.mapToFilterType(
type,
buildOptions.dateScalarMode,
buildOptions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
}

View File

@ -69,6 +69,9 @@ export class ObjectTypeDefinitionFactory {
{
nullable: fieldMetadata.isNullable,
isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT,
settings: fieldMetadata.settings,
// Scalar type is already defined in the entity itself.
isIdField: false,
},
);

View File

@ -32,8 +32,8 @@ export class OutputTypeFactory {
let gqlType: GraphQLOutputType | undefined =
this.typeMapperService.mapToScalarType(
type,
buildOtions.dateScalarMode,
buildOtions.numberScalarMode,
typeOptions.settings,
typeOptions.isIdField,
);
gqlType ??= this.typeDefinitionsStorage.getOutputTypeByKey(target, kind);

View File

@ -0,0 +1,17 @@
import { GraphQLID, GraphQLInputObjectType, GraphQLList } from 'graphql';
import { FilterIs } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/filter-is.input-type';
export const IDFilterType = new GraphQLInputObjectType({
name: 'IDFilter',
fields: {
eq: { type: GraphQLID },
gt: { type: GraphQLID },
gte: { type: GraphQLID },
in: { type: new GraphQLList(GraphQLID) },
lt: { type: GraphQLID },
lte: { type: GraphQLID },
neq: { type: GraphQLID },
is: { type: FilterIs },
},
});

View File

@ -1,9 +1,10 @@
import { GraphQLScalarType } from 'graphql';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
export interface ArgMetadata<T = any> {
kind?: InputTypeDefinitionKind;
type?: FieldMetadataType;
type?: GraphQLScalarType;
isNullable?: boolean;
isArray?: boolean;
defaultValue?: T;

View File

@ -1,10 +1,11 @@
import { Injectable } from '@nestjs/common';
import { GraphQLISODateTime, GraphQLTimestamp } from '@nestjs/graphql';
import { GraphQLISODateTime } from '@nestjs/graphql';
import {
GraphQLBoolean,
GraphQLEnumType,
GraphQLFloat,
GraphQLID,
GraphQLInputObjectType,
GraphQLInputType,
GraphQLInt,
@ -15,22 +16,17 @@ import {
GraphQLType,
} from 'graphql';
import {
DateScalarMode,
NumberScalarMode,
} from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import {
UUIDFilterType,
StringFilterType,
DatetimeFilterType,
DateFilterType,
FloatFilterType,
IntFilterType,
BooleanFilterType,
BigFloatFilterType,
RawJsonFilterType,
IntFilterType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input';
import { OrderByDirectionType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/enum';
import {
@ -39,38 +35,46 @@ import {
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar';
import { JsonScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/json.scalar';
import { IDFilterType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/input/id-filter.input-type';
export interface TypeOptions<T = any> {
nullable?: boolean;
isArray?: boolean;
arrayDepth?: number;
defaultValue?: T;
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>;
isIdField?: boolean;
}
@Injectable()
export class TypeMapperService {
mapToScalarType(
fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate',
numberScalarMode: NumberScalarMode = 'float',
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
isIdField?: boolean,
): GraphQLScalarType | undefined {
const dateScalar =
dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime;
if (isIdField || settings?.isForeignKey) {
return GraphQLID;
}
const numberScalar =
numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt;
fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? GraphQLInt
: GraphQLFloat;
const typeScalarMapping = new Map<FieldMetadataType, GraphQLScalarType>([
[FieldMetadataType.UUID, UUIDScalarType],
[FieldMetadataType.TEXT, GraphQLString],
[FieldMetadataType.PHONE, GraphQLString],
[FieldMetadataType.EMAIL, GraphQLString],
[FieldMetadataType.DATE_TIME, dateScalar],
[FieldMetadataType.DATE, dateScalar],
[FieldMetadataType.DATE_TIME, GraphQLISODateTime],
[FieldMetadataType.DATE, GraphQLISODateTime],
[FieldMetadataType.BOOLEAN, GraphQLBoolean],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatScalarType],
[FieldMetadataType.PROBABILITY, GraphQLFloat],
[FieldMetadataType.RELATION, UUIDScalarType],
[FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.RAW_JSON, JsonScalarType],
]);
@ -80,29 +84,34 @@ export class TypeMapperService {
mapToFilterType(
fieldMetadataType: FieldMetadataType,
dateScalarMode: DateScalarMode = 'isoDate',
numberScalarMode: NumberScalarMode = 'float',
settings?: FieldMetadataSettings<FieldMetadataType | 'default'>,
isIdField?: boolean,
): GraphQLInputObjectType | GraphQLScalarType | undefined {
const dateFilter =
dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType;
if (isIdField || settings?.isForeignKey) {
return IDFilterType;
}
const numberScalar =
numberScalarMode === 'float' ? FloatFilterType : IntFilterType;
fieldMetadataType === FieldMetadataType.NUMBER &&
(settings as FieldMetadataSettings<FieldMetadataType.NUMBER>)
?.precision === 0
? IntFilterType
: FloatFilterType;
const typeFilterMapping = new Map<
FieldMetadataType,
GraphQLInputObjectType | GraphQLScalarType
>([
[FieldMetadataType.UUID, UUIDFilterType],
[FieldMetadataType.UUID, IDFilterType],
[FieldMetadataType.TEXT, StringFilterType],
[FieldMetadataType.PHONE, StringFilterType],
[FieldMetadataType.EMAIL, StringFilterType],
[FieldMetadataType.DATE_TIME, dateFilter],
[FieldMetadataType.DATE_TIME, DateFilterType],
[FieldMetadataType.DATE, DateFilterType],
[FieldMetadataType.BOOLEAN, BooleanFilterType],
[FieldMetadataType.NUMBER, numberScalar],
[FieldMetadataType.NUMERIC, BigFloatFilterType],
[FieldMetadataType.PROBABILITY, FloatFilterType],
[FieldMetadataType.RELATION, UUIDFilterType],
[FieldMetadataType.POSITION, FloatFilterType],
[FieldMetadataType.RAW_JSON, RawJsonFilterType],
]);

View File

@ -1,18 +1,20 @@
import { GraphQLID, GraphQLInt, GraphQLString } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
describe('getResolverArgs', () => {
const expectedOutputs = {
findMany: {
first: { type: FieldMetadataType.NUMBER, isNullable: true },
last: { type: FieldMetadataType.NUMBER, isNullable: true },
before: { type: FieldMetadataType.TEXT, isNullable: true },
after: { type: FieldMetadataType.TEXT, isNullable: true },
first: { type: GraphQLInt, isNullable: true },
last: { type: GraphQLInt, isNullable: true },
before: { type: GraphQLString, isNullable: true },
after: { type: GraphQLString, isNullable: true },
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true },
orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true },
limit: { type: GraphQLInt, isNullable: true },
},
findOne: {
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
@ -28,14 +30,14 @@ describe('getResolverArgs', () => {
data: { kind: InputTypeDefinitionKind.Create, isNullable: false },
},
updateOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
data: { kind: InputTypeDefinitionKind.Update, isNullable: false },
},
deleteOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
},
executeQuickActionOnOne: {
id: { type: FieldMetadataType.UUID, isNullable: false },
id: { type: GraphQLID, isNullable: false },
},
};

View File

@ -1,7 +1,8 @@
import { GraphQLString, GraphQLInt, GraphQLID } from 'graphql';
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { ArgMetadata } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/param-metadata.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
export const getResolverArgs = (
@ -11,19 +12,23 @@ export const getResolverArgs = (
case 'findMany':
return {
first: {
type: FieldMetadataType.NUMBER,
type: GraphQLInt,
isNullable: true,
},
last: {
type: FieldMetadataType.NUMBER,
type: GraphQLInt,
isNullable: true,
},
before: {
type: FieldMetadataType.TEXT,
type: GraphQLString,
isNullable: true,
},
after: {
type: FieldMetadataType.TEXT,
type: GraphQLString,
isNullable: true,
},
limit: {
type: GraphQLInt,
isNullable: true,
},
filter: {
@ -61,7 +66,7 @@ export const getResolverArgs = (
case 'updateOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
data: {
@ -72,7 +77,7 @@ export const getResolverArgs = (
case 'findDuplicates':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: true,
},
data: {
@ -83,14 +88,14 @@ export const getResolverArgs = (
case 'deleteOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
};
case 'executeQuickActionOnOne':
return {
id: {
type: FieldMetadataType.UUID,
type: GraphQLID,
isNullable: false,
},
};

View File

@ -16,7 +16,7 @@ export class FindManyQueryFactory {
$filter: ${objectNameSingular}FilterInput,
$orderBy: ${objectNameSingular}OrderByInput,
$lastCursor: String,
$limit: Float = 60
$limit: Int = 60
) {
${objectNamePlural}(
filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor

View File

@ -22,7 +22,6 @@ export enum FeatureFlagKeys {
IsAirtableIntegrationEnabled = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IsPostgreSQLIntegrationEnabled = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IsMultiSelectEnabled = 'IS_MULTI_SELECT_ENABLED',
IsRelationForRemoteObjectsEnabled = 'IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED',
}
@Entity({ name: 'featureFlag', schema: 'core' })

View File

@ -26,6 +26,7 @@ import {
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@ -120,6 +121,10 @@ export class FieldMetadataDTO<
@Field(() => GraphQLJSON, { nullable: true })
options?: FieldMetadataOptions<T>;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
settings?: FieldMetadataSettings<T>;
@HideField()
workspaceId: string;

View File

@ -14,6 +14,7 @@ import {
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -87,6 +88,9 @@ export class FieldMetadataEntity<
@Column('jsonb', { nullable: true })
options: FieldMetadataOptions<T>;
@Column('jsonb', { nullable: true })
settings?: FieldMetadataSettings<T>;
@Column({ default: false })
isCustom: boolean;

View File

@ -17,7 +17,6 @@ import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-m
import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
import { IsFieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-options.validator';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { FieldMetadataService } from './field-metadata.service';
import { FieldMetadataEntity } from './field-metadata.entity';
@ -29,10 +28,7 @@ import { UpdateFieldInput } from './dtos/update-field.input';
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature(
[FieldMetadataEntity, RelationMetadataEntity],
'metadata',
),
NestjsQueryTypeOrmModule.forFeature([FieldMetadataEntity], 'metadata'),
WorkspaceMigrationModule,
WorkspaceMigrationRunnerModule,
ObjectMetadataModule,

View File

@ -58,8 +58,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
private readonly metadataDataSource: DataSource,
@InjectRepository(FieldMetadataEntity, 'metadata')
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(RelationMetadataEntity, 'metadata')
private readonly relationMetadataRepository: Repository<RelationMetadataEntity>,
private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
private readonly workspaceMigrationService: WorkspaceMigrationService,

View File

@ -0,0 +1,24 @@
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
type FieldMetadataDefaultSettings = {
isForeignKey?: boolean;
};
type FieldMetadataNumberSettings = {
precision: number;
};
type FieldMetadataSettingsMapping = {
[FieldMetadataType.NUMBER]: FieldMetadataNumberSettings;
};
type SettingsByFieldMetadata<T extends FieldMetadataType | 'default'> =
T extends keyof FieldMetadataSettingsMapping
? FieldMetadataSettingsMapping[T] & FieldMetadataDefaultSettings
: T extends 'default'
? FieldMetadataDefaultSettings
: never;
export type FieldMetadataSettings<
T extends FieldMetadataType | 'default' = 'default',
> = SettingsByFieldMetadata<T>;

View File

@ -1,5 +1,6 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
@ -13,6 +14,7 @@ export interface FieldMetadataInterface<
label: string;
defaultValue?: FieldMetadataDefaultValue<T>;
options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
objectMetadataId: string;
workspaceId?: string;
description?: string;

View File

@ -8,9 +8,13 @@ import {
IsString,
IsUUID,
} from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
import { BeforeCreateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-create-one-object.hook';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
@InputType()
@BeforeCreateOne(BeforeCreateOneObject)
@ -70,5 +74,11 @@ export class CreateObjectInput {
@IsOptional()
@Field({ nullable: true })
remoteTablePrimaryKeyColumnType?: string;
primaryKeyColumnType?: string;
@IsOptional()
@Field(() => GraphQLJSON, { nullable: true })
primaryKeyFieldMetadataSettings?: FieldMetadataSettings<
FieldMetadataType | 'default'
>;
}

View File

@ -16,6 +16,8 @@ import {
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
import {
@ -54,12 +56,10 @@ import {
import { createWorkspaceMigrationsForCustomObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-custom-object.util';
import { createWorkspaceMigrationsForRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/create-workspace-migrations-for-remote-object.util';
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
import {
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { validateObjectMetadataInput } from 'src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-input.util';
import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util';
import { ObjectMetadataEntity } from './object-metadata.entity';
@ -469,35 +469,38 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceDataSource: DataSource | undefined,
isRemoteObject = false,
) {
const isRelationEnabledForRemoteObjects =
await this.isRelationEnabledForRemoteObjects(
objectMetadataInput.workspaceId,
);
if (isRemoteObject && !isRelationEnabledForRemoteObjects) {
return;
}
const { timelineActivityObjectMetadata } =
await this.createTimelineActivityRelation(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
);
const { activityTargetObjectMetadata } =
await this.createActivityTargetRelation(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
),
objectMetadataInput.primaryKeyFieldMetadataSettings,
);
const { favoriteObjectMetadata } = await this.createFavoriteRelation(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings,
);
const { attachmentObjectMetadata } = await this.createAttachmentRelation(
objectMetadataInput.workspaceId,
createdObjectMetadata,
mapUdtNameToFieldType(objectMetadataInput.primaryKeyColumnType ?? 'uuid'),
objectMetadataInput.primaryKeyFieldMetadataSettings,
);
return this.workspaceMigrationService.createCustomMigration(
@ -511,7 +514,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
timelineActivityObjectMetadata,
favoriteObjectMetadata,
lastDataSourceMetadata.schema,
objectMetadataInput.remoteTablePrimaryKeyColumnType ?? 'uuid',
objectMetadataInput.primaryKeyColumnType ?? 'uuid',
workspaceDataSource,
)
: createWorkspaceMigrationsForCustomObject(
@ -527,6 +530,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createActivityTargetRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const activityTargetObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
@ -577,7 +584,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `ActivityTarget ${createdObjectMetadata.labelSingular} id foreign key`,
@ -585,6 +592,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
]);
@ -622,6 +630,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createAttachmentRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const attachmentObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
@ -672,7 +684,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Attachment ${createdObjectMetadata.labelSingular} id foreign key`,
@ -680,6 +692,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
]);
@ -715,6 +728,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createTimelineActivityRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const timelineActivityObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
@ -765,7 +782,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Timeline Activity ${createdObjectMetadata.labelSingular} id foreign key`,
@ -773,6 +790,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
]);
@ -810,6 +828,10 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
private async createFavoriteRelation(
workspaceId: string,
createdObjectMetadata: ObjectMetadataEntity,
objectPrimaryKeyType: FieldMetadataType,
objectPrimaryKeyFieldSettings:
| FieldMetadataSettings<FieldMetadataType | 'default'>
| undefined,
) {
const favoriteObjectMetadata =
await this.objectMetadataRepository.findOneByOrFail({
@ -861,7 +883,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
workspaceId: workspaceId,
isCustom: false,
isActive: true,
type: FieldMetadataType.UUID,
type: objectPrimaryKeyType,
name: `${createdObjectMetadata.nameSingular}Id`,
label: `${createdObjectMetadata.labelSingular} ID (foreign key)`,
description: `Favorite ${createdObjectMetadata.labelSingular} id foreign key`,
@ -869,6 +891,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
isNullable: true,
isSystem: true,
defaultValue: undefined,
settings: { ...objectPrimaryKeyFieldSettings, isForeignKey: true },
},
]);
@ -900,14 +923,4 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
return { favoriteObjectMetadata };
}
private async isRelationEnabledForRemoteObjects(workspaceId: string) {
const featureFlag = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKeys.IsRelationForRemoteObjectsEnabled,
value: true,
});
return featureFlag && featureFlag.value;
}
}

View File

@ -1,9 +1,11 @@
import { BadRequestException } from '@nestjs/common';
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
export const assertMutationNotOnRemoteObject = (
objectMetadataItem: ObjectMetadataInterface,
) => {
if (objectMetadataItem.isRemote) {
throw new Error('Remote objects are read-only');
throw new BadRequestException('Remote objects are read-only');
}
};

View File

@ -54,7 +54,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
eventObjectMetadata: ObjectMetadataEntity,
favoriteObjectMetadata: ObjectMetadataEntity,
schema: string,
remoteTablePrimaryKeyColumnType: string,
primaryKeyColumnType: string,
workspaceDataSource: DataSource | undefined,
): Promise<WorkspaceMigrationTableAction[]> => {
const createdObjectName = createdObjectMetadata.nameSingular;
@ -69,7 +69,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
@ -99,7 +99,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
@ -129,7 +129,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],
@ -159,7 +159,7 @@ export const createWorkspaceMigrationsForRemoteObject = async (
columnName: computeColumnName(createdObjectMetadata.nameSingular, {
isForeignKey: true,
}),
columnType: remoteTablePrimaryKeyColumnType,
columnType: primaryKeyColumnType,
isNullable: true,
} satisfies WorkspaceMigrationColumnCreate,
],

View File

@ -1,5 +1,7 @@
import { Repository } from 'typeorm/repository/Repository';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { decryptText } from 'src/engine/core-modules/auth/auth.util';
import {
FeatureFlagEntity,
@ -42,11 +44,32 @@ export const mapUdtNameToFieldType = (udtName: string): FieldMetadataType => {
case 'timestamp':
case 'timestamptz':
return FieldMetadataType.DATE_TIME;
case 'integer':
case 'int2':
case 'int4':
case 'int8':
return FieldMetadataType.NUMBER;
default:
return FieldMetadataType.TEXT;
}
};
export const mapUdtNameToSettings = (
udtName: string,
): FieldMetadataSettings<FieldMetadataType> | undefined => {
switch (udtName) {
case 'integer':
case 'int2':
case 'int4':
case 'int8':
return {
precision: 0,
} satisfies FieldMetadataSettings<FieldMetadataType.NUMBER>;
default:
return undefined;
}
};
export const isPostgreSQLIntegrationEnabled = async (
featureFlagRepository: Repository<FeatureFlagEntity>,
workspaceId: string,

View File

@ -12,6 +12,7 @@ import { RemoteTableStatus } from 'src/engine/metadata-modules/remote-server/rem
import {
isPostgreSQLIntegrationEnabled,
mapUdtNameToFieldType,
mapUdtNameToSettings,
} from 'src/engine/metadata-modules/remote-server/remote-table/remote-postgres-table/utils/remote-postgres-table.util';
import { RemoteTableInput } from 'src/engine/metadata-modules/remote-server/remote-table/dtos/remote-table-input';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@ -429,7 +430,11 @@ export class RemoteTableService {
workspaceId: workspaceId,
icon: 'IconPlug',
isRemote: true,
remoteTablePrimaryKeyColumnType: remoteTableIdColumn.udtName,
primaryKeyColumnType: remoteTableIdColumn.udtName,
// TODO: function should work for other types than Postgres
primaryKeyFieldMetadataSettings: mapUdtNameToSettings(
remoteTableIdColumn.udtName,
),
} satisfies CreateObjectInput);
for (const column of remoteTableColumns) {
@ -444,6 +449,8 @@ export class RemoteTableService {
isRemoteCreation: true,
isNullable: true,
icon: 'IconPlug',
// TODO: function should work for other types than Postgres
settings: mapUdtNameToSettings(column.udtName),
} satisfies CreateFieldInput);
if (column.columnName === 'id') {

View File

@ -59,7 +59,6 @@ export class AddStandardIdCommand extends CommandRunner {
IS_AIRTABLE_INTEGRATION_ENABLED: true,
IS_POSTGRESQL_INTEGRATION_ENABLED: true,
IS_MULTI_SELECT_ENABLED: false,
IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED: false,
},
);
const standardFieldMetadataCollection = this.standardFieldFactory.create(
@ -75,7 +74,6 @@ export class AddStandardIdCommand extends CommandRunner {
IS_AIRTABLE_INTEGRATION_ENABLED: true,
IS_POSTGRESQL_INTEGRATION_ENABLED: true,
IS_MULTI_SELECT_ENABLED: false,
IS_RELATION_FOR_REMOTE_OBJECTS_ENABLED: false,
},
);

View File

@ -48,6 +48,7 @@ export function FieldMetadata<T extends FieldMetadataType>(
description: `${restParams.description} id foreign key`,
defaultValue: null,
options: undefined,
settings: undefined,
},
joinColumn,
isNullable,

View File

@ -1,6 +1,7 @@
import { FieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface';
import { GateDecoratorParams } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/gate-decorator.interface';
import { FieldMetadataOptions } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-options.interface';
import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface';
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
@ -16,12 +17,13 @@ export interface FieldMetadataDecoratorParams<
defaultValue?: FieldMetadataDefaultValue<T>;
joinColumn?: string;
options?: FieldMetadataOptions<T>;
settings?: FieldMetadataSettings<T>;
}
export interface ReflectFieldMetadata {
[key: string]: Omit<
FieldMetadataDecoratorParams<'default'>,
'defaultValue' | 'type' | 'options'
'defaultValue' | 'type' | 'options' | 'settings'
> & {
name: string;
type: FieldMetadataType;
@ -31,5 +33,6 @@ export interface ReflectFieldMetadata {
defaultValue: FieldMetadataDefaultValue<'default'> | null;
gate?: GateDecoratorParams;
options?: FieldMetadataOptions<'default'> | null;
settings?: FieldMetadataSettings<'default'> | null;
};
}

View File

@ -241,7 +241,9 @@ export class WorkspaceMetadataUpdaterService {
manager: EntityManager,
entityClass: EntityTarget<Entity>,
updateCollection: Array<
DeepPartial<Omit<Entity, 'fields' | 'options'>> & { id: string }
DeepPartial<Omit<Entity, 'fields' | 'options' | 'settings'>> & {
id: string;
}
>,
keysToOmit: (keyof Entity)[] = [],
): Promise<{ current: Entity; altered: Entity }[]> {