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:
co-authored by
Thomas Trompette
Weiko
parent
dc576d0818
commit
224c8d361b
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -861,6 +861,7 @@ export type Field = {
|
||||
name: Scalars['String'];
|
||||
options?: Maybe<Scalars['JSON']>;
|
||||
relationDefinition?: Maybe<RelationDefinition>;
|
||||
settings?: Maybe<Scalars['JSON']>;
|
||||
toRelationMetadata?: Maybe<Relation>;
|
||||
type: FieldMetadataType;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
|
||||
@@ -51,7 +51,7 @@ const mocks: MockedResponse[] = [
|
||||
$filter: ActivityTargetFilterInput
|
||||
$orderBy: ActivityTargetOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
$limit: Int
|
||||
) {
|
||||
activityTargets(
|
||||
filter: $filter
|
||||
@@ -105,7 +105,7 @@ const mocks: MockedResponse[] = [
|
||||
$filter: ActivityFilterInput
|
||||
$orderBy: ActivityOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
$limit: Int
|
||||
) {
|
||||
activities(
|
||||
filter: $filter
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ const mocks: MockedResponse[] = [
|
||||
$filter: ActivityTargetFilterInput
|
||||
$orderBy: ActivityTargetOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
$limit: Int
|
||||
) {
|
||||
activityTargets(
|
||||
filter: $filter
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindOneWorkspaceMember($objectRecordId: UUID!) {
|
||||
query FindOneWorkspaceMember($objectRecordId: ID!) {
|
||||
workspaceMember(filter: { id: { eq: $objectRecordId } }) {
|
||||
__typename
|
||||
colorScheme
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ const mocks: MockedResponse[] = [
|
||||
request: {
|
||||
query: gql`
|
||||
mutation UpdateOneActivity(
|
||||
$idToUpdate: UUID!
|
||||
$idToUpdate: ID!
|
||||
$input: ActivityUpdateInput!
|
||||
) {
|
||||
updateActivity(id: $idToUpdate, data: $input) {
|
||||
|
||||
@@ -177,7 +177,7 @@ export const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
mutation DeleteOneFavorite($idToDelete: UUID!) {
|
||||
mutation DeleteOneFavorite($idToDelete: ID!) {
|
||||
deleteFavorite(id: $idToDelete) {
|
||||
id
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export const mocks = [
|
||||
request: {
|
||||
query: gql`
|
||||
mutation UpdateOneFavorite(
|
||||
$idToUpdate: UUID!
|
||||
$idToUpdate: ID!
|
||||
$input: FavoriteUpdateInput!
|
||||
) {
|
||||
updateFavorite(id: $idToUpdate, data: $input) {
|
||||
|
||||
@@ -48,6 +48,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
nameSingular
|
||||
namePlural
|
||||
isSystem
|
||||
isRemote
|
||||
}
|
||||
toFieldMetadataId
|
||||
}
|
||||
@@ -60,6 +61,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
|
||||
nameSingular
|
||||
namePlural
|
||||
isSystem
|
||||
isRemote
|
||||
}
|
||||
fromFieldMetadataId
|
||||
}
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export const findManyViewsQuery = gql`
|
||||
$filter: ViewFilterInput
|
||||
$orderBy: ViewOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
$limit: Int
|
||||
) {
|
||||
views(
|
||||
filter: $filter
|
||||
|
||||
@@ -21,6 +21,7 @@ export type FieldMetadataItem = Omit<
|
||||
| 'toRelationMetadata'
|
||||
| 'defaultValue'
|
||||
| 'options'
|
||||
| 'settings'
|
||||
| 'relationDefinition'
|
||||
> & {
|
||||
__typename?: string;
|
||||
@@ -28,7 +29,7 @@ export type FieldMetadataItem = Omit<
|
||||
| (Pick<Relation, 'id' | 'toFieldMetadataId' | 'relationType'> & {
|
||||
toObjectMetadata: Pick<
|
||||
Relation['toObjectMetadata'],
|
||||
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
|
||||
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
@@ -36,7 +37,7 @@ export type FieldMetadataItem = Omit<
|
||||
| (Pick<Relation, 'id' | 'fromFieldMetadataId' | 'relationType'> & {
|
||||
fromObjectMetadata: Pick<
|
||||
Relation['fromObjectMetadata'],
|
||||
'id' | 'nameSingular' | 'namePlural' | 'isSystem'
|
||||
'id' | 'nameSingular' | 'namePlural' | 'isSystem' | 'isRemote'
|
||||
>;
|
||||
})
|
||||
| null;
|
||||
|
||||
+8
-3
@@ -2,10 +2,15 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const isObjectMetadataAvailableForRelation = (
|
||||
objectMetadataItem: Pick<ObjectMetadataItem, 'isSystem' | 'nameSingular'>,
|
||||
objectMetadataItem: Pick<
|
||||
ObjectMetadataItem,
|
||||
'isSystem' | 'nameSingular' | 'isRemote'
|
||||
>,
|
||||
) => {
|
||||
return (
|
||||
!objectMetadataItem.isSystem ||
|
||||
objectMetadataItem.nameSingular === CoreObjectNameSingular.WorkspaceMember
|
||||
(!objectMetadataItem.isSystem ||
|
||||
objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkspaceMember) &&
|
||||
!objectMetadataItem.isRemote
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useContext } from 'react';
|
||||
import { EntityChip, EntityChipVariant } from 'twenty-ui';
|
||||
|
||||
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||
|
||||
@@ -25,16 +23,8 @@ export const RecordChip = ({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
// Will only exists if the chip is inside a record table.
|
||||
// This is temporary until we have the show page for remote objects.
|
||||
const { isReadOnly } = useContext(RecordTableRowContext);
|
||||
|
||||
const objectRecordIdentifier = mapToObjectRecordIdentifier(record);
|
||||
|
||||
const linkToEntity = isReadOnly
|
||||
? undefined
|
||||
: objectRecordIdentifier.linkToShowPage;
|
||||
|
||||
return (
|
||||
<EntityChip
|
||||
entityId={record.id}
|
||||
@@ -43,7 +33,7 @@ export const RecordChip = ({
|
||||
avatarUrl={
|
||||
getImageAbsoluteURIOrBase64(objectRecordIdentifier.avatarUrl) || ''
|
||||
}
|
||||
linkToEntity={linkToEntity}
|
||||
linkToEntity={objectRecordIdentifier.linkToShowPage}
|
||||
maxWidth={maxWidth}
|
||||
className={className}
|
||||
variant={variant}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const query = gql`
|
||||
mutation DeleteOnePerson($idToDelete: UUID!) {
|
||||
mutation DeleteOnePerson($idToDelete: ID!) {
|
||||
deletePerson(id: $idToDelete) {
|
||||
id
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
|
||||
export { responseData } from './useUpdateOneRecord';
|
||||
|
||||
export const query = gql`
|
||||
mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: UUID!) {
|
||||
mutation ExecuteQuickActionOnOnePerson($idToExecuteQuickActionOn: ID!) {
|
||||
executeQuickActionOnPerson(id: $idToExecuteQuickActionOn) {
|
||||
__typename
|
||||
xLink {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ export const query = gql`
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: PersonOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float
|
||||
$limit: Int
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
|
||||
@@ -3,7 +3,7 @@ import { gql } from '@apollo/client';
|
||||
import { responseData as person } from './useUpdateOneRecord';
|
||||
|
||||
export const query = gql`
|
||||
query FindOnePerson($objectRecordId: UUID!) {
|
||||
query FindOnePerson($objectRecordId: ID!) {
|
||||
person(filter: { id: { eq: $objectRecordId } }) {
|
||||
__typename
|
||||
xLink {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const query = gql`
|
||||
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
|
||||
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
|
||||
updatePerson(id: $idToUpdate, data: $input) {
|
||||
__typename
|
||||
xLink {
|
||||
|
||||
@@ -26,7 +26,7 @@ export const useDeleteOneRecordMutation = ({
|
||||
);
|
||||
|
||||
const deleteOneRecordMutation = gql`
|
||||
mutation DeleteOne${capitalizedObjectName}($idToDelete: UUID!) {
|
||||
mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) {
|
||||
${mutationResponseField}(id: $idToDelete) {
|
||||
id
|
||||
}
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export const useExecuteQuickActionOnOneRecordMutation = ({
|
||||
});
|
||||
|
||||
const executeQuickActionOnOneRecordMutation = gql`
|
||||
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: UUID!) {
|
||||
mutation ExecuteQuickActionOnOne${capitalizedObjectName}($idToExecuteQuickActionOn: ID!) {
|
||||
${graphQLFieldForExecuteQuickActionOnOneRecordMutation}(id: $idToExecuteQuickActionOn) ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export const useFindDuplicateRecordsQuery = ({
|
||||
const findDuplicateRecordsQuery = gql`
|
||||
query FindDuplicate${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}($id: UUID) {
|
||||
)}($id: ID!) {
|
||||
${getFindDuplicateRecordsQueryResponseField(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}(id: $id) {
|
||||
|
||||
@@ -22,7 +22,7 @@ export const useFindOneRecordQuery = ({
|
||||
const findOneRecordQuery = gql`
|
||||
query FindOne${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}($objectRecordId: UUID!) {
|
||||
)}($objectRecordId: ID!) {
|
||||
${objectMetadataItem.nameSingular}(filter: {
|
||||
id: {
|
||||
eq: $objectRecordId
|
||||
@@ -32,7 +32,7 @@ export const useFindOneRecordQuery = ({
|
||||
objectMetadataItem,
|
||||
depth,
|
||||
})}
|
||||
}
|
||||
},
|
||||
`;
|
||||
|
||||
return {
|
||||
|
||||
@@ -35,7 +35,7 @@ export const useUpdateOneRecordMutation = ({
|
||||
);
|
||||
|
||||
const updateOneRecordMutation = gql`
|
||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: UUID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
||||
mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) {
|
||||
${mutationResponseField}(id: $idToUpdate, data: $input) ${mapObjectMetadataToGraphQLQuery(
|
||||
{
|
||||
objectMetadataItems,
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({
|
||||
const limitPerMetadataItemArray = capitalizedObjectNameSingulars
|
||||
.map(
|
||||
(capitalizedObjectNameSingular) =>
|
||||
`$limit${capitalizedObjectNameSingular}: Float`,
|
||||
`$limit${capitalizedObjectNameSingular}: Int`,
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
|
||||
const query = gql`
|
||||
mutation UpdateOnePerson($idToUpdate: UUID!, $input: PersonUpdateInput!) {
|
||||
mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) {
|
||||
updatePerson(id: $idToUpdate, data: $input) {
|
||||
__typename
|
||||
xLink {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ const mocks: MockedResponse[] = [
|
||||
request: {
|
||||
query: gql`
|
||||
mutation UpdateOneCompany(
|
||||
$idToUpdate: UUID!
|
||||
$idToUpdate: ID!
|
||||
$input: CompanyUpdateInput!
|
||||
) {
|
||||
updateCompany(id: $idToUpdate, data: $input) {
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { isFieldDateValue } from '@/object-record/record-field/types/guards/isFi
|
||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue.ts';
|
||||
import { isFieldRawJson } from '@/object-record/record-field/types/guards/isFieldRawJson';
|
||||
import { isFieldRawJsonValue } from '@/object-record/record-field/types/guards/isFieldRawJsonValue';
|
||||
import { isFieldSelect } from '@/object-record/record-field/types/guards/isFieldSelect';
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ const query = gql`
|
||||
$filterNameSingular: NameSingularFilterInput
|
||||
$orderByNameSingular: NameSingularOrderByInput
|
||||
$lastCursorNameSingular: String
|
||||
$limitNameSingular: Float
|
||||
$limitNameSingular: Int
|
||||
) {
|
||||
namePlural(
|
||||
filter: $filterNameSingular
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ query FindMany${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}FilterInput, $orderBy: ${capitalize(
|
||||
objectMetadataItem.nameSingular,
|
||||
)}OrderByInput, $lastCursor: String, $limit: Float) {
|
||||
)}OrderByInput, $lastCursor: String, $limit: Int) {
|
||||
${
|
||||
objectMetadataItem.namePlural
|
||||
}(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ export const query = gql`
|
||||
$filter: PersonFilterInput
|
||||
$orderBy: PersonOrderByInput
|
||||
$lastCursor: String
|
||||
$limit: Float = 60
|
||||
$limit: Int = 60
|
||||
) {
|
||||
people(
|
||||
filter: $filter
|
||||
|
||||
+6
-1
@@ -65,7 +65,12 @@ export const ShowPageRightContainer = ({
|
||||
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
|
||||
const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED');
|
||||
const shouldDisplayCalendarTab =
|
||||
useIsFeatureEnabled('IS_CALENDAR_ENABLED') &&
|
||||
(targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Company ||
|
||||
targetableObject.targetObjectNameSingular ===
|
||||
CoreObjectNameSingular.Person);
|
||||
const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED');
|
||||
|
||||
const shouldDisplayEmailsTab =
|
||||
|
||||
@@ -96,11 +96,6 @@ export const RecordShowPage = () => {
|
||||
? `${pageName} - ${capitalize(objectNameSingular)}`
|
||||
: capitalize(objectNameSingular);
|
||||
|
||||
// Temporarily since we don't have relations for remote objects yet
|
||||
if (objectMetadataItem.isRemote) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageTitle title={pageTitle} />
|
||||
|
||||
@@ -88,6 +88,8 @@ export const SettingsObjectDetail = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const shouldDisplayAddFieldButton = !activeObjectMetadataItem.isRemote;
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
@@ -207,21 +209,23 @@ export const SettingsObjectDetail = () => {
|
||||
</TableSection>
|
||||
)}
|
||||
</Table>
|
||||
<StyledDiv>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add Field"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
disabledMetadataFields.length
|
||||
? './new-field/step-1'
|
||||
: './new-field/step-2',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledDiv>
|
||||
{shouldDisplayAddFieldButton && (
|
||||
<StyledDiv>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="Add Field"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
disabledMetadataFields.length
|
||||
? './new-field/step-1'
|
||||
: './new-field/step-2',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledDiv>
|
||||
)}
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
|
||||
@@ -9,6 +9,7 @@ export const mockedClientConfig: ClientConfig = {
|
||||
google: true,
|
||||
password: true,
|
||||
magicLink: false,
|
||||
microsoft: false,
|
||||
__typename: 'AuthProviders',
|
||||
},
|
||||
telemetry: {
|
||||
|
||||
@@ -63,6 +63,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: 'c756f6ff-8c00-4fe5-a923-c6cfc7b1ac4a',
|
||||
},
|
||||
@@ -91,6 +92,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'opportunity',
|
||||
namePlural: 'opportunities',
|
||||
isSystem: false,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '00468e2a-a601-4635-ae9c-a9bb826cc860',
|
||||
},
|
||||
@@ -119,6 +121,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'activityTarget',
|
||||
namePlural: 'activityTargets',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: 'bba19feb-c248-487b-92d7-98df54c51e44',
|
||||
},
|
||||
@@ -221,6 +224,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'attachment',
|
||||
namePlural: 'attachments',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '0880dac5-37d2-43a6-b143-722126d4923f',
|
||||
},
|
||||
@@ -331,6 +335,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'workspaceMember',
|
||||
namePlural: 'workspaceMembers',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
fromFieldMetadataId: '0f3e456f-3bb4-4261-a436-95246dc0e159',
|
||||
},
|
||||
@@ -378,6 +383,7 @@ export const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
nameSingular: 'favorite',
|
||||
namePlural: 'favorites',
|
||||
isSystem: true,
|
||||
isRemote: false,
|
||||
},
|
||||
toFieldMetadataId: '8fd8965b-bd4e-4a9b-90e9-c75652dadda1',
|
||||
},
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddSettingsColumnToFieldMetadata1713793656356
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddSettingsColumnToFieldMetadata1713793656356';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" ADD "settings" jsonb`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."remoteServer" DROP COLUMN "foreignDataWrapperType"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."remoteServer" ADD "foreignDataWrapperType" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."remoteServer" DROP COLUMN "foreignDataWrapperType"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."remoteServer" ADD "foreignDataWrapperType" character varying`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "metadata"."fieldMetadata" DROP COLUMN "settings"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ export class WorkspaceQueryBuilderFactory {
|
||||
}
|
||||
|
||||
findDuplicatesExistingRecord(
|
||||
id: string,
|
||||
id: string | number,
|
||||
options: WorkspaceQueryBuilderOptions,
|
||||
): string {
|
||||
return this.findDuplicatesQueryFactory.buildQueryForExistingRecord(
|
||||
|
||||
+81
-5
@@ -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' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+117
-11
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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`);
|
||||
}
|
||||
};
|
||||
+38
-8
@@ -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',
|
||||
|
||||
+13
@@ -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,
|
||||
|
||||
+1
-21
@@ -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,
|
||||
|
||||
+4
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+4
-4
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+17
@@ -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 },
|
||||
},
|
||||
});
|
||||
+3
-2
@@ -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;
|
||||
|
||||
+33
-24
@@ -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],
|
||||
]);
|
||||
|
||||
+10
-8
@@ -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 },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+14
-9
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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' })
|
||||
|
||||
+5
@@ -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;
|
||||
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
|
||||
+1
-5
@@ -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,
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
+24
@@ -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>;
|
||||
+2
@@ -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;
|
||||
|
||||
+11
-1
@@ -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'
|
||||
>;
|
||||
}
|
||||
|
||||
+41
-28
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
+5
-5
@@ -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,
|
||||
],
|
||||
|
||||
+23
@@ -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,
|
||||
|
||||
+8
-1
@@ -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') {
|
||||
|
||||
-2
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+1
@@ -48,6 +48,7 @@ export function FieldMetadata<T extends FieldMetadataType>(
|
||||
description: `${restParams.description} id foreign key`,
|
||||
defaultValue: null,
|
||||
options: undefined,
|
||||
settings: undefined,
|
||||
},
|
||||
joinColumn,
|
||||
isNullable,
|
||||
|
||||
+4
-1
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
+3
-1
@@ -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 }[]> {
|
||||
|
||||
Reference in New Issue
Block a user