From 04c7c1a33400d2f70a80c2fbfd03f7840bea724c Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:40:44 +0100 Subject: [PATCH] Feature flags seeds, queries and hooks (#2769) * seed is working * allow graphql to retrieve feature flag data * create useIsFeatureEnabled hook * hook is working * Update icons.ts --- front/src/generated/graphql.tsx | 51 ++++++++++++++++++- .../auth/states/currentWorkspaceState.ts | 7 ++- .../users/graphql/queries/getCurrentUser.ts | 6 +++ .../workspace/hooks/useIsFeatureEnabled.ts | 17 +++++++ .../SettingsObjectNewFieldStep2.tsx | 34 ++++++++----- server/src/core/core.module.ts | 10 +++- .../core/feature-flag/feature-flag.entity.ts | 8 +++ .../core/feature-flag/feature-flag.module.ts | 23 +++++++++ server/src/core/workspace/workspace.entity.ts | 3 +- server/src/core/workspace/workspace.module.ts | 6 ++- .../typeorm-seeds/core/feature-flags.ts | 24 +++++++++ .../src/database/typeorm-seeds/core/index.ts | 2 + 12 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 front/src/modules/workspace/hooks/useIsFeatureEnabled.ts create mode 100644 server/src/core/feature-flag/feature-flag.module.ts create mode 100644 server/src/database/typeorm-seeds/core/feature-flags.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index e5583ac1e..2c5a2cd1e 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -88,6 +88,30 @@ export type CursorPaging = { last?: InputMaybe; }; +export type FeatureFlag = { + __typename?: 'FeatureFlag'; + id: Scalars['ID']; + key: Scalars['String']; + value: Scalars['Boolean']; + workspaceId: Scalars['String']; +}; + +export type FeatureFlagFilter = { + and?: InputMaybe>; + id?: InputMaybe; + or?: InputMaybe>; +}; + +export type FeatureFlagSort = { + direction: SortDirection; + field: FeatureFlagSortFields; + nulls?: InputMaybe; +}; + +export enum FeatureFlagSortFields { + Id = 'id' +} + export type FieldConnection = { __typename?: 'FieldConnection'; /** Array of edges. */ @@ -387,6 +411,18 @@ export enum RelationMetadataType { OneToOne = 'ONE_TO_ONE' } +/** Sort Directions */ +export enum SortDirection { + Asc = 'ASC', + Desc = 'DESC' +} + +/** Sort Nulls Options */ +export enum SortNulls { + NullsFirst = 'NULLS_FIRST', + NullsLast = 'NULLS_LAST' +} + export type Support = { __typename?: 'Support'; supportDriver: Scalars['String']; @@ -466,12 +502,19 @@ export type Workspace = { deletedAt?: Maybe; displayName?: Maybe; domainName?: Maybe; + featureFlags?: Maybe>; id: Scalars['ID']; inviteHash?: Maybe; logo?: Maybe; updatedAt: Scalars['DateTime']; }; + +export type WorkspaceFeatureFlagsArgs = { + filter?: FeatureFlagFilter; + sorting?: Array; +}; + export type WorkspaceEdge = { __typename?: 'WorkspaceEdge'; /** Cursor for this node. */ @@ -671,7 +714,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean } } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember: { __typename?: 'UserWorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'UserWorkspaceMemberName', firstName: string, lastName: string } }, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } } }; export type DeleteCurrentWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -1204,6 +1247,12 @@ export const GetCurrentUserDocument = gql` domainName inviteHash allowImpersonation + featureFlags { + id + key + value + workspaceId + } } } } diff --git a/front/src/modules/auth/states/currentWorkspaceState.ts b/front/src/modules/auth/states/currentWorkspaceState.ts index 597b883ec..95da94336 100644 --- a/front/src/modules/auth/states/currentWorkspaceState.ts +++ b/front/src/modules/auth/states/currentWorkspaceState.ts @@ -4,7 +4,12 @@ import { Workspace } from '~/generated/graphql'; export type CurrentWorkspace = Pick< Workspace, - 'id' | 'inviteHash' | 'logo' | 'displayName' | 'allowImpersonation' + | 'id' + | 'inviteHash' + | 'logo' + | 'displayName' + | 'allowImpersonation' + | 'featureFlags' >; export const currentWorkspaceState = atom({ diff --git a/front/src/modules/users/graphql/queries/getCurrentUser.ts b/front/src/modules/users/graphql/queries/getCurrentUser.ts index eba2e3584..891afbc63 100644 --- a/front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -26,6 +26,12 @@ export const GET_CURRENT_USER = gql` domainName inviteHash allowImpersonation + featureFlags { + id + key + value + workspaceId + } } } } diff --git a/front/src/modules/workspace/hooks/useIsFeatureEnabled.ts b/front/src/modules/workspace/hooks/useIsFeatureEnabled.ts new file mode 100644 index 000000000..6e75f25f4 --- /dev/null +++ b/front/src/modules/workspace/hooks/useIsFeatureEnabled.ts @@ -0,0 +1,17 @@ +import { useRecoilValue } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; + +export const useIsFeatureEnabled = (featureKey: string): boolean => { + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const featureFlag = currentWorkspace?.featureFlags?.find( + (flag) => flag.key === featureKey, + ); + + if (!featureFlag) { + return false; + } + + return featureFlag.value; +}; diff --git a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx index 3df092c38..1963e25c5 100644 --- a/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx @@ -20,6 +20,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer' import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; import { View } from '@/views/types/View'; import { ViewType } from '@/views/types/ViewType'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const SettingsObjectNewFieldStep2 = () => { @@ -37,6 +38,10 @@ export const SettingsObjectNewFieldStep2 = () => { findActiveObjectMetadataItemBySlug(objectSlug); const { createMetadataField } = useFieldMetadataItem(); + const isRelationFieldTypeEnabled = useIsFeatureEnabled( + 'IS_RELATION_FIELD_TYPE_ENABLED', + ); + const { formValues, handleFormChange, @@ -177,6 +182,22 @@ export const SettingsObjectNewFieldStep2 = () => { } }; + const excludedFieldTypes = [ + FieldMetadataType.Currency, + FieldMetadataType.Email, + FieldMetadataType.Enum, + FieldMetadataType.Numeric, + FieldMetadataType.FullName, + FieldMetadataType.Link, + FieldMetadataType.Phone, + FieldMetadataType.Probability, + FieldMetadataType.Uuid, + ]; + + if (!isRelationFieldTypeEnabled) { + excludedFieldTypes.push(FieldMetadataType.Relation); + } + return ( @@ -204,18 +225,7 @@ export const SettingsObjectNewFieldStep2 = () => { onChange={handleFormChange} /> ID) @PrimaryGeneratedColumn('uuid') id: string; + @Field() @Column({ nullable: false, type: 'text' }) key: string; + @Field() @Column({ nullable: false, type: 'uuid' }) workspaceId: string; @@ -27,6 +34,7 @@ export class FeatureFlagEntity { }) workspace: Workspace; + @Field() @Column({ nullable: false }) value: boolean; diff --git a/server/src/core/feature-flag/feature-flag.module.ts b/server/src/core/feature-flag/feature-flag.module.ts new file mode 100644 index 000000000..be8d796ef --- /dev/null +++ b/server/src/core/feature-flag/feature-flag.module.ts @@ -0,0 +1,23 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; + +@Module({ + imports: [ + TypeORMModule, + NestjsQueryGraphQLModule.forFeature({ + imports: [ + NestjsQueryTypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + ], + services: [], + resolvers: [], + }), + ], + exports: [], + providers: [], +}) +export class FeatureFlagModule {} diff --git a/server/src/core/workspace/workspace.entity.ts b/server/src/core/workspace/workspace.entity.ts index c840d5892..78afeb7ff 100644 --- a/server/src/core/workspace/workspace.entity.ts +++ b/server/src/core/workspace/workspace.entity.ts @@ -1,6 +1,6 @@ import { Field, ID, ObjectType } from '@nestjs/graphql'; -import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { IDField, UnPagedRelation } from '@ptc-org/nestjs-query-graphql'; import { Column, CreateDateColumn, @@ -15,6 +15,7 @@ import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; @Entity({ name: 'workspace', schema: 'core' }) @ObjectType('Workspace') +@UnPagedRelation('featureFlags', () => FeatureFlagEntity, { nullable: true }) export class Workspace { @IDField(() => ID) @PrimaryGeneratedColumn('uuid') diff --git a/server/src/core/workspace/workspace.module.ts b/server/src/core/workspace/workspace.module.ts index a8bfea917..052b81950 100644 --- a/server/src/core/workspace/workspace.module.ts +++ b/server/src/core/workspace/workspace.module.ts @@ -7,6 +7,7 @@ import { FileModule } from 'src/core/file/file.module'; import { WorkspaceManagerModule } from 'src/workspace/workspace-manager/workspace-manager.module'; import { WorkspaceResolver } from 'src/core/workspace/workspace.resolver'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { FeatureFlagEntity } from 'src/core/feature-flag/feature-flag.entity'; import { Workspace } from './workspace.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; @@ -18,7 +19,10 @@ import { WorkspaceService } from './services/workspace.service'; TypeORMModule, NestjsQueryGraphQLModule.forFeature({ imports: [ - NestjsQueryTypeOrmModule.forFeature([Workspace], 'core'), + NestjsQueryTypeOrmModule.forFeature( + [Workspace, FeatureFlagEntity], + 'core', + ), WorkspaceManagerModule, FileModule, ], diff --git a/server/src/database/typeorm-seeds/core/feature-flags.ts b/server/src/database/typeorm-seeds/core/feature-flags.ts new file mode 100644 index 000000000..f2028fb0b --- /dev/null +++ b/server/src/database/typeorm-seeds/core/feature-flags.ts @@ -0,0 +1,24 @@ +import { DataSource } from 'typeorm'; + +const tableName = 'featureFlag'; + +import { SeedWorkspaceId } from 'src/database/typeorm-seeds/core/workspaces'; + +export const seedFeatureFlags = async ( + workspaceDataSource: DataSource, + schemaName: string, +) => { + await workspaceDataSource + .createQueryBuilder() + .insert() + .into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value']) + .orIgnore() + .values([ + { + key: 'IS_RELATION_FIELD_TYPE_ENABLED', + workspaceId: SeedWorkspaceId, + value: true, + }, + ]) + .execute(); +}; diff --git a/server/src/database/typeorm-seeds/core/index.ts b/server/src/database/typeorm-seeds/core/index.ts index 3cb929163..767f692f7 100644 --- a/server/src/database/typeorm-seeds/core/index.ts +++ b/server/src/database/typeorm-seeds/core/index.ts @@ -2,9 +2,11 @@ import { DataSource } from 'typeorm'; import { seedUsers } from 'src/database/typeorm-seeds/core/users'; import { seedWorkspaces } from 'src/database/typeorm-seeds/core/workspaces'; +import { seedFeatureFlags } from 'src/database/typeorm-seeds/core/feature-flags'; export const seedCoreSchema = async (workspaceDataSource: DataSource) => { const schemaName = 'core'; await seedWorkspaces(workspaceDataSource, schemaName); await seedUsers(workspaceDataSource, schemaName); + await seedFeatureFlags(workspaceDataSource, schemaName); };