From 1ed4965a959a6493662081ea8c2b51ad1368c93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Fri, 3 Nov 2023 17:16:37 +0100 Subject: [PATCH] feat: refactor schema builder and resolver builder (#2215) * feat: wip refactor schema builder * feat: wip store types and first queries generation * feat: refactor schema-builder and resolver-builder * fix: clean & small type fix * fix: avoid breaking change * fix: remove util from pg-graphql classes * fix: required default fields * Refactor frontend accordingly --------- Co-authored-by: Charles Bochet --- front/src/generated-metadata/graphql.ts | 28 +- front/src/generated/graphql.tsx | 23 +- .../ActivityAssigneeEditableField.tsx | 2 +- .../components/ActivityEditorDateField.tsx | 2 +- .../tasks/hooks/useCurrentUserDueTaskCount.ts | 2 +- .../companiesAvailableFieldDefinitions.tsx | 24 +- .../hooks/useCreateOneMetadataField.ts | 12 +- .../hooks/useSeedCustomObjectsTemp.ts | 44 --- .../formatMetadataFieldAsFilterDefinition.ts | 2 +- .../utils/generateCreateOneObjectMutation.ts | 2 +- .../utils/generateDeleteOneObjectMutation.ts | 2 +- .../generateFindManyCustomObjectsQuery.ts | 2 +- .../utils/generateUpdateOneObjectMutation.ts | 2 +- .../utils/mapFieldMetadataToGraphQLQuery.ts | 18 +- .../modules/metadata/utils/parseFieldType.ts | 4 +- .../peopleAvailableFieldDefinitions.tsx | 18 +- .../pipelineAvailableFieldDefinitions.tsx | 8 +- .../SettingsObjectFieldTypeSelectSection.tsx | 2 +- ...gsObjectFieldTypeSelectSection.stories.tsx | 2 +- .../data-model/constants/dataTypes.ts | 10 +- .../SettingsObjectFieldDataType.tsx | 2 +- .../SettingsObjectFieldItemTableRow.tsx | 8 +- .../data-model/types/ObjectFieldDataType.ts | 10 +- .../__stories__/DateFieldDisplay.stories.tsx | 2 +- .../DoubleTextFieldDisplay.stories.tsx | 2 +- .../__stories__/EmailFieldDisplay.stories.tsx | 2 +- .../__stories__/MoneyFieldDisplay.stories.tsx | 2 +- .../NumberFieldDisplay.stories.tsx | 2 +- .../__stories__/PhoneFieldDisplay.stories.tsx | 2 +- .../__stories__/TextFieldDisplay.stories.tsx | 2 +- .../__stories__/URLFieldDisplay.stories.tsx | 2 +- .../field/meta-types/hooks/useBooleanField.ts | 2 +- .../field/meta-types/hooks/useChipField.ts | 2 +- .../field/meta-types/hooks/useDateField.ts | 2 +- .../hooks/useDoubleTextChipField.ts | 2 +- .../meta-types/hooks/useDoubleTextField.ts | 2 +- .../field/meta-types/hooks/useEmailField.ts | 2 +- .../meta-types/hooks/useMoneyAmountV2Field.ts | 2 +- .../field/meta-types/hooks/useMoneyField.ts | 2 +- .../field/meta-types/hooks/useNumberField.ts | 2 +- .../field/meta-types/hooks/usePhoneField.ts | 2 +- .../meta-types/hooks/useProbabilityField.ts | 2 +- .../meta-types/hooks/useRelationField.ts | 2 +- .../field/meta-types/hooks/useTextField.ts | 2 +- .../field/meta-types/hooks/useURLField.ts | 2 +- .../field/meta-types/hooks/useURLV2Field.ts | 2 +- .../__stories__/BooleanFieldInput.stories.tsx | 2 +- .../__stories__/ChipFieldInput.stories.tsx | 2 +- .../__stories__/DateFieldInput.stories.tsx | 2 +- .../DoubleTextChipFieldInput.stories.tsx | 2 +- .../DoubleTextFieldInput.stories.tsx | 2 +- .../__stories__/EmailFieldInput.stories.tsx | 2 +- .../__stories__/MoneyFieldInput.stories.tsx | 2 +- .../__stories__/NumberFieldInput.stories.tsx | 2 +- .../__stories__/PhoneFieldInput.stories.tsx | 2 +- .../ProbabilityFieldInput.stories.tsx | 2 +- .../RelationFieldInput.stories.tsx | 2 +- .../__stories__/TextFieldInput.stories.tsx | 2 +- .../__stories__/URLFieldInput.stories.tsx | 2 +- .../ui/object/field/types/FieldType.ts | 32 +-- .../field/types/guards/assertFieldMetadata.ts | 30 +-- .../field/types/guards/isFieldBoolean.ts | 2 +- .../object/field/types/guards/isFieldChip.ts | 2 +- .../object/field/types/guards/isFieldDate.ts | 2 +- .../field/types/guards/isFieldDoubleText.ts | 2 +- .../types/guards/isFieldDoubleTextChip.ts | 2 +- .../object/field/types/guards/isFieldEmail.ts | 2 +- .../object/field/types/guards/isFieldMoney.ts | 3 +- .../types/guards/isFieldMoneyAmountV2.ts | 2 +- .../field/types/guards/isFieldNumber.ts | 2 +- .../object/field/types/guards/isFieldPhone.ts | 2 +- .../field/types/guards/isFieldProbability.ts | 2 +- .../field/types/guards/isFieldRelation.ts | 2 +- .../object/field/types/guards/isFieldText.ts | 2 +- .../object/field/types/guards/isFieldURL.ts | 2 +- .../object/field/types/guards/isFieldURLV2.ts | 2 +- .../MultipleFiltersDropdownContent.tsx | 10 +- .../components/ObjectFilterDropdownButton.tsx | 2 +- .../ObjectFilterDropdownEntitySelect.tsx | 2 +- .../ObjectFilterDropdownFilterSelect.tsx | 2 +- .../types/FilterType.ts | 2 +- .../utils/getOperandsForFilterType.ts | 8 +- .../utils/turnFilterIntoWhereClause.ts | 8 +- .../utils/turnFiltersIntoWhereClauseV2.ts | 2 +- .../constants/companyShowFieldDefinitions.tsx | 14 +- .../companyTableFilterDefinitions.tsx | 12 +- .../opportunityBoardFilterDefinitions.tsx | 8 +- .../constants/personShowFieldDefinitions.tsx | 16 +- .../personTableFilterDefinitions.tsx | 14 +- .../SettingsObjectNewFieldStep2.tsx | 2 +- .../pages/tasks/tasks-filter-definitions.tsx | 2 +- front/src/testing/mock-data/metadata.ts | 32 +-- server/package.json | 2 + .../typeorm-seeds/metadata/field-metadata.ts | 40 +-- server/src/main.ts | 3 + .../field-metadata/dtos/create-field.input.ts | 18 +- .../field-metadata/field-metadata.entity.ts | 32 ++- ...ld-metadata-target-column-map.interface.ts | 35 +++ .../utils/field-metadata.util.ts | 59 ++-- .../custom-table-default-column.util.ts | 25 ++ .../migration-runner.service.ts | 26 +- .../object-metadata/object-metadata.entity.ts | 4 +- .../services/object-metadata.service.ts | 7 + .../companies/companies.metadata.ts | 8 +- .../view-fields/view-fields.metadata.ts | 8 +- .../view-filters/view-filters.metadata.ts | 10 +- .../view-sorts/view-sorts.metadata.ts | 6 +- .../standard-objects/views/views.metadata.ts | 6 +- .../entity-resolver/entity-resolver.module.ts | 12 - .../entity-resolver.service.spec.ts | 27 -- .../entity-resolver.service.ts | 106 -------- .../pg-graphql-query-builder.util.ts | 135 ---------- .../pg-graphql-query-runner.util.ts | 113 -------- .../factories/create-many-resolver.factory.ts | 33 +++ .../factories/create-one-resolver.factory.ts | 35 +++ .../factories/delete-one-resolver.factory.ts | 35 +++ .../resolver-builder/factories/factories.ts | 28 ++ .../factories/find-many-resolver.factory.ts | 33 +++ .../factories/find-one-resolver.factory.ts | 35 +++ .../factories/update-one-resolver.factory.ts | 35 +++ .../interfaces/factory.interface.ts | 7 + .../interfaces/pg-graphql.interface.ts | 14 + .../interfaces/record.interface.ts | 21 ++ .../interfaces/resolvers-builder.interface.ts | 55 ++++ .../pg-graphql-query-builder.spec.ts | 15 +- .../pg-graphql/pg-graphql-query-builder.ts | 149 +++++++++++ .../pg-graphql/pg-graphql-query-runner.ts | 148 ++++++++++ .../resolver-builder.module.ts | 14 + .../resolver-builder/resolver.factory.ts | 100 +++++++ .../utils/__tests__/convert-arguments.spec.ts | 12 +- .../convert-fields-to-graphql.spec.ts | 9 +- .../__tests__/generate-args-input.spec.ts | 2 +- .../utils/__tests__/parse-result.spec.ts | 2 +- .../stringify-without-key-quote.spec.ts | 2 +- .../utils/convert-arguments.util.ts | 7 +- .../utils/convert-fields-to-graphql.util.ts | 4 +- .../utils/generate-args-input.util.ts | 0 .../utils/parse-result.util.ts | 0 .../utils/stringify-without-key-quote.util.ts | 0 .../schema-builder/factories/args.factory.ts | 98 +++++++ .../connection-type-definition.factory.ts | 76 ++++++ .../factories/connection-type.factory.ts | 58 ++++ .../factories/edge-type-definition.factory.ts | 74 +++++ .../factories/edge-type.factory.ts | 58 ++++ .../schema-builder/factories/factories.ts | 35 +++ .../filter-type-definition.factory.ts | 85 ++++++ .../factories/filter-type.factory.ts | 60 +++++ .../input-type-definition.factory.ts | 70 +++++ .../factories/input-type.factory.ts | 63 +++++ .../factories/mutation-type.factory.ts | 26 ++ .../object-type-definition.factory.ts | 65 +++++ .../order-by-type-definition.factory.ts | 61 +++++ .../factories/order-by-type.factory.ts | 58 ++++ .../factories/output-type.factory.ts | 62 +++++ .../factories/query-type.factory.ts | 26 ++ .../factories/root-type.factory.ts | 112 ++++++++ .../schema-builder/graphql-schema.factory.ts | 51 ++++ .../graphql-types/enum/index.ts | 1 + ...ype.ts => order-by-direction.enum-type.ts} | 0 ...type.ts => big-float-filter.input-type.ts} | 5 +- ...r.type.ts => big-int-filter.input-type.ts} | 3 - ...lter.type.ts => date-filter.input-type.ts} | 5 +- ...type.ts => date-time-filter.input-type.ts} | 5 +- .../input/filter-is-enum-filter.type.ts | 9 - ...ter.type.ts => float-filter.input-type.ts} | 5 +- .../graphql-types/input/index.ts | 9 + ...ilter.type.ts => int-filter.input-type.ts} | 5 +- .../graphql-types/input/money-filter.type.ts | 12 - ...er.type.ts => string-filter.input-type.ts} | 3 - ...lter.type.ts => time-filter.input-type.ts} | 7 +- .../graphql-types/input/url-filter.type.ts | 11 - ...lter.type.ts => uuid-filter.input-type.ts} | 5 +- .../graphql-types/object/index.ts | 1 + ...-into.type.ts => page-into.object-type.ts} | 0 .../graphql-types/scalars/index.ts | 8 + .../build-schema-optionts.interface.ts | 16 ++ .../interfaces/field-metadata.interface.ts | 15 ++ .../interfaces/object-metadata.interface.ts | 12 + .../interfaces/param-metadata.interface.ts | 18 ++ .../schema-builder-context.interface.ts | 6 +- .../money.object-definition.ts | 28 ++ .../url.object-definition.ts | 28 ++ .../schema-builder/schema-builder.module.ts | 22 +- .../schema-builder.service.spec.ts | 27 -- .../schema-builder/schema-builder.service.ts | 252 ------------------ .../services/type-mapper.service.ts | 152 +++++++++++ .../storages/type-definitions.storage.ts | 76 ++++++ .../type-definitions.generator.ts | 208 +++++++++++++++ .../generate-connection-type.spec.ts | 52 ---- .../generate-create-input-type.spec.ts | 56 ---- .../__tests__/generate-edge-type.spec.ts | 38 --- .../generate-filter-input-type.spec.ts | 53 ---- .../__tests__/generate-object-type.spec.ts | 73 ----- .../generate-order-by-input-type.spec.ts | 26 -- .../generate-udpate-input-type.spec.ts | 51 ---- .../__tests__/get-field-metadata-type.spec.ts | 21 ++ .../utils/__tests__/get-resolver-args.spec.ts | 55 ++++ .../map-column-type-to-filter-type.spec.ts | 47 ---- .../map-column-type-to-graphql-type.spec.ts | 77 ------ .../utils/generate-connection-type.util.ts | 24 -- .../utils/generate-create-input-type.util.ts | 34 --- .../utils/generate-edge-type.util.ts | 22 -- .../utils/generate-filter-input-type.util.ts | 52 ---- .../utils/generate-object-type.util.ts | 41 --- .../generate-order-by-input-type.util.ts | 37 --- .../utils/generate-update-input-type.util.ts | 34 --- .../utils/get-field-metadata-type.util.ts | 17 ++ .../utils/get-resolver-args.util.ts | 81 ++++++ .../map-column-type-to-filter-type.util.ts | 39 --- .../map-column-type-to-graphql-type.util.ts | 87 ------ server/src/tenant/tenant.module.ts | 2 + server/src/tenant/tenant.service.spec.ts | 15 +- server/src/tenant/tenant.service.ts | 42 ++- .../utils/__tests__/get-resolver-name.spec.ts | 30 +++ .../tenant/utils/get-resolver-name.util.ts | 27 ++ server/yarn.lock | 11 +- 216 files changed, 3215 insertions(+), 2028 deletions(-) delete mode 100644 front/src/modules/metadata/hooks/useSeedCustomObjectsTemp.ts create mode 100644 server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts create mode 100644 server/src/metadata/migration-runner/custom-table-default-column.util.ts delete mode 100644 server/src/tenant/entity-resolver/entity-resolver.module.ts delete mode 100644 server/src/tenant/entity-resolver/entity-resolver.service.spec.ts delete mode 100644 server/src/tenant/entity-resolver/entity-resolver.service.ts delete mode 100644 server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util.ts delete mode 100644 server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-runner.util.ts create mode 100644 server/src/tenant/resolver-builder/factories/create-many-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/factories/create-one-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/factories/delete-one-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/factories/factories.ts create mode 100644 server/src/tenant/resolver-builder/factories/find-many-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/factories/find-one-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/factories/update-one-resolver.factory.ts create mode 100644 server/src/tenant/resolver-builder/interfaces/factory.interface.ts create mode 100644 server/src/tenant/resolver-builder/interfaces/pg-graphql.interface.ts create mode 100644 server/src/tenant/resolver-builder/interfaces/record.interface.ts create mode 100644 server/src/tenant/resolver-builder/interfaces/resolvers-builder.interface.ts rename server/src/tenant/{entity-resolver => resolver-builder}/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts (93%) create mode 100644 server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder.ts create mode 100644 server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner.ts create mode 100644 server/src/tenant/resolver-builder/resolver-builder.module.ts create mode 100644 server/src/tenant/resolver-builder/resolver.factory.ts rename server/src/tenant/{entity-resolver => resolver-builder}/utils/__tests__/convert-arguments.spec.ts (87%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/__tests__/convert-fields-to-graphql.spec.ts (90%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/__tests__/generate-args-input.spec.ts (94%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/__tests__/parse-result.spec.ts (97%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/__tests__/stringify-without-key-quote.spec.ts (93%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/convert-arguments.util.ts (85%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/convert-fields-to-graphql.util.ts (91%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/generate-args-input.util.ts (100%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/parse-result.util.ts (100%) rename server/src/tenant/{entity-resolver => resolver-builder}/utils/stringify-without-key-quote.util.ts (100%) create mode 100644 server/src/tenant/schema-builder/factories/args.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/connection-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/connection-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/edge-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/edge-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/factories.ts create mode 100644 server/src/tenant/schema-builder/factories/filter-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/filter-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/input-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/input-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/mutation-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/object-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/order-by-type-definition.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/order-by-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/output-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/query-type.factory.ts create mode 100644 server/src/tenant/schema-builder/factories/root-type.factory.ts create mode 100644 server/src/tenant/schema-builder/graphql-schema.factory.ts create mode 100644 server/src/tenant/schema-builder/graphql-types/enum/index.ts rename server/src/tenant/schema-builder/graphql-types/enum/{order-by-direction.type.ts => order-by-direction.enum-type.ts} (100%) rename server/src/tenant/schema-builder/graphql-types/input/{big-float-filter.type.ts => big-float-filter.input-type.ts} (78%) rename server/src/tenant/schema-builder/graphql-types/input/{big-int-filter.type.ts => big-int-filter.input-type.ts} (81%) rename server/src/tenant/schema-builder/graphql-types/input/{date-filter.type.ts => date-filter.input-type.ts} (79%) rename server/src/tenant/schema-builder/graphql-types/input/{date-time-filter.type.ts => date-time-filter.input-type.ts} (79%) delete mode 100644 server/src/tenant/schema-builder/graphql-types/input/filter-is-enum-filter.type.ts rename server/src/tenant/schema-builder/graphql-types/input/{float-filter.type.ts => float-filter.input-type.ts} (72%) create mode 100644 server/src/tenant/schema-builder/graphql-types/input/index.ts rename server/src/tenant/schema-builder/graphql-types/input/{int-filter.type.ts => int-filter.input-type.ts} (71%) delete mode 100644 server/src/tenant/schema-builder/graphql-types/input/money-filter.type.ts rename server/src/tenant/schema-builder/graphql-types/input/{string-filter.type.ts => string-filter.input-type.ts} (86%) rename server/src/tenant/schema-builder/graphql-types/input/{time-filter.type.ts => time-filter.input-type.ts} (71%) delete mode 100644 server/src/tenant/schema-builder/graphql-types/input/url-filter.type.ts rename server/src/tenant/schema-builder/graphql-types/input/{uuid-filter.type.ts => uuid-filter.input-type.ts} (72%) create mode 100644 server/src/tenant/schema-builder/graphql-types/object/index.ts rename server/src/tenant/schema-builder/graphql-types/object/{page-into.type.ts => page-into.object-type.ts} (100%) create mode 100644 server/src/tenant/schema-builder/interfaces/build-schema-optionts.interface.ts create mode 100644 server/src/tenant/schema-builder/interfaces/field-metadata.interface.ts create mode 100644 server/src/tenant/schema-builder/interfaces/object-metadata.interface.ts create mode 100644 server/src/tenant/schema-builder/interfaces/param-metadata.interface.ts create mode 100644 server/src/tenant/schema-builder/object-definitions/money.object-definition.ts create mode 100644 server/src/tenant/schema-builder/object-definitions/url.object-definition.ts delete mode 100644 server/src/tenant/schema-builder/schema-builder.service.spec.ts delete mode 100644 server/src/tenant/schema-builder/schema-builder.service.ts create mode 100644 server/src/tenant/schema-builder/services/type-mapper.service.ts create mode 100644 server/src/tenant/schema-builder/storages/type-definitions.storage.ts create mode 100644 server/src/tenant/schema-builder/type-definitions.generator.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-connection-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-create-input-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-edge-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-filter-input-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-object-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-order-by-input-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/generate-udpate-input-type.spec.ts create mode 100644 server/src/tenant/schema-builder/utils/__tests__/get-field-metadata-type.spec.ts create mode 100644 server/src/tenant/schema-builder/utils/__tests__/get-resolver-args.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-filter-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-graphql-type.spec.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-connection-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-edge-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-filter-input-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-object-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-order-by-input-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts create mode 100644 server/src/tenant/schema-builder/utils/get-field-metadata-type.util.ts create mode 100644 server/src/tenant/schema-builder/utils/get-resolver-args.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/map-column-type-to-filter-type.util.ts delete mode 100644 server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts create mode 100644 server/src/tenant/utils/__tests__/get-resolver-name.spec.ts create mode 100644 server/src/tenant/utils/get-resolver-name.util.ts diff --git a/front/src/generated-metadata/graphql.ts b/front/src/generated-metadata/graphql.ts index f0b706457..8b25c5a11 100644 --- a/front/src/generated-metadata/graphql.ts +++ b/front/src/generated-metadata/graphql.ts @@ -174,7 +174,7 @@ export type CreateFieldInput = { label: Scalars['String']['input']; name: Scalars['String']['input']; objectId: Scalars['String']['input']; - type: Scalars['String']['input']; + type: FieldMetadataType; }; export type CreateObjectInput = { @@ -424,10 +424,24 @@ export type FieldDeleteResponse = { name?: Maybe; /** @deprecated Use label name instead */ placeholder?: Maybe; - type?: Maybe; + type?: Maybe; updatedAt?: Maybe; }; +/** Type of the field */ +export enum FieldMetadataType { + Boolean = 'BOOLEAN', + Date = 'DATE', + Email = 'EMAIL', + Enum = 'ENUM', + Money = 'MONEY', + Number = 'NUMBER', + Phone = 'PHONE', + Text = 'TEXT', + Url = 'URL', + Uuid = 'UUID' +} + export type Mutation = { __typename?: 'Mutation'; createOneField: Field; @@ -764,7 +778,7 @@ export type Field = { name: Scalars['String']['output']; /** @deprecated Use label name instead */ placeholder?: Maybe; - type: Scalars['String']['output']; + type: FieldMetadataType; updatedAt: Scalars['DateTime']['output']; }; @@ -818,7 +832,7 @@ export type CreateOneMetadataFieldMutationVariables = Exact<{ }>; -export type CreateOneMetadataFieldMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'field', id: string, type: string, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }; +export type CreateOneMetadataFieldMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }; export type UpdateOneMetadataFieldMutationVariables = Exact<{ idToUpdate: Scalars['ID']['input']; @@ -826,7 +840,7 @@ export type UpdateOneMetadataFieldMutationVariables = Exact<{ }>; -export type UpdateOneMetadataFieldMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'field', id: string, type: string, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }; +export type UpdateOneMetadataFieldMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }; export type UpdateOneObjectMetadataItemMutationVariables = Exact<{ idToUpdate: Scalars['ID']['input']; @@ -848,12 +862,12 @@ export type DeleteOneMetadataFieldMutationVariables = Exact<{ }>; -export type DeleteOneMetadataFieldMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'FieldDeleteResponse', id?: string | null, type?: string | null, name?: string | null, label?: string | null, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt?: any | null, updatedAt?: any | null } }; +export type DeleteOneMetadataFieldMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'FieldDeleteResponse', id?: string | null, type?: FieldMetadataType | null, name?: string | null, label?: string | null, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt?: any | null, updatedAt?: any | null } }; export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>; -export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', totalCount: number, edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isActive: boolean, createdAt: any, updatedAt: any, fields: { __typename?: 'ObjectFieldsConnection', totalCount: number, edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: string, type: string, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', totalCount: number, edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: string, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isActive: boolean, createdAt: any, updatedAt: any, fields: { __typename?: 'ObjectFieldsConnection', totalCount: number, edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode; diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 268e0a677..54e61db1e 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1309,10 +1309,24 @@ export type FieldDeleteResponse = { name?: Maybe; /** @deprecated Use label name instead */ placeholder?: Maybe; - type?: Maybe; + type?: Maybe; updatedAt?: Maybe; }; +/** Type of the field */ +export enum FieldMetadataType { + Boolean = 'BOOLEAN', + Date = 'DATE', + Email = 'EMAIL', + Enum = 'ENUM', + Money = 'MONEY', + Number = 'NUMBER', + Phone = 'PHONE', + Text = 'TEXT', + Url = 'URL', + Uuid = 'UUID' +} + export enum FileFolder { Attachment = 'Attachment', PersonPicture = 'PersonPicture', @@ -1377,7 +1391,6 @@ export type Mutation = { createOneApiKey: ApiKeyToken; createOneComment: Comment; createOneCompany: Company; - createOneField: Field; createOneObject: Object; createOnePerson: Person; createOnePipelineProgress: PipelineProgress; @@ -1389,7 +1402,6 @@ export type Mutation = { deleteManyCompany: AffectedRows; deleteManyPerson: AffectedRows; deleteManyPipelineProgress: AffectedRows; - deleteOneField: FieldDeleteResponse; deleteOneObject: ObjectDeleteResponse; deleteOnePipelineStage: PipelineStage; deleteOneWebHook: WebHook; @@ -1402,7 +1414,6 @@ export type Mutation = { updateOneActivity: Activity; updateOneCompany?: Maybe; updateOneFavorites: Favorite; - updateOneField: Field; updateOneObject: Object; updateOnePerson?: Maybe; updateOnePipelineProgress?: Maybe; @@ -2393,8 +2404,6 @@ export type Query = { clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; - field: Field; - fields: FieldConnection; findFavorites: Array; findManyActivities: Array; findManyApiKey: Array; @@ -3093,7 +3102,7 @@ export type Field = { name: Scalars['String']; /** @deprecated Use label name instead */ placeholder?: Maybe; - type: Scalars['String']; + type: FieldMetadataType; updatedAt: Scalars['DateTime']; }; diff --git a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx index f0aa470f5..bf2d4d756 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityAssigneeEditableField.tsx @@ -26,7 +26,7 @@ export const ActivityAssigneeEditableField = ({ fieldId: 'assignee', label: 'Assignee', Icon: IconUserCircle, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'assignee', relationType: Entity.User, diff --git a/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx b/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx index 8b5193431..21a59757c 100644 --- a/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx +++ b/front/src/modules/activities/editable-fields/components/ActivityEditorDateField.tsx @@ -24,7 +24,7 @@ export const ActivityEditorDateField = ({ fieldId: 'activityDueAt', label: 'Due date', Icon: IconCalendar, - type: 'date', + type: 'DATE', metadata: { fieldName: 'dueAt', }, diff --git a/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts index d3541e369..c31af5e60 100644 --- a/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts +++ b/front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts @@ -23,7 +23,7 @@ export const useCurrentUserTaskCount = () => { displayValue: currentUser.displayName, displayAvatarUrl: currentUser.avatarUrl ?? undefined, definition: { - type: 'entity', + type: 'ENTITY', }, }) : {}), diff --git a/front/src/modules/companies/constants/companiesAvailableFieldDefinitions.tsx b/front/src/modules/companies/constants/companiesAvailableFieldDefinitions.tsx index acac7df1f..24d1bac75 100644 --- a/front/src/modules/companies/constants/companiesAvailableFieldDefinitions.tsx +++ b/front/src/modules/companies/constants/companiesAvailableFieldDefinitions.tsx @@ -33,7 +33,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconBuildingSkyscraper, size: 180, position: 0, - type: 'chip', + type: 'CHIP', metadata: { urlFieldName: 'domainName', contentFieldName: 'name', @@ -50,7 +50,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconLink, size: 100, position: 1, - type: 'url', + type: 'URL', metadata: { fieldName: 'domainName', placeHolder: 'example.com', @@ -65,7 +65,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconUserCircle, size: 150, position: 2, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'accountOwner', relationType: Entity.User, @@ -87,7 +87,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconCalendarEvent, size: 150, position: 3, - type: 'date', + type: 'DATE', metadata: { fieldName: 'createdAt', }, @@ -100,7 +100,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconUsers, size: 150, position: 4, - type: 'number', + type: 'NUMBER', metadata: { fieldName: 'employees', isPositive: true, @@ -115,7 +115,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconBrandLinkedin, size: 170, position: 5, - type: 'url', + type: 'URL', metadata: { fieldName: 'linkedinUrl', placeHolder: 'LinkedIn URL', @@ -129,7 +129,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconMap, size: 170, position: 6, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'address', placeHolder: 'Addre​ss', // Hack: Fake character to prevent password-manager from filling the field @@ -143,7 +143,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconTarget, size: 150, position: 7, - type: 'boolean', + type: 'BOOLEAN', metadata: { fieldName: 'idealCustomerProfile', }, @@ -157,7 +157,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconMoneybag, size: 150, position: 8, - type: 'moneyAmount', + type: 'MONEY_AMOUNT', metadata: { fieldName: 'annualRecurringRevenue', placeHolder: 'ARR', @@ -171,7 +171,7 @@ export const companiesAvailableFieldDefinitions: ColumnDefinition Icon: IconBrandX, size: 150, position: 9, - type: 'url', + type: 'URL', metadata: { fieldName: 'xUrl', placeHolder: 'X', @@ -189,7 +189,7 @@ export const suppliersAvailableColumnDefinitions: ColumnDefinition & { + type: FieldType; +}; export const useCreateOneMetadataField = () => { const apolloMetadataClient = useApolloMetadataClient(); @@ -33,6 +36,7 @@ export const useCreateOneMetadataField = () => { input: { field: { ...input, + type: input.type as FieldMetadataType, // Todo improve typing once we have aligned backend and frontend }, }, }, diff --git a/front/src/modules/metadata/hooks/useSeedCustomObjectsTemp.ts b/front/src/modules/metadata/hooks/useSeedCustomObjectsTemp.ts deleted file mode 100644 index 916626ebf..000000000 --- a/front/src/modules/metadata/hooks/useSeedCustomObjectsTemp.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; - -import { useCreateOneMetadataField } from './useCreateOneMetadataField'; -import { useCreateOneObjectMetadataItem } from './useCreateOneObjectMetadataItem'; - -export const useSeedCustomObjectsTemp = () => { - const { createOneObjectMetadataItem } = useCreateOneObjectMetadataItem(); - const { createOneMetadataField } = useCreateOneMetadataField(); - - return async () => { - const { data: createdObjectMetadataItem, errors } = - await createOneObjectMetadataItem({ - labelPlural: 'Suppliers', - labelSingular: 'Supplier', - nameSingular: 'supplier', - namePlural: 'suppliers', - description: 'Suppliers', - icon: 'IconBuilding', - }); - - if (!isNonEmptyArray(errors)) { - const supplierObjectId = - createdObjectMetadataItem?.createOneObject?.id ?? ''; - - await createOneMetadataField({ - objectId: supplierObjectId, - name: 'name', - type: 'text', - description: 'Name', - label: 'Name', - icon: 'IconBuilding', - }); - - await createOneMetadataField({ - objectId: supplierObjectId, - label: 'City', - name: 'city', - type: 'text', - description: 'City', - icon: 'IconMap', - }); - } - }; -}; diff --git a/front/src/modules/metadata/utils/formatMetadataFieldAsFilterDefinition.ts b/front/src/modules/metadata/utils/formatMetadataFieldAsFilterDefinition.ts index 62257a282..016fb1016 100644 --- a/front/src/modules/metadata/utils/formatMetadataFieldAsFilterDefinition.ts +++ b/front/src/modules/metadata/utils/formatMetadataFieldAsFilterDefinition.ts @@ -12,5 +12,5 @@ export const formatMetadataFieldAsFilterDefinition = ({ fieldId: field.id, label: field.label, Icon: icons[field.icon ?? 'Icon123'], - type: 'text', + type: 'TEXT', }); diff --git a/front/src/modules/metadata/utils/generateCreateOneObjectMutation.ts b/front/src/modules/metadata/utils/generateCreateOneObjectMutation.ts index a9b526787..630910215 100644 --- a/front/src/modules/metadata/utils/generateCreateOneObjectMutation.ts +++ b/front/src/modules/metadata/utils/generateCreateOneObjectMutation.ts @@ -13,7 +13,7 @@ export const generateCreateOneObjectMutation = ({ return gql` mutation CreateOne${capitalizedObjectName}($input: ${capitalizedObjectName}CreateInput!) { - createOne${capitalizedObjectName}(data: $input) { + create${capitalizedObjectName}(data: $input) { id } } diff --git a/front/src/modules/metadata/utils/generateDeleteOneObjectMutation.ts b/front/src/modules/metadata/utils/generateDeleteOneObjectMutation.ts index 22fe348b8..b10a7a2a6 100644 --- a/front/src/modules/metadata/utils/generateDeleteOneObjectMutation.ts +++ b/front/src/modules/metadata/utils/generateDeleteOneObjectMutation.ts @@ -13,7 +13,7 @@ export const generateDeleteOneObjectMutation = ({ return gql` mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { - deleteOne${capitalizedObjectName}(id: $idToDelete) { + delete${capitalizedObjectName}(id: $idToDelete) { id } } diff --git a/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts b/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts index 9abd7afa9..8dd975668 100644 --- a/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts +++ b/front/src/modules/metadata/utils/generateFindManyCustomObjectsQuery.ts @@ -18,7 +18,7 @@ export const generateFindManyCustomObjectsQuery = ({ objectMetadataItem.nameSingular, )}FilterInput, $orderBy: ${capitalize( objectMetadataItem.nameSingular, - )}OrderBy) { + )}OrderByInput) { ${objectMetadataItem.namePlural}(filter: $filter, orderBy: $orderBy){ edges { node { diff --git a/front/src/modules/metadata/utils/generateUpdateOneObjectMutation.ts b/front/src/modules/metadata/utils/generateUpdateOneObjectMutation.ts index a31904a1d..a2c856ae8 100644 --- a/front/src/modules/metadata/utils/generateUpdateOneObjectMutation.ts +++ b/front/src/modules/metadata/utils/generateUpdateOneObjectMutation.ts @@ -13,7 +13,7 @@ export const generateUpdateOneObjectMutation = ({ return gql` mutation UpdateOne${capitalizedObjectName}($idToUpdate: ID!, $input: ${capitalizedObjectName}UpdateInput!) { - updateOne${capitalizedObjectName}(id: $idToUpdate, data: $input) { + update${capitalizedObjectName}(id: $idToUpdate, data: $input) { id } } diff --git a/front/src/modules/metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/front/src/modules/metadata/utils/mapFieldMetadataToGraphQLQuery.ts index b2566a112..e8eddf449 100644 --- a/front/src/modules/metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/front/src/modules/metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -7,20 +7,20 @@ export const mapFieldMetadataToGraphQLQuery = (field: Field) => { const fieldIsSimpleValue = ( [ - 'text', - 'phone', - 'date', - 'email', - 'number', - 'boolean', - 'date', + 'TEXT', + 'PHONE', + 'DATE', + 'EMAIL', + 'NUMBER', + 'BOOLEAN', + 'DATE', ] as FieldType[] ).includes(fieldType); - const fieldIsURL = fieldType === 'url' || fieldType === 'urlV2'; + const fieldIsURL = fieldType === 'URL' || fieldType === 'URL_V2'; const fieldIsMoneyAmount = - fieldType === 'money' || fieldType === 'moneyAmountV2'; + fieldType === 'MONEY' || fieldType === 'MONEY_AMOUNT_V2'; if (fieldIsSimpleValue) { return field.name; diff --git a/front/src/modules/metadata/utils/parseFieldType.ts b/front/src/modules/metadata/utils/parseFieldType.ts index 82f0cad82..f50c3038e 100644 --- a/front/src/modules/metadata/utils/parseFieldType.ts +++ b/front/src/modules/metadata/utils/parseFieldType.ts @@ -2,11 +2,11 @@ import { FieldType } from '@/ui/object/field/types/FieldType'; export const parseFieldType = (fieldType: string): FieldType => { if (fieldType === 'url') { - return 'urlV2'; + return 'URL_V2'; } if (fieldType === 'money') { - return 'moneyAmountV2'; + return 'MONEY_AMOUNT_V2'; } return fieldType as FieldType; diff --git a/front/src/modules/people/constants/peopleAvailableFieldDefinitions.tsx b/front/src/modules/people/constants/peopleAvailableFieldDefinitions.tsx index f62155ca8..ad82e7914 100644 --- a/front/src/modules/people/constants/peopleAvailableFieldDefinitions.tsx +++ b/front/src/modules/people/constants/peopleAvailableFieldDefinitions.tsx @@ -32,7 +32,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconUser, size: 210, position: 0, - type: 'double-text-chip', + type: 'DOUBLE_TEXT_CHIP', metadata: { firstValueFieldName: 'firstName', secondValueFieldName: 'lastName', @@ -49,7 +49,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] label: 'Email', Icon: IconMail, size: 150, - type: 'email', + type: 'EMAIL', position: 1, metadata: { fieldName: 'email', @@ -63,7 +63,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconBuildingSkyscraper, size: 150, position: 2, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'company', relationType: Entity.Company, @@ -83,7 +83,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconPhone, size: 150, position: 3, - type: 'phone', + type: 'PHONE', metadata: { fieldName: 'phone', placeHolder: 'Phon​e', // Hack: Fake character to prevent password-manager from filling the field @@ -96,7 +96,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconCalendarEvent, size: 150, position: 4, - type: 'date', + type: 'DATE', metadata: { fieldName: 'createdAt', }, @@ -108,7 +108,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconMap, size: 150, position: 5, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'city', placeHolder: 'Cit​y', // Hack: Fake character to prevent password-manager from filling the field @@ -121,7 +121,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconBriefcase, size: 150, position: 6, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'jobTitle', placeHolder: 'Job title', @@ -134,7 +134,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconBrandLinkedin, size: 150, position: 7, - type: 'url', + type: 'URL', metadata: { fieldName: 'linkedinUrl', placeHolder: 'LinkedIn', @@ -147,7 +147,7 @@ export const peopleAvailableFieldDefinitions: ColumnDefinition[] Icon: IconBrandX, size: 150, position: 8, - type: 'url', + type: 'URL', metadata: { fieldName: 'xUrl', placeHolder: 'X', diff --git a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx index b0d4806e4..c29caff32 100644 --- a/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx +++ b/front/src/modules/pipeline/constants/pipelineAvailableFieldDefinitions.tsx @@ -22,7 +22,7 @@ export const pipelineAvailableFieldDefinitions: ColumnDefinition[ label: 'Close Date', Icon: IconCalendarEvent, position: 0, - type: 'date', + type: 'DATE', metadata: { fieldName: 'closeDate', }, @@ -36,7 +36,7 @@ export const pipelineAvailableFieldDefinitions: ColumnDefinition[ label: 'Amount', Icon: IconCurrencyDollar, position: 1, - type: 'number', + type: 'NUMBER', metadata: { fieldName: 'amount', placeHolder: '0', @@ -50,7 +50,7 @@ export const pipelineAvailableFieldDefinitions: ColumnDefinition[ label: 'Probability', Icon: IconProgressCheck, position: 2, - type: 'probability', + type: 'PROBABILITY', metadata: { fieldName: 'probability', }, @@ -64,7 +64,7 @@ export const pipelineAvailableFieldDefinitions: ColumnDefinition[ label: 'Point of Contact', Icon: IconUser, position: 3, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'pointOfContact', relationType: Entity.Person, diff --git a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx index 258e1ea89..58db2b820 100644 --- a/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx +++ b/front/src/modules/settings/data-model/components/SettingsObjectFieldTypeSelectSection.tsx @@ -12,7 +12,7 @@ type SettingsObjectFieldTypeSelectSectionProps = { }; // TODO: remove "relation" type for now, add it back when the backend is ready. -const { relation: _, ...dataTypesWithoutRelation } = dataTypes; +const { RELATION: _, ...dataTypesWithoutRelation } = dataTypes; export const SettingsObjectFieldTypeSelectSection = ({ disabled, diff --git a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx index 477c1b41c..83c504d3a 100644 --- a/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx +++ b/front/src/modules/settings/data-model/components/__stories__/SettingsObjectFieldTypeSelectSection.stories.tsx @@ -9,7 +9,7 @@ const meta: Meta = { title: 'Modules/Settings/DataModel/SettingsObjectFieldTypeSelectSection', component: SettingsObjectFieldTypeSelectSection, decorators: [ComponentDecorator], - args: { type: 'number' }, + args: { type: 'NUMBER' }, }; export default meta; diff --git a/front/src/modules/settings/data-model/constants/dataTypes.ts b/front/src/modules/settings/data-model/constants/dataTypes.ts index 66a3ca28c..ed8c17374 100644 --- a/front/src/modules/settings/data-model/constants/dataTypes.ts +++ b/front/src/modules/settings/data-model/constants/dataTypes.ts @@ -13,9 +13,9 @@ export const dataTypes: Record< MetadataFieldDataType, { label: string; Icon: IconComponent } > = { - number: { label: 'Number', Icon: IconNumbers }, - text: { label: 'Text', Icon: IconTextSize }, - url: { label: 'Link', Icon: IconLink }, - boolean: { label: 'True/False', Icon: IconCheck }, - relation: { label: 'Relation', Icon: IconPlug }, + NUMBER: { label: 'Number', Icon: IconNumbers }, + TEXT: { label: 'Text', Icon: IconTextSize }, + URL: { label: 'Link', Icon: IconLink }, + BOOLEAN: { label: 'True/False', Icon: IconCheck }, + RELATION: { label: 'Relation', Icon: IconPlug }, }; diff --git a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx index 419e013a0..c897f56ef 100644 --- a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx +++ b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx @@ -15,7 +15,7 @@ const StyledDataType = styled.div<{ value: MetadataFieldDataType }>` padding: 0 ${({ theme }) => theme.spacing(2)}; ${({ theme, value }) => - value === 'relation' + value === 'RELATION' ? css` border-color: ${theme.color.purple20}; color: ${theme.color.purple}; diff --git a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 049328b2e..27a4059f0 100644 --- a/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -39,10 +39,10 @@ export const SettingsObjectFieldItemTableRow = ({ // TODO: parse with zod and merge types with FieldType (create a subset of FieldType for example) const fieldDataTypeIsSupported = [ - 'text', - 'number', - 'boolean', - 'url', + 'TEXT', + 'NUMBER', + 'BOOLEAN', + 'URL', ].includes(fieldItem.type); if (!fieldDataTypeIsSupported) { diff --git a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts index 64a91aa3e..a40052a4a 100644 --- a/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts +++ b/front/src/modules/settings/data-model/types/ObjectFieldDataType.ts @@ -1,6 +1,6 @@ export type MetadataFieldDataType = - | 'boolean' - | 'number' - | 'relation' - | 'text' - | 'url'; + | 'BOOLEAN' + | 'NUMBER' + | 'RELATION' + | 'TEXT' + | 'URL'; diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx index d2019a4a4..8fcca38a5 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DateFieldDisplay.stories.tsx @@ -33,7 +33,7 @@ const DateFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'date', label: 'Date', - type: 'date', + type: 'DATE', metadata: { fieldName: 'Date', }, diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx index 80de96485..36d329d8a 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/DoubleTextFieldDisplay.stories.tsx @@ -40,7 +40,7 @@ const DoubleTextFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'double-text', label: 'Double-Text', - type: 'double-text', + type: 'DOUBLE_TEXT', metadata: { firstValueFieldName: 'First-text', firstValuePlaceholder: 'First-text', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx index ce5cf4bf2..2f56f62bd 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/EmailFieldDisplay.stories.tsx @@ -32,7 +32,7 @@ const EmailFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'email', label: 'Email', - type: 'email', + type: 'EMAIL', metadata: { fieldName: 'Email', placeHolder: 'Email', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx index 1f8700d9e..e84dcb4a8 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/MoneyFieldDisplay.stories.tsx @@ -34,7 +34,7 @@ const MoneyFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'money', label: 'Money', - type: 'moneyAmount', + type: 'MONEY_AMOUNT', metadata: { fieldName: 'Amount', placeHolder: 'Amount', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx index a5afb3302..fe4f1b592 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/NumberFieldDisplay.stories.tsx @@ -34,7 +34,7 @@ const NumberFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'number', label: 'Number', - type: 'number', + type: 'NUMBER', metadata: { fieldName: 'Number', placeHolder: 'Number', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx index 5ddffc4dd..3e28bce46 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/PhoneFieldDisplay.stories.tsx @@ -32,7 +32,7 @@ const PhoneFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'phone', label: 'Phone', - type: 'phone', + type: 'PHONE', metadata: { fieldName: 'Phone', placeHolder: 'Phone', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx index a4a1ae4d5..0fdc13488 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/TextFieldDisplay.stories.tsx @@ -34,7 +34,7 @@ const TextFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'text', label: 'Text', - type: 'text', + type: 'TEXT', metadata: { fieldName: 'Text', placeHolder: 'Text', diff --git a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx index 83071da66..9a5cd9d50 100644 --- a/front/src/modules/ui/object/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/display/components/__stories__/URLFieldDisplay.stories.tsx @@ -32,7 +32,7 @@ const URLFieldDisplayWithContext = ({ fieldDefinition={{ fieldId: 'URL', label: 'URL', - type: 'url', + type: 'URL', metadata: { fieldName: 'URL', placeHolder: 'URL', diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useBooleanField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useBooleanField.ts index b9f130975..4298dbc2a 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useBooleanField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useBooleanField.ts @@ -9,7 +9,7 @@ import { isFieldBoolean } from '../../types/guards/isFieldBoolean'; export const useBooleanField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('boolean', isFieldBoolean, fieldDefinition); + assertFieldMetadata('BOOLEAN', isFieldBoolean, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useChipField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useChipField.ts index 616c7b7d0..79b25015d 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useChipField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useChipField.ts @@ -10,7 +10,7 @@ import { isFieldChip } from '../../types/guards/isFieldChip'; export const useChipField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('chip', isFieldChip, fieldDefinition); + assertFieldMetadata('CHIP', isFieldChip, fieldDefinition); const contentFieldName = fieldDefinition.metadata.contentFieldName; const avatarUrlFieldName = fieldDefinition.metadata.urlFieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useDateField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useDateField.ts index c47a0a700..cd4deca74 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useDateField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useDateField.ts @@ -9,7 +9,7 @@ import { isFieldDate } from '../../types/guards/isFieldDate'; export const useDateField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('date', isFieldDate, fieldDefinition); + assertFieldMetadata('DATE', isFieldDate, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextChipField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextChipField.ts index 42c789bf9..cb7571c6e 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextChipField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextChipField.ts @@ -11,7 +11,7 @@ export const useDoubleTextChipField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); assertFieldMetadata( - 'double-text-chip', + 'DOUBLE_TEXT_CHIP', isFieldDoubleTextChip, fieldDefinition, ); diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextField.ts index d60a6f041..965950dae 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useDoubleTextField.ts @@ -10,7 +10,7 @@ import { isFieldDoubleText } from '../../types/guards/isFieldDoubleText'; export const useDoubleTextField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('double-text', isFieldDoubleText, fieldDefinition); + assertFieldMetadata('DOUBLE_TEXT', isFieldDoubleText, fieldDefinition); const [firstValue, setFirstValue] = useRecoilState( entityFieldsFamilySelector({ diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useEmailField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useEmailField.ts index 6dbaccfad..b0b8793d9 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useEmailField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useEmailField.ts @@ -10,7 +10,7 @@ import { isFieldEmail } from '../../types/guards/isFieldEmail'; export const useEmailField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('email', isFieldEmail, fieldDefinition); + assertFieldMetadata('EMAIL', isFieldEmail, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useMoneyAmountV2Field.ts b/front/src/modules/ui/object/field/meta-types/hooks/useMoneyAmountV2Field.ts index d41816af9..a1240a982 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useMoneyAmountV2Field.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useMoneyAmountV2Field.ts @@ -13,7 +13,7 @@ import { isFieldMoneyAmountV2Value } from '../../types/guards/isFieldMoneyAmount export const useMoneyAmountV2Field = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('moneyAmountV2', isFieldMoneyAmountV2, fieldDefinition); + assertFieldMetadata('MONEY_AMOUNT_V2', isFieldMoneyAmountV2, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useMoneyField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useMoneyField.ts index 865ee1e76..4baa9cd27 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useMoneyField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useMoneyField.ts @@ -16,7 +16,7 @@ import { isFieldMoney } from '../../types/guards/isFieldMoney'; export const useMoneyField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('moneyAmount', isFieldMoney, fieldDefinition); + assertFieldMetadata('MONEY_AMOUNT', isFieldMoney, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useNumberField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useNumberField.ts index 6da233554..d7c31165d 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useNumberField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useNumberField.ts @@ -16,7 +16,7 @@ import { isFieldNumber } from '../../types/guards/isFieldNumber'; export const useNumberField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('number', isFieldNumber, fieldDefinition); + assertFieldMetadata('NUMBER', isFieldNumber, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/usePhoneField.ts b/front/src/modules/ui/object/field/meta-types/hooks/usePhoneField.ts index 7c69ebce0..231b8de7d 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/usePhoneField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/usePhoneField.ts @@ -12,7 +12,7 @@ import { isFieldPhone } from '../../types/guards/isFieldPhone'; export const usePhoneField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('phone', isFieldPhone, fieldDefinition); + assertFieldMetadata('PHONE', isFieldPhone, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts index 54a088497..985922c32 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useProbabilityField.ts @@ -9,7 +9,7 @@ import { isFieldProbability } from '../../types/guards/isFieldProbability'; export const useProbabilityField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('probability', isFieldProbability, fieldDefinition); + assertFieldMetadata('PROBABILITY', isFieldProbability, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useRelationField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useRelationField.ts index 45ba5acbd..a48f60a26 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useRelationField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useRelationField.ts @@ -11,7 +11,7 @@ import { isFieldRelation } from '../../types/guards/isFieldRelation'; export const useRelationField = () => { const { entityId, fieldDefinition } = useContext(FieldContext); - assertFieldMetadata('relation', isFieldRelation, fieldDefinition); + assertFieldMetadata('RELATION', isFieldRelation, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts index 11968e8fc..dc8675517 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useTextField.ts @@ -10,7 +10,7 @@ import { isFieldText } from '../../types/guards/isFieldText'; export const useTextField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('text', isFieldText, fieldDefinition); + assertFieldMetadata('TEXT', isFieldText, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useURLField.ts b/front/src/modules/ui/object/field/meta-types/hooks/useURLField.ts index fb3de1a83..1c738e639 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useURLField.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useURLField.ts @@ -13,7 +13,7 @@ import { isFieldURL } from '../../types/guards/isFieldURL'; export const useURLField = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('url', isFieldURL, fieldDefinition); + assertFieldMetadata('URL', isFieldURL, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/hooks/useURLV2Field.ts b/front/src/modules/ui/object/field/meta-types/hooks/useURLV2Field.ts index 9ea08fc31..47072d18d 100644 --- a/front/src/modules/ui/object/field/meta-types/hooks/useURLV2Field.ts +++ b/front/src/modules/ui/object/field/meta-types/hooks/useURLV2Field.ts @@ -13,7 +13,7 @@ import { isFieldURLV2Value } from '../../types/guards/isFieldURLV2Value'; export const useURLV2Field = () => { const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext); - assertFieldMetadata('urlV2', isFieldURLV2, fieldDefinition); + assertFieldMetadata('URL_V2', isFieldURLV2, fieldDefinition); const fieldName = fieldDefinition.metadata.fieldName; diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx index 686fc7d88..9b9df6376 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -36,7 +36,7 @@ const BooleanFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'boolean', label: 'Boolean', - type: 'boolean', + type: 'BOOLEAN', metadata: { fieldName: 'Boolean', }, diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx index 51f157ab0..5451eb569 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ChipFieldInput.stories.tsx @@ -46,7 +46,7 @@ const ChipFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'chip', label: 'Chip', - type: 'chip', + type: 'CHIP', metadata: { contentFieldName: 'name', urlFieldName: 'xURL', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx index 8ee7665a3..d8a491762 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DateFieldInput.stories.tsx @@ -46,7 +46,7 @@ const DateFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'date', label: 'Date', - type: 'date', + type: 'DATE', metadata: { fieldName: 'Date', }, diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx index edaeedb0e..872ed48c3 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextChipFieldInput.stories.tsx @@ -59,7 +59,7 @@ const DoubleTextChipFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'double-text-chip', label: 'Double-Text-Chip', - type: 'double-text-chip', + type: 'DOUBLE_TEXT_CHIP', metadata: { firstValueFieldName: 'First-text', firstValuePlaceholder: 'First-text', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx index 8e79b893e..75732b38f 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/DoubleTextFieldInput.stories.tsx @@ -57,7 +57,7 @@ const DoubleTextFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'double-text', label: 'Double-Text', - type: 'double-text', + type: 'DOUBLE_TEXT', metadata: { firstValueFieldName: 'First-text', firstValuePlaceholder: 'First-text', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx index eb59b0b67..3688ef7cc 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/EmailFieldInput.stories.tsx @@ -45,7 +45,7 @@ const EmailFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'email', label: 'Email', - type: 'email', + type: 'EMAIL', metadata: { fieldName: 'email', placeHolder: 'username@email.com', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx index 177243f8b..94a382123 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/MoneyFieldInput.stories.tsx @@ -45,7 +45,7 @@ const MoneyFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'moneyAmount', label: 'MoneyAmout', - type: 'moneyAmount', + type: 'MONEY_AMOUNT', metadata: { fieldName: 'moneyAmount', placeHolder: 'Enter Amount', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 1d47ef863..f9a231a38 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -45,7 +45,7 @@ const NumberFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'number', label: 'Number', - type: 'number', + type: 'NUMBER', metadata: { fieldName: 'number', placeHolder: 'Enter number', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx index 2485eb7fb..71b5931e0 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/PhoneFieldInput.stories.tsx @@ -45,7 +45,7 @@ const PhoneFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'phone', label: 'Phone', - type: 'phone', + type: 'PHONE', metadata: { fieldName: 'Phone', placeHolder: 'Enter phone number', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx index 64a8e53cf..905aee2f7 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/ProbabilityFieldInput.stories.tsx @@ -43,7 +43,7 @@ const ProbabilityFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'probability', label: 'Probability', - type: 'probability', + type: 'PROBABILITY', metadata: { fieldName: 'Probability', }, diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx index 04a24b893..7cc64c869 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -48,7 +48,7 @@ const RelationFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'relation', label: 'Relation', - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'Relation', relationType: Entity.Person, diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx index e98f96226..759852f27 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -45,7 +45,7 @@ const TextFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'text', label: 'Text', - type: 'text', + type: 'TEXT', metadata: { fieldName: 'Text', placeHolder: 'Enter text', diff --git a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx index 04bc51c6a..629d262a9 100644 --- a/front/src/modules/ui/object/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx +++ b/front/src/modules/ui/object/field/meta-types/input/components/__stories__/URLFieldInput.stories.tsx @@ -45,7 +45,7 @@ const URLFieldInputWithContext = ({ fieldDefinition={{ fieldId: 'url', label: 'URL', - type: 'url', + type: 'URL', metadata: { fieldName: 'URL', placeHolder: 'Enter URL', diff --git a/front/src/modules/ui/object/field/types/FieldType.ts b/front/src/modules/ui/object/field/types/FieldType.ts index f3bf440ab..431d8367f 100644 --- a/front/src/modules/ui/object/field/types/FieldType.ts +++ b/front/src/modules/ui/object/field/types/FieldType.ts @@ -1,17 +1,17 @@ export type FieldType = - | 'text' - | 'relation' - | 'chip' - | 'double-text-chip' - | 'double-text' - | 'number' - | 'email' - | 'boolean' - | 'date' - | 'phone' - | 'url' - | 'urlV2' - | 'probability' - | 'moneyAmountV2' - | 'moneyAmount' - | 'money'; + | 'TEXT' + | 'RELATION' + | 'CHIP' + | 'DOUBLE_TEXT_CHIP' + | 'DOUBLE_TEXT' + | 'NUMBER' + | 'EMAIL' + | 'BOOLEAN' + | 'DATE' + | 'PHONE' + | 'URL' + | 'URL_V2' + | 'PROBABILITY' + | 'MONEY_AMOUNT_V2' + | 'MONEY_AMOUNT' + | 'MONEY'; diff --git a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts index 75e2a5f82..dc094b207 100644 --- a/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts +++ b/front/src/modules/ui/object/field/types/guards/assertFieldMetadata.ts @@ -21,35 +21,35 @@ import { FieldType } from '../FieldType'; type AssertFieldMetadataFunction = < E extends FieldType, - T extends E extends 'text' + T extends E extends 'TEXT' ? FieldTextMetadata - : E extends 'relation' + : E extends 'RELATION' ? FieldRelationMetadata - : E extends 'chip' + : E extends 'CHIP' ? FieldChipMetadata - : E extends 'double-text-chip' + : E extends 'DOUBLE_TEXT_CHIP' ? FieldDoubleTextChipMetadata - : E extends 'double-text' + : E extends 'DOUBLE_TEXT' ? FieldDoubleTextMetadata - : E extends 'number' + : E extends 'NUMBER' ? FieldNumberMetadata - : E extends 'email' + : E extends 'EMAIL' ? FieldEmailMetadata - : E extends 'boolean' + : E extends 'BOOLEAN' ? FieldBooleanMetadata - : E extends 'date' + : E extends 'DATE' ? FieldDateMetadata - : E extends 'phone' + : E extends 'PHONE' ? FieldPhoneMetadata - : E extends 'url' + : E extends 'URL' ? FieldURLMetadata - : E extends 'urlV2' + : E extends 'URL_V2' ? FieldURLV2Metadata - : E extends 'probability' + : E extends 'PROBABILITY' ? FieldProbabilityMetadata - : E extends 'moneyAmount' + : E extends 'MONEY_AMOUNT' ? FieldMoneyMetadata - : E extends 'moneyAmountV2' + : E extends 'MONEY_AMOUNT_V2' ? FieldMoneyAmountV2Metadata : never, >( diff --git a/front/src/modules/ui/object/field/types/guards/isFieldBoolean.ts b/front/src/modules/ui/object/field/types/guards/isFieldBoolean.ts index 0adcc3128..55a82da56 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldBoolean.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldBoolean.ts @@ -3,4 +3,4 @@ import { FieldBooleanMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldBoolean = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'boolean'; +): field is FieldDefinition => field.type === 'BOOLEAN'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldChip.ts b/front/src/modules/ui/object/field/types/guards/isFieldChip.ts index 3d92e070b..1ed7d631a 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldChip.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldChip.ts @@ -3,4 +3,4 @@ import { FieldChipMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldChip = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'chip'; +): field is FieldDefinition => field.type === 'CHIP'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldDate.ts b/front/src/modules/ui/object/field/types/guards/isFieldDate.ts index 08ebd89da..f6e077451 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldDate.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldDate.ts @@ -3,4 +3,4 @@ import { FieldDateMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldDate = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'date'; +): field is FieldDefinition => field.type === 'DATE'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldDoubleText.ts b/front/src/modules/ui/object/field/types/guards/isFieldDoubleText.ts index e47363cfe..ed999cbb1 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldDoubleText.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldDoubleText.ts @@ -4,4 +4,4 @@ import { FieldDoubleTextMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldDoubleText = ( field: FieldDefinition, ): field is FieldDefinition => - field.type === 'double-text'; + field.type === 'DOUBLE_TEXT'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldDoubleTextChip.ts b/front/src/modules/ui/object/field/types/guards/isFieldDoubleTextChip.ts index c529f066d..591274597 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldDoubleTextChip.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldDoubleTextChip.ts @@ -4,4 +4,4 @@ import { FieldDoubleTextChipMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldDoubleTextChip = ( field: FieldDefinition, ): field is FieldDefinition => - field.type === 'double-text-chip'; + field.type === 'DOUBLE_TEXT_CHIP'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldEmail.ts b/front/src/modules/ui/object/field/types/guards/isFieldEmail.ts index 7554c1df7..eb9d79555 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldEmail.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldEmail.ts @@ -3,4 +3,4 @@ import { FieldEmailMetadata, FieldMetadata } from '../FieldMetadata'; export const isFieldEmail = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'email'; +): field is FieldDefinition => field.type === 'EMAIL'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldMoney.ts b/front/src/modules/ui/object/field/types/guards/isFieldMoney.ts index b4e301349..18a0fab15 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldMoney.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldMoney.ts @@ -3,4 +3,5 @@ import { FieldMetadata, FieldMoneyMetadata } from '../FieldMetadata'; export const isFieldMoney = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'moneyAmount'; +): field is FieldDefinition => + field.type === 'MONEY_AMOUNT'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2.ts b/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2.ts index eb3ab2a41..b8e40b696 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldMoneyAmountV2.ts @@ -4,4 +4,4 @@ import { FieldMetadata, FieldMoneyAmountV2Metadata } from '../FieldMetadata'; export const isFieldMoneyAmountV2 = ( field: FieldDefinition, ): field is FieldDefinition => - field.type === 'moneyAmountV2'; + field.type === 'MONEY_AMOUNT_V2'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldNumber.ts b/front/src/modules/ui/object/field/types/guards/isFieldNumber.ts index 851acf1e6..4555b0e06 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldNumber.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldNumber.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldNumberMetadata } from '../FieldMetadata'; export const isFieldNumber = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'number'; +): field is FieldDefinition => field.type === 'NUMBER'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldPhone.ts b/front/src/modules/ui/object/field/types/guards/isFieldPhone.ts index d5762bf2c..b96e25c4d 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldPhone.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldPhone.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldPhoneMetadata } from '../FieldMetadata'; export const isFieldPhone = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'phone'; +): field is FieldDefinition => field.type === 'PHONE'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts b/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts index 9b605f20f..404d5b791 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldProbability.ts @@ -4,4 +4,4 @@ import { FieldMetadata, FieldProbabilityMetadata } from '../FieldMetadata'; export const isFieldProbability = ( field: FieldDefinition, ): field is FieldDefinition => - field.type === 'probability'; + field.type === 'PROBABILITY'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldRelation.ts b/front/src/modules/ui/object/field/types/guards/isFieldRelation.ts index 4ea65e76d..af277da43 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldRelation.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldRelation.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldRelationMetadata } from '../FieldMetadata'; export const isFieldRelation = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'relation'; +): field is FieldDefinition => field.type === 'RELATION'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldText.ts b/front/src/modules/ui/object/field/types/guards/isFieldText.ts index c37927773..c8094a085 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldText.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldText.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldTextMetadata } from '../FieldMetadata'; export const isFieldText = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'text'; +): field is FieldDefinition => field.type === 'TEXT'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldURL.ts b/front/src/modules/ui/object/field/types/guards/isFieldURL.ts index 83feec5dd..195c7f0a7 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldURL.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldURL.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldURLMetadata } from '../FieldMetadata'; export const isFieldURL = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'url'; +): field is FieldDefinition => field.type === 'URL'; diff --git a/front/src/modules/ui/object/field/types/guards/isFieldURLV2.ts b/front/src/modules/ui/object/field/types/guards/isFieldURLV2.ts index f555a2fd9..eb78383e9 100644 --- a/front/src/modules/ui/object/field/types/guards/isFieldURLV2.ts +++ b/front/src/modules/ui/object/field/types/guards/isFieldURLV2.ts @@ -3,4 +3,4 @@ import { FieldMetadata, FieldURLV2Metadata } from '../FieldMetadata'; export const isFieldURLV2 = ( field: FieldDefinition, -): field is FieldDefinition => field.type === 'urlV2'; +): field is FieldDefinition => field.type === 'URL_V2'; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 71d01033a..e24d298f1 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -30,19 +30,19 @@ export const MultipleFiltersDropdownContent = () => { <> - {filterDefinitionUsedInDropdown.type === 'text' && ( + {filterDefinitionUsedInDropdown.type === 'TEXT' && ( )} - {filterDefinitionUsedInDropdown.type === 'number' && ( + {filterDefinitionUsedInDropdown.type === 'NUMBER' && ( )} - {filterDefinitionUsedInDropdown.type === 'date' && ( + {filterDefinitionUsedInDropdown.type === 'DATE' && ( )} - {filterDefinitionUsedInDropdown.type === 'entity' && ( + {filterDefinitionUsedInDropdown.type === 'ENTITY' && ( )} - {filterDefinitionUsedInDropdown.type === 'entity' && ( + {filterDefinitionUsedInDropdown.type === 'ENTITY' && ( )} diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx index 08e410035..c00993935 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx @@ -16,7 +16,7 @@ export const ObjectFilterDropdownButton = ({ const hasOnlyOneEntityFilter = availableFilterDefinitions.length === 1 && - availableFilterDefinitions[0].type === 'entity'; + availableFilterDefinitions[0].type === 'ENTITY'; if (!availableFilterDefinitions.length) { return <>; diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx index 62679648b..df846f817 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownEntitySelect.tsx @@ -6,7 +6,7 @@ import { useFilter } from '../hooks/useFilter'; export const ObjectFilterDropdownEntitySelect = () => { const { filterDefinitionUsedInDropdown } = useFilter(); - if (filterDefinitionUsedInDropdown?.type !== 'entity') { + if (filterDefinitionUsedInDropdown?.type !== 'ENTITY') { return null; } diff --git a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 4ab76b600..541d41c76 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/front/src/modules/ui/object/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -25,7 +25,7 @@ export const ObjectFilterDropdownFilterSelect = () => { onClick={() => { setFilterDefinitionUsedInDropdown(availableFilterDefinition); - if (availableFilterDefinition.type === 'entity') { + if (availableFilterDefinition.type === 'ENTITY') { setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); } diff --git a/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts b/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts index d298fbf79..4445d5426 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/types/FilterType.ts @@ -1 +1 @@ -export type FilterType = 'text' | 'date' | 'entity' | 'number'; +export type FilterType = 'TEXT' | 'DATE' | 'ENTITY' | 'NUMBER'; diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts index a8030f444..8d9515ba8 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -6,12 +6,12 @@ export const getOperandsForFilterType = ( filterType: FilterType | null | undefined, ): ViewFilterOperand[] => { switch (filterType) { - case 'text': + case 'TEXT': return [ViewFilterOperand.Contains, ViewFilterOperand.DoesNotContain]; - case 'number': - case 'date': + case 'NUMBER': + case 'DATE': return [ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan]; - case 'entity': + case 'ENTITY': return [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; default: return []; diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFilterIntoWhereClause.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFilterIntoWhereClause.ts index 1776b7f90..e6c600f4a 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFilterIntoWhereClause.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFilterIntoWhereClause.ts @@ -24,7 +24,7 @@ export const turnFilterIntoWhereClause = ( }; default: switch (filter.definition.type) { - case 'text': + case 'TEXT': switch (filter.operand) { case ViewFilterOperand.Contains: return { @@ -47,7 +47,7 @@ export const turnFilterIntoWhereClause = ( `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, ); } - case 'number': + case 'NUMBER': switch (filter.operand) { case ViewFilterOperand.GreaterThan: return { @@ -66,7 +66,7 @@ export const turnFilterIntoWhereClause = ( `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, ); } - case 'date': + case 'DATE': switch (filter.operand) { case ViewFilterOperand.GreaterThan: return { @@ -85,7 +85,7 @@ export const turnFilterIntoWhereClause = ( `Unknown operand ${filter.operand} for ${filter.definition.type} filter`, ); } - case 'entity': + case 'ENTITY': switch (filter.operand) { case ViewFilterOperand.Is: return { diff --git a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts index 7270022f6..145883006 100644 --- a/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts +++ b/front/src/modules/ui/object/object-filter-dropdown/utils/turnFiltersIntoWhereClauseV2.ts @@ -26,7 +26,7 @@ export const turnFiltersIntoWhereClauseV2 = ( } switch (filter.definition.type) { - case 'text': + case 'TEXT': switch (filter.operand) { case ViewFilterOperand.Contains: whereClause[correspondingField.name] = { diff --git a/front/src/pages/companies/constants/companyShowFieldDefinitions.tsx b/front/src/pages/companies/constants/companyShowFieldDefinitions.tsx index 13e4e4298..8d8ff215c 100644 --- a/front/src/pages/companies/constants/companyShowFieldDefinitions.tsx +++ b/front/src/pages/companies/constants/companyShowFieldDefinitions.tsx @@ -25,7 +25,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'domainName', label: 'Domain name', Icon: IconLink, - type: 'url', + type: 'URL', metadata: { fieldName: 'domainName', placeHolder: 'URL', @@ -35,7 +35,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'accountOwner', label: 'Account owner', Icon: IconUserCircle, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'accountOwner', relationType: Entity.User, @@ -52,7 +52,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'employees', label: 'Employees', Icon: IconUsers, - type: 'number', + type: 'NUMBER', metadata: { fieldName: 'employees', placeHolder: 'Employees', @@ -62,7 +62,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'address', label: 'Address', Icon: IconMap, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'address', placeHolder: 'Address', @@ -72,7 +72,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'idealCustomerProfile', label: 'ICP', Icon: IconTarget, - type: 'boolean', + type: 'BOOLEAN', metadata: { fieldName: 'idealCustomerProfile', }, @@ -81,7 +81,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'xUrl', label: 'Twitter', Icon: IconBrandX, - type: 'url', + type: 'URL', metadata: { fieldName: 'xUrl', placeHolder: 'X', @@ -91,7 +91,7 @@ export const companyShowFieldDefinitions: FieldDefinition[] = [ fieldId: 'createdAt', label: 'Created at', Icon: IconCalendar, - type: 'date', + type: 'DATE', metadata: { fieldName: 'createdAt', }, diff --git a/front/src/pages/companies/constants/companyTableFilterDefinitions.tsx b/front/src/pages/companies/constants/companyTableFilterDefinitions.tsx index d29bd0697..8a467c913 100644 --- a/front/src/pages/companies/constants/companyTableFilterDefinitions.tsx +++ b/front/src/pages/companies/constants/companyTableFilterDefinitions.tsx @@ -16,37 +16,37 @@ export const companyTableFilterDefinitions: FilterDefinitionByEntity[] fieldId: 'name', label: 'Name', Icon: IconBuildingSkyscraper, - type: 'text', + type: 'TEXT', }, { fieldId: 'employees', label: 'Employees', Icon: IconUsers, - type: 'number', + type: 'NUMBER', }, { fieldId: 'domainName', label: 'URL', Icon: IconLink, - type: 'text', + type: 'TEXT', }, { fieldId: 'address', label: 'Address', Icon: IconMap, - type: 'text', + type: 'TEXT', }, { fieldId: 'createdAt', label: 'Created at', Icon: IconCalendarEvent, - type: 'date', + type: 'DATE', }, { fieldId: 'accountOwnerId', label: 'Account owner', Icon: IconUser, - type: 'entity', + type: 'ENTITY', entitySelectComponent: , }, ]; diff --git a/front/src/pages/opportunities/constants/opportunityBoardFilterDefinitions.tsx b/front/src/pages/opportunities/constants/opportunityBoardFilterDefinitions.tsx index fcaa7757b..00210f28e 100644 --- a/front/src/pages/opportunities/constants/opportunityBoardFilterDefinitions.tsx +++ b/front/src/pages/opportunities/constants/opportunityBoardFilterDefinitions.tsx @@ -16,26 +16,26 @@ export const opportunityBoardFilterDefinitions: FilterDefinitionByEntity, }, { fieldId: 'pointOfContactId', label: 'Point of contact', Icon: IconUser, - type: 'entity', + type: 'ENTITY', entitySelectComponent: , }, ]; diff --git a/front/src/pages/people/constants/personShowFieldDefinitions.tsx b/front/src/pages/people/constants/personShowFieldDefinitions.tsx index 098e155d0..38a4ed0c4 100644 --- a/front/src/pages/people/constants/personShowFieldDefinitions.tsx +++ b/front/src/pages/people/constants/personShowFieldDefinitions.tsx @@ -26,7 +26,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'email', label: 'Email', Icon: IconMail, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'email', placeHolder: 'Email', @@ -36,7 +36,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'company', label: 'Company', Icon: IconBuildingSkyscraper, - type: 'relation', + type: 'RELATION', metadata: { fieldName: 'company', relationType: Entity.Company, @@ -53,7 +53,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'phone', label: 'Phone', Icon: IconPhone, - type: 'phone', + type: 'PHONE', metadata: { fieldName: 'phone', placeHolder: 'Phone', @@ -63,7 +63,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'jobTitle', label: 'Job Title', Icon: IconBriefcase, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'jobTitle', placeHolder: 'Job Title', @@ -73,7 +73,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'city', label: 'City', Icon: IconMap, - type: 'text', + type: 'TEXT', metadata: { fieldName: 'city', placeHolder: 'City', @@ -83,7 +83,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'linkedinUrl', label: 'Linkedin URL', Icon: IconBrandLinkedin, - type: 'url', + type: 'URL', metadata: { fieldName: 'linkedinUrl', placeHolder: 'Linkedin URL', @@ -93,7 +93,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'xUrl', label: 'X URL', Icon: IconBrandX, - type: 'url', + type: 'URL', metadata: { fieldName: 'xUrl', placeHolder: 'X URL', @@ -103,7 +103,7 @@ export const personShowFieldDefinition: FieldDefinition[] = [ fieldId: 'createdAt', label: 'Created at', Icon: IconCalendar, - type: 'date', + type: 'DATE', metadata: { fieldName: 'createdAt', }, diff --git a/front/src/pages/people/constants/personTableFilterDefinitions.tsx b/front/src/pages/people/constants/personTableFilterDefinitions.tsx index 2eb609ad8..983c6de72 100644 --- a/front/src/pages/people/constants/personTableFilterDefinitions.tsx +++ b/front/src/pages/people/constants/personTableFilterDefinitions.tsx @@ -16,25 +16,25 @@ export const personTableFilterDefinitions: FilterDefinitionByEntity[] = fieldId: 'firstName', label: 'First name', Icon: IconUser, - type: 'text', + type: 'TEXT', }, { fieldId: 'lastName', label: 'Last name', Icon: IconUser, - type: 'text', + type: 'TEXT', }, { fieldId: 'email', label: 'Email', Icon: IconMail, - type: 'text', + type: 'TEXT', }, { fieldId: 'companyId', label: 'Company', Icon: IconBuildingSkyscraper, - type: 'entity', + type: 'ENTITY', // TODO: replace this with a component that selects the dropdown to use based on the entity type entitySelectComponent: , }, @@ -42,18 +42,18 @@ export const personTableFilterDefinitions: FilterDefinitionByEntity[] = fieldId: 'phone', label: 'Phone', Icon: IconPhone, - type: 'text', + type: 'TEXT', }, { fieldId: 'createdAt', label: 'Created at', Icon: IconCalendarEvent, - type: 'date', + type: 'DATE', }, { fieldId: 'city', label: 'City', Icon: IconMap, - type: 'text', + type: 'TEXT', }, ]; diff --git a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index 9665af77c..563c3a42f 100644 --- a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -40,7 +40,7 @@ export const SettingsObjectNewFieldStep2 = () => { icon: string; label: string; type: MetadataFieldDataType; - }>({ icon: 'IconUsers', label: '', type: 'number' }); + }>({ icon: 'IconUsers', label: '', type: 'NUMBER' }); const [objectViews, setObjectViews] = useState([]); diff --git a/front/src/pages/tasks/tasks-filter-definitions.tsx b/front/src/pages/tasks/tasks-filter-definitions.tsx index 4fd1788d3..7d860b454 100644 --- a/front/src/pages/tasks/tasks-filter-definitions.tsx +++ b/front/src/pages/tasks/tasks-filter-definitions.tsx @@ -8,7 +8,7 @@ export const tasksFilterDefinitions: FilterDefinitionByEntity[] = [ fieldId: 'assigneeId', label: 'Assignee', Icon: IconUser, - type: 'entity', + type: 'ENTITY', entitySelectComponent: , selectAllLabel: 'All assignees', SelectAllIcon: IconUserCircle, diff --git a/front/src/testing/mock-data/metadata.ts b/front/src/testing/mock-data/metadata.ts index 7bff83720..8d9ea5913 100644 --- a/front/src/testing/mock-data/metadata.ts +++ b/front/src/testing/mock-data/metadata.ts @@ -19,7 +19,7 @@ export const mockedObjectMetadataItems = { { node: { id: '5db475e7-8208-402d-97a1-62c9ce344dd4', - type: 'text', + type: 'TEXT', name: 'objectId', label: 'Object Id', description: 'View target object', @@ -35,7 +35,7 @@ export const mockedObjectMetadataItems = { { node: { id: 'ddc89a56-9add-4110-aa53-4ecdbba36767', - type: 'text', + type: 'TEXT', name: 'type', label: 'Type', description: 'View type', @@ -51,7 +51,7 @@ export const mockedObjectMetadataItems = { { node: { id: '35fa806b-5d9d-446d-bd0e-1a6874b871ee', - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', description: 'View name', @@ -94,7 +94,7 @@ export const mockedObjectMetadataItems = { { node: { id: '1d718fcf-5a17-4694-91a4-4d3968a51aa4', - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', description: 'View Field related view', @@ -110,7 +110,7 @@ export const mockedObjectMetadataItems = { { node: { id: '8ead2e86-7b60-4a47-9b4f-ad008e744d52', - type: 'number', + type: 'NUMBER', name: 'position', label: 'Position', description: 'View Field position', @@ -126,7 +126,7 @@ export const mockedObjectMetadataItems = { { node: { id: '4d77c2dd-2b04-4989-b11e-cb0e386d1b4d', - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', description: 'View Field target field', @@ -142,7 +142,7 @@ export const mockedObjectMetadataItems = { { node: { id: '0f5ab566-9fc4-44b7-85c5-1e05db9f6b49', - type: 'boolean', + type: 'BOOLEAN', name: 'isVisible', label: 'Visible', description: 'View Field visibility', @@ -158,7 +158,7 @@ export const mockedObjectMetadataItems = { { node: { id: '21268ece-7002-4b04-a442-f25278f8ca13', - type: 'number', + type: 'NUMBER', name: 'size', label: 'Size', description: 'View Field size', @@ -201,7 +201,7 @@ export const mockedObjectMetadataItems = { { node: { id: '397eabc0-c5a1-4550-8e68-839c878a8d0e', - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', description: 'The company name.', @@ -217,8 +217,8 @@ export const mockedObjectMetadataItems = { { node: { id: '7ad234c7-f3b9-4efc-813c-43dc97070b07', - type: 'url', - name: 'url', + type: 'URL', + name: 'URL', label: 'URL', description: 'The company website URL. We use this url to fetch the company icon.', @@ -234,7 +234,7 @@ export const mockedObjectMetadataItems = { { node: { id: 'a578ffb2-13db-483c-ace7-5c30a13dff2d', - type: 'relation', + type: 'RELATION', name: 'accountOwner', label: 'Account Owner', description: @@ -251,7 +251,7 @@ export const mockedObjectMetadataItems = { { node: { id: 'b7fd622d-7d8b-4f5a-b148-a7e9fd2c4660', - type: 'number', + type: 'NUMBER', name: 'employees', label: 'Employees', description: 'Number of employees in the company.', @@ -267,7 +267,7 @@ export const mockedObjectMetadataItems = { { node: { id: '60ab27ed-a959-471e-b583-887387f7accd', - type: 'url', + type: 'URL', name: 'linkedin', label: 'Linkedin', description: null, @@ -283,7 +283,7 @@ export const mockedObjectMetadataItems = { { node: { id: '6daadb98-83ca-4c85-bca5-7792a7d958ad', - type: 'boolean', + type: 'BOOLEAN', name: 'prioritySupport', label: 'Priority Support', description: 'Whether the company has priority support.', @@ -326,7 +326,7 @@ export const mockedObjectMetadataItems = { { node: { id: 'f955402c-9e8f-4b91-a82c-95f6de392b99', - type: 'text', + type: 'TEXT', name: 'slug', label: 'Slug', description: null, diff --git a/server/package.json b/server/package.json index 435137381..ddacb30c3 100644 --- a/server/package.json +++ b/server/package.json @@ -43,6 +43,7 @@ "@aws-sdk/credential-providers": "^3.363.0", "@casl/ability": "^6.5.0", "@casl/prisma": "1.4.0", + "@graphql-tools/schema": "^10.0.0", "@graphql-yoga/nestjs": "^2.1.0", "@nestjs/apollo": "^11.0.5", "@nestjs/common": "^9.0.0", @@ -78,6 +79,7 @@ "graphql": "16.8.0", "graphql-fields": "^2.0.3", "graphql-subscriptions": "2.0.0", + "graphql-tag": "^2.12.6", "graphql-type-json": "^0.3.2", "graphql-upload": "^13.0.0", "graphql-yoga": "^4.0.4", diff --git a/server/src/database/typeorm-seeds/metadata/field-metadata.ts b/server/src/database/typeorm-seeds/metadata/field-metadata.ts index 19034e610..d3c913c5f 100644 --- a/server/src/database/typeorm-seeds/metadata/field-metadata.ts +++ b/server/src/database/typeorm-seeds/metadata/field-metadata.ts @@ -30,7 +30,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', targetColumnMap: { @@ -45,7 +45,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'domainName', label: 'Domain Name', targetColumnMap: { @@ -60,7 +60,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'address', label: 'Address', targetColumnMap: { @@ -75,7 +75,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'employees', label: 'Employees', targetColumnMap: { @@ -91,7 +91,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', targetColumnMap: { @@ -106,7 +106,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'objectId', label: 'Object Id', targetColumnMap: { @@ -121,7 +121,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'type', label: 'Type', targetColumnMap: { @@ -137,7 +137,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -152,7 +152,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -167,7 +167,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'boolean', + type: 'BOOLEAN', name: 'isVisible', label: 'Visible', targetColumnMap: { @@ -182,7 +182,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'number', + type: 'NUMBER', name: 'size', label: 'Size', targetColumnMap: { @@ -197,7 +197,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'number', + type: 'NUMBER', name: 'position', label: 'Position', targetColumnMap: { @@ -213,7 +213,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -228,7 +228,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -243,7 +243,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'operand', label: 'Operand', targetColumnMap: { @@ -258,7 +258,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'value', label: 'Value', targetColumnMap: { @@ -273,7 +273,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'displayValue', label: 'Display Value', targetColumnMap: { @@ -289,7 +289,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -304,7 +304,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -319,7 +319,7 @@ export const seedFieldMetadata = async ( isCustom: false, workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', isActive: true, - type: 'text', + type: 'TEXT', name: 'direction', label: 'Direction', targetColumnMap: { diff --git a/server/src/main.ts b/server/src/main.ts index b337f3b2e..4524d9d5d 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -14,6 +14,9 @@ import { EnvironmentService } from './integrations/environment/environment.servi const bootstrap = async () => { const app = await NestFactory.create(AppModule, { cors: true, + logger: process.env.DEBUG_MODE + ? ['error', 'warn', 'log', 'verbose', 'debug'] + : ['error', 'warn', 'log'], }); // Apply validation pipes globally diff --git a/server/src/metadata/field-metadata/dtos/create-field.input.ts b/server/src/metadata/field-metadata/dtos/create-field.input.ts index 7a1155097..04b445aac 100644 --- a/server/src/metadata/field-metadata/dtos/create-field.input.ts +++ b/server/src/metadata/field-metadata/dtos/create-field.input.ts @@ -8,6 +8,8 @@ import { IsUUID, } from 'class-validator'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + @InputType() export class CreateFieldInput { @IsString() @@ -20,20 +22,10 @@ export class CreateFieldInput { @Field() label: string; - // Todo: use a type enum and share with typeorm entity - @IsEnum([ - 'text', - 'phone', - 'email', - 'number', - 'boolean', - 'date', - 'url', - 'money', - ]) + @IsEnum(FieldMetadataType) @IsNotEmpty() - @Field() - type: string; + @Field(() => FieldMetadataType) + type: FieldMetadataType; @IsUUID() @Field() diff --git a/server/src/metadata/field-metadata/field-metadata.entity.ts b/server/src/metadata/field-metadata/field-metadata.entity.ts index 8a584f1c6..baf0bdbbd 100644 --- a/server/src/metadata/field-metadata/field-metadata.entity.ts +++ b/server/src/metadata/field-metadata/field-metadata.entity.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType } from '@nestjs/graphql'; +import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Column, @@ -17,13 +17,31 @@ import { QueryOptions, } from '@ptc-org/nestjs-query-graphql'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; + import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; import { BeforeCreateOneField } from './hooks/before-create-one-field.hook'; +import { FieldMetadataTargetColumnMap } from './interfaces/field-metadata-target-column-map.interface'; + +export enum FieldMetadataType { + UUID = 'uuid', + TEXT = 'TEXT', + PHONE = 'PHONE', + EMAIL = 'EMAIL', + DATE = 'DATE', + BOOLEAN = 'BOOLEAN', + NUMBER = 'NUMBER', + ENUM = 'ENUM', + URL = 'URL', + MONEY = 'MONEY', +} + +registerEnumType(FieldMetadataType, { + name: 'FieldMetadataType', + description: 'Type of the field', +}); -export type FieldMetadataTargetColumnMap = { - [key: string]: string; -}; @Entity('field_metadata') @ObjectType('field') @BeforeCreateOne(BeforeCreateOneField) @@ -43,7 +61,7 @@ export type FieldMetadataTargetColumnMap = { 'objectId', 'workspaceId', ]) -export class FieldMetadata { +export class FieldMetadata implements FieldMetadataInterface { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') id: string; @@ -51,9 +69,9 @@ export class FieldMetadata { @Column({ nullable: false, name: 'object_id' }) objectId: string; - @Field() + @Field(() => FieldMetadataType) @Column({ nullable: false }) - type: string; + type: FieldMetadataType; @Field() @Column({ nullable: false }) diff --git a/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts b/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts new file mode 100644 index 000000000..20ca30472 --- /dev/null +++ b/server/src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface.ts @@ -0,0 +1,35 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export interface FieldMetadataTargetColumnMapValue { + value: string; +} + +export interface FieldMetadataTargetColumnMapUrl { + text: string; + link: string; +} + +export interface FieldMetadataTargetColumnMapMoney { + value: number; + currency: string; +} + +type AllFieldMetadataTypes = { + [key: string]: any; +}; + +type FieldMetadataTypeMapping = { + [FieldMetadataType.URL]: FieldMetadataTargetColumnMapUrl; + [FieldMetadataType.MONEY]: FieldMetadataTargetColumnMapMoney; +}; + +type TypeByFieldMetadata = + T extends keyof FieldMetadataTypeMapping + ? FieldMetadataTypeMapping[T] + : T extends 'default' + ? AllFieldMetadataTypes + : FieldMetadataTargetColumnMapValue; + +export type FieldMetadataTargetColumnMap< + T extends FieldMetadataType | 'default' = 'default', +> = TypeByFieldMetadata; diff --git a/server/src/metadata/field-metadata/utils/field-metadata.util.ts b/server/src/metadata/field-metadata/utils/field-metadata.util.ts index 1fd0ab587..3fd82dc4e 100644 --- a/server/src/metadata/field-metadata/utils/field-metadata.util.ts +++ b/server/src/metadata/field-metadata/utils/field-metadata.util.ts @@ -1,9 +1,11 @@ import { v4 } from 'uuid'; +import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; + import { uuidToBase36 } from 'src/metadata/data-source/data-source.util'; import { FieldMetadata, - FieldMetadataTargetColumnMap, + FieldMetadataType, } from 'src/metadata/field-metadata/field-metadata.entity'; import { TenantMigrationColumnAction } from 'src/metadata/tenant-migration/tenant-migration.entity'; @@ -25,24 +27,24 @@ export function generateColumnName(name: string): string { * @returns FieldMetadataTargetColumnMap */ export function generateTargetColumnMap( - type: string, + type: FieldMetadataType, ): FieldMetadataTargetColumnMap { switch (type) { - case 'text': - case 'phone': - case 'email': - case 'number': - case 'boolean': - case 'date': + case FieldMetadataType.TEXT: + case FieldMetadataType.PHONE: + case FieldMetadataType.EMAIL: + case FieldMetadataType.NUMBER: + case FieldMetadataType.BOOLEAN: + case FieldMetadataType.DATE: return { value: `column_${uuidToBase36(v4())}`, }; - case 'url': + case FieldMetadataType.URL: return { text: `column_${uuidToBase36(v4())}`, link: `column_${uuidToBase36(v4())}`, }; - case 'money': + case FieldMetadataType.MONEY: return { amount: `column_${uuidToBase36(v4())}`, currency: `column_${uuidToBase36(v4())}`, @@ -56,7 +58,7 @@ export function convertFieldMetadataToColumnActions( fieldMetadata: FieldMetadata, ): TenantMigrationColumnAction[] { switch (fieldMetadata.type) { - case 'text': + case FieldMetadataType.TEXT: return [ { name: fieldMetadata.targetColumnMap.value, @@ -64,8 +66,8 @@ export function convertFieldMetadataToColumnActions( type: 'text', }, ]; - case 'phone': - case 'email': + case FieldMetadataType.PHONE: + case FieldMetadataType.EMAIL: return [ { name: fieldMetadata.targetColumnMap.value, @@ -73,7 +75,7 @@ export function convertFieldMetadataToColumnActions( type: 'varchar', }, ]; - case 'number': + case FieldMetadataType.NUMBER: return [ { name: fieldMetadata.targetColumnMap.value, @@ -81,7 +83,7 @@ export function convertFieldMetadataToColumnActions( type: 'integer', }, ]; - case 'boolean': + case FieldMetadataType.BOOLEAN: return [ { name: fieldMetadata.targetColumnMap.value, @@ -89,7 +91,7 @@ export function convertFieldMetadataToColumnActions( type: 'boolean', }, ]; - case 'date': + case FieldMetadataType.DATE: return [ { name: fieldMetadata.targetColumnMap.value, @@ -97,7 +99,7 @@ export function convertFieldMetadataToColumnActions( type: 'timestamp', }, ]; - case 'url': + case FieldMetadataType.URL: return [ { name: fieldMetadata.targetColumnMap.text, @@ -110,7 +112,7 @@ export function convertFieldMetadataToColumnActions( type: 'varchar', }, ]; - case 'money': + case FieldMetadataType.MONEY: return [ { name: fieldMetadata.targetColumnMap.amount, @@ -127,24 +129,3 @@ export function convertFieldMetadataToColumnActions( throw new Error(`Unknown type ${fieldMetadata.type}`); } } - -// Deprecated with target_column_name deprecation -export function convertMetadataTypeToColumnType(type: string) { - switch (type) { - case 'text': - case 'url': - case 'phone': - case 'email': - return 'text'; - case 'number': - return 'int'; - case 'boolean': - return 'boolean'; - case 'date': - return 'timestamp'; - case 'money': - return 'integer'; - default: - throw new Error('Invalid type'); - } -} diff --git a/server/src/metadata/migration-runner/custom-table-default-column.util.ts b/server/src/metadata/migration-runner/custom-table-default-column.util.ts new file mode 100644 index 000000000..14f5ec5a0 --- /dev/null +++ b/server/src/metadata/migration-runner/custom-table-default-column.util.ts @@ -0,0 +1,25 @@ +import { TableColumnOptions } from 'typeorm'; + +export const customTableDefaultColumns: TableColumnOptions[] = [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + default: 'public.uuid_generate_v4()', + }, + { + name: 'createdAt', + type: 'timestamp', + default: 'now()', + }, + { + name: 'updatedAt', + type: 'timestamp', + default: 'now()', + }, + { + name: 'deletedAt', + type: 'timestamp', + isNullable: true, + }, +]; diff --git a/server/src/metadata/migration-runner/migration-runner.service.ts b/server/src/metadata/migration-runner/migration-runner.service.ts index c85130b33..66f681815 100644 --- a/server/src/metadata/migration-runner/migration-runner.service.ts +++ b/server/src/metadata/migration-runner/migration-runner.service.ts @@ -9,6 +9,8 @@ import { } from 'src/metadata/tenant-migration/tenant-migration.entity'; import { TenantMigrationService } from 'src/metadata/tenant-migration/tenant-migration.service'; +import { customTableDefaultColumns } from './custom-table-default-column.util'; + @Injectable() export class MigrationRunnerService { constructor( @@ -114,29 +116,7 @@ export class MigrationRunnerService { new Table({ name: tableName, schema: schemaName, - columns: [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'public.uuid_generate_v4()', - }, - { - name: 'createdAt', - type: 'timestamp', - default: 'now()', - }, - { - name: 'updatedAt', - type: 'timestamp', - default: 'now()', - }, - { - name: 'deletedAt', - type: 'timestamp', - isNullable: true, - }, - ], + columns: customTableDefaultColumns, }), true, ); diff --git a/server/src/metadata/object-metadata/object-metadata.entity.ts b/server/src/metadata/object-metadata/object-metadata.entity.ts index cc10cd74d..8005e438e 100644 --- a/server/src/metadata/object-metadata/object-metadata.entity.ts +++ b/server/src/metadata/object-metadata/object-metadata.entity.ts @@ -17,6 +17,8 @@ import { QueryOptions, } from '@ptc-org/nestjs-query-graphql'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook'; @@ -41,7 +43,7 @@ import { BeforeCreateOneObject } from './hooks/before-create-one-object.hook'; 'workspaceId', ]) @Unique('IndexOnNamePluralAndWorkspaceIdUnique', ['namePlural', 'workspaceId']) -export class ObjectMetadata { +export class ObjectMetadata implements ObjectMetadataInterface { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') id: string; diff --git a/server/src/metadata/object-metadata/services/object-metadata.service.ts b/server/src/metadata/object-metadata/services/object-metadata.service.ts index cc113c656..6ff25823b 100644 --- a/server/src/metadata/object-metadata/services/object-metadata.service.ts +++ b/server/src/metadata/object-metadata/services/object-metadata.service.ts @@ -70,6 +70,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { return createdObjectMetadata; } + public async getObjectMetadataFromWorkspaceId(workspaceId: string) { + return this.objectMetadataRepository.find({ + where: { workspaceId }, + relations: ['fields'], + }); + } + public async getObjectMetadataFromDataSourceId(dataSourceId: string) { return this.objectMetadataRepository.find({ where: { dataSourceId }, diff --git a/server/src/metadata/standard-objects/companies/companies.metadata.ts b/server/src/metadata/standard-objects/companies/companies.metadata.ts index 8ef69cc0f..f7394e471 100644 --- a/server/src/metadata/standard-objects/companies/companies.metadata.ts +++ b/server/src/metadata/standard-objects/companies/companies.metadata.ts @@ -8,7 +8,7 @@ const companiesMetadata = { icon: 'IconBuildingSkyscraper', fields: [ { - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', targetColumnMap: { @@ -19,7 +19,7 @@ const companiesMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'domainName', label: 'Domain Name', targetColumnMap: { @@ -30,7 +30,7 @@ const companiesMetadata = { isNullable: true, }, { - type: 'text', + type: 'TEXT', name: 'address', label: 'Address', targetColumnMap: { @@ -41,7 +41,7 @@ const companiesMetadata = { isNullable: true, }, { - type: 'number', + type: 'NUMBER', name: 'employees', label: 'Employees', targetColumnMap: { diff --git a/server/src/metadata/standard-objects/view-fields/view-fields.metadata.ts b/server/src/metadata/standard-objects/view-fields/view-fields.metadata.ts index b39a281f4..98214887e 100644 --- a/server/src/metadata/standard-objects/view-fields/view-fields.metadata.ts +++ b/server/src/metadata/standard-objects/view-fields/view-fields.metadata.ts @@ -8,7 +8,7 @@ const viewFieldsMetadata = { icon: 'IconColumns3', fields: [ { - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -19,7 +19,7 @@ const viewFieldsMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -41,7 +41,7 @@ const viewFieldsMetadata = { isNullable: false, }, { - type: 'number', + type: 'NUMBER', name: 'size', label: 'Size', targetColumnMap: { @@ -52,7 +52,7 @@ const viewFieldsMetadata = { isNullable: false, }, { - type: 'number', + type: 'NUMBER', name: 'position', label: 'Position', targetColumnMap: { diff --git a/server/src/metadata/standard-objects/view-filters/view-filters.metadata.ts b/server/src/metadata/standard-objects/view-filters/view-filters.metadata.ts index d6188c583..8fa165fd4 100644 --- a/server/src/metadata/standard-objects/view-filters/view-filters.metadata.ts +++ b/server/src/metadata/standard-objects/view-filters/view-filters.metadata.ts @@ -8,7 +8,7 @@ const viewFiltersMetadata = { icon: 'IconFilterBolt', fields: [ { - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -19,7 +19,7 @@ const viewFiltersMetadata = { isNullable: true, }, { - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -30,7 +30,7 @@ const viewFiltersMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'operand', label: 'Operand', targetColumnMap: { @@ -41,7 +41,7 @@ const viewFiltersMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'value', label: 'Value', targetColumnMap: { @@ -52,7 +52,7 @@ const viewFiltersMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'displayValue', label: 'Display Value', targetColumnMap: { diff --git a/server/src/metadata/standard-objects/view-sorts/view-sorts.metadata.ts b/server/src/metadata/standard-objects/view-sorts/view-sorts.metadata.ts index a326f314c..eb08b3921 100644 --- a/server/src/metadata/standard-objects/view-sorts/view-sorts.metadata.ts +++ b/server/src/metadata/standard-objects/view-sorts/view-sorts.metadata.ts @@ -8,7 +8,7 @@ const viewSortsMetadata = { icon: 'IconArrowsSort', fields: [ { - type: 'text', + type: 'TEXT', name: 'fieldId', label: 'Field Id', targetColumnMap: { @@ -19,7 +19,7 @@ const viewSortsMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'viewId', label: 'View Id', targetColumnMap: { @@ -30,7 +30,7 @@ const viewSortsMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'direction', label: 'Direction', targetColumnMap: { diff --git a/server/src/metadata/standard-objects/views/views.metadata.ts b/server/src/metadata/standard-objects/views/views.metadata.ts index 130a7f810..82dd3ac6f 100644 --- a/server/src/metadata/standard-objects/views/views.metadata.ts +++ b/server/src/metadata/standard-objects/views/views.metadata.ts @@ -8,7 +8,7 @@ const viewsMetadata = { icon: 'IconLayoutCollage', fields: [ { - type: 'text', + type: 'TEXT', name: 'name', label: 'Name', targetColumnMap: { @@ -19,7 +19,7 @@ const viewsMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'objectId', label: 'Object Id', targetColumnMap: { @@ -30,7 +30,7 @@ const viewsMetadata = { isNullable: false, }, { - type: 'text', + type: 'TEXT', name: 'type', label: 'Type', targetColumnMap: { diff --git a/server/src/tenant/entity-resolver/entity-resolver.module.ts b/server/src/tenant/entity-resolver/entity-resolver.module.ts deleted file mode 100644 index 723529b56..000000000 --- a/server/src/tenant/entity-resolver/entity-resolver.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; - -import { EntityResolverService } from './entity-resolver.service'; - -@Module({ - imports: [DataSourceModule], - providers: [EntityResolverService], - exports: [EntityResolverService], -}) -export class EntityResolverModule {} diff --git a/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts b/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts deleted file mode 100644 index b453d0c67..000000000 --- a/server/src/tenant/entity-resolver/entity-resolver.service.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; - -import { EntityResolverService } from './entity-resolver.service'; - -describe('EntityResolverService', () => { - let service: EntityResolverService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - EntityResolverService, - { - provide: DataSourceService, - useValue: {}, - }, - ], - }).compile(); - - service = module.get(EntityResolverService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/server/src/tenant/entity-resolver/entity-resolver.service.ts b/server/src/tenant/entity-resolver/entity-resolver.service.ts deleted file mode 100644 index 0312c3aa4..000000000 --- a/server/src/tenant/entity-resolver/entity-resolver.service.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { GraphQLResolveInfo } from 'graphql'; - -import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; - -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; - -import { PGGraphQLQueryRunner } from './pg-graphql/pg-graphql-query-runner.util'; - -@Injectable() -export class EntityResolverService { - constructor(private readonly dataSourceService: DataSourceService) {} - - async findMany( - args: { - first?: number; - last?: number; - before?: string; - after?: string; - filter?: any; - orderBy?: any; - }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const runner = new PGGraphQLQueryRunner(this.dataSourceService, { - tableName: context.tableName, - workspaceId: context.workspaceId, - info, - fields: context.fields, - }); - - return runner.findMany(args); - } - - async findOne( - args: { filter?: any }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const runner = new PGGraphQLQueryRunner(this.dataSourceService, { - tableName: context.tableName, - workspaceId: context.workspaceId, - info, - fields: context.fields, - }); - - return runner.findOne(args); - } - - async createOne( - args: { data: any }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const records = await this.createMany({ data: [args.data] }, context, info); - - return records?.[0]; - } - - async createMany( - args: { data: any[] }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const runner = new PGGraphQLQueryRunner(this.dataSourceService, { - tableName: context.tableName, - workspaceId: context.workspaceId, - info, - fields: context.fields, - }); - - return runner.createMany(args); - } - - async updateOne( - args: { id: string; data: any }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const runner = new PGGraphQLQueryRunner(this.dataSourceService, { - tableName: context.tableName, - workspaceId: context.workspaceId, - info, - fields: context.fields, - }); - - return runner.updateOne(args); - } - - async deleteOne( - args: { id: string }, - context: SchemaBuilderContext, - info: GraphQLResolveInfo, - ) { - const runner = new PGGraphQLQueryRunner(this.dataSourceService, { - tableName: context.tableName, - workspaceId: context.workspaceId, - info, - fields: context.fields, - }); - - return runner.deleteOne(args); - } -} diff --git a/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util.ts b/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util.ts deleted file mode 100644 index 60be59a87..000000000 --- a/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { GraphQLResolveInfo } from 'graphql'; -import graphqlFields from 'graphql-fields'; -import { v4 as uuidv4 } from 'uuid'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { stringifyWithoutKeyQuote } from 'src/tenant/entity-resolver/utils/stringify-without-key-quote.util'; -import { convertFieldsToGraphQL } from 'src/tenant/entity-resolver/utils/convert-fields-to-graphql.util'; -import { convertArguments } from 'src/tenant/entity-resolver/utils/convert-arguments.util'; -import { generateArgsInput } from 'src/tenant/entity-resolver/utils/generate-args-input.util'; - -type CommandArgs = { - findMany: { - first?: number; - last?: number; - before?: string; - after?: string; - filter?: any; - }; - findOne: { filter?: any }; - createMany: { data: any[] }; - updateOne: { id: string; data: any }; - deleteOne: { id: string }; -}; - -export interface PGGraphQLQueryBuilderOptions { - tableName: string; - info: GraphQLResolveInfo; - fields: FieldMetadata[]; -} - -export class PGGraphQLQueryBuilder { - private options: PGGraphQLQueryBuilderOptions; - - constructor(options: PGGraphQLQueryBuilderOptions) { - this.options = options; - } - - private getFieldsString(): string { - const select = graphqlFields(this.options.info); - - return convertFieldsToGraphQL(select, this.options.fields); - } - - // Define command setters - findMany(args?: CommandArgs['findMany']) { - const { tableName } = this.options; - const fieldsString = this.getFieldsString(); - const convertedArgs = convertArguments(args, this.options.fields); - const argsString = generateArgsInput(convertedArgs); - - return ` - query { - ${tableName}Collection${argsString ? `(${argsString})` : ''} { - ${fieldsString} - } - } - `; - } - - findOne(args: CommandArgs['findOne']) { - const { tableName } = this.options; - const fieldsString = this.getFieldsString(); - const convertedArgs = convertArguments(args, this.options.fields); - const argsString = generateArgsInput(convertedArgs); - - return ` - query { - ${tableName}Collection${argsString ? `(${argsString})` : ''} { - edges { - node { - ${fieldsString} - } - } - } - } - `; - } - - createMany(initialArgs: CommandArgs['createMany']) { - const { tableName } = this.options; - const fieldsString = this.getFieldsString(); - const args = convertArguments(initialArgs, this.options.fields); - - return ` - mutation { - insertInto${tableName}Collection(objects: ${stringifyWithoutKeyQuote( - args.data.map((datum) => ({ - id: uuidv4(), - ...datum, - })), - )}) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } - - updateOne(initialArgs: CommandArgs['updateOne']) { - const { tableName } = this.options; - const fieldsString = this.getFieldsString(); - const args = convertArguments(initialArgs, this.options.fields); - - return ` - mutation { - update${tableName}Collection(set: ${stringifyWithoutKeyQuote( - args.data, - )}, filter: { id: { eq: "${args.id}" } }) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } - - deleteOne(args: CommandArgs['deleteOne']) { - const { tableName } = this.options; - const fieldsString = this.getFieldsString(); - - return ` - mutation { - deleteFrom${tableName}Collection(filter: { id: { eq: "${args.id}" } }) { - affectedCount - records { - ${fieldsString} - } - } - } - `; - } -} diff --git a/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-runner.util.ts b/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-runner.util.ts deleted file mode 100644 index 5c7526e83..000000000 --- a/server/src/tenant/entity-resolver/pg-graphql/pg-graphql-query-runner.util.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; - -import { GraphQLResolveInfo } from 'graphql'; - -import { DataSourceService } from 'src/metadata/data-source/data-source.service'; -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { parseResult } from 'src/tenant/entity-resolver/utils/parse-result.util'; - -import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder.util'; - -interface QueryRunnerOptions { - tableName: string; - workspaceId: string; - info: GraphQLResolveInfo; - fields: FieldMetadata[]; -} - -export class PGGraphQLQueryRunner { - private queryBuilder: PGGraphQLQueryBuilder; - private options: QueryRunnerOptions; - - constructor( - private dataSourceService: DataSourceService, - options: QueryRunnerOptions, - ) { - this.queryBuilder = new PGGraphQLQueryBuilder({ - tableName: options.tableName, - info: options.info, - fields: options.fields, - }); - this.options = options; - } - - private async execute(query: string, workspaceId: string): Promise { - const workspaceDataSource = - await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); - - await workspaceDataSource?.query(` - SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; - `); - - return workspaceDataSource?.query(` - SELECT graphql.resolve($$ - ${query} - $$); - `); - } - - private parseResult(graphqlResult: any, command: string): any { - const tableName = this.options.tableName; - const entityKey = `${command}${tableName}Collection`; - const result = graphqlResult?.[0]?.resolve?.data?.[entityKey]; - - if (!result) { - throw new BadRequestException('Malformed result from GraphQL query'); - } - - return parseResult(result); - } - - async findMany(args: { - first?: number; - last?: number; - before?: string; - after?: string; - filter?: any; - orderBy?: any; - }): Promise { - const query = this.queryBuilder.findMany(args); - const result = await this.execute(query, this.options.workspaceId); - - return this.parseResult(result, ''); - } - - async findOne(args: { filter?: any }): Promise { - if (!args.filter || Object.keys(args.filter).length === 0) { - throw new BadRequestException('Missing filter argument'); - } - - const query = this.queryBuilder.findOne(args); - const result = await this.execute(query, this.options.workspaceId); - const parsedResult = this.parseResult(result, ''); - - return parsedResult?.edges?.[0]?.node; - } - - async createMany(args: { data: any[] }): Promise { - const query = this.queryBuilder.createMany(args); - const result = await this.execute(query, this.options.workspaceId); - - return this.parseResult(result, 'insertInto')?.records; - } - - async createOne(args: { data: any }): Promise { - const records = await this.createMany({ data: [args.data] }); - - return records?.[0]; - } - - async updateOne(args: { id: string; data: any }): Promise { - const query = this.queryBuilder.updateOne(args); - const result = await this.execute(query, this.options.workspaceId); - - return this.parseResult(result, 'update')?.records?.[0]; - } - - async deleteOne(args: { id: string }): Promise { - const query = this.queryBuilder.deleteOne(args); - const result = await this.execute(query, this.options.workspaceId); - - return this.parseResult(result, 'deleteFrom')?.records?.[0]; - } -} diff --git a/server/src/tenant/resolver-builder/factories/create-many-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/create-many-resolver.factory.ts new file mode 100644 index 000000000..93a827282 --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/create-many-resolver.factory.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; + +import { + CreateManyResolverArgs, + Resolver, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; + +@Injectable() +export class CreateManyResolverFactory implements FactoryInterface { + public static methodName = 'createMany' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + }); + + return runner.createMany(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/factories/create-one-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/create-one-resolver.factory.ts new file mode 100644 index 000000000..a0f743cb2 --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/create-one-resolver.factory.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { + CreateOneResolverArgs, + Resolver, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +@Injectable() +export class CreateOneResolverFactory implements FactoryInterface { + public static methodName = 'createOne' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: + internalContext.fieldMetadataCollection as FieldMetadata[], + }); + + return runner.createOne(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/factories/delete-one-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/delete-one-resolver.factory.ts new file mode 100644 index 000000000..984e1c43a --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/delete-one-resolver.factory.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { + DeleteOneResolverArgs, + Resolver, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +@Injectable() +export class DeleteOneResolverFactory implements FactoryInterface { + public static methodName = 'deleteOne' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: + internalContext.fieldMetadataCollection as FieldMetadata[], + }); + + return runner.deleteOne(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/factories/factories.ts b/server/src/tenant/resolver-builder/factories/factories.ts new file mode 100644 index 000000000..7b1d2d40b --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/factories.ts @@ -0,0 +1,28 @@ +import { FindManyResolverFactory } from './find-many-resolver.factory'; +import { FindOneResolverFactory } from './find-one-resolver.factory'; +import { CreateManyResolverFactory } from './create-many-resolver.factory'; +import { CreateOneResolverFactory } from './create-one-resolver.factory'; +import { UpdateOneResolverFactory } from './update-one-resolver.factory'; +import { DeleteOneResolverFactory } from './delete-one-resolver.factory'; + +export const resolverBuilderFactories = [ + FindManyResolverFactory, + FindOneResolverFactory, + CreateManyResolverFactory, + CreateOneResolverFactory, + UpdateOneResolverFactory, + DeleteOneResolverFactory, +]; + +export const resolverBuilderMethodNames = { + queries: [ + FindManyResolverFactory.methodName, + FindOneResolverFactory.methodName, + ], + mutations: [ + CreateManyResolverFactory.methodName, + CreateOneResolverFactory.methodName, + UpdateOneResolverFactory.methodName, + DeleteOneResolverFactory.methodName, + ], +} as const; diff --git a/server/src/tenant/resolver-builder/factories/find-many-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/find-many-resolver.factory.ts new file mode 100644 index 000000000..2b3da9709 --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/find-many-resolver.factory.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; + +import { + FindManyResolverArgs, + Resolver, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; + +@Injectable() +export class FindManyResolverFactory implements FactoryInterface { + public static methodName = 'findMany' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + }); + + return runner.findMany(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/factories/find-one-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/find-one-resolver.factory.ts new file mode 100644 index 000000000..200ecd2f5 --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/find-one-resolver.factory.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { + FindOneResolverArgs, + Resolver, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +@Injectable() +export class FindOneResolverFactory implements FactoryInterface { + public static methodName = 'findOne' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: + internalContext.fieldMetadataCollection as FieldMetadata[], + }); + + return runner.findOne(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/factories/update-one-resolver.factory.ts b/server/src/tenant/resolver-builder/factories/update-one-resolver.factory.ts new file mode 100644 index 000000000..9c9bb3119 --- /dev/null +++ b/server/src/tenant/resolver-builder/factories/update-one-resolver.factory.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; + +import { + Resolver, + UpdateOneResolverArgs, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; +import { FactoryInterface } from 'src/tenant/resolver-builder/interfaces/factory.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { PGGraphQLQueryRunner } from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +@Injectable() +export class UpdateOneResolverFactory implements FactoryInterface { + public static methodName = 'updateOne' as const; + + constructor(private readonly dataSourceService: DataSourceService) {} + + create(context: SchemaBuilderContext): Resolver { + const internalContext = context; + + return (_source, args, context, info) => { + const runner = new PGGraphQLQueryRunner(this.dataSourceService, { + targetTableName: internalContext.targetTableName, + workspaceId: internalContext.workspaceId, + info, + fieldMetadataCollection: + internalContext.fieldMetadataCollection as FieldMetadata[], + }); + + return runner.updateOne(args); + }; + } +} diff --git a/server/src/tenant/resolver-builder/interfaces/factory.interface.ts b/server/src/tenant/resolver-builder/interfaces/factory.interface.ts new file mode 100644 index 000000000..158030efa --- /dev/null +++ b/server/src/tenant/resolver-builder/interfaces/factory.interface.ts @@ -0,0 +1,7 @@ +import { SchemaBuilderContext } from 'src/tenant/schema-builder/interfaces/schema-builder-context.interface'; + +import { Resolver } from './resolvers-builder.interface'; + +export interface FactoryInterface { + create(context: SchemaBuilderContext): Resolver; +} diff --git a/server/src/tenant/resolver-builder/interfaces/pg-graphql.interface.ts b/server/src/tenant/resolver-builder/interfaces/pg-graphql.interface.ts new file mode 100644 index 000000000..464f1f890 --- /dev/null +++ b/server/src/tenant/resolver-builder/interfaces/pg-graphql.interface.ts @@ -0,0 +1,14 @@ +import { Record as IRecord } from './record.interface'; + +export interface PGGraphQLResponse { + resolve: { + data: Data; + }; +} + +export type PGGraphQLResult = [PGGraphQLResponse]; + +export interface PGGraphQLMutation { + affectedRows: number; + records: Record[]; +} diff --git a/server/src/tenant/resolver-builder/interfaces/record.interface.ts b/server/src/tenant/resolver-builder/interfaces/record.interface.ts new file mode 100644 index 000000000..18139b551 --- /dev/null +++ b/server/src/tenant/resolver-builder/interfaces/record.interface.ts @@ -0,0 +1,21 @@ +export interface Record { + id?: string; + [key: string]: any; + createdAt?: Date; + updatedAt?: Date; +} + +export type RecordFilter = { + [Property in keyof Record]: any; +}; + +export enum OrderByDirection { + AscNullsFirst = 'AscNullsFirst', + AscNullsLast = 'AscNullsLast', + DescNullsFirst = 'DescNullsFirst', + DescNullsLast = 'DescNullsLast', +} + +export type RecordOrderBy = { + [Property in keyof Record]: OrderByDirection; +}; diff --git a/server/src/tenant/resolver-builder/interfaces/resolvers-builder.interface.ts b/server/src/tenant/resolver-builder/interfaces/resolvers-builder.interface.ts new file mode 100644 index 000000000..13de6ac50 --- /dev/null +++ b/server/src/tenant/resolver-builder/interfaces/resolvers-builder.interface.ts @@ -0,0 +1,55 @@ +import { GraphQLFieldResolver } from 'graphql'; + +import { resolverBuilderMethodNames } from 'src/tenant/resolver-builder/factories/factories'; + +import { Record, RecordFilter, RecordOrderBy } from './record.interface'; + +export type Resolver = GraphQLFieldResolver; + +export interface FindManyResolverArgs< + Filter extends RecordFilter = RecordFilter, + OrderBy extends RecordOrderBy = RecordOrderBy, +> { + first?: number; + last?: number; + before?: string; + after?: string; + filter?: Filter; + orderBy?: OrderBy; +} + +export interface FindOneResolverArgs { + filter?: Filter; +} + +export interface CreateOneResolverArgs { + data: Data; +} + +export interface CreateManyResolverArgs { + data: Data[]; +} + +export interface UpdateOneResolverArgs { + id: string; + data: Data; +} + +export interface DeleteOneResolverArgs { + id: string; +} + +export type ResolverBuilderQueryMethodNames = + (typeof resolverBuilderMethodNames.queries)[number]; + +export type ResolverBuilderMutationMethodNames = + (typeof resolverBuilderMethodNames.mutations)[number]; + +export type ResolverBuilderMethodNames = + | ResolverBuilderQueryMethodNames + | ResolverBuilderMutationMethodNames; + +export interface ResolverBuilderMethods { + readonly queries: readonly ResolverBuilderQueryMethodNames[]; + readonly mutations: readonly ResolverBuilderMutationMethodNames[]; +} diff --git a/server/src/tenant/entity-resolver/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts b/server/src/tenant/resolver-builder/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts similarity index 93% rename from server/src/tenant/entity-resolver/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts rename to server/src/tenant/resolver-builder/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts index 60ef4e8a8..0905dab46 100644 --- a/server/src/tenant/entity-resolver/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts +++ b/server/src/tenant/resolver-builder/pg-graphql/__tests__/pg-graphql-query-builder.spec.ts @@ -1,13 +1,12 @@ import { GraphQLResolveInfo } from 'graphql'; -import { - FieldMetadata, - FieldMetadataTargetColumnMap, -} from 'src/metadata/field-metadata/field-metadata.entity'; +import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; import { PGGraphQLQueryBuilder, PGGraphQLQueryBuilderOptions, -} from 'src/tenant/entity-resolver/pg-graphql/pg-graphql-query-builder.util'; +} from 'src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder'; const testUUID = '123e4567-e89b-12d3-a456-426614174001'; @@ -34,7 +33,7 @@ describe('PGGraphQLQueryBuilder', () => { let mockOptions: PGGraphQLQueryBuilderOptions; beforeEach(() => { - const fields = [ + const fieldMetadataCollection = [ { name: 'name', targetColumnMap: { @@ -57,9 +56,9 @@ describe('PGGraphQLQueryBuilder', () => { ] as FieldMetadata[]; mockOptions = { - tableName: 'TestTable', + targetTableName: 'TestTable', info: {} as GraphQLResolveInfo, - fields, + fieldMetadataCollection, }; queryBuilder = new PGGraphQLQueryBuilder(mockOptions); diff --git a/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder.ts b/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder.ts new file mode 100644 index 000000000..47e7da00f --- /dev/null +++ b/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-builder.ts @@ -0,0 +1,149 @@ +import { GraphQLResolveInfo } from 'graphql'; +import graphqlFields from 'graphql-fields'; +import { v4 as uuidv4 } from 'uuid'; + +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; +import { + CreateManyResolverArgs, + DeleteOneResolverArgs, + FindManyResolverArgs, + FindOneResolverArgs, + UpdateOneResolverArgs, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { + Record as IRecord, + RecordFilter, + RecordOrderBy, +} from 'src/tenant/resolver-builder/interfaces/record.interface'; + +import { stringifyWithoutKeyQuote } from 'src/tenant/resolver-builder/utils/stringify-without-key-quote.util'; +import { convertFieldsToGraphQL } from 'src/tenant/resolver-builder/utils/convert-fields-to-graphql.util'; +import { convertArguments } from 'src/tenant/resolver-builder/utils/convert-arguments.util'; +import { generateArgsInput } from 'src/tenant/resolver-builder/utils/generate-args-input.util'; + +export interface PGGraphQLQueryBuilderOptions { + targetTableName: string; + info: GraphQLResolveInfo; + fieldMetadataCollection: FieldMetadataInterface[]; +} + +export class PGGraphQLQueryBuilder< + Record extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + OrderBy extends RecordOrderBy = RecordOrderBy, +> { + private options: PGGraphQLQueryBuilderOptions; + + constructor(options: PGGraphQLQueryBuilderOptions) { + this.options = options; + } + + private getFieldsString(): string { + const select = graphqlFields(this.options.info); + + return convertFieldsToGraphQL(select, this.options.fieldMetadataCollection); + } + + findMany(args?: FindManyResolverArgs): string { + const { targetTableName } = this.options; + const fieldsString = this.getFieldsString(); + const convertedArgs = convertArguments( + args, + this.options.fieldMetadataCollection, + ); + const argsString = generateArgsInput(convertedArgs); + + return ` + query { + ${targetTableName}Collection${argsString ? `(${argsString})` : ''} { + ${fieldsString} + } + } + `; + } + + findOne(args: FindOneResolverArgs): string { + const { targetTableName } = this.options; + const fieldsString = this.getFieldsString(); + const convertedArgs = convertArguments( + args, + this.options.fieldMetadataCollection, + ); + const argsString = generateArgsInput(convertedArgs); + + return ` + query { + ${targetTableName}Collection${argsString ? `(${argsString})` : ''} { + edges { + node { + ${fieldsString} + } + } + } + } + `; + } + + createMany(initialArgs: CreateManyResolverArgs): string { + const { targetTableName } = this.options; + const fieldsString = this.getFieldsString(); + const args = convertArguments( + initialArgs, + this.options.fieldMetadataCollection, + ); + + return ` + mutation { + insertInto${targetTableName}Collection(objects: ${stringifyWithoutKeyQuote( + args.data.map((datum) => ({ + id: uuidv4(), + ...datum, + })), + )}) { + affectedCount + records { + ${fieldsString} + } + } + } + `; + } + + updateOne(initialArgs: UpdateOneResolverArgs): string { + const { targetTableName } = this.options; + const fieldsString = this.getFieldsString(); + const args = convertArguments( + initialArgs, + this.options.fieldMetadataCollection, + ); + + return ` + mutation { + update${targetTableName}Collection(set: ${stringifyWithoutKeyQuote( + args.data, + )}, filter: { id: { eq: "${args.id}" } }) { + affectedCount + records { + ${fieldsString} + } + } + } + `; + } + + deleteOne(args: DeleteOneResolverArgs): string { + const { targetTableName } = this.options; + const fieldsString = this.getFieldsString(); + + return ` + mutation { + deleteFrom${targetTableName}Collection(filter: { id: { eq: "${args.id}" } }) { + affectedCount + records { + ${fieldsString} + } + } + } + `; + } +} diff --git a/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner.ts b/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner.ts new file mode 100644 index 000000000..254821a9e --- /dev/null +++ b/server/src/tenant/resolver-builder/pg-graphql/pg-graphql-query-runner.ts @@ -0,0 +1,148 @@ +import { BadRequestException } from '@nestjs/common'; + +import { GraphQLResolveInfo } from 'graphql'; + +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; +import { + CreateManyResolverArgs, + CreateOneResolverArgs, + DeleteOneResolverArgs, + FindManyResolverArgs, + FindOneResolverArgs, + UpdateOneResolverArgs, +} from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { + Record as IRecord, + RecordFilter, + RecordOrderBy, +} from 'src/tenant/resolver-builder/interfaces/record.interface'; +import { IConnection } from 'src/utils/pagination/interfaces/connection.interface'; +import { + PGGraphQLMutation, + PGGraphQLResult, +} from 'src/tenant/resolver-builder/interfaces/pg-graphql.interface'; + +import { DataSourceService } from 'src/metadata/data-source/data-source.service'; +import { parseResult } from 'src/tenant/resolver-builder/utils/parse-result.util'; + +import { PGGraphQLQueryBuilder } from './pg-graphql-query-builder'; + +interface QueryRunnerOptions { + targetTableName: string; + workspaceId: string; + info: GraphQLResolveInfo; + fieldMetadataCollection: FieldMetadataInterface[]; +} + +export class PGGraphQLQueryRunner< + Record extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + OrderBy extends RecordOrderBy = RecordOrderBy, +> { + private queryBuilder: PGGraphQLQueryBuilder; + private options: QueryRunnerOptions; + + constructor( + private dataSourceService: DataSourceService, + options: QueryRunnerOptions, + ) { + this.queryBuilder = new PGGraphQLQueryBuilder({ + targetTableName: options.targetTableName, + info: options.info, + fieldMetadataCollection: options.fieldMetadataCollection, + }); + this.options = options; + } + + private async execute( + query: string, + workspaceId: string, + ): Promise { + const workspaceDataSource = + await this.dataSourceService.connectToWorkspaceDataSource(workspaceId); + + await workspaceDataSource?.query(` + SET search_path TO ${this.dataSourceService.getSchemaName(workspaceId)}; + `); + + return workspaceDataSource?.query(` + SELECT graphql.resolve($$ + ${query} + $$); + `); + } + + private parseResult( + graphqlResult: PGGraphQLResult | undefined, + command: string, + ): Result { + const tableName = this.options.targetTableName; + const entityKey = `${command}${tableName}Collection`; + const result = graphqlResult?.[0]?.resolve?.data?.[entityKey]; + + if (!result) { + throw new BadRequestException('Malformed result from GraphQL query'); + } + + return parseResult(result); + } + + async findMany( + args: FindManyResolverArgs, + ): Promise | undefined> { + const query = this.queryBuilder.findMany(args); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResult>(result, ''); + } + + async findOne( + args: FindOneResolverArgs, + ): Promise { + if (!args.filter || Object.keys(args.filter).length === 0) { + throw new BadRequestException('Missing filter argument'); + } + + const query = this.queryBuilder.findOne(args); + const result = await this.execute(query, this.options.workspaceId); + const parsedResult = this.parseResult>(result, ''); + + return parsedResult?.edges?.[0]?.node; + } + + async createMany( + args: CreateManyResolverArgs, + ): Promise { + const query = this.queryBuilder.createMany(args); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResult>(result, 'insertInto') + ?.records; + } + + async createOne( + args: CreateOneResolverArgs, + ): Promise { + const records = await this.createMany({ data: [args.data] }); + + return records?.[0]; + } + + async updateOne( + args: UpdateOneResolverArgs, + ): Promise { + const query = this.queryBuilder.updateOne(args); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResult>(result, 'update') + ?.records?.[0]; + } + + async deleteOne(args: DeleteOneResolverArgs): Promise { + const query = this.queryBuilder.deleteOne(args); + const result = await this.execute(query, this.options.workspaceId); + + return this.parseResult>(result, 'deleteFrom') + ?.records?.[0]; + } +} diff --git a/server/src/tenant/resolver-builder/resolver-builder.module.ts b/server/src/tenant/resolver-builder/resolver-builder.module.ts new file mode 100644 index 000000000..a580fb463 --- /dev/null +++ b/server/src/tenant/resolver-builder/resolver-builder.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { DataSourceModule } from 'src/metadata/data-source/data-source.module'; + +import { ResolverFactory } from './resolver.factory'; + +import { resolverBuilderFactories } from './factories/factories'; + +@Module({ + imports: [DataSourceModule], + providers: [...resolverBuilderFactories, ResolverFactory], + exports: [ResolverFactory], +}) +export class ResolverBuilderModule {} diff --git a/server/src/tenant/resolver-builder/resolver.factory.ts b/server/src/tenant/resolver-builder/resolver.factory.ts new file mode 100644 index 000000000..2c4a9c890 --- /dev/null +++ b/server/src/tenant/resolver-builder/resolver.factory.ts @@ -0,0 +1,100 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { IResolvers } from '@graphql-tools/utils'; + +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { getResolverName } from 'src/tenant/utils/get-resolver-name.util'; + +import { FindManyResolverFactory } from './factories/find-many-resolver.factory'; +import { FindOneResolverFactory } from './factories/find-one-resolver.factory'; +import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; +import { CreateOneResolverFactory } from './factories/create-one-resolver.factory'; +import { UpdateOneResolverFactory } from './factories/update-one-resolver.factory'; +import { DeleteOneResolverFactory } from './factories/delete-one-resolver.factory'; +import { + ResolverBuilderMethodNames, + ResolverBuilderMethods, +} from './interfaces/resolvers-builder.interface'; +import { FactoryInterface } from './interfaces/factory.interface'; + +@Injectable() +export class ResolverFactory { + private readonly logger = new Logger(ResolverFactory.name); + + constructor( + private readonly findManyResolverFactory: FindManyResolverFactory, + private readonly findOneResolverFactory: FindOneResolverFactory, + private readonly createManyResolverFactory: CreateManyResolverFactory, + private readonly createOneResolverFactory: CreateOneResolverFactory, + private readonly updateOneResolverFactory: UpdateOneResolverFactory, + private readonly deleteOneResolverFactory: DeleteOneResolverFactory, + ) {} + + async create( + workspaceId: string, + objectMetadataCollection: ObjectMetadataInterface[], + resolverBuilderMethods: ResolverBuilderMethods, + ): Promise { + const factories = new Map([ + ['findMany', this.findManyResolverFactory], + ['findOne', this.findOneResolverFactory], + ['createMany', this.createManyResolverFactory], + ['createOne', this.createOneResolverFactory], + ['updateOne', this.updateOneResolverFactory], + ['deleteOne', this.deleteOneResolverFactory], + ]); + const resolvers: IResolvers = { + Query: {}, + Mutation: {}, + }; + + for (const objectMetadata of objectMetadataCollection) { + // Generate query resolvers + for (const methodName of resolverBuilderMethods.queries) { + const resolverName = getResolverName(objectMetadata, methodName); + const resolverFactory = factories.get(methodName); + + if (!resolverFactory) { + this.logger.error(`Unknown query resolver type: ${methodName}`, { + objectMetadata, + methodName, + resolverName, + }); + + throw new Error(`Unknown query resolver type: ${methodName}`); + } + + resolvers.Query[resolverName] = resolverFactory.create({ + workspaceId, + targetTableName: objectMetadata.targetTableName, + fieldMetadataCollection: objectMetadata.fields, + }); + } + + // Generate mutation resolvers + for (const methodName of resolverBuilderMethods.mutations) { + const resolverName = getResolverName(objectMetadata, methodName); + const resolverFactory = factories.get(methodName); + + if (!resolverFactory) { + this.logger.error(`Unknown mutation resolver type: ${methodName}`, { + objectMetadata, + methodName, + resolverName, + }); + + throw new Error(`Unknown mutation resolver type: ${methodName}`); + } + + resolvers.Mutation[resolverName] = resolverFactory.create({ + workspaceId, + targetTableName: objectMetadata.targetTableName, + fieldMetadataCollection: objectMetadata.fields, + }); + } + } + + return resolvers; + } +} diff --git a/server/src/tenant/entity-resolver/utils/__tests__/convert-arguments.spec.ts b/server/src/tenant/resolver-builder/utils/__tests__/convert-arguments.spec.ts similarity index 87% rename from server/src/tenant/entity-resolver/utils/__tests__/convert-arguments.spec.ts rename to server/src/tenant/resolver-builder/utils/__tests__/convert-arguments.spec.ts index 7f1a152cc..582b9aeee 100644 --- a/server/src/tenant/entity-resolver/utils/__tests__/convert-arguments.spec.ts +++ b/server/src/tenant/resolver-builder/utils/__tests__/convert-arguments.spec.ts @@ -1,8 +1,10 @@ +import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; + import { FieldMetadata, - FieldMetadataTargetColumnMap, + FieldMetadataType, } from 'src/metadata/field-metadata/field-metadata.entity'; -import { convertArguments } from 'src/tenant/entity-resolver/utils/convert-arguments.util'; +import { convertArguments } from 'src/tenant/resolver-builder/utils/convert-arguments.util'; describe('convertArguments', () => { let fields; @@ -14,14 +16,14 @@ describe('convertArguments', () => { targetColumnMap: { value: 'column_1randomFirstNameKey', } as FieldMetadataTargetColumnMap, - type: 'text', + type: FieldMetadataType.TEXT, }, { name: 'age', targetColumnMap: { value: 'column_randomAgeKey', } as FieldMetadataTargetColumnMap, - type: 'text', + type: FieldMetadataType.TEXT, }, { name: 'website', @@ -29,7 +31,7 @@ describe('convertArguments', () => { link: 'column_randomLinkKey', text: 'column_randomTex7Key', } as FieldMetadataTargetColumnMap, - type: 'url', + type: FieldMetadataType.URL, }, ] as FieldMetadata[]; }); diff --git a/server/src/tenant/entity-resolver/utils/__tests__/convert-fields-to-graphql.spec.ts b/server/src/tenant/resolver-builder/utils/__tests__/convert-fields-to-graphql.spec.ts similarity index 90% rename from server/src/tenant/entity-resolver/utils/__tests__/convert-fields-to-graphql.spec.ts rename to server/src/tenant/resolver-builder/utils/__tests__/convert-fields-to-graphql.spec.ts index bb75195dd..54c7a9ec4 100644 --- a/server/src/tenant/entity-resolver/utils/__tests__/convert-fields-to-graphql.spec.ts +++ b/server/src/tenant/resolver-builder/utils/__tests__/convert-fields-to-graphql.spec.ts @@ -1,8 +1,7 @@ -import { - FieldMetadata, - FieldMetadataTargetColumnMap, -} from 'src/metadata/field-metadata/field-metadata.entity'; -import { convertFieldsToGraphQL } from 'src/tenant/entity-resolver/utils/convert-fields-to-graphql.util'; +import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { convertFieldsToGraphQL } from 'src/tenant/resolver-builder/utils/convert-fields-to-graphql.util'; const normalizeWhitespace = (str) => str.replace(/\s+/g, ' ').trim(); diff --git a/server/src/tenant/entity-resolver/utils/__tests__/generate-args-input.spec.ts b/server/src/tenant/resolver-builder/utils/__tests__/generate-args-input.spec.ts similarity index 94% rename from server/src/tenant/entity-resolver/utils/__tests__/generate-args-input.spec.ts rename to server/src/tenant/resolver-builder/utils/__tests__/generate-args-input.spec.ts index db702ea66..cbce15772 100644 --- a/server/src/tenant/entity-resolver/utils/__tests__/generate-args-input.spec.ts +++ b/server/src/tenant/resolver-builder/utils/__tests__/generate-args-input.spec.ts @@ -1,4 +1,4 @@ -import { generateArgsInput } from 'src/tenant/entity-resolver/utils/generate-args-input.util'; +import { generateArgsInput } from 'src/tenant/resolver-builder/utils/generate-args-input.util'; const normalizeWhitespace = (str) => str.replace(/\s+/g, ''); diff --git a/server/src/tenant/entity-resolver/utils/__tests__/parse-result.spec.ts b/server/src/tenant/resolver-builder/utils/__tests__/parse-result.spec.ts similarity index 97% rename from server/src/tenant/entity-resolver/utils/__tests__/parse-result.spec.ts rename to server/src/tenant/resolver-builder/utils/__tests__/parse-result.spec.ts index 38a1b33a2..442591c07 100644 --- a/server/src/tenant/entity-resolver/utils/__tests__/parse-result.spec.ts +++ b/server/src/tenant/resolver-builder/utils/__tests__/parse-result.spec.ts @@ -2,7 +2,7 @@ import { isSpecialKey, handleSpecialKey, parseResult, -} from 'src/tenant/entity-resolver/utils/parse-result.util'; +} from 'src/tenant/resolver-builder/utils/parse-result.util'; describe('isSpecialKey', () => { test('should return true if the key starts with "___"', () => { diff --git a/server/src/tenant/entity-resolver/utils/__tests__/stringify-without-key-quote.spec.ts b/server/src/tenant/resolver-builder/utils/__tests__/stringify-without-key-quote.spec.ts similarity index 93% rename from server/src/tenant/entity-resolver/utils/__tests__/stringify-without-key-quote.spec.ts rename to server/src/tenant/resolver-builder/utils/__tests__/stringify-without-key-quote.spec.ts index ce1d8c40b..c450afc17 100644 --- a/server/src/tenant/entity-resolver/utils/__tests__/stringify-without-key-quote.spec.ts +++ b/server/src/tenant/resolver-builder/utils/__tests__/stringify-without-key-quote.spec.ts @@ -1,4 +1,4 @@ -import { stringifyWithoutKeyQuote } from 'src/tenant/entity-resolver/utils/stringify-without-key-quote.util'; +import { stringifyWithoutKeyQuote } from 'src/tenant/resolver-builder/utils/stringify-without-key-quote.util'; describe('stringifyWithoutKeyQuote', () => { test('should stringify object correctly without quotes around keys', () => { diff --git a/server/src/tenant/entity-resolver/utils/convert-arguments.util.ts b/server/src/tenant/resolver-builder/utils/convert-arguments.util.ts similarity index 85% rename from server/src/tenant/entity-resolver/utils/convert-arguments.util.ts rename to server/src/tenant/resolver-builder/utils/convert-arguments.util.ts index d90b5b152..c08000889 100644 --- a/server/src/tenant/entity-resolver/utils/convert-arguments.util.ts +++ b/server/src/tenant/resolver-builder/utils/convert-arguments.util.ts @@ -1,6 +1,9 @@ -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; -export const convertArguments = (args: any, fields: FieldMetadata[]): any => { +export const convertArguments = ( + args: any, + fields: FieldMetadataInterface[], +): any => { const fieldsMap = new Map( fields.map((metadata) => [metadata.name, metadata]), ); diff --git a/server/src/tenant/entity-resolver/utils/convert-fields-to-graphql.util.ts b/server/src/tenant/resolver-builder/utils/convert-fields-to-graphql.util.ts similarity index 91% rename from server/src/tenant/entity-resolver/utils/convert-fields-to-graphql.util.ts rename to server/src/tenant/resolver-builder/utils/convert-fields-to-graphql.util.ts index ce1647205..b9c6def08 100644 --- a/server/src/tenant/entity-resolver/utils/convert-fields-to-graphql.util.ts +++ b/server/src/tenant/resolver-builder/utils/convert-fields-to-graphql.util.ts @@ -1,10 +1,10 @@ import isEmpty from 'lodash.isempty'; -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; export const convertFieldsToGraphQL = ( select: any, - fields: FieldMetadata[], + fields: FieldMetadataInterface[], acc = '', ) => { const fieldsMap = new Map( diff --git a/server/src/tenant/entity-resolver/utils/generate-args-input.util.ts b/server/src/tenant/resolver-builder/utils/generate-args-input.util.ts similarity index 100% rename from server/src/tenant/entity-resolver/utils/generate-args-input.util.ts rename to server/src/tenant/resolver-builder/utils/generate-args-input.util.ts diff --git a/server/src/tenant/entity-resolver/utils/parse-result.util.ts b/server/src/tenant/resolver-builder/utils/parse-result.util.ts similarity index 100% rename from server/src/tenant/entity-resolver/utils/parse-result.util.ts rename to server/src/tenant/resolver-builder/utils/parse-result.util.ts diff --git a/server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts b/server/src/tenant/resolver-builder/utils/stringify-without-key-quote.util.ts similarity index 100% rename from server/src/tenant/entity-resolver/utils/stringify-without-key-quote.util.ts rename to server/src/tenant/resolver-builder/utils/stringify-without-key-quote.util.ts diff --git a/server/src/tenant/schema-builder/factories/args.factory.ts b/server/src/tenant/schema-builder/factories/args.factory.ts new file mode 100644 index 000000000..17c26cf8c --- /dev/null +++ b/server/src/tenant/schema-builder/factories/args.factory.ts @@ -0,0 +1,98 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLFieldConfigArgumentMap } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ArgsMetadata } from 'src/tenant/schema-builder/interfaces/param-metadata.interface'; + +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; +import { TypeMapperService } from 'src/tenant/schema-builder/services/type-mapper.service'; + +@Injectable() +export class ArgsFactory { + private readonly logger = new Logger(ArgsFactory.name); + + constructor( + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + private readonly typeMapperService: TypeMapperService, + ) {} + + public create( + { args, objectMetadata }: ArgsMetadata, + options: BuildSchemaOptions, + ): GraphQLFieldConfigArgumentMap { + const fieldConfigMap: GraphQLFieldConfigArgumentMap = {}; + + for (const key in args) { + if (!args.hasOwnProperty(key)) { + continue; + } + const arg = args[key]; + + // Argument is a scalar type + if (arg.type) { + const fieldType = this.typeMapperService.mapToScalarType( + arg.type, + options.dateScalarMode, + options.numberScalarMode, + ); + + if (!fieldType) { + this.logger.error( + `Could not find a GraphQL type for ${arg.type.toString()}`, + { + arg, + options, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${arg.type.toString()}`, + ); + } + + const gqlType = this.typeMapperService.mapToGqlType(fieldType, { + nullable: arg.isNullable, + isArray: arg.isArray, + }); + + fieldConfigMap[key] = { + type: gqlType, + }; + } + + // Argument is an input type + if (arg.kind) { + const inputType = this.typeDefinitionsStorage.getInputTypeByKey( + objectMetadata.id, + arg.kind, + ); + + if (!inputType) { + this.logger.error( + `Could not find a GraphQL input type for ${objectMetadata.id}`, + { + objectMetadata, + options, + }, + ); + + throw new Error( + `Could not find a GraphQL input type for ${objectMetadata.id}`, + ); + } + + const gqlType = this.typeMapperService.mapToGqlType(inputType, { + nullable: arg.isNullable, + isArray: arg.isArray, + }); + + fieldConfigMap[key] = { + type: gqlType, + }; + } + } + + return fieldConfigMap; + } +} diff --git a/server/src/tenant/schema-builder/factories/connection-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/connection-type-definition.factory.ts new file mode 100644 index 000000000..bb68e7e20 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/connection-type-definition.factory.ts @@ -0,0 +1,76 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; + +import { + ObjectTypeDefinition, + ObjectTypeDefinitionKind, +} from './object-type-definition.factory'; +import { ConnectionTypeFactory } from './connection-type.factory'; + +export enum ConnectionTypeDefinitionKind { + Edge = 'Edge', + PageInfo = 'PageInfo', +} + +@Injectable() +export class ConnectionTypeDefinitionFactory { + private readonly logger = new Logger(ConnectionTypeDefinitionFactory.name); + + constructor(private readonly connectionTypeFactory: ConnectionTypeFactory) {} + + public create( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): ObjectTypeDefinition { + const kind = ObjectTypeDefinitionKind.Connection; + + return { + target: objectMetadata.id, + kind, + type: new GraphQLObjectType({ + name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, + description: objectMetadata.description, + fields: this.generateFields(objectMetadata, options), + }), + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): GraphQLFieldConfigMap { + const fields: GraphQLFieldConfigMap = {}; + + fields.edges = { + type: this.connectionTypeFactory.create( + objectMetadata, + ConnectionTypeDefinitionKind.Edge, + options, + { + isArray: true, + arrayDepth: 1, + nullable: false, + }, + ), + }; + + fields.pageInfo = { + type: this.connectionTypeFactory.create( + objectMetadata, + ConnectionTypeDefinitionKind.PageInfo, + options, + { + nullable: false, + }, + ), + }; + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/connection-type.factory.ts b/server/src/tenant/schema-builder/factories/connection-type.factory.ts new file mode 100644 index 000000000..f74dbea1b --- /dev/null +++ b/server/src/tenant/schema-builder/factories/connection-type.factory.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLOutputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; +import { PageInfoType } from 'src/tenant/schema-builder/graphql-types/object'; + +import { ConnectionTypeDefinitionKind } from './connection-type-definition.factory'; +import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; + +@Injectable() +export class ConnectionTypeFactory { + private readonly logger = new Logger(ConnectionTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + objectMetadata: ObjectMetadataInterface, + kind: ConnectionTypeDefinitionKind, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLOutputType { + if (kind === ConnectionTypeDefinitionKind.PageInfo) { + return this.typeMapperService.mapToGqlType(PageInfoType, typeOptions); + } + + const edgeType = this.typeDefinitionsStorage.getObjectTypeByKey( + objectMetadata.id, + kind as unknown as ObjectTypeDefinitionKind, + ); + + if (!edgeType) { + this.logger.error( + `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + { + objectMetadata, + buildOtions, + }, + ); + + throw new Error( + `Edge type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + ); + } + + return this.typeMapperService.mapToGqlType(edgeType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/edge-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/edge-type-definition.factory.ts new file mode 100644 index 000000000..55bf440c5 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/edge-type-definition.factory.ts @@ -0,0 +1,74 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; + +import { + ObjectTypeDefinition, + ObjectTypeDefinitionKind, +} from './object-type-definition.factory'; +import { EdgeTypeFactory } from './edge-type.factory'; + +export enum EdgeTypeDefinitionKind { + Node = 'Node', + Cursor = 'Cursor', +} + +@Injectable() +export class EdgeTypeDefinitionFactory { + private readonly logger = new Logger(EdgeTypeDefinitionFactory.name); + + constructor(private readonly edgeTypeFactory: EdgeTypeFactory) {} + + public create( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): ObjectTypeDefinition { + const kind = ObjectTypeDefinitionKind.Edge; + + return { + target: objectMetadata.id, + kind, + type: new GraphQLObjectType({ + name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, + description: objectMetadata.description, + fields: this.generateFields(objectMetadata, options), + }), + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): GraphQLFieldConfigMap { + const fields: GraphQLFieldConfigMap = {}; + + fields.node = { + type: this.edgeTypeFactory.create( + objectMetadata, + EdgeTypeDefinitionKind.Node, + options, + { + nullable: false, + }, + ), + }; + + fields.cursor = { + type: this.edgeTypeFactory.create( + objectMetadata, + EdgeTypeDefinitionKind.Cursor, + options, + { + nullable: false, + }, + ), + }; + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/edge-type.factory.ts b/server/src/tenant/schema-builder/factories/edge-type.factory.ts new file mode 100644 index 000000000..1e90d0187 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/edge-type.factory.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLOutputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; +import { CursorScalarType } from 'src/tenant/schema-builder/graphql-types/scalars'; + +import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; +import { EdgeTypeDefinitionKind } from './edge-type-definition.factory'; + +@Injectable() +export class EdgeTypeFactory { + private readonly logger = new Logger(EdgeTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + objectMetadata: ObjectMetadataInterface, + kind: EdgeTypeDefinitionKind, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLOutputType { + if (kind === EdgeTypeDefinitionKind.Cursor) { + return this.typeMapperService.mapToGqlType(CursorScalarType, typeOptions); + } + + const objectType = this.typeDefinitionsStorage.getObjectTypeByKey( + objectMetadata.id, + ObjectTypeDefinitionKind.Plain, + ); + + if (!objectType) { + this.logger.error( + `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + { + objectMetadata, + buildOtions, + }, + ); + + throw new Error( + `Node type for ${objectMetadata.nameSingular} was not found. Please, check if you have defined it.`, + ); + } + + return this.typeMapperService.mapToGqlType(objectType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/factories.ts b/server/src/tenant/schema-builder/factories/factories.ts new file mode 100644 index 000000000..1ea47e1b7 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/factories.ts @@ -0,0 +1,35 @@ +import { ArgsFactory } from './args.factory'; +import { InputTypeFactory } from './input-type.factory'; +import { InputTypeDefinitionFactory } from './input-type-definition.factory'; +import { ObjectTypeDefinitionFactory } from './object-type-definition.factory'; +import { OutputTypeFactory } from './output-type.factory'; +import { QueryTypeFactory } from './query-type.factory'; +import { RootTypeFactory } from './root-type.factory'; +import { FilterTypeFactory } from './filter-type.factory'; +import { FilterTypeDefinitionFactory } from './filter-type-definition.factory'; +import { ConnectionTypeFactory } from './connection-type.factory'; +import { ConnectionTypeDefinitionFactory } from './connection-type-definition.factory'; +import { EdgeTypeFactory } from './edge-type.factory'; +import { EdgeTypeDefinitionFactory } from './edge-type-definition.factory'; +import { MutationTypeFactory } from './mutation-type.factory'; +import { OrderByTypeFactory } from './order-by-type.factory'; +import { OrderByTypeDefinitionFactory } from './order-by-type-definition.factory'; + +export const schemaBuilderFactories = [ + ArgsFactory, + InputTypeFactory, + InputTypeDefinitionFactory, + OutputTypeFactory, + ObjectTypeDefinitionFactory, + FilterTypeFactory, + FilterTypeDefinitionFactory, + OrderByTypeFactory, + OrderByTypeDefinitionFactory, + ConnectionTypeFactory, + ConnectionTypeDefinitionFactory, + EdgeTypeFactory, + EdgeTypeDefinitionFactory, + RootTypeFactory, + QueryTypeFactory, + MutationTypeFactory, +]; diff --git a/server/src/tenant/schema-builder/factories/filter-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/filter-type-definition.factory.ts new file mode 100644 index 000000000..6b330b2c6 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/filter-type-definition.factory.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { TypeMapperService } from 'src/tenant/schema-builder/services/type-mapper.service'; + +import { FilterTypeFactory } from './filter-type.factory'; +import { + InputTypeDefinition, + InputTypeDefinitionKind, +} from './input-type-definition.factory'; + +@Injectable() +export class FilterTypeDefinitionFactory { + constructor( + private readonly filterTypeFactory: FilterTypeFactory, + private readonly typeMapperService: TypeMapperService, + ) {} + + public create( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): InputTypeDefinition { + const kind = InputTypeDefinitionKind.Filter; + const filterInputType = new GraphQLInputObjectType({ + name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}Input`, + description: objectMetadata.description, + fields: () => { + const andOrType = this.typeMapperService.mapToGqlType(filterInputType, { + isArray: true, + arrayDepth: 1, + nullable: true, + }); + + return { + ...this.generateFields(objectMetadata, options), + and: { + type: andOrType, + }, + or: { + type: andOrType, + }, + not: { + type: this.typeMapperService.mapToGqlType(filterInputType, { + nullable: true, + }), + }, + }; + }, + }); + + return { + target: objectMetadata.id, + kind, + type: filterInputType, + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): GraphQLInputFieldConfigMap { + const fields: GraphQLInputFieldConfigMap = {}; + + objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => { + const type = this.filterTypeFactory.create(fieldMetadata, options, { + nullable: fieldMetadata.isNullable, + }); + + fields[fieldMetadata.name] = { + type, + description: fieldMetadata.description, + // TODO: Add default value + defaultValue: undefined, + }; + }); + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/filter-type.factory.ts b/server/src/tenant/schema-builder/factories/filter-type.factory.ts new file mode 100644 index 000000000..36d1084be --- /dev/null +++ b/server/src/tenant/schema-builder/factories/filter-type.factory.ts @@ -0,0 +1,60 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLInputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; + +import { InputTypeDefinitionKind } from './input-type-definition.factory'; + +@Injectable() +export class FilterTypeFactory { + private readonly logger = new Logger(FilterTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + fieldMetadata: FieldMetadataInterface, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLInputType { + let filterType = this.typeMapperService.mapToFilterType( + fieldMetadata.type, + buildOtions.dateScalarMode, + buildOtions.numberScalarMode, + ); + + if (!filterType) { + filterType = this.typeDefinitionsStorage.getInputTypeByKey( + fieldMetadata.type.toString(), + InputTypeDefinitionKind.Filter, + ); + + if (!filterType) { + this.logger.error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + { + fieldMetadata, + buildOtions, + typeOptions, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + ); + } + } + + return this.typeMapperService.mapToGqlType(filterType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/input-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/input-type-definition.factory.ts new file mode 100644 index 000000000..9dcb3e0e6 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/input-type-definition.factory.ts @@ -0,0 +1,70 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +import { InputTypeFactory } from './input-type.factory'; + +export enum InputTypeDefinitionKind { + Create = 'Create', + Update = 'Update', + Filter = 'Filter', + OrderBy = 'OrderBy', +} + +export interface InputTypeDefinition { + target: string; + kind: InputTypeDefinitionKind; + type: GraphQLInputObjectType; +} + +@Injectable() +export class InputTypeDefinitionFactory { + constructor(private readonly inputTypeFactory: InputTypeFactory) {} + + public create( + objectMetadata: ObjectMetadataInterface, + kind: InputTypeDefinitionKind, + options: BuildSchemaOptions, + ): InputTypeDefinition { + return { + target: objectMetadata.id, + kind, + type: new GraphQLInputObjectType({ + name: `${pascalCase( + objectMetadata.nameSingular, + )}${kind.toString()}Input`, + description: objectMetadata.description, + fields: this.generateFields(objectMetadata, kind, options), + }), + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + kind: InputTypeDefinitionKind, + options: BuildSchemaOptions, + ): GraphQLInputFieldConfigMap { + const fields: GraphQLInputFieldConfigMap = {}; + + objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => { + const type = this.inputTypeFactory.create(fieldMetadata, kind, options, { + nullable: fieldMetadata.isNullable, + }); + + fields[fieldMetadata.name] = { + type, + description: fieldMetadata.description, + // TODO: Add default value + defaultValue: undefined, + }; + }); + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/input-type.factory.ts b/server/src/tenant/schema-builder/factories/input-type.factory.ts new file mode 100644 index 000000000..804ae43b0 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/input-type.factory.ts @@ -0,0 +1,63 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLInputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; + +import { InputTypeDefinitionKind } from './input-type-definition.factory'; + +@Injectable() +export class InputTypeFactory { + private readonly logger = new Logger(InputTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + fieldMetadata: FieldMetadataInterface, + kind: InputTypeDefinitionKind, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLInputType { + let inputType: GraphQLInputType | undefined = + this.typeMapperService.mapToScalarType( + fieldMetadata.type, + buildOtions.dateScalarMode, + buildOtions.numberScalarMode, + ); + + if (!inputType) { + inputType = this.typeDefinitionsStorage.getInputTypeByKey( + fieldMetadata.type.toString(), + kind, + ); + + if (!inputType) { + this.logger.error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + { + fieldMetadata, + kind, + buildOtions, + typeOptions, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + ); + } + } + + return this.typeMapperService.mapToGqlType(inputType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/mutation-type.factory.ts b/server/src/tenant/schema-builder/factories/mutation-type.factory.ts new file mode 100644 index 000000000..a7d42b79f --- /dev/null +++ b/server/src/tenant/schema-builder/factories/mutation-type.factory.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ResolverBuilderMutationMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { ObjectTypeName, RootTypeFactory } from './root-type.factory'; + +@Injectable() +export class MutationTypeFactory { + constructor(private readonly rootTypeFactory: RootTypeFactory) {} + create( + objectMetadataCollection: ObjectMetadataInterface[], + resolverMethodNames: ResolverBuilderMutationMethodNames[], + options: BuildSchemaOptions, + ): GraphQLObjectType { + return this.rootTypeFactory.create( + objectMetadataCollection, + resolverMethodNames, + ObjectTypeName.Mutation, + options, + ); + } +} diff --git a/server/src/tenant/schema-builder/factories/object-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/object-type-definition.factory.ts new file mode 100644 index 000000000..336ee7600 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/object-type-definition.factory.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { pascalCase } from 'src/utils/pascal-case'; + +import { OutputTypeFactory } from './output-type.factory'; + +export enum ObjectTypeDefinitionKind { + Connection = 'Connection', + Edge = 'Edge', + Plain = '', +} + +export interface ObjectTypeDefinition { + target: string; + kind: ObjectTypeDefinitionKind; + type: GraphQLObjectType; +} + +@Injectable() +export class ObjectTypeDefinitionFactory { + constructor(private readonly outputTypeFactory: OutputTypeFactory) {} + + public create( + objectMetadata: ObjectMetadataInterface, + kind: ObjectTypeDefinitionKind, + options: BuildSchemaOptions, + ): ObjectTypeDefinition { + return { + target: objectMetadata.id, + kind, + type: new GraphQLObjectType({ + name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, + description: objectMetadata.description, + fields: this.generateFields(objectMetadata, kind, options), + }), + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + kind: ObjectTypeDefinitionKind, + options: BuildSchemaOptions, + ): GraphQLFieldConfigMap { + const fields: GraphQLFieldConfigMap = {}; + + objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => { + const type = this.outputTypeFactory.create(fieldMetadata, kind, options, { + nullable: fieldMetadata.isNullable, + }); + + fields[fieldMetadata.name] = { + type, + description: fieldMetadata.description, + }; + }); + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/order-by-type-definition.factory.ts b/server/src/tenant/schema-builder/factories/order-by-type-definition.factory.ts new file mode 100644 index 000000000..74452fd5b --- /dev/null +++ b/server/src/tenant/schema-builder/factories/order-by-type-definition.factory.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { pascalCase } from 'src/utils/pascal-case'; +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; + +import { + InputTypeDefinition, + InputTypeDefinitionKind, +} from './input-type-definition.factory'; +import { OrderByTypeFactory } from './order-by-type.factory'; + +@Injectable() +export class OrderByTypeDefinitionFactory { + constructor(private readonly orderByTypeFactory: OrderByTypeFactory) {} + + public create( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): InputTypeDefinition { + const kind = InputTypeDefinitionKind.OrderBy; + + return { + target: objectMetadata.id, + kind, + type: new GraphQLInputObjectType({ + name: `${pascalCase( + objectMetadata.nameSingular, + )}${kind.toString()}Input`, + description: objectMetadata.description, + fields: this.generateFields(objectMetadata, options), + }), + }; + } + + private generateFields( + objectMetadata: ObjectMetadataInterface, + options: BuildSchemaOptions, + ): GraphQLInputFieldConfigMap { + const fields: GraphQLInputFieldConfigMap = {}; + + objectMetadata.fields.forEach((fieldMetadata: FieldMetadata) => { + const type = this.orderByTypeFactory.create(fieldMetadata, options, { + nullable: fieldMetadata.isNullable, + }); + + fields[fieldMetadata.name] = { + type, + description: fieldMetadata.description, + // TODO: Add default value + defaultValue: undefined, + }; + }); + + return fields; + } +} diff --git a/server/src/tenant/schema-builder/factories/order-by-type.factory.ts b/server/src/tenant/schema-builder/factories/order-by-type.factory.ts new file mode 100644 index 000000000..5db2d2256 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/order-by-type.factory.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLInputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; + +import { InputTypeDefinitionKind } from './input-type-definition.factory'; + +@Injectable() +export class OrderByTypeFactory { + private readonly logger = new Logger(OrderByTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + fieldMetadata: FieldMetadataInterface, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLInputType { + let orderByType = this.typeMapperService.mapToOrderByType( + fieldMetadata.type, + ); + + if (!orderByType) { + orderByType = this.typeDefinitionsStorage.getInputTypeByKey( + fieldMetadata.type.toString(), + InputTypeDefinitionKind.OrderBy, + ); + + if (!orderByType) { + this.logger.error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + { + fieldMetadata, + buildOtions, + typeOptions, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + ); + } + } + + return this.typeMapperService.mapToGqlType(orderByType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/output-type.factory.ts b/server/src/tenant/schema-builder/factories/output-type.factory.ts new file mode 100644 index 000000000..79be5d7ba --- /dev/null +++ b/server/src/tenant/schema-builder/factories/output-type.factory.ts @@ -0,0 +1,62 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLOutputType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { FieldMetadataInterface } from 'src/tenant/schema-builder/interfaces/field-metadata.interface'; + +import { + TypeMapperService, + TypeOptions, +} from 'src/tenant/schema-builder/services/type-mapper.service'; +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; + +import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; + +@Injectable() +export class OutputTypeFactory { + private readonly logger = new Logger(OutputTypeFactory.name); + + constructor( + private readonly typeMapperService: TypeMapperService, + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + ) {} + + public create( + fieldMetadata: FieldMetadataInterface, + kind: ObjectTypeDefinitionKind, + buildOtions: BuildSchemaOptions, + typeOptions: TypeOptions, + ): GraphQLOutputType { + let gqlType: GraphQLOutputType | undefined = + this.typeMapperService.mapToScalarType( + fieldMetadata.type, + buildOtions.dateScalarMode, + buildOtions.numberScalarMode, + ); + + if (!gqlType) { + gqlType = this.typeDefinitionsStorage.getObjectTypeByKey( + fieldMetadata.type.toString(), + kind, + ); + + if (!gqlType) { + this.logger.error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + { + fieldMetadata, + buildOtions, + typeOptions, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${fieldMetadata.type.toString()}`, + ); + } + } + + return this.typeMapperService.mapToGqlType(gqlType, typeOptions); + } +} diff --git a/server/src/tenant/schema-builder/factories/query-type.factory.ts b/server/src/tenant/schema-builder/factories/query-type.factory.ts new file mode 100644 index 000000000..3dce50c7c --- /dev/null +++ b/server/src/tenant/schema-builder/factories/query-type.factory.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ResolverBuilderQueryMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { ObjectTypeName, RootTypeFactory } from './root-type.factory'; + +@Injectable() +export class QueryTypeFactory { + constructor(private readonly rootTypeFactory: RootTypeFactory) {} + create( + objectMetadataCollection: ObjectMetadataInterface[], + resolverMethodNames: ResolverBuilderQueryMethodNames[], + options: BuildSchemaOptions, + ): GraphQLObjectType { + return this.rootTypeFactory.create( + objectMetadataCollection, + resolverMethodNames, + ObjectTypeName.Query, + options, + ); + } +} diff --git a/server/src/tenant/schema-builder/factories/root-type.factory.ts b/server/src/tenant/schema-builder/factories/root-type.factory.ts new file mode 100644 index 000000000..7d05f43b4 --- /dev/null +++ b/server/src/tenant/schema-builder/factories/root-type.factory.ts @@ -0,0 +1,112 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; + +import { BuildSchemaOptions } from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; +import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { TypeDefinitionsStorage } from 'src/tenant/schema-builder/storages/type-definitions.storage'; +import { getResolverName } from 'src/tenant/utils/get-resolver-name.util'; +import { getResolverArgs } from 'src/tenant/schema-builder/utils/get-resolver-args.util'; + +import { ArgsFactory } from './args.factory'; +import { ObjectTypeDefinitionKind } from './object-type-definition.factory'; + +export enum ObjectTypeName { + Query = 'Query', + Mutation = 'Mutation', + Subscription = 'Subscription', +} + +@Injectable() +export class RootTypeFactory { + private readonly logger = new Logger(RootTypeFactory.name); + + constructor( + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + private readonly argsFactory: ArgsFactory, + ) {} + + create( + objectMetadataCollection: ObjectMetadataInterface[], + resolverMethodNames: ResolverBuilderMethodNames[], + objectTypeName: ObjectTypeName, + options: BuildSchemaOptions, + ): GraphQLObjectType { + if (resolverMethodNames.length === 0) { + this.logger.error( + `No resolver methods were found for ${objectTypeName.toString()}`, + { + resolverMethodNames, + objectTypeName, + options, + }, + ); + + throw new Error( + `No resolvers were found for ${objectTypeName.toString()}`, + ); + } + + return new GraphQLObjectType({ + name: objectTypeName.toString(), + fields: this.generateFields( + objectMetadataCollection, + resolverMethodNames, + options, + ), + }); + } + + private generateFields( + objectMetadataCollection: ObjectMetadataInterface[], + resolverMethodNames: ResolverBuilderMethodNames[], + options: BuildSchemaOptions, + ): GraphQLFieldConfigMap { + const fieldConfigMap: GraphQLFieldConfigMap = {}; + + for (const objectMetadata of objectMetadataCollection) { + for (const methodName of resolverMethodNames) { + const name = getResolverName(objectMetadata, methodName); + const args = getResolverArgs(methodName); + const outputType = this.typeDefinitionsStorage.getObjectTypeByKey( + objectMetadata.id, + methodName === 'findMany' + ? ObjectTypeDefinitionKind.Connection + : ObjectTypeDefinitionKind.Plain, + ); + const argsType = this.argsFactory.create( + { + args, + objectMetadata, + }, + options, + ); + + if (!outputType) { + this.logger.error( + `Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`, + { + objectMetadata, + methodName, + options, + }, + ); + + throw new Error( + `Could not find a GraphQL type for ${objectMetadata.id} for method ${methodName}`, + ); + } + + fieldConfigMap[name] = { + type: outputType, + args: argsType, + resolve: undefined, + }; + } + } + + return fieldConfigMap; + } +} diff --git a/server/src/tenant/schema-builder/graphql-schema.factory.ts b/server/src/tenant/schema-builder/graphql-schema.factory.ts new file mode 100644 index 000000000..70d8cc14f --- /dev/null +++ b/server/src/tenant/schema-builder/graphql-schema.factory.ts @@ -0,0 +1,51 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { GraphQLSchema } from 'graphql'; + +import { ResolverBuilderMethods } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; + +import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; + +import { TypeDefinitionsGenerator } from './type-definitions.generator'; + +import { BuildSchemaOptions } from './interfaces/build-schema-optionts.interface'; +import { QueryTypeFactory } from './factories/query-type.factory'; +import { MutationTypeFactory } from './factories/mutation-type.factory'; +import { ObjectMetadataInterface } from './interfaces/object-metadata.interface'; + +@Injectable() +export class GraphQLSchemaFactory { + private readonly logger = new Logger(GraphQLSchemaFactory.name); + + constructor( + private readonly objectMetadataService: ObjectMetadataService, + private readonly typeDefinitionsGenerator: TypeDefinitionsGenerator, + private readonly queryTypeFactory: QueryTypeFactory, + private readonly mutationTypeFactory: MutationTypeFactory, + ) {} + + async create( + objectMetadataCollection: ObjectMetadataInterface[], + resolverBuilderMethods: ResolverBuilderMethods, + options: BuildSchemaOptions = {}, + ): Promise { + // Generate type definitions + this.typeDefinitionsGenerator.generate(objectMetadataCollection, options); + + // Generate schema + const schema = new GraphQLSchema({ + query: this.queryTypeFactory.create( + objectMetadataCollection, + [...resolverBuilderMethods.queries], + options, + ), + mutation: this.mutationTypeFactory.create( + objectMetadataCollection, + [...resolverBuilderMethods.mutations], + options, + ), + }); + + return schema; + } +} diff --git a/server/src/tenant/schema-builder/graphql-types/enum/index.ts b/server/src/tenant/schema-builder/graphql-types/enum/index.ts new file mode 100644 index 000000000..31526754d --- /dev/null +++ b/server/src/tenant/schema-builder/graphql-types/enum/index.ts @@ -0,0 +1 @@ +export * from './order-by-direction.enum-type'; diff --git a/server/src/tenant/schema-builder/graphql-types/enum/order-by-direction.type.ts b/server/src/tenant/schema-builder/graphql-types/enum/order-by-direction.enum-type.ts similarity index 100% rename from server/src/tenant/schema-builder/graphql-types/enum/order-by-direction.type.ts rename to server/src/tenant/schema-builder/graphql-types/enum/order-by-direction.enum-type.ts diff --git a/server/src/tenant/schema-builder/graphql-types/input/big-float-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/big-float-filter.input-type.ts similarity index 78% rename from server/src/tenant/schema-builder/graphql-types/input/big-float-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/big-float-filter.input-type.ts index 89de6263c..217a7070e 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/big-float-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/big-float-filter.input-type.ts @@ -5,10 +5,8 @@ import { GraphQLFloat, } from 'graphql'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - export const BigFloatFilterType = new GraphQLInputObjectType({ - name: 'BigFloatType', + name: 'BigFloatFilter', fields: { eq: { type: GraphQLFloat }, gt: { type: GraphQLFloat }, @@ -17,6 +15,5 @@ export const BigFloatFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLFloat }, lte: { type: GraphQLFloat }, neq: { type: GraphQLFloat }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/big-int-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/big-int-filter.input-type.ts similarity index 81% rename from server/src/tenant/schema-builder/graphql-types/input/big-int-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/big-int-filter.input-type.ts index 0d30efb58..ab404aad6 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/big-int-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/big-int-filter.input-type.ts @@ -5,8 +5,6 @@ import { GraphQLInt, } from 'graphql'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - export const BigIntFilterType = new GraphQLInputObjectType({ name: 'BigIntFilter', fields: { @@ -17,6 +15,5 @@ export const BigIntFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLInt }, lte: { type: GraphQLInt }, neq: { type: GraphQLInt }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/date-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/date-filter.input-type.ts similarity index 79% rename from server/src/tenant/schema-builder/graphql-types/input/date-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/date-filter.input-type.ts index d89d0d46a..5b771e083 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/date-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/date-filter.input-type.ts @@ -1,8 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { DateScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date.scalar'; - -import { FilterIsEnumType } from './filter-is-enum-filter.type'; +import { DateScalarType } from 'src/tenant/schema-builder/graphql-types/scalars'; export const DateFilterType = new GraphQLInputObjectType({ name: 'DateFilter', @@ -14,6 +12,5 @@ export const DateFilterType = new GraphQLInputObjectType({ lt: { type: DateScalarType }, lte: { type: DateScalarType }, neq: { type: DateScalarType }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/date-time-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/date-time-filter.input-type.ts similarity index 79% rename from server/src/tenant/schema-builder/graphql-types/input/date-time-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/date-time-filter.input-type.ts index 1ffe15306..91d5570bc 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/date-time-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/date-time-filter.input-type.ts @@ -1,8 +1,6 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar'; - -import { FilterIsEnumType } from './filter-is-enum-filter.type'; +import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars'; export const DatetimeFilterType = new GraphQLInputObjectType({ name: 'DateTimeFilter', @@ -14,6 +12,5 @@ export const DatetimeFilterType = new GraphQLInputObjectType({ lt: { type: DateTimeScalarType }, lte: { type: DateTimeScalarType }, neq: { type: DateTimeScalarType }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/filter-is-enum-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/filter-is-enum-filter.type.ts deleted file mode 100644 index 3e61b0da5..000000000 --- a/server/src/tenant/schema-builder/graphql-types/input/filter-is-enum-filter.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { GraphQLEnumType } from 'graphql'; - -export const FilterIsEnumType = new GraphQLEnumType({ - name: 'FilterIs', - values: { - PENDING: { value: 'PENDING' }, - RELEASED: { value: 'RELEASED' }, - }, -}); diff --git a/server/src/tenant/schema-builder/graphql-types/input/float-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/float-filter.input-type.ts similarity index 72% rename from server/src/tenant/schema-builder/graphql-types/input/float-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/float-filter.input-type.ts index 990f5dd40..12e3c89e8 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/float-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/float-filter.input-type.ts @@ -5,9 +5,7 @@ import { GraphQLNonNull, } from 'graphql'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - -export const FloatFilter = new GraphQLInputObjectType({ +export const FloatFilterType = new GraphQLInputObjectType({ name: 'FloatFilter', fields: { eq: { type: GraphQLFloat }, @@ -17,6 +15,5 @@ export const FloatFilter = new GraphQLInputObjectType({ lt: { type: GraphQLFloat }, lte: { type: GraphQLFloat }, neq: { type: GraphQLFloat }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/index.ts b/server/src/tenant/schema-builder/graphql-types/input/index.ts new file mode 100644 index 000000000..fb1411477 --- /dev/null +++ b/server/src/tenant/schema-builder/graphql-types/input/index.ts @@ -0,0 +1,9 @@ +export * from './big-float-filter.input-type'; +export * from './big-int-filter.input-type'; +export * from './date-filter.input-type'; +export * from './date-time-filter.input-type'; +export * from './float-filter.input-type'; +export * from './int-filter.input-type'; +export * from './string-filter.input-type'; +export * from './time-filter.input-type'; +export * from './uuid-filter.input-type'; diff --git a/server/src/tenant/schema-builder/graphql-types/input/int-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/int-filter.input-type.ts similarity index 71% rename from server/src/tenant/schema-builder/graphql-types/input/int-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/int-filter.input-type.ts index 162840288..9b4c33aed 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/int-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/int-filter.input-type.ts @@ -5,9 +5,7 @@ import { GraphQLInt, } from 'graphql'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - -export const IntFilter = new GraphQLInputObjectType({ +export const IntFilterType = new GraphQLInputObjectType({ name: 'IntFilter', fields: { eq: { type: GraphQLInt }, @@ -17,6 +15,5 @@ export const IntFilter = new GraphQLInputObjectType({ lt: { type: GraphQLInt }, lte: { type: GraphQLInt }, neq: { type: GraphQLInt }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/money-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/money-filter.type.ts deleted file mode 100644 index a3baf0665..000000000 --- a/server/src/tenant/schema-builder/graphql-types/input/money-filter.type.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { GraphQLInputObjectType } from 'graphql'; - -import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type'; -import { IntFilter } from 'src/tenant/schema-builder/graphql-types/input/int-filter.type'; - -export const MoneyFilterType = new GraphQLInputObjectType({ - name: 'MoneyFilter', - fields: { - amount: { type: IntFilter }, - currency: { type: StringFilterType }, - }, -}); diff --git a/server/src/tenant/schema-builder/graphql-types/input/string-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/string-filter.input-type.ts similarity index 86% rename from server/src/tenant/schema-builder/graphql-types/input/string-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/string-filter.input-type.ts index 961753b22..6ea2bf9c1 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/string-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/string-filter.input-type.ts @@ -5,8 +5,6 @@ import { GraphQLString, } from 'graphql'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - export const StringFilterType = new GraphQLInputObjectType({ name: 'StringFilter', fields: { @@ -17,7 +15,6 @@ export const StringFilterType = new GraphQLInputObjectType({ lt: { type: GraphQLString }, lte: { type: GraphQLString }, neq: { type: GraphQLString }, - is: { type: FilterIsEnumType }, startsWith: { type: GraphQLString }, like: { type: GraphQLString }, ilike: { type: GraphQLString }, diff --git a/server/src/tenant/schema-builder/graphql-types/input/time-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/time-filter.input-type.ts similarity index 71% rename from server/src/tenant/schema-builder/graphql-types/input/time-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/time-filter.input-type.ts index 645949a80..b52e40f7b 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/time-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/time-filter.input-type.ts @@ -1,10 +1,8 @@ import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; -import { TimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/time.scalar'; +import { TimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars'; -import { FilterIsEnumType } from './filter-is-enum-filter.type'; - -export const TimeFilter = new GraphQLInputObjectType({ +export const TimeFilterType = new GraphQLInputObjectType({ name: 'TimeFilter', fields: { eq: { type: TimeScalarType }, @@ -14,6 +12,5 @@ export const TimeFilter = new GraphQLInputObjectType({ lt: { type: TimeScalarType }, lte: { type: TimeScalarType }, neq: { type: TimeScalarType }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/input/url-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/url-filter.type.ts deleted file mode 100644 index e89cd3742..000000000 --- a/server/src/tenant/schema-builder/graphql-types/input/url-filter.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GraphQLInputObjectType } from 'graphql'; - -import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type'; - -export const UrlFilterType = new GraphQLInputObjectType({ - name: 'UrlFilter', - fields: { - text: { type: StringFilterType }, - link: { type: StringFilterType }, - }, -}); diff --git a/server/src/tenant/schema-builder/graphql-types/input/uuid-filter.type.ts b/server/src/tenant/schema-builder/graphql-types/input/uuid-filter.input-type.ts similarity index 72% rename from server/src/tenant/schema-builder/graphql-types/input/uuid-filter.type.ts rename to server/src/tenant/schema-builder/graphql-types/input/uuid-filter.input-type.ts index d81f7601a..2fed155e8 100644 --- a/server/src/tenant/schema-builder/graphql-types/input/uuid-filter.type.ts +++ b/server/src/tenant/schema-builder/graphql-types/input/uuid-filter.input-type.ts @@ -1,8 +1,6 @@ import { GraphQLInputObjectType, GraphQLList } from 'graphql'; -import { UUIDScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/uuid.scalar'; - -import { FilterIsEnumType } from './filter-is-enum-filter.type'; +import { UUIDScalarType } from 'src/tenant/schema-builder/graphql-types/scalars'; export const UUIDFilterType = new GraphQLInputObjectType({ name: 'UUIDFilter', @@ -10,6 +8,5 @@ export const UUIDFilterType = new GraphQLInputObjectType({ eq: { type: UUIDScalarType }, in: { type: new GraphQLList(UUIDScalarType) }, neq: { type: UUIDScalarType }, - is: { type: FilterIsEnumType }, }, }); diff --git a/server/src/tenant/schema-builder/graphql-types/object/index.ts b/server/src/tenant/schema-builder/graphql-types/object/index.ts new file mode 100644 index 000000000..413ba8a02 --- /dev/null +++ b/server/src/tenant/schema-builder/graphql-types/object/index.ts @@ -0,0 +1 @@ +export * from './page-into.object-type'; diff --git a/server/src/tenant/schema-builder/graphql-types/object/page-into.type.ts b/server/src/tenant/schema-builder/graphql-types/object/page-into.object-type.ts similarity index 100% rename from server/src/tenant/schema-builder/graphql-types/object/page-into.type.ts rename to server/src/tenant/schema-builder/graphql-types/object/page-into.object-type.ts diff --git a/server/src/tenant/schema-builder/graphql-types/scalars/index.ts b/server/src/tenant/schema-builder/graphql-types/scalars/index.ts index f892e2d2d..e26a17182 100644 --- a/server/src/tenant/schema-builder/graphql-types/scalars/index.ts +++ b/server/src/tenant/schema-builder/graphql-types/scalars/index.ts @@ -6,6 +6,14 @@ import { DateTimeScalarType } from './date-time.scalar'; import { TimeScalarType } from './time.scalar'; import { UUIDScalarType } from './uuid.scalar'; +export * from './big-float.scalar'; +export * from './big-int.scalar'; +export * from './cursor.scalar'; +export * from './date.scalar'; +export * from './date-time.scalar'; +export * from './time.scalar'; +export * from './uuid.scalar'; + export const scalars = [ BigFloatScalarType, BigIntScalarType, diff --git a/server/src/tenant/schema-builder/interfaces/build-schema-optionts.interface.ts b/server/src/tenant/schema-builder/interfaces/build-schema-optionts.interface.ts new file mode 100644 index 000000000..e651f133d --- /dev/null +++ b/server/src/tenant/schema-builder/interfaces/build-schema-optionts.interface.ts @@ -0,0 +1,16 @@ +export type DateScalarMode = 'isoDate' | 'timestamp'; +export type NumberScalarMode = 'float' | 'integer'; + +export interface BuildSchemaOptions { + /** + * Date scalar mode + * @default 'isoDate' + */ + dateScalarMode?: DateScalarMode; + + /** + * Number scalar mode + * @default 'float' + */ + numberScalarMode?: NumberScalarMode; +} diff --git a/server/src/tenant/schema-builder/interfaces/field-metadata.interface.ts b/server/src/tenant/schema-builder/interfaces/field-metadata.interface.ts new file mode 100644 index 000000000..3ea023842 --- /dev/null +++ b/server/src/tenant/schema-builder/interfaces/field-metadata.interface.ts @@ -0,0 +1,15 @@ +import { FieldMetadataTargetColumnMap } from 'src/metadata/field-metadata/interfaces/field-metadata-target-column-map.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export interface FieldMetadataInterface< + T extends FieldMetadataType | 'default' = 'default', +> { + id: string; + type: FieldMetadataType; + name: string; + label: string; + targetColumnMap: FieldMetadataTargetColumnMap; + description?: string; + isNullable?: boolean; +} diff --git a/server/src/tenant/schema-builder/interfaces/object-metadata.interface.ts b/server/src/tenant/schema-builder/interfaces/object-metadata.interface.ts new file mode 100644 index 000000000..7a93c372d --- /dev/null +++ b/server/src/tenant/schema-builder/interfaces/object-metadata.interface.ts @@ -0,0 +1,12 @@ +import { FieldMetadataInterface } from './field-metadata.interface'; + +export interface ObjectMetadataInterface { + id: string; + nameSingular: string; + namePlural: string; + labelSingular: string; + labelPlural: string; + description?: string; + targetTableName: string; + fields: FieldMetadataInterface[]; +} diff --git a/server/src/tenant/schema-builder/interfaces/param-metadata.interface.ts b/server/src/tenant/schema-builder/interfaces/param-metadata.interface.ts new file mode 100644 index 000000000..79f7df810 --- /dev/null +++ b/server/src/tenant/schema-builder/interfaces/param-metadata.interface.ts @@ -0,0 +1,18 @@ +import { InputTypeDefinitionKind } from 'src/tenant/schema-builder/factories/input-type-definition.factory'; +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +import { ObjectMetadataInterface } from './object-metadata.interface'; + +export interface ArgMetadata { + kind?: InputTypeDefinitionKind; + type?: FieldMetadataType; + isNullable?: boolean; + isArray?: boolean; +} + +export interface ArgsMetadata { + args: { + [key: string]: ArgMetadata; + }; + objectMetadata: ObjectMetadataInterface; +} diff --git a/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts b/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts index f3f2e9e1d..46751bc66 100644 --- a/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts +++ b/server/src/tenant/schema-builder/interfaces/schema-builder-context.interface.ts @@ -1,7 +1,7 @@ -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { FieldMetadataInterface } from './field-metadata.interface'; export interface SchemaBuilderContext { - tableName: string; workspaceId: string; - fields: FieldMetadata[]; + targetTableName: string; + fieldMetadataCollection: FieldMetadataInterface[]; } diff --git a/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts b/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts new file mode 100644 index 000000000..b27a7a136 --- /dev/null +++ b/server/src/tenant/schema-builder/object-definitions/money.object-definition.ts @@ -0,0 +1,28 @@ +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const moneyObjectDefinition = { + id: FieldMetadataType.MONEY.toString(), + nameSingular: 'Money', + namePlural: 'Money', + labelSingular: 'Money', + labelPlural: 'Money', + targetTableName: 'money', + fields: [ + { + id: 'amount', + type: FieldMetadataType.NUMBER, + name: 'amount', + label: 'Amount', + targetColumnMap: { value: 'amount' }, + }, + { + id: 'currency', + type: FieldMetadataType.TEXT, + name: 'currency', + label: 'Currency', + targetColumnMap: { value: 'currency' }, + }, + ], +} as ObjectMetadataInterface; diff --git a/server/src/tenant/schema-builder/object-definitions/url.object-definition.ts b/server/src/tenant/schema-builder/object-definitions/url.object-definition.ts new file mode 100644 index 000000000..e10bc1155 --- /dev/null +++ b/server/src/tenant/schema-builder/object-definitions/url.object-definition.ts @@ -0,0 +1,28 @@ +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +export const urlObjectDefinition = { + id: FieldMetadataType.URL.toString(), + nameSingular: 'Url', + namePlural: 'Url', + labelSingular: 'Url', + labelPlural: 'Url', + targetTableName: 'url', + fields: [ + { + id: 'text', + type: FieldMetadataType.TEXT, + name: 'text', + label: 'Text', + targetColumnMap: { value: 'text' }, + }, + { + id: 'link', + type: FieldMetadataType.TEXT, + name: 'link', + label: 'Link', + targetColumnMap: { value: 'link' }, + }, + ], +} as ObjectMetadataInterface; diff --git a/server/src/tenant/schema-builder/schema-builder.module.ts b/server/src/tenant/schema-builder/schema-builder.module.ts index 93247c2f7..9bfb0780d 100644 --- a/server/src/tenant/schema-builder/schema-builder.module.ts +++ b/server/src/tenant/schema-builder/schema-builder.module.ts @@ -1,13 +1,25 @@ import { Module } from '@nestjs/common'; -import { EntityResolverModule } from 'src/tenant/entity-resolver/entity-resolver.module'; import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; +import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metadata.module'; -import { SchemaBuilderService } from './schema-builder.service'; +import { TypeDefinitionsGenerator } from './type-definitions.generator'; +import { GraphQLSchemaFactory } from './graphql-schema.factory'; + +import { schemaBuilderFactories } from './factories/factories'; +import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; +import { TypeMapperService } from './services/type-mapper.service'; @Module({ - imports: [EntityResolverModule], - providers: [SchemaBuilderService, JwtAuthGuard], - exports: [SchemaBuilderService], + imports: [ObjectMetadataModule], + providers: [ + ...schemaBuilderFactories, + TypeDefinitionsGenerator, + TypeDefinitionsStorage, + TypeMapperService, + GraphQLSchemaFactory, + JwtAuthGuard, + ], + exports: [GraphQLSchemaFactory], }) export class SchemaBuilderModule {} diff --git a/server/src/tenant/schema-builder/schema-builder.service.spec.ts b/server/src/tenant/schema-builder/schema-builder.service.spec.ts deleted file mode 100644 index 37bb294b7..000000000 --- a/server/src/tenant/schema-builder/schema-builder.service.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service'; - -import { SchemaBuilderService } from './schema-builder.service'; - -describe('SchemaBuilderService', () => { - let service: SchemaBuilderService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - SchemaBuilderService, - { - provide: EntityResolverService, - useValue: {}, - }, - ], - }).compile(); - - service = module.get(SchemaBuilderService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/server/src/tenant/schema-builder/schema-builder.service.ts b/server/src/tenant/schema-builder/schema-builder.service.ts deleted file mode 100644 index 65aeab287..000000000 --- a/server/src/tenant/schema-builder/schema-builder.service.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { - GraphQLFieldConfigMap, - GraphQLID, - GraphQLInputObjectType, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLSchema, -} from 'graphql'; -import upperFirst from 'lodash.upperfirst'; - -import { EntityResolverService } from 'src/tenant/entity-resolver/entity-resolver.service'; -import { ObjectMetadata } from 'src/metadata/object-metadata/object-metadata.entity'; - -import { generateEdgeType } from './utils/generate-edge-type.util'; -import { generateConnectionType } from './utils/generate-connection-type.util'; -import { generateObjectType } from './utils/generate-object-type.util'; -import { generateCreateInputType } from './utils/generate-create-input-type.util'; -import { generateUpdateInputType } from './utils/generate-update-input-type.util'; -import { SchemaBuilderContext } from './interfaces/schema-builder-context.interface'; -import { cleanEntityName } from './utils/clean-entity-name.util'; -import { scalars } from './graphql-types/scalars'; -import { CursorScalarType } from './graphql-types/scalars/cursor.scalar'; -import { generateFilterInputType } from './utils/generate-filter-input-type.util'; -import { generateOrderByInputType } from './utils/generate-order-by-input-type.util'; - -@Injectable() -export class SchemaBuilderService { - workspaceId: string; - - constructor(private readonly entityResolverService: EntityResolverService) {} - - private generateQueryFieldForEntity( - entityName: { - singular: string; - plural: string; - }, - tableName: string, - ObjectType: GraphQLObjectType, - objectDefinition: ObjectMetadata, - ) { - const schemaBuilderContext: SchemaBuilderContext = { - tableName, - workspaceId: this.workspaceId, - fields: objectDefinition.fields, - }; - - const EdgeType = generateEdgeType(ObjectType); - const ConnectionType = generateConnectionType(EdgeType); - const FilterInputType = generateFilterInputType( - entityName.singular, - objectDefinition.fields, - ); - const OrderByInputType = generateOrderByInputType( - entityName.singular, - objectDefinition.fields, - ); - - return { - [`${entityName.plural}`]: { - type: ConnectionType, - args: { - first: { type: GraphQLInt }, - last: { type: GraphQLInt }, - before: { type: CursorScalarType }, - after: { type: CursorScalarType }, - filter: { type: FilterInputType }, - orderBy: { type: OrderByInputType }, - }, - resolve: async (root, args, context, info) => { - return this.entityResolverService.findMany( - args, - schemaBuilderContext, - info, - ); - }, - }, - [`${entityName.singular}`]: { - type: ObjectType, - args: { - filter: { type: FilterInputType }, - }, - resolve: (root, args, context, info) => { - return this.entityResolverService.findOne( - args, - schemaBuilderContext, - info, - ); - }, - }, - } as GraphQLFieldConfigMap; - } - - private generateMutationFieldForEntity( - entityName: { - singular: string; - plural: string; - }, - tableName: string, - ObjectType: GraphQLObjectType, - CreateInputType: GraphQLInputObjectType, - UpdateInputType: GraphQLInputObjectType, - objectDefinition: ObjectMetadata, - ) { - const schemaBuilderContext: SchemaBuilderContext = { - tableName, - workspaceId: this.workspaceId, - fields: objectDefinition.fields, - }; - - return { - [`createOne${upperFirst(entityName.singular)}`]: { - type: new GraphQLNonNull(ObjectType), - args: { - data: { type: new GraphQLNonNull(CreateInputType) }, - }, - resolve: (root, args, context, info) => { - return this.entityResolverService.createOne( - args, - schemaBuilderContext, - info, - ); - }, - }, - [`createMany${upperFirst(entityName.singular)}`]: { - type: new GraphQLList(ObjectType), - args: { - data: { - type: new GraphQLNonNull( - new GraphQLList(new GraphQLNonNull(CreateInputType)), - ), - }, - }, - resolve: (root, args, context, info) => { - return this.entityResolverService.createMany( - args, - schemaBuilderContext, - info, - ); - }, - }, - [`updateOne${upperFirst(entityName.singular)}`]: { - type: new GraphQLNonNull(ObjectType), - args: { - id: { type: new GraphQLNonNull(GraphQLID) }, - data: { type: new GraphQLNonNull(UpdateInputType) }, - }, - resolve: (root, args, context, info) => { - return this.entityResolverService.updateOne( - args, - schemaBuilderContext, - info, - ); - }, - }, - [`deleteOne${upperFirst(entityName.singular)}`]: { - type: new GraphQLNonNull(ObjectType), - args: { - id: { type: new GraphQLNonNull(GraphQLID) }, - }, - resolve: (root, args, context, info) => { - return this.entityResolverService.deleteOne( - args, - schemaBuilderContext, - info, - ); - }, - }, - } as GraphQLFieldConfigMap; - } - - private generateQueryAndMutationTypes(objectMetadata: ObjectMetadata[]): { - query: GraphQLObjectType; - mutation: GraphQLObjectType; - } { - const queryFields: any = {}; - const mutationFields: any = {}; - - for (const objectDefinition of objectMetadata) { - const entityName = { - singular: cleanEntityName(objectDefinition.nameSingular), - plural: cleanEntityName(objectDefinition.namePlural), - }; - - const tableName = objectDefinition?.targetTableName ?? ''; - const ObjectType = generateObjectType( - entityName.singular, - objectDefinition.fields, - ); - const CreateInputType = generateCreateInputType( - entityName.singular, - objectDefinition.fields, - ); - const UpdateInputType = generateUpdateInputType( - entityName.singular, - objectDefinition.fields, - ); - - Object.assign( - queryFields, - this.generateQueryFieldForEntity( - entityName, - tableName, - ObjectType, - objectDefinition, - ), - ); - - Object.assign( - mutationFields, - this.generateMutationFieldForEntity( - entityName, - tableName, - ObjectType, - CreateInputType, - UpdateInputType, - objectDefinition, - ), - ); - } - - return { - query: new GraphQLObjectType({ - name: 'Query', - fields: queryFields, - }), - mutation: new GraphQLObjectType({ - name: 'Mutation', - fields: mutationFields, - }), - }; - } - - async generateSchema( - workspaceId: string, - objectMetadata: ObjectMetadata[], - ): Promise { - this.workspaceId = workspaceId; - - const { query, mutation } = - this.generateQueryAndMutationTypes(objectMetadata); - - return new GraphQLSchema({ - query, - mutation, - types: [...scalars], - }); - } -} diff --git a/server/src/tenant/schema-builder/services/type-mapper.service.ts b/server/src/tenant/schema-builder/services/type-mapper.service.ts new file mode 100644 index 000000000..3ef70a3e8 --- /dev/null +++ b/server/src/tenant/schema-builder/services/type-mapper.service.ts @@ -0,0 +1,152 @@ +import { Injectable } from '@nestjs/common'; +import { GraphQLISODateTime, GraphQLTimestamp } from '@nestjs/graphql'; + +import { + GraphQLBoolean, + GraphQLEnumType, + GraphQLFloat, + GraphQLID, + GraphQLInputObjectType, + GraphQLInputType, + GraphQLInt, + GraphQLList, + GraphQLNonNull, + GraphQLScalarType, + GraphQLString, + GraphQLType, +} from 'graphql'; + +import { + DateScalarMode, + NumberScalarMode, +} from 'src/tenant/schema-builder/interfaces/build-schema-optionts.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + UUIDFilterType, + StringFilterType, + DatetimeFilterType, + DateFilterType, + FloatFilterType, + IntFilterType, +} from 'src/tenant/schema-builder/graphql-types/input'; +import { OrderByDirectionType } from 'src/tenant/schema-builder/graphql-types/enum'; + +export interface TypeOptions { + nullable?: boolean; + isArray?: boolean; + arrayDepth?: number; + defaultValue?: T; +} + +@Injectable() +export class TypeMapperService { + mapToScalarType( + fieldMetadataType: FieldMetadataType, + dateScalarMode: DateScalarMode = 'isoDate', + numberScalarMode: NumberScalarMode = 'float', + ): GraphQLScalarType | undefined { + const dateScalar = + dateScalarMode === 'timestamp' ? GraphQLTimestamp : GraphQLISODateTime; + const numberScalar = + numberScalarMode === 'float' ? GraphQLFloat : GraphQLInt; + + // URL and MONEY are handled in the factories because they are objects + const typeScalarMapping = new Map([ + [FieldMetadataType.UUID, GraphQLID], + [FieldMetadataType.TEXT, GraphQLString], + [FieldMetadataType.PHONE, GraphQLString], + [FieldMetadataType.EMAIL, GraphQLString], + [FieldMetadataType.DATE, dateScalar], + [FieldMetadataType.BOOLEAN, GraphQLBoolean], + [FieldMetadataType.NUMBER, numberScalar], + ]); + + return typeScalarMapping.get(fieldMetadataType); + } + + mapToFilterType( + fieldMetadataType: FieldMetadataType, + dateScalarMode: DateScalarMode = 'isoDate', + numberScalarMode: NumberScalarMode = 'float', + ): GraphQLInputObjectType | GraphQLScalarType | undefined { + const dateFilter = + dateScalarMode === 'timestamp' ? DatetimeFilterType : DateFilterType; + const numberScalar = + numberScalarMode === 'float' ? FloatFilterType : IntFilterType; + + // URL and MONEY are handled in the factories because they are objects + const typeFilterMapping = new Map< + FieldMetadataType, + GraphQLInputObjectType | GraphQLScalarType + >([ + [FieldMetadataType.UUID, UUIDFilterType], + [FieldMetadataType.TEXT, StringFilterType], + [FieldMetadataType.PHONE, StringFilterType], + [FieldMetadataType.EMAIL, StringFilterType], + [FieldMetadataType.DATE, dateFilter], + [FieldMetadataType.BOOLEAN, GraphQLBoolean], + [FieldMetadataType.NUMBER, numberScalar], + ]); + + return typeFilterMapping.get(fieldMetadataType); + } + + mapToOrderByType( + fieldMetadataType: FieldMetadataType, + ): GraphQLInputType | undefined { + // URL and MONEY are handled in the factories because they are objects + const typeOrderByMapping = new Map([ + [FieldMetadataType.UUID, OrderByDirectionType], + [FieldMetadataType.TEXT, OrderByDirectionType], + [FieldMetadataType.PHONE, OrderByDirectionType], + [FieldMetadataType.EMAIL, OrderByDirectionType], + [FieldMetadataType.DATE, OrderByDirectionType], + [FieldMetadataType.BOOLEAN, OrderByDirectionType], + [FieldMetadataType.NUMBER, OrderByDirectionType], + ]); + + return typeOrderByMapping.get(fieldMetadataType); + } + + mapToGqlType( + typeRef: T, + options: TypeOptions, + ): T { + let graphqlType: T | GraphQLList | GraphQLNonNull = typeRef; + + if (options.isArray) { + graphqlType = this.mapToGqlList( + graphqlType, + options.arrayDepth ?? 1, + options.nullable ?? false, + ); + } + + if (!options.nullable) { + graphqlType = new GraphQLNonNull(graphqlType) as unknown as T; + } + + return graphqlType as T; + } + + private mapToGqlList( + targetType: T, + depth: number, + nullable: boolean, + ): GraphQLList { + const targetTypeNonNull = nullable + ? targetType + : new GraphQLNonNull(targetType); + + if (depth === 0) { + return targetType as GraphQLList; + } + + return this.mapToGqlList( + new GraphQLList(targetTypeNonNull) as unknown as T, + depth - 1, + nullable, + ); + } +} diff --git a/server/src/tenant/schema-builder/storages/type-definitions.storage.ts b/server/src/tenant/schema-builder/storages/type-definitions.storage.ts new file mode 100644 index 000000000..34a221c58 --- /dev/null +++ b/server/src/tenant/schema-builder/storages/type-definitions.storage.ts @@ -0,0 +1,76 @@ +import { Injectable } from '@nestjs/common'; + +import { GraphQLInputObjectType, GraphQLObjectType } from 'graphql'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { + InputTypeDefinition, + InputTypeDefinitionKind, +} from 'src/tenant/schema-builder/factories/input-type-definition.factory'; +import { + ObjectTypeDefinition, + ObjectTypeDefinitionKind, +} from 'src/tenant/schema-builder/factories/object-type-definition.factory'; + +@Injectable() +export class TypeDefinitionsStorage { + private readonly objectTypeDefinitions = new Map< + string, + ObjectTypeDefinition + >(); + private readonly inputTypeDefinitions = new Map< + string, + InputTypeDefinition + >(); + + addObjectTypes(objectDefs: ObjectTypeDefinition[]) { + objectDefs.forEach((item) => + this.objectTypeDefinitions.set( + this.generateCompositeKey(item.target, item.kind), + item, + ), + ); + } + + getObjectTypeByKey( + target: string, + kind: ObjectTypeDefinitionKind, + ): GraphQLObjectType | undefined { + return this.objectTypeDefinitions.get( + this.generateCompositeKey(target, kind), + )?.type; + } + + getAllObjectTypeDefinitions(): ObjectTypeDefinition[] { + return Array.from(this.objectTypeDefinitions.values()); + } + + addInputTypes(inputDefs: InputTypeDefinition[]) { + inputDefs.forEach((item) => + this.inputTypeDefinitions.set( + this.generateCompositeKey(item.target, item.kind), + item, + ), + ); + } + + getInputTypeByKey( + target: string, + kind: InputTypeDefinitionKind, + ): GraphQLInputObjectType | undefined { + return this.inputTypeDefinitions.get( + this.generateCompositeKey(target, kind), + )?.type; + } + + getAllInputTypeDefinitions(): InputTypeDefinition[] { + return Array.from(this.inputTypeDefinitions.values()); + } + + private generateCompositeKey( + target: string | FieldMetadataType, + kind: ObjectTypeDefinitionKind | InputTypeDefinitionKind, + ): string { + return `${target.toString()}_${kind.toString()}`; + } +} diff --git a/server/src/tenant/schema-builder/type-definitions.generator.ts b/server/src/tenant/schema-builder/type-definitions.generator.ts new file mode 100644 index 000000000..2ce205961 --- /dev/null +++ b/server/src/tenant/schema-builder/type-definitions.generator.ts @@ -0,0 +1,208 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; +import { customTableDefaultColumns } from 'src/metadata/migration-runner/custom-table-default-column.util'; + +import { TypeDefinitionsStorage } from './storages/type-definitions.storage'; +import { + ObjectTypeDefinitionFactory, + ObjectTypeDefinitionKind, +} from './factories/object-type-definition.factory'; +import { + InputTypeDefinitionFactory, + InputTypeDefinitionKind, +} from './factories/input-type-definition.factory'; +import { getFieldMetadataType } from './utils/get-field-metadata-type.util'; +import { BuildSchemaOptions } from './interfaces/build-schema-optionts.interface'; +import { moneyObjectDefinition } from './object-definitions/money.object-definition'; +import { urlObjectDefinition } from './object-definitions/url.object-definition'; +import { ObjectMetadataInterface } from './interfaces/object-metadata.interface'; +import { FieldMetadataInterface } from './interfaces/field-metadata.interface'; +import { FilterTypeDefinitionFactory } from './factories/filter-type-definition.factory'; +import { ConnectionTypeDefinitionFactory } from './factories/connection-type-definition.factory'; +import { EdgeTypeDefinitionFactory } from './factories/edge-type-definition.factory'; +import { OrderByTypeDefinitionFactory } from './factories/order-by-type-definition.factory'; + +// Create a default field for each custom table default column +const defaultFields = customTableDefaultColumns.map((column) => { + return { + type: getFieldMetadataType(column.type), + name: column.name, + isNullable: true, + } as FieldMetadata; +}); + +@Injectable() +export class TypeDefinitionsGenerator { + private readonly logger = new Logger(TypeDefinitionsGenerator.name); + + constructor( + private readonly typeDefinitionsStorage: TypeDefinitionsStorage, + private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory, + private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory, + private readonly filterTypeDefintionFactory: FilterTypeDefinitionFactory, + private readonly orderByTypeDefinitionFactory: OrderByTypeDefinitionFactory, + private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory, + private readonly connectionTypeDefinitionFactory: ConnectionTypeDefinitionFactory, + ) {} + + generate( + objectMetadataCollection: ObjectMetadataInterface[], + options: BuildSchemaOptions, + ) { + // Generate static objects first because they can be used in dynamic objects + this.generateStaticObjectTypeDefs(options); + // Generate dynamic objects + this.generateDynamicObjectTypeDefs(objectMetadataCollection, options); + } + + private generateStaticObjectTypeDefs(options: BuildSchemaOptions) { + const staticObjectMetadataCollection = [ + moneyObjectDefinition, + urlObjectDefinition, + ]; + + this.logger.log( + `Generating staticObjects: [${staticObjectMetadataCollection + .map((object) => object.nameSingular) + .join(', ')}]`, + ); + + // Generate static objects first because they can be used in dynamic objects + this.generateObjectTypeDefs(staticObjectMetadataCollection, options); + this.generateInputTypeDefs(staticObjectMetadataCollection, options); + } + + private generateDynamicObjectTypeDefs( + dynamicObjectMetadataCollection: ObjectMetadataInterface[], + options: BuildSchemaOptions, + ) { + this.logger.log( + `Generating dynamicObjects: [${dynamicObjectMetadataCollection + .map((object) => object.nameSingular) + .join(', ')}]`, + ); + + // Generate dynamic objects + this.generateObjectTypeDefs(dynamicObjectMetadataCollection, options); + this.generatePaginationTypeDefs(dynamicObjectMetadataCollection, options); + this.generateInputTypeDefs(dynamicObjectMetadataCollection, options); + } + + private generateObjectTypeDefs( + objectMetadataCollection: ObjectMetadataInterface[], + options: BuildSchemaOptions, + ) { + const objectTypeDefs = objectMetadataCollection.map((objectMetadata) => { + const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); + const extendedObjectMetadata = { + ...objectMetadata, + fields, + }; + + return this.objectTypeDefinitionFactory.create( + extendedObjectMetadata, + ObjectTypeDefinitionKind.Plain, + options, + ); + }); + + this.typeDefinitionsStorage.addObjectTypes(objectTypeDefs); + } + + private generatePaginationTypeDefs( + objectMetadataCollection: ObjectMetadataInterface[], + options: BuildSchemaOptions, + ) { + const edgeTypeDefs = objectMetadataCollection.map((objectMetadata) => { + const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); + const extendedObjectMetadata = { + ...objectMetadata, + fields, + }; + + return this.edgeTypeDefinitionFactory.create( + extendedObjectMetadata, + options, + ); + }); + + this.typeDefinitionsStorage.addObjectTypes(edgeTypeDefs); + + // Connection type defs are using edge type defs + const connectionTypeDefs = objectMetadataCollection.map( + (objectMetadata) => { + const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); + const extendedObjectMetadata = { + ...objectMetadata, + fields, + }; + + return this.connectionTypeDefinitionFactory.create( + extendedObjectMetadata, + options, + ); + }, + ); + + this.typeDefinitionsStorage.addObjectTypes(connectionTypeDefs); + } + + private generateInputTypeDefs( + objectMetadataCollection: ObjectMetadataInterface[], + options: BuildSchemaOptions, + ) { + const inputTypeDefs = objectMetadataCollection + .map((objectMetadata) => { + const fields = this.mergeFieldsWithDefaults(objectMetadata.fields); + const requiredExtendedObjectMetadata = { + ...objectMetadata, + fields, + }; + const optionalExtendedObjectMetadata = { + ...objectMetadata, + fields: fields.map((field) => ({ ...field, isNullable: true })), + }; + + return [ + // Input type for create + this.inputTypeDefinitionFactory.create( + requiredExtendedObjectMetadata, + InputTypeDefinitionKind.Create, + options, + ), + // Input type for update + this.inputTypeDefinitionFactory.create( + optionalExtendedObjectMetadata, + InputTypeDefinitionKind.Update, + options, + ), + // Filter input type + this.filterTypeDefintionFactory.create( + optionalExtendedObjectMetadata, + options, + ), + // OrderBy input type + this.orderByTypeDefinitionFactory.create( + optionalExtendedObjectMetadata, + options, + ), + ]; + }) + .flat(); + + this.typeDefinitionsStorage.addInputTypes(inputTypeDefs); + } + + private mergeFieldsWithDefaults( + fields: FieldMetadataInterface[], + ): FieldMetadataInterface[] { + const fieldNames = new Set(fields.map((field) => field.name)); + + const uniqueDefaultFields = defaultFields.filter( + (defaultField) => !fieldNames.has(defaultField.name), + ); + + return [...fields, ...uniqueDefaultFields]; + } +} diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-connection-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-connection-type.spec.ts deleted file mode 100644 index 4c7f45df4..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-connection-type.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; - -import { PageInfoType } from 'src/tenant/schema-builder/graphql-types/object/page-into.type'; -import { generateConnectionType } from 'src/tenant/schema-builder/utils/generate-connection-type.util'; - -describe('generateConnectionType', () => { - // Create a mock EdgeType for testing - const mockEdgeType = new GraphQLObjectType({ - name: 'MockEdge', - fields: { - node: { type: GraphQLString }, - cursor: { type: GraphQLString }, - }, - }); - - // Generate a connection type using the mock - const MockConnectionType = generateConnectionType(mockEdgeType); - - test('should generate a GraphQLObjectType', () => { - expect(MockConnectionType).toBeInstanceOf(GraphQLObjectType); - }); - - test('should generate a type with the correct name', () => { - expect(MockConnectionType.name).toBe('MockConnection'); - }); - - test('should include the correct fields', () => { - const fields = MockConnectionType.getFields(); - - expect(fields).toHaveProperty('edges'); - if ( - fields.edges.type instanceof GraphQLList || - fields.edges.type instanceof GraphQLNonNull - ) { - expect(fields.edges.type.ofType).toBe(mockEdgeType); - } else { - fail('edges.type is not an instance of GraphQLList or GraphQLNonNull'); - } - - expect(fields).toHaveProperty('pageInfo'); - if (fields.pageInfo.type instanceof GraphQLNonNull) { - expect(fields.pageInfo.type.ofType).toBe(PageInfoType); - } else { - fail('pageInfo.type is not an instance of GraphQLNonNull'); - } - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-create-input-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-create-input-type.spec.ts deleted file mode 100644 index 714385584..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-create-input-type.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { - GraphQLID, - GraphQLInputObjectType, - GraphQLInt, - GraphQLNonNull, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { generateCreateInputType } from 'src/tenant/schema-builder/utils/generate-create-input-type.util'; - -describe('generateCreateInputType', () => { - test('should generate a GraphQLInputObjectType with correct name', () => { - const columns = []; - const name = 'testType'; - const inputType = generateCreateInputType(name, columns); - expect(inputType).toBeInstanceOf(GraphQLInputObjectType); - expect(inputType.name).toBe('TestTypeCreateInput'); - }); - - test('should include default id field', () => { - const columns = []; - const name = 'testType'; - const inputType = generateCreateInputType(name, columns); - const fields = inputType.getFields(); - expect(fields.id).toBeDefined(); - expect(fields.id.type).toBe(GraphQLID); - }); - - test('should generate fields with correct types and descriptions', () => { - const columns = [ - { - name: 'firstName', - type: 'text', - isNullable: false, - }, - { - name: 'age', - type: 'number', - isNullable: true, - }, - ] as FieldMetadata[]; - - const name = 'testType'; - const inputType = generateCreateInputType(name, columns); - const fields = inputType.getFields(); - - if (fields.firstName.type instanceof GraphQLNonNull) { - expect(fields.firstName.type.ofType).toBe(GraphQLString); - } else { - fail('firstName type is not an instance of GraphQLNonNull'); - } - - expect(fields.age.type).toBe(GraphQLInt); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-edge-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-edge-type.spec.ts deleted file mode 100644 index 779ab842d..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-edge-type.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; - -import { generateEdgeType } from 'src/tenant/schema-builder/utils/generate-edge-type.util'; - -describe('generateEdgeType', () => { - // Mock GraphQLObjectType for testing - const mockObjectType = new GraphQLObjectType({ - name: 'MockItem', - fields: { - sampleField: { type: GraphQLString }, - }, - }); - - test('should generate a GraphQLObjectType', () => { - const edgeType = generateEdgeType(mockObjectType); - expect(edgeType).toBeInstanceOf(GraphQLObjectType); - }); - - test('should generate a type with the correct name', () => { - const edgeType = generateEdgeType(mockObjectType); - expect(edgeType.name).toBe('MockItemEdge'); - }); - - test('should have a "node" field of the provided ObjectType', () => { - const edgeType = generateEdgeType(mockObjectType); - const fields = edgeType.getFields(); - expect(fields.node.type).toBe(mockObjectType); - }); - - test('should have a "cursor" field of type GraphQLNonNull(GraphQLString)', () => { - const edgeType = generateEdgeType(mockObjectType); - const fields = edgeType.getFields(); - expect(fields.cursor.type).toBeInstanceOf(GraphQLNonNull); - if (fields.cursor.type instanceof GraphQLNonNull) { - expect(fields.cursor.type.ofType).toBe(GraphQLString); - } - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-filter-input-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-filter-input-type.spec.ts deleted file mode 100644 index 65aa30825..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-filter-input-type.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { GraphQLList, GraphQLNonNull, GraphQLInputObjectType } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { generateFilterInputType } from 'src/tenant/schema-builder/utils/generate-filter-input-type.util'; -import { mapColumnTypeToFilterType } from 'src/tenant/schema-builder/utils/map-column-type-to-filter-type.util'; - -describe('generateFilterInputType', () => { - it('handles empty columns array', () => { - const FilterInputType = generateFilterInputType('EmptyTest', []); - - expect(FilterInputType.name).toBe('EmptyTestFilterInput'); - - expect(FilterInputType.getFields()).toHaveProperty('id'); - expect(FilterInputType.getFields()).toHaveProperty('createdAt'); - expect(FilterInputType.getFields()).toHaveProperty('updatedAt'); - expect(FilterInputType.getFields()).toHaveProperty('and'); - expect(FilterInputType.getFields()).toHaveProperty('or'); - expect(FilterInputType.getFields()).toHaveProperty('not'); - }); - - it('handles various column types', () => { - const columns = [ - { name: 'stringField', type: 'text' }, - { name: 'intField', type: 'number' }, - { name: 'booleanField', type: 'boolean' }, - ] as FieldMetadata[]; - - const FilterInputType = generateFilterInputType('MultiTypeTest', columns); - - columns.forEach((column) => { - const expectedType = mapColumnTypeToFilterType(column); - - expect(FilterInputType.getFields()[column.name].type).toBe(expectedType); - }); - }); - - it('handles nested logical fields', () => { - const FilterInputType = generateFilterInputType('NestedTest', []); - - const andFieldType = FilterInputType.getFields().and.type; - const orFieldType = FilterInputType.getFields().or.type; - const notFieldType = FilterInputType.getFields().not.type; - - expect(andFieldType).toBeInstanceOf(GraphQLList); - expect(orFieldType).toBeInstanceOf(GraphQLList); - - if (notFieldType instanceof GraphQLNonNull) { - expect(notFieldType.ofType).toBe(FilterInputType); - } else { - expect(notFieldType).toBeInstanceOf(GraphQLInputObjectType); - } - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-object-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-object-type.spec.ts deleted file mode 100644 index 073532fae..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-object-type.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - GraphQLID, - GraphQLInt, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { generateObjectType } from 'src/tenant/schema-builder/utils/generate-object-type.util'; -import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar'; - -describe('generateObjectType', () => { - test('should generate a GraphQLObjectType with correct name', () => { - const columns = []; - const name = 'testType'; - const objectType = generateObjectType(name, columns); - expect(objectType).toBeInstanceOf(GraphQLObjectType); - expect(objectType.name).toBe('TestType'); - }); - - test('should include default fields', () => { - const columns = []; - const name = 'testType'; - const objectType = generateObjectType(name, columns); - const fields = objectType.getFields(); - - if (fields.id.type instanceof GraphQLNonNull) { - expect(fields.id.type.ofType).toBe(GraphQLID); - } else { - fail('id.type is not an instance of GraphQLNonNull'); - } - - if (fields.createdAt.type instanceof GraphQLNonNull) { - expect(fields.createdAt.type.ofType).toBe(DateTimeScalarType); - } else { - fail('createdAt.type is not an instance of GraphQLNonNull'); - } - - if (fields.updatedAt.type instanceof GraphQLNonNull) { - expect(fields.updatedAt.type.ofType).toBe(DateTimeScalarType); - } else { - fail('updatedAt.type is not an instance of GraphQLNonNull'); - } - }); - - test('should generate fields based on provided columns', () => { - const columns = [ - { - name: 'firstName', - type: 'text', - isNullable: false, - }, - { - name: 'age', - type: 'number', - isNullable: true, - }, - ] as FieldMetadata[]; - - const name = 'testType'; - const objectType = generateObjectType(name, columns); - const fields = objectType.getFields(); - - if (fields.firstName.type instanceof GraphQLNonNull) { - expect(fields.firstName.type.ofType).toBe(GraphQLString); - } else { - fail('firstName.type is not an instance of GraphQLNonNull'); - } - - expect(fields.age.type).toBe(GraphQLInt); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-order-by-input-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-order-by-input-type.spec.ts deleted file mode 100644 index ddce4a73c..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-order-by-input-type.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { OrderByDirectionType } from 'src/tenant/schema-builder/graphql-types/enum/order-by-direction.type'; -import { generateOrderByInputType } from 'src/tenant/schema-builder/utils/generate-order-by-input-type.util'; - -describe('generateOrderByInputType', () => { - it('includes default fields', () => { - const result = generateOrderByInputType('SampleType', []); - const fields = result.getFields(); - - expect(fields.id.type).toBe(OrderByDirectionType); - expect(fields.createdAt.type).toBe(OrderByDirectionType); - expect(fields.updatedAt.type).toBe(OrderByDirectionType); - }); - - it('adds fields from provided FieldMetadata columns', () => { - const testColumns = [ - { name: 'customField1' }, - { name: 'customField2' }, - ] as FieldMetadata[]; - const result = generateOrderByInputType('SampleType', testColumns); - const fields = result.getFields(); - - expect(fields.customField1.type).toBe(OrderByDirectionType); - expect(fields.customField2.type).toBe(OrderByDirectionType); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/generate-udpate-input-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/generate-udpate-input-type.spec.ts deleted file mode 100644 index 24672393e..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/generate-udpate-input-type.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - GraphQLID, - GraphQLInputObjectType, - GraphQLInt, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { generateUpdateInputType } from 'src/tenant/schema-builder/utils/generate-update-input-type.util'; - -describe('generateUpdateInputType', () => { - test('should generate a GraphQLInputObjectType with correct name', () => { - const columns = []; - const name = 'testType'; - const inputType = generateUpdateInputType(name, columns); - expect(inputType).toBeInstanceOf(GraphQLInputObjectType); - expect(inputType.name).toBe('TestTypeUpdateInput'); - }); - - test('should include default id field', () => { - const columns = []; - const name = 'testType'; - const inputType = generateUpdateInputType(name, columns); - const fields = inputType.getFields(); - expect(fields.id).toBeDefined(); - expect(fields.id.type).toBe(GraphQLID); - }); - - test('should generate fields with correct types and descriptions', () => { - const columns = [ - { - name: 'firstName', - type: 'text', - isNullable: true, - }, - { - name: 'age', - type: 'number', - isNullable: true, - }, - ] as FieldMetadata[]; - - const name = 'testType'; - const inputType = generateUpdateInputType(name, columns); - const fields = inputType.getFields(); - - expect(fields.firstName.type).toBe(GraphQLString); - - expect(fields.age.type).toBe(GraphQLInt); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/get-field-metadata-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/get-field-metadata-type.spec.ts new file mode 100644 index 000000000..27b1761dc --- /dev/null +++ b/server/src/tenant/schema-builder/utils/__tests__/get-field-metadata-type.spec.ts @@ -0,0 +1,21 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { getFieldMetadataType } from 'src/tenant/schema-builder/utils/get-field-metadata-type.util'; + +describe('getFieldMetadataType', () => { + it.each([ + ['uuid', FieldMetadataType.UUID], + ['timestamp', FieldMetadataType.DATE], + ])( + 'should return correct FieldMetadataType for type %s', + (type, expectedMetadataType) => { + expect(getFieldMetadataType(type)).toBe(expectedMetadataType); + }, + ); + + it('should throw an error for an unknown type', () => { + const unknownType = 'unknownType'; + expect(() => getFieldMetadataType(unknownType)).toThrow( + `Unknown type ${unknownType}`, + ); + }); +}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/get-resolver-args.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/get-resolver-args.spec.ts new file mode 100644 index 000000000..86271e8c3 --- /dev/null +++ b/server/src/tenant/schema-builder/utils/__tests__/get-resolver-args.spec.ts @@ -0,0 +1,55 @@ +import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { InputTypeDefinitionKind } from 'src/tenant/schema-builder/factories/input-type-definition.factory'; +import { getResolverArgs } from 'src/tenant/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 }, + filter: { kind: InputTypeDefinitionKind.Filter, isNullable: true }, + orderBy: { kind: InputTypeDefinitionKind.OrderBy, isNullable: true }, + }, + findOne: { + filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false }, + }, + createMany: { + data: { + kind: InputTypeDefinitionKind.Create, + isNullable: false, + isArray: true, + }, + }, + createOne: { + data: { kind: InputTypeDefinitionKind.Create, isNullable: false }, + }, + updateOne: { + id: { type: FieldMetadataType.UUID, isNullable: false }, + data: { kind: InputTypeDefinitionKind.Update, isNullable: false }, + }, + deleteOne: { + id: { type: FieldMetadataType.UUID, isNullable: false }, + }, + }; + + // Test each resolver type + Object.entries(expectedOutputs).forEach(([resolverType, expectedOutput]) => { + it(`should return correct args for ${resolverType} resolver`, () => { + expect( + getResolverArgs(resolverType as ResolverBuilderMethodNames), + ).toEqual(expectedOutput); + }); + }); + + // Test for an unknown resolver type + it('should throw an error for an unknown resolver type', () => { + const unknownType = 'unknownType'; + expect(() => + getResolverArgs(unknownType as ResolverBuilderMethodNames), + ).toThrow(`Unknown resolver type: ${unknownType}`); + }); +}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-filter-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-filter-type.spec.ts deleted file mode 100644 index 244dc8a6f..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-filter-type.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { GraphQLBoolean } from 'graphql'; - -import { mapColumnTypeToFilterType } from 'src/tenant/schema-builder/utils/map-column-type-to-filter-type.util'; // Update with the actual path -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { UUIDFilterType } from 'src/tenant/schema-builder/graphql-types/input/uuid-filter.type'; -import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type'; -import { DateFilterType } from 'src/tenant/schema-builder/graphql-types/input/date-filter.type'; -import { IntFilter } from 'src/tenant/schema-builder/graphql-types/input/int-filter.type'; -import { UrlFilterType } from 'src/tenant/schema-builder/graphql-types/input/url-filter.type'; -import { MoneyFilterType } from 'src/tenant/schema-builder/graphql-types/input/money-filter.type'; - -describe('mapColumnTypeToFilterType', () => { - it('should map column types to corresponding filter types', () => { - const fields: { column: FieldMetadata; expected: any }[] = [ - { column: { type: 'uuid' } as FieldMetadata, expected: UUIDFilterType }, - { column: { type: 'text' } as FieldMetadata, expected: StringFilterType }, - { - column: { type: 'phone' } as FieldMetadata, - expected: StringFilterType, - }, - { - column: { type: 'email' } as FieldMetadata, - expected: StringFilterType, - }, - { column: { type: 'date' } as FieldMetadata, expected: DateFilterType }, - { - column: { type: 'boolean' } as FieldMetadata, - expected: GraphQLBoolean, - }, - { column: { type: 'number' } as FieldMetadata, expected: IntFilter }, - { column: { type: 'url' } as FieldMetadata, expected: UrlFilterType }, - { column: { type: 'money' } as FieldMetadata, expected: MoneyFilterType }, - ]; - - fields.forEach((field) => { - expect(mapColumnTypeToFilterType(field.column)).toBe(field.expected); - }); - }); - - it('should throw an error for unimplemented filter types', () => { - const column = { type: 'enum' } as FieldMetadata; - - expect(() => mapColumnTypeToFilterType(column)).toThrowError( - 'enum filter type not yet implemented', - ); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-graphql-type.spec.ts b/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-graphql-type.spec.ts deleted file mode 100644 index 2868778b3..000000000 --- a/server/src/tenant/schema-builder/utils/__tests__/map-column-type-to-graphql-type.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - GraphQLBoolean, - GraphQLEnumType, - GraphQLID, - GraphQLInt, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { mapColumnTypeToGraphQLType } from 'src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util'; - -describe('mapColumnTypeToGraphQLType', () => { - test('should map uuid to GraphQLID', () => { - const column = new FieldMetadata(); - column.type = 'uuid'; - expect(mapColumnTypeToGraphQLType(column)).toBe(GraphQLID); - }); - - test('should map text, phone, email, and date to GraphQLString', () => { - const types = ['text', 'phone', 'email', 'date']; - types.forEach((type) => { - const column = new FieldMetadata(); - column.type = type; - expect(mapColumnTypeToGraphQLType(column)).toBe(GraphQLString); - }); - }); - - test('should map boolean to GraphQLBoolean', () => { - const column = new FieldMetadata(); - column.type = 'boolean'; - expect(mapColumnTypeToGraphQLType(column)).toBe(GraphQLBoolean); - }); - - test('should map number to GraphQLInt', () => { - const column = new FieldMetadata(); - column.type = 'number'; - expect(mapColumnTypeToGraphQLType(column)).toBe(GraphQLInt); - }); - - test('should create a GraphQLEnumType for enum fields', () => { - const column = new FieldMetadata(); - column.type = 'enum'; - column.name = 'Status'; - column.enums = ['ACTIVE', 'INACTIVE']; - const result = mapColumnTypeToGraphQLType(column); - - if (result instanceof GraphQLEnumType) { - expect(result.name).toBe('StatusEnum'); - - const values = result.getValues().map((value) => value.value); - expect(values).toContain('ACTIVE'); - expect(values).toContain('INACTIVE'); - } else { - fail('Result is not an instance of GraphQLEnumType'); - } - }); - - test('should map url to UrlObjectType or UrlInputType based on input flag', () => { - const column = new FieldMetadata(); - column.type = 'url'; - expect(mapColumnTypeToGraphQLType(column, false).name).toBe('Url'); - expect(mapColumnTypeToGraphQLType(column, true).name).toBe('UrlInput'); - }); - - test('should map money to MoneyObjectType or MoneyInputType based on input flag', () => { - const column = new FieldMetadata(); - column.type = 'money'; - expect(mapColumnTypeToGraphQLType(column, false).name).toBe('Money'); - expect(mapColumnTypeToGraphQLType(column, true).name).toBe('MoneyInput'); - }); - - test('should default to GraphQLString for unknown types', () => { - const column = new FieldMetadata(); - column.type = 'unknown'; - expect(mapColumnTypeToGraphQLType(column)).toBe(GraphQLString); - }); -}); diff --git a/server/src/tenant/schema-builder/utils/generate-connection-type.util.ts b/server/src/tenant/schema-builder/utils/generate-connection-type.util.ts deleted file mode 100644 index bba542888..000000000 --- a/server/src/tenant/schema-builder/utils/generate-connection-type.util.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { GraphQLList, GraphQLNonNull, GraphQLObjectType } from 'graphql'; - -import { PageInfoType } from 'src/tenant/schema-builder/graphql-types/object/page-into.type'; - -/** - * Generate a GraphQL connection type based on the EdgeType. - * @param EdgeType Edge type to be used in the connection. - * @returns GraphQL connection type. - */ -export const generateConnectionType = ( - EdgeType: T, -): GraphQLObjectType => { - return new GraphQLObjectType({ - name: `${EdgeType.name.slice(0, -4)}Connection`, // Removing 'Edge' from the name - fields: { - edges: { - type: new GraphQLList(EdgeType), - }, - pageInfo: { - type: new GraphQLNonNull(PageInfoType), - }, - }, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts deleted file mode 100644 index 8c8ffc8af..000000000 --- a/server/src/tenant/schema-builder/utils/generate-create-input-type.util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GraphQLID, GraphQLInputObjectType, GraphQLNonNull } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; - -import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; - -/** - * Generate a GraphQL create input type based on the name and columns. - * @param name Name for the GraphQL input. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateCreateInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const fields: Record = { - id: { type: GraphQLID }, - }; - - columns.forEach((column) => { - const graphqlType = mapColumnTypeToGraphQLType(column, true); - - fields[column.name] = { - type: !column.isNullable ? new GraphQLNonNull(graphqlType) : graphqlType, - }; - }); - - return new GraphQLInputObjectType({ - name: `${pascalCase(name)}CreateInput`, - fields, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/generate-edge-type.util.ts b/server/src/tenant/schema-builder/utils/generate-edge-type.util.ts deleted file mode 100644 index d344a48af..000000000 --- a/server/src/tenant/schema-builder/utils/generate-edge-type.util.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GraphQLNonNull, GraphQLObjectType, GraphQLString } from 'graphql'; - -/** - * Generate a GraphQL edge type based on the ObjectType. - * @param ObjectType Object type to be used in the Edge. - * @returns GraphQL edge type. - */ -export const generateEdgeType = ( - ObjectType: T, -): GraphQLObjectType => { - return new GraphQLObjectType({ - name: `${ObjectType.name}Edge`, - fields: { - node: { - type: ObjectType, - }, - cursor: { - type: new GraphQLNonNull(GraphQLString), - }, - }, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/generate-filter-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-filter-input-type.util.ts deleted file mode 100644 index 1907a9c3d..000000000 --- a/server/src/tenant/schema-builder/utils/generate-filter-input-type.util.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GraphQLInputObjectType, GraphQLList, GraphQLNonNull } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; -import { UUIDFilterType } from 'src/tenant/schema-builder/graphql-types/input/uuid-filter.type'; -import { DatetimeFilterType } from 'src/tenant/schema-builder/graphql-types/input/date-time-filter.type'; - -import { mapColumnTypeToFilterType } from './map-column-type-to-filter-type.util'; - -const defaultFields = { - id: { type: UUIDFilterType }, - createdAt: { type: DatetimeFilterType }, - updatedAt: { type: DatetimeFilterType }, -}; - -/** - * Generate a GraphQL filter input type with filters based on the columns from metadata. - * @param name Name for the GraphQL object. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateFilterInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const filterInputType = new GraphQLInputObjectType({ - name: `${pascalCase(name)}FilterInput`, - fields: () => ({ - ...defaultFields, - ...columns.reduce((fields, column) => { - const graphqlType = mapColumnTypeToFilterType(column); - - fields[column.name] = { - type: graphqlType, - }; - - return fields; - }, {}), - and: { - type: new GraphQLList(new GraphQLNonNull(filterInputType)), - }, - or: { - type: new GraphQLList(new GraphQLNonNull(filterInputType)), - }, - not: { - type: filterInputType, - }, - }), - }); - - return filterInputType; -}; diff --git a/server/src/tenant/schema-builder/utils/generate-object-type.util.ts b/server/src/tenant/schema-builder/utils/generate-object-type.util.ts deleted file mode 100644 index e198e3d0f..000000000 --- a/server/src/tenant/schema-builder/utils/generate-object-type.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { GraphQLID, GraphQLNonNull, GraphQLObjectType } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; -import { DateTimeScalarType } from 'src/tenant/schema-builder/graphql-types/scalars/date-time.scalar'; - -import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; - -const defaultFields = { - id: { type: new GraphQLNonNull(GraphQLID) }, - createdAt: { type: new GraphQLNonNull(DateTimeScalarType) }, - updatedAt: { type: new GraphQLNonNull(DateTimeScalarType) }, -}; - -/** - * Generate a GraphQL object type based on the name and columns. - * @param name Name for the GraphQL object. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLObjectType - */ -export const generateObjectType = ( - name: string, - columns: FieldMetadata[], -): GraphQLObjectType => { - const fields = { - ...defaultFields, - }; - - columns.forEach((column) => { - const graphqlType = mapColumnTypeToGraphQLType(column); - - fields[column.name] = { - type: !column.isNullable ? new GraphQLNonNull(graphqlType) : graphqlType, - }; - }); - - return new GraphQLObjectType({ - name: pascalCase(name), - fields, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/generate-order-by-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-order-by-input-type.util.ts deleted file mode 100644 index c808dfb1d..000000000 --- a/server/src/tenant/schema-builder/utils/generate-order-by-input-type.util.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { GraphQLInputObjectType } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; -import { OrderByDirectionType } from 'src/tenant/schema-builder/graphql-types/enum/order-by-direction.type'; - -const defaultFields = { - id: { type: OrderByDirectionType }, - createdAt: { type: OrderByDirectionType }, - updatedAt: { type: OrderByDirectionType }, -}; - -/** - * Generate a GraphQL order by input type with order by fields based on the columns from metadata. - * @param name Name for the GraphQL object. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateOrderByInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const fields = { - ...defaultFields, - }; - - columns.forEach((column) => { - fields[column.name] = { - type: OrderByDirectionType, - }; - }); - - return new GraphQLInputObjectType({ - name: `${pascalCase(name)}OrderBy`, - fields, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts b/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts deleted file mode 100644 index 808c679e6..000000000 --- a/server/src/tenant/schema-builder/utils/generate-update-input-type.util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { GraphQLID, GraphQLInputObjectType } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; - -import { mapColumnTypeToGraphQLType } from './map-column-type-to-graphql-type.util'; - -/** - * Generate a GraphQL update input type based on the name and columns. - * @param name Name for the GraphQL input. - * @param columns Array of FieldMetadata columns. - * @returns GraphQLInputObjectType - */ -export const generateUpdateInputType = ( - name: string, - columns: FieldMetadata[], -): GraphQLInputObjectType => { - const fields: Record = { - id: { type: GraphQLID }, - }; - - columns.forEach((column) => { - const graphqlType = mapColumnTypeToGraphQLType(column, true); - // No GraphQLNonNull wrapping here, so all fields are optional - fields[column.name] = { - type: graphqlType, - }; - }); - - return new GraphQLInputObjectType({ - name: `${pascalCase(name)}UpdateInput`, - fields, - }); -}; diff --git a/server/src/tenant/schema-builder/utils/get-field-metadata-type.util.ts b/server/src/tenant/schema-builder/utils/get-field-metadata-type.util.ts new file mode 100644 index 000000000..0de04521f --- /dev/null +++ b/server/src/tenant/schema-builder/utils/get-field-metadata-type.util.ts @@ -0,0 +1,17 @@ +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; + +const typeOrmTypeMapping = new Map([ + ['uuid', FieldMetadataType.UUID], + ['timestamp', FieldMetadataType.DATE], + // Add more types here if we need to support more than id, and createdAt/updatedAt/deletedAt +]); + +export const getFieldMetadataType = (type: string) => { + const fieldType = typeOrmTypeMapping.get(type); + + if (fieldType === undefined || fieldType === null) { + throw new Error(`Unknown type ${type}`); + } + + return fieldType; +}; diff --git a/server/src/tenant/schema-builder/utils/get-resolver-args.util.ts b/server/src/tenant/schema-builder/utils/get-resolver-args.util.ts new file mode 100644 index 000000000..3ccaa73fc --- /dev/null +++ b/server/src/tenant/schema-builder/utils/get-resolver-args.util.ts @@ -0,0 +1,81 @@ +import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { ArgMetadata } from 'src/tenant/schema-builder/interfaces/param-metadata.interface'; + +import { FieldMetadataType } from 'src/metadata/field-metadata/field-metadata.entity'; +import { InputTypeDefinitionKind } from 'src/tenant/schema-builder/factories/input-type-definition.factory'; + +export const getResolverArgs = ( + type: ResolverBuilderMethodNames, +): { [key: string]: ArgMetadata } => { + switch (type) { + case 'findMany': + return { + first: { + type: FieldMetadataType.NUMBER, + isNullable: true, + }, + last: { + type: FieldMetadataType.NUMBER, + isNullable: true, + }, + before: { + type: FieldMetadataType.TEXT, + isNullable: true, + }, + after: { + type: FieldMetadataType.TEXT, + isNullable: true, + }, + filter: { + kind: InputTypeDefinitionKind.Filter, + isNullable: true, + }, + orderBy: { + kind: InputTypeDefinitionKind.OrderBy, + isNullable: true, + }, + }; + case 'findOne': + return { + filter: { + kind: InputTypeDefinitionKind.Filter, + isNullable: false, + }, + }; + case 'createMany': + return { + data: { + kind: InputTypeDefinitionKind.Create, + isNullable: false, + isArray: true, + }, + }; + case 'createOne': + return { + data: { + kind: InputTypeDefinitionKind.Create, + isNullable: false, + }, + }; + case 'updateOne': + return { + id: { + type: FieldMetadataType.UUID, + isNullable: false, + }, + data: { + kind: InputTypeDefinitionKind.Update, + isNullable: false, + }, + }; + case 'deleteOne': + return { + id: { + type: FieldMetadataType.UUID, + isNullable: false, + }, + }; + default: + throw new Error(`Unknown resolver type: ${type}`); + } +}; diff --git a/server/src/tenant/schema-builder/utils/map-column-type-to-filter-type.util.ts b/server/src/tenant/schema-builder/utils/map-column-type-to-filter-type.util.ts deleted file mode 100644 index 6c88b79fb..000000000 --- a/server/src/tenant/schema-builder/utils/map-column-type-to-filter-type.util.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GraphQLBoolean } from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { UUIDFilterType } from 'src/tenant/schema-builder/graphql-types/input/uuid-filter.type'; -import { StringFilterType } from 'src/tenant/schema-builder/graphql-types/input/string-filter.type'; -import { DateFilterType } from 'src/tenant/schema-builder/graphql-types/input/date-filter.type'; -import { IntFilter } from 'src/tenant/schema-builder/graphql-types/input/int-filter.type'; -import { UrlFilterType } from 'src/tenant/schema-builder/graphql-types/input/url-filter.type'; -import { MoneyFilterType } from 'src/tenant/schema-builder/graphql-types/input/money-filter.type'; - -/** - * Map the column type from field-metadata to its corresponding filter type. - * @param columnType Type of the column in the database. - */ -export const mapColumnTypeToFilterType = (column: FieldMetadata) => { - switch (column.type) { - case 'uuid': - return UUIDFilterType; - case 'text': - case 'phone': - case 'email': - return StringFilterType; - case 'date': - return DateFilterType; - case 'boolean': - return GraphQLBoolean; - case 'number': - return IntFilter; - case 'url': { - return UrlFilterType; - } - case 'money': { - return MoneyFilterType; - } - case 'enum': - default: - throw new Error(`${column.type} filter type not yet implemented`); - } -}; diff --git a/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts b/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts deleted file mode 100644 index 6cd1e0328..000000000 --- a/server/src/tenant/schema-builder/utils/map-column-type-to-graphql-type.util.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - GraphQLBoolean, - GraphQLEnumType, - GraphQLID, - GraphQLInputObjectType, - GraphQLInt, - GraphQLObjectType, - GraphQLString, -} from 'graphql'; - -import { FieldMetadata } from 'src/metadata/field-metadata/field-metadata.entity'; -import { pascalCase } from 'src/utils/pascal-case'; - -const UrlObjectType = new GraphQLObjectType({ - name: 'Url', - fields: { - text: { type: GraphQLString }, - link: { type: GraphQLString }, - }, -}); - -const UrlInputType = new GraphQLInputObjectType({ - name: 'UrlInput', - fields: { - text: { type: GraphQLString }, - link: { type: GraphQLString }, - }, -}); - -const MoneyObjectType = new GraphQLObjectType({ - name: 'Money', - fields: { - amount: { type: GraphQLInt }, - currency: { type: GraphQLString }, - }, -}); - -const MoneyInputType = new GraphQLInputObjectType({ - name: 'MoneyInput', - fields: { - amount: { type: GraphQLInt }, - currency: { type: GraphQLString }, - }, -}); - -/** - * Map the column type from field-metadata to its corresponding GraphQL type. - * @param columnType Type of the column in the database. - */ -export const mapColumnTypeToGraphQLType = ( - column: FieldMetadata, - input = false, -) => { - switch (column.type) { - case 'uuid': - return GraphQLID; - case 'text': - case 'phone': - case 'email': - case 'date': - return GraphQLString; - case 'boolean': - return GraphQLBoolean; - case 'number': - return GraphQLInt; - case 'enum': { - if (column.enums && column.enums.length > 0) { - const enumName = `${pascalCase(column.name)}Enum`; - - return new GraphQLEnumType({ - name: enumName, - values: Object.fromEntries( - column.enums.map((value) => [value, { value }]), - ), - }); - } - } - case 'url': { - return input ? UrlInputType : UrlObjectType; - } - case 'money': { - return input ? MoneyInputType : MoneyObjectType; - } - default: - return GraphQLString; - } -}; diff --git a/server/src/tenant/tenant.module.ts b/server/src/tenant/tenant.module.ts index e3e670cf5..03501c03f 100644 --- a/server/src/tenant/tenant.module.ts +++ b/server/src/tenant/tenant.module.ts @@ -7,6 +7,7 @@ import { ObjectMetadataModule } from 'src/metadata/object-metadata/object-metada import { TenantService } from './tenant.service'; import { SchemaBuilderModule } from './schema-builder/schema-builder.module'; +import { ResolverBuilderModule } from './resolver-builder/resolver-builder.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { SchemaBuilderModule } from './schema-builder/schema-builder.module'; SchemaBuilderModule, DataSourceMetadataModule, ObjectMetadataModule, + ResolverBuilderModule, ], providers: [TenantService], exports: [TenantService], diff --git a/server/src/tenant/tenant.service.spec.ts b/server/src/tenant/tenant.service.spec.ts index 5bdc2a4c4..a8c64dd97 100644 --- a/server/src/tenant/tenant.service.spec.ts +++ b/server/src/tenant/tenant.service.spec.ts @@ -5,7 +5,8 @@ import { ObjectMetadataService } from 'src/metadata/object-metadata/services/obj import { TenantService } from './tenant.service'; -import { SchemaBuilderService } from './schema-builder/schema-builder.service'; +import { ResolverFactory } from './resolver-builder/resolver.factory'; +import { GraphQLSchemaFactory } from './schema-builder/graphql-schema.factory'; describe('TenantService', () => { let service: TenantService; @@ -14,10 +15,6 @@ describe('TenantService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ TenantService, - { - provide: SchemaBuilderService, - useValue: {}, - }, { provide: DataSourceMetadataService, useValue: {}, @@ -26,6 +23,14 @@ describe('TenantService', () => { provide: ObjectMetadataService, useValue: {}, }, + { + provide: GraphQLSchemaFactory, + useValue: {}, + }, + { + provide: ResolverFactory, + useValue: {}, + }, ], }).compile(); diff --git a/server/src/tenant/tenant.service.ts b/server/src/tenant/tenant.service.ts index ec7ee5825..679e09e11 100644 --- a/server/src/tenant/tenant.service.ts +++ b/server/src/tenant/tenant.service.ts @@ -1,18 +1,24 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ import { Injectable } from '@nestjs/common'; -import { GraphQLSchema } from 'graphql'; +import { GraphQLSchema, printSchema } from 'graphql'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { gql } from 'graphql-tag'; import { DataSourceMetadataService } from 'src/metadata/data-source-metadata/data-source-metadata.service'; import { ObjectMetadataService } from 'src/metadata/object-metadata/services/object-metadata.service'; -import { SchemaBuilderService } from './schema-builder/schema-builder.service'; +import { GraphQLSchemaFactory } from './schema-builder/graphql-schema.factory'; +import { resolverBuilderMethodNames } from './resolver-builder/factories/factories'; +import { ResolverFactory } from './resolver-builder/resolver.factory'; @Injectable() export class TenantService { constructor( - private readonly schemaBuilderService: SchemaBuilderService, private readonly dataSourceMetadataService: DataSourceMetadataService, private readonly objectMetadataService: ObjectMetadataService, + private readonly graphQLSchemaFactory: GraphQLSchemaFactory, + private readonly resolverFactory: ResolverFactory, ) {} async createTenantSchema(workspaceId: string | undefined) { @@ -30,16 +36,30 @@ export class TenantService { return new GraphQLSchema({}); } - const dataSourceMetadata = dataSourcesMetadata[0]; - - const objectMetadata = - await this.objectMetadataService.getObjectMetadataFromDataSourceId( - dataSourceMetadata.id, + const objectMetadataCollection = + await this.objectMetadataService.getObjectMetadataFromWorkspaceId( + workspaceId, ); - return this.schemaBuilderService.generateSchema( - workspaceId, - objectMetadata, + const autoGeneratedSchema = await this.graphQLSchemaFactory.create( + objectMetadataCollection, + resolverBuilderMethodNames, ); + const autoGeneratedResolvers = await this.resolverFactory.create( + workspaceId, + objectMetadataCollection, + resolverBuilderMethodNames, + ); + + // TODO: Cache the generate type definitions + const typeDefs = printSchema(autoGeneratedSchema); + const executableSchema = makeExecutableSchema({ + typeDefs: gql` + ${typeDefs} + `, + resolvers: autoGeneratedResolvers, + }); + + return executableSchema; } } diff --git a/server/src/tenant/utils/__tests__/get-resolver-name.spec.ts b/server/src/tenant/utils/__tests__/get-resolver-name.spec.ts new file mode 100644 index 000000000..3f826a8dd --- /dev/null +++ b/server/src/tenant/utils/__tests__/get-resolver-name.spec.ts @@ -0,0 +1,30 @@ +import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; + +import { getResolverName } from 'src/tenant/utils/get-resolver-name.util'; + +describe('getResolverName', () => { + const metadata = { + nameSingular: 'entity', + namePlural: 'entities', + }; + + it.each([ + ['findMany', 'entities'], + ['findOne', 'entity'], + ['createMany', 'createEntities'], + ['createOne', 'createEntity'], + ['updateOne', 'updateEntity'], + ['deleteOne', 'deleteEntity'], + ])('should return correct name for %s resolver', (type, expectedResult) => { + expect(getResolverName(metadata, type as ResolverBuilderMethodNames)).toBe( + expectedResult, + ); + }); + + it('should throw an error for an unknown resolver type', () => { + const unknownType = 'unknownType'; + expect(() => + getResolverName(metadata, unknownType as ResolverBuilderMethodNames), + ).toThrow(`Unknown resolver type: ${unknownType}`); + }); +}); diff --git a/server/src/tenant/utils/get-resolver-name.util.ts b/server/src/tenant/utils/get-resolver-name.util.ts new file mode 100644 index 000000000..b8a650b36 --- /dev/null +++ b/server/src/tenant/utils/get-resolver-name.util.ts @@ -0,0 +1,27 @@ +import { ResolverBuilderMethodNames } from 'src/tenant/resolver-builder/interfaces/resolvers-builder.interface'; +import { ObjectMetadataInterface } from 'src/tenant/schema-builder/interfaces/object-metadata.interface'; + +import { camelCase } from 'src/utils/camel-case'; +import { pascalCase } from 'src/utils/pascal-case'; + +export const getResolverName = ( + objectMetadata: Pick, + type: ResolverBuilderMethodNames, +) => { + switch (type) { + case 'findMany': + return `${camelCase(objectMetadata.namePlural)}`; + case 'findOne': + return `${camelCase(objectMetadata.nameSingular)}`; + case 'createMany': + return `create${pascalCase(objectMetadata.namePlural)}`; + case 'createOne': + return `create${pascalCase(objectMetadata.nameSingular)}`; + case 'updateOne': + return `update${pascalCase(objectMetadata.nameSingular)}`; + case 'deleteOne': + return `delete${pascalCase(objectMetadata.nameSingular)}`; + default: + throw new Error(`Unknown resolver type: ${type}`); + } +}; diff --git a/server/yarn.lock b/server/yarn.lock index e169b6aac..2c41d81ea 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -1390,7 +1390,7 @@ "@graphql-tools/schema@10.0.0", "@graphql-tools/schema@^10.0.0": version "10.0.0" - resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.0.tgz" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.0.tgz#7b5f6b6a59f51c927de8c9069bde4ebbfefc64b3" integrity sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg== dependencies: "@graphql-tools/merge" "^9.0.0" @@ -5885,13 +5885,20 @@ graphql-subscriptions@2.0.0: dependencies: iterall "^1.3.0" -graphql-tag@2.12.6, graphql-tag@^2.11.0, graphql-tag@^2.12.6: +graphql-tag@2.12.6, graphql-tag@^2.11.0: version "2.12.6" resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz" integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== dependencies: tslib "^2.1.0" +graphql-tag@^2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== + dependencies: + tslib "^2.1.0" + graphql-type-json@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115"