From a69711429b7dc5f07e6b2c74ed3f3cfcf3234495 Mon Sep 17 00:00:00 2001 From: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:54:04 +0545 Subject: [PATCH] chore(backend): convert basic RefreshToken model to TypeORM entity (#2401) * chore: convert basic RefreshToken model to TypeORM entity Co-authored-by: v1b3m * Fix import Co-authored-by: v1b3m --------- Co-authored-by: v1b3m --- front/src/generated-metadata/graphql.ts | 33 +++++++++-- front/src/generated/graphql.tsx | 38 +++++++++++- server/src/coreV2/core.module.ts | 2 + .../dtos/create-refresh-token.input.ts | 11 ++++ .../before-create-one-refresh-token.hook.ts | 24 ++++++++ .../refresh-token.auto-resolver-opts.ts | 41 +++++++++++++ .../refresh-token/refresh-token.entity.ts | 59 +++++++++++++++++++ .../refresh-token/refresh-token.module.ts | 20 +++++++ .../services/refresh-token.service.spec.ts | 28 +++++++++ .../services/refresh-token.service.ts | 5 ++ server/src/coreV2/userv2/user.entity.ts | 7 ++- 11 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 server/src/coreV2/refresh-token/dtos/create-refresh-token.input.ts create mode 100644 server/src/coreV2/refresh-token/hooks/before-create-one-refresh-token.hook.ts create mode 100644 server/src/coreV2/refresh-token/refresh-token.auto-resolver-opts.ts create mode 100644 server/src/coreV2/refresh-token/refresh-token.entity.ts create mode 100644 server/src/coreV2/refresh-token/refresh-token.module.ts create mode 100644 server/src/coreV2/refresh-token/services/refresh-token.service.spec.ts create mode 100644 server/src/coreV2/refresh-token/services/refresh-token.service.ts diff --git a/front/src/generated-metadata/graphql.ts b/front/src/generated-metadata/graphql.ts index f9dae4c69..7bf60a40e 100644 --- a/front/src/generated-metadata/graphql.ts +++ b/front/src/generated-metadata/graphql.ts @@ -207,7 +207,7 @@ export type CreateRelationInput = { fromLabel: Scalars['String']['input']; fromName: Scalars['String']['input']; fromObjectMetadataId: Scalars['String']['input']; - relationType: Scalars['String']['input']; + relationType: RelationMetadataType; toIcon?: InputMaybe; toLabel: Scalars['String']['input']; toName: Scalars['String']['input']; @@ -452,10 +452,10 @@ export enum FieldMetadataType { Date = 'DATE', Email = 'EMAIL', Enum = 'ENUM', - Probability = 'PROBABILITY', Money = 'MONEY', Number = 'NUMBER', Phone = 'PHONE', + Probability = 'PROBABILITY', Relation = 'RELATION', Text = 'TEXT', Url = 'URL', @@ -527,6 +527,7 @@ export type ObjectDeleteResponse = { id?: Maybe; isActive?: Maybe; isCustom?: Maybe; + isSystem?: Maybe; labelPlural?: Maybe; labelSingular?: Maybe; namePlural?: Maybe; @@ -671,6 +672,22 @@ export type QueryRelationsArgs = { paging?: CursorPaging; }; +export type RefreshToken = { + __typename?: 'RefreshToken'; + createdAt: Scalars['DateTime']['output']; + expiresAt: Scalars['DateTime']['output']; + id: Scalars['ID']['output']; + updatedAt: Scalars['DateTime']['output']; +}; + +export type RefreshTokenEdge = { + __typename?: 'RefreshTokenEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']['output']; + /** The node containing the RefreshToken */ + node: RefreshToken; +}; + export type RelationConnection = { __typename?: 'RelationConnection'; /** Array of edges. */ @@ -681,6 +698,13 @@ export type RelationConnection = { totalCount: Scalars['Int']['output']; }; +/** Type of the relation */ +export enum RelationMetadataType { + ManyToMany = 'MANY_TO_MANY', + OneToMany = 'ONE_TO_MANY', + OneToOne = 'ONE_TO_ONE' +} + export type Support = { __typename?: 'Support'; supportDriver: Scalars['String']['output']; @@ -850,6 +874,7 @@ export type Object = { id: Scalars['ID']['output']; isActive: Scalars['Boolean']['output']; isCustom: Scalars['Boolean']['output']; + isSystem: Scalars['Boolean']['output']; labelPlural: Scalars['String']['output']; labelSingular: Scalars['String']['output']; namePlural: Scalars['String']['output']; @@ -877,7 +902,7 @@ export type Relation = { fromObjectMetadata: Object; fromObjectMetadataId: Scalars['String']['output']; id: Scalars['ID']['output']; - relationType: Scalars['String']['output']; + relationType: RelationMetadataType; toFieldMetadataId: Scalars['String']['output']; toObjectMetadata: Object; toObjectMetadataId: Scalars['String']['output']; @@ -939,7 +964,7 @@ export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', dele 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: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, placeholder?: string | null, isCustom: boolean, isActive: boolean, isNullable: boolean, createdAt: any, updatedAt: any, fromRelationMetadata?: { __typename?: 'relation', id: string, relationType: string } | null, toRelationMetadata?: { __typename?: 'relation', id: string, relationType: string } | null } }>, 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, fromRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType } | null, toRelationMetadata?: { __typename?: 'relation', id: string, relationType: RelationMetadataType } | null } }>, 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 21ac44001..6d8cf8ab7 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1322,6 +1322,7 @@ export enum FieldMetadataType { Money = 'MONEY', Number = 'NUMBER', Phone = 'PHONE', + Probability = 'PROBABILITY', Relation = 'RELATION', Text = 'TEXT', Url = 'URL', @@ -1799,6 +1800,7 @@ export type ObjectDeleteResponse = { id?: Maybe; isActive?: Maybe; isCustom?: Maybe; + isSystem?: Maybe; labelPlural?: Maybe; labelSingular?: Maybe; namePlural?: Maybe; @@ -2558,6 +2560,32 @@ export enum QueryMode { Insensitive = 'insensitive' } +export type RefreshToken = { + __typename?: 'RefreshToken'; + createdAt: Scalars['DateTime']; + expiresAt: Scalars['DateTime']; + id: Scalars['ID']; + updatedAt: Scalars['DateTime']; +}; + +export type RefreshTokenConnection = { + __typename?: 'RefreshTokenConnection'; + /** Array of edges. */ + edges: Array; + /** Paging information */ + pageInfo: PageInfo; + /** Fetch total count of records */ + totalCount: Scalars['Int']; +}; + +export type RefreshTokenEdge = { + __typename?: 'RefreshTokenEdge'; + /** Cursor for this node. */ + cursor: Scalars['ConnectionCursor']; + /** The node containing the RefreshToken */ + node: RefreshToken; +}; + export type RelationConnection = { __typename?: 'RelationConnection'; /** Array of edges. */ @@ -2568,6 +2596,13 @@ export type RelationConnection = { totalCount: Scalars['Int']; }; +/** Type of the relation */ +export enum RelationMetadataType { + ManyToMany = 'MANY_TO_MANY', + OneToMany = 'ONE_TO_MANY', + OneToOne = 'ONE_TO_ONE' +} + export enum SortOrder { Asc = 'asc', Desc = 'desc' @@ -3151,6 +3186,7 @@ export type Object = { id: Scalars['ID']; isActive: Scalars['Boolean']; isCustom: Scalars['Boolean']; + isSystem: Scalars['Boolean']; labelPlural: Scalars['String']; labelSingular: Scalars['String']; namePlural: Scalars['String']; @@ -3178,7 +3214,7 @@ export type Relation = { fromObjectMetadata: Object; fromObjectMetadataId: Scalars['String']; id: Scalars['ID']; - relationType: Scalars['String']; + relationType: RelationMetadataType; toFieldMetadataId: Scalars['String']; toObjectMetadata: Object; toObjectMetadataId: Scalars['String']; diff --git a/server/src/coreV2/core.module.ts b/server/src/coreV2/core.module.ts index ebd6789bd..0c88d53b1 100644 --- a/server/src/coreV2/core.module.ts +++ b/server/src/coreV2/core.module.ts @@ -9,6 +9,7 @@ import GraphQLJSON from 'graphql-type-json'; import config from '../../ormconfig'; import { UserModule } from './userv2/user.module'; +import { RefreshTokenModule } from './refresh-token/refresh-token.module'; @Module({ imports: [ @@ -23,6 +24,7 @@ import { UserModule } from './userv2/user.module'; path: '/graphqlv2', }), UserModule, + RefreshTokenModule, ], exports: [UserModule], }) diff --git a/server/src/coreV2/refresh-token/dtos/create-refresh-token.input.ts b/server/src/coreV2/refresh-token/dtos/create-refresh-token.input.ts new file mode 100644 index 000000000..a31883037 --- /dev/null +++ b/server/src/coreV2/refresh-token/dtos/create-refresh-token.input.ts @@ -0,0 +1,11 @@ +import { Field, InputType } from '@nestjs/graphql'; + +import { IsDate, IsNotEmpty } from 'class-validator'; + +@InputType() +export class CreateRefreshTokenInput { + @IsDate() + @IsNotEmpty() + @Field() + expiresAt: Date; +} diff --git a/server/src/coreV2/refresh-token/hooks/before-create-one-refresh-token.hook.ts b/server/src/coreV2/refresh-token/hooks/before-create-one-refresh-token.hook.ts new file mode 100644 index 000000000..cacc807e4 --- /dev/null +++ b/server/src/coreV2/refresh-token/hooks/before-create-one-refresh-token.hook.ts @@ -0,0 +1,24 @@ +import { + BeforeCreateOneHook, + CreateOneInputType, +} from '@ptc-org/nestjs-query-graphql'; +import { v4 as uuidv4 } from 'uuid'; + +import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity'; + +export class BeforeCreateOneRefreshToken + implements BeforeCreateOneHook +{ + async run( + instance: CreateOneInputType, + context: any, + ): Promise> { + const userId = context?.req?.user?.user?.id; + + instance.input.userId = userId; + // FIXME: These fields should be autogenerated, we need to run a migration for this + instance.input.id = uuidv4(); + instance.input.updatedAt = new Date(); + return instance; + } +} diff --git a/server/src/coreV2/refresh-token/refresh-token.auto-resolver-opts.ts b/server/src/coreV2/refresh-token/refresh-token.auto-resolver-opts.ts new file mode 100644 index 000000000..174863620 --- /dev/null +++ b/server/src/coreV2/refresh-token/refresh-token.auto-resolver-opts.ts @@ -0,0 +1,41 @@ +import { + AutoResolverOpts, + PagingStrategies, + ReadResolverOpts, +} from '@ptc-org/nestjs-query-graphql'; +import { SortDirection } from '@ptc-org/nestjs-query-core'; + +import { JwtAuthGuard } from 'src/guards/jwt.auth.guard'; + +import { RefreshToken } from './refresh-token.entity'; + +import { CreateRefreshTokenInput } from './dtos/create-refresh-token.input'; + +export const refreshTokenAutoResolverOpts: AutoResolverOpts< + any, + any, + unknown, + unknown, + ReadResolverOpts, + PagingStrategies +>[] = [ + { + EntityClass: RefreshToken, + DTOClass: RefreshToken, + CreateDTOClass: CreateRefreshTokenInput, + enableTotalCount: true, + pagingStrategy: PagingStrategies.CURSOR, + read: { + defaultSort: [{ field: 'id', direction: SortDirection.DESC }], + }, + create: { + many: { disabled: true }, + }, + update: { + many: { disabled: true }, + one: { disabled: true }, + }, + delete: { many: { disabled: true }, one: { disabled: true } }, + guards: [JwtAuthGuard], + }, +]; diff --git a/server/src/coreV2/refresh-token/refresh-token.entity.ts b/server/src/coreV2/refresh-token/refresh-token.entity.ts new file mode 100644 index 000000000..05da8babc --- /dev/null +++ b/server/src/coreV2/refresh-token/refresh-token.entity.ts @@ -0,0 +1,59 @@ +import { Field, ID, ObjectType } from '@nestjs/graphql'; + +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToOne, + JoinColumn, + CreateDateColumn, + UpdateDateColumn, +} from 'typeorm'; +import { + Authorize, + BeforeCreateOne, + IDField, +} from '@ptc-org/nestjs-query-graphql'; + +import { User } from 'src/coreV2/userv2/user.entity'; + +import { BeforeCreateOneRefreshToken } from './hooks/before-create-one-refresh-token.hook'; + +@Entity('refresh_tokens') +@ObjectType('RefreshToken') +@BeforeCreateOne(BeforeCreateOneRefreshToken) +@Authorize({ + authorize: (context: any) => ({ + userId: { eq: context?.req?.user?.user?.id }, + }), +}) +export class RefreshToken { + @IDField(() => ID) + @PrimaryGeneratedColumn('uuid') + id: string; + + @ManyToOne(() => User, (user) => user.refreshTokens) + @JoinColumn({ name: 'userId' }) + user: User; + + @Column() + userId: string; + + @Field() + @Column('time with time zone') + expiresAt: Date; + + @Column('timestamp with time zone', { nullable: true }) + deletedAt: Date | null; + + @Column('timestamp with time zone', { nullable: true }) + revokedAt: Date | null; + + @Field() + @CreateDateColumn({ type: 'timestamp with time zone' }) + createdAt: Date; + + @Field() + @UpdateDateColumn({ type: 'timestamp with time zone' }) + updatedAt: Date; +} diff --git a/server/src/coreV2/refresh-token/refresh-token.module.ts b/server/src/coreV2/refresh-token/refresh-token.module.ts new file mode 100644 index 000000000..b0315c5c7 --- /dev/null +++ b/server/src/coreV2/refresh-token/refresh-token.module.ts @@ -0,0 +1,20 @@ +import { Module } from '@nestjs/common'; + +import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql'; +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { RefreshToken } from './refresh-token.entity'; +import { refreshTokenAutoResolverOpts } from './refresh-token.auto-resolver-opts'; + +import { RefreshTokenService } from './services/refresh-token.service'; + +@Module({ + imports: [ + NestjsQueryGraphQLModule.forFeature({ + imports: [NestjsQueryTypeOrmModule.forFeature([RefreshToken])], + services: [RefreshTokenService], + resolvers: refreshTokenAutoResolverOpts, + }), + ], +}) +export class RefreshTokenModule {} diff --git a/server/src/coreV2/refresh-token/services/refresh-token.service.spec.ts b/server/src/coreV2/refresh-token/services/refresh-token.service.spec.ts new file mode 100644 index 000000000..b3f948efc --- /dev/null +++ b/server/src/coreV2/refresh-token/services/refresh-token.service.spec.ts @@ -0,0 +1,28 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity'; + +import { RefreshTokenService } from './refresh-token.service'; + +describe('RefreshTokenService', () => { + let service: RefreshTokenService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RefreshTokenService, + { + provide: getRepositoryToken(RefreshToken), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(RefreshTokenService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/server/src/coreV2/refresh-token/services/refresh-token.service.ts b/server/src/coreV2/refresh-token/services/refresh-token.service.ts new file mode 100644 index 000000000..acad308e3 --- /dev/null +++ b/server/src/coreV2/refresh-token/services/refresh-token.service.ts @@ -0,0 +1,5 @@ +import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; + +import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity'; + +export class RefreshTokenService extends TypeOrmQueryService {} diff --git a/server/src/coreV2/userv2/user.entity.ts b/server/src/coreV2/userv2/user.entity.ts index cde86c733..d54f3c81c 100644 --- a/server/src/coreV2/userv2/user.entity.ts +++ b/server/src/coreV2/userv2/user.entity.ts @@ -1,4 +1,6 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; + +import { RefreshToken } from 'src/coreV2/refresh-token/refresh-token.entity'; @Entity('users') export class User { @@ -16,4 +18,7 @@ export class User { @Column({ default: false }) emailVerified: boolean; + + @OneToMany(() => RefreshToken, (refreshToken) => refreshToken.user) + refreshTokens: RefreshToken[]; }