-
Notifications
You must be signed in to change notification settings - Fork 490
Add pipeline draft CRUD endpoints for RDI instances #6467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1e4d08b
feat(api): add pipeline draft CRUD endpoints for RDI instances
ArtemHoruzhenko e81e79a
fix(rdi): address PR review findings on pipeline draft CRUD
ArtemHoruzhenko 94817cc
fix(rdi): address further PR review findings on pipeline draft CRUD
ArtemHoruzhenko e20ca94
fix(rdi): address more PR review findings on pipeline draft CRUD
ArtemHoruzhenko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
redisinsight/api/migration/1784100000000-pipeline-draft.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
|
||
| export class PipelineDraft1784100000000 implements MigrationInterface { | ||
| name = 'PipelineDraft1784100000000'; | ||
|
|
||
| public async up(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query( | ||
| `CREATE TABLE "pipeline_draft" ( | ||
| "id" varchar PRIMARY KEY NOT NULL, | ||
| "rdiInstanceId" varchar NOT NULL, | ||
| "data" text NOT NULL, | ||
| "encryption" varchar, | ||
| "createdAt" datetime NOT NULL DEFAULT (datetime('now')), | ||
| "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), | ||
| CONSTRAINT "FK_pipeline_draft_rdiInstanceId" FOREIGN KEY ("rdiInstanceId") REFERENCES "rdi" ("id") ON DELETE CASCADE ON UPDATE NO ACTION | ||
| )`, | ||
| ); | ||
| await queryRunner.query( | ||
| `CREATE INDEX "IDX_pipeline_draft_rdiInstanceId" ON "pipeline_draft" ("rdiInstanceId")`, | ||
| ); | ||
| } | ||
|
|
||
| public async down(queryRunner: QueryRunner): Promise<void> { | ||
| await queryRunner.query(`DROP INDEX "IDX_pipeline_draft_rdiInstanceId"`); | ||
| await queryRunner.query(`DROP TABLE "pipeline_draft"`); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
redisinsight/api/src/modules/rdi/__tests__/pipeline-draft.factory.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { Factory } from 'fishery'; | ||
| import { faker } from '@faker-js/faker'; | ||
| import { PipelineDraft } from '../models/pipeline-draft'; | ||
| import { PipelineDraftEntity } from '../entities/pipeline-draft.entity'; | ||
| import { CreatePipelineDraftDto } from '../dto/create.pipeline-draft.dto'; | ||
|
|
||
| export const pipelineDraftFactory = Factory.define<PipelineDraft>(() => ({ | ||
| id: faker.string.uuid(), | ||
| rdiInstanceId: faker.string.uuid(), | ||
| data: { [faker.word.noun()]: faker.word.words(3) }, | ||
| createdAt: faker.date.recent(), | ||
| updatedAt: faker.date.recent(), | ||
| })); | ||
|
|
||
| // Entity-level `data` is the persisted string form (what DataAsJsonString | ||
| // produces before encryption), not the parsed object the API model exposes. | ||
| export const pipelineDraftEntityFactory = Factory.define<PipelineDraftEntity>( | ||
| () => { | ||
| const draft = pipelineDraftFactory.build(); | ||
|
|
||
| return { | ||
| ...draft, | ||
| data: JSON.stringify(draft.data), | ||
| }; | ||
| }, | ||
| ); | ||
|
|
||
| export const createPipelineDraftDtoFactory = | ||
| Factory.define<CreatePipelineDraftDto>(() => { | ||
| const { data } = pipelineDraftFactory.build(); | ||
|
|
||
| return { data }; | ||
| }); |
9 changes: 9 additions & 0 deletions
9
redisinsight/api/src/modules/rdi/dto/create.pipeline-draft.dto.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { OmitType } from '@nestjs/swagger'; | ||
| import { PipelineDraft } from 'src/modules/rdi/models'; | ||
|
|
||
| export class CreatePipelineDraftDto extends OmitType(PipelineDraft, [ | ||
| 'id', | ||
| 'rdiInstanceId', | ||
| 'createdAt', | ||
| 'updatedAt', | ||
| ] as const) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
redisinsight/api/src/modules/rdi/dto/update.pipeline-draft.dto.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { ApiPropertyOptional } from '@nestjs/swagger'; | ||
| import { IsNotEmpty, IsObject, ValidateIf } from 'class-validator'; | ||
|
|
||
| export class UpdatePipelineDraftDto { | ||
| @ApiPropertyOptional({ | ||
| description: 'Draft data as a JSON object. Structure is not validated.', | ||
| type: Object, | ||
| }) | ||
| // skip validation only when omitted, not when explicitly null | ||
| @ValidateIf((dto) => dto.data !== undefined) | ||
| @IsNotEmpty() | ||
| @IsObject() | ||
| data?: object; | ||
| } |
45 changes: 45 additions & 0 deletions
45
redisinsight/api/src/modules/rdi/entities/pipeline-draft.entity.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { | ||
| Column, | ||
| CreateDateColumn, | ||
| Entity, | ||
| Index, | ||
| JoinColumn, | ||
| ManyToOne, | ||
| PrimaryGeneratedColumn, | ||
| UpdateDateColumn, | ||
| } from 'typeorm'; | ||
| import { Expose } from 'class-transformer'; | ||
| import { DataAsJsonString } from 'src/common/decorators'; | ||
| import { RdiEntity } from 'src/modules/rdi/entities/rdi.entity'; | ||
|
|
||
| @Entity('pipeline_draft') | ||
| export class PipelineDraftEntity { | ||
| @Expose() | ||
| @PrimaryGeneratedColumn('uuid') | ||
| id: string; | ||
|
|
||
| @Expose() | ||
| @Index('IDX_pipeline_draft_rdiInstanceId') | ||
| @Column({ nullable: false }) | ||
| rdiInstanceId: string; | ||
|
|
||
| @ManyToOne(() => RdiEntity, { nullable: false, onDelete: 'CASCADE' }) | ||
| @JoinColumn({ name: 'rdiInstanceId' }) | ||
| rdiInstance?: RdiEntity; | ||
|
|
||
| @Column({ nullable: false, type: 'text' }) | ||
| @DataAsJsonString() | ||
| @Expose() | ||
| data: string; | ||
|
|
||
| @Column({ nullable: true }) | ||
| encryption?: string; | ||
|
|
||
| @Expose() | ||
| @CreateDateColumn() | ||
| createdAt: Date; | ||
|
|
||
| @Expose() | ||
| @UpdateDateColumn() | ||
| updatedAt: Date; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { ApiProperty } from '@nestjs/swagger'; | ||
| import { Expose } from 'class-transformer'; | ||
| import { IsNotEmpty, IsObject } from 'class-validator'; | ||
|
|
||
| export class PipelineDraft { | ||
| @ApiProperty({ | ||
| description: 'Pipeline draft id.', | ||
| type: String, | ||
| }) | ||
| @Expose() | ||
| id: string; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'Id of the RDI instance this draft belongs to.', | ||
| type: String, | ||
| }) | ||
| @Expose() | ||
| rdiInstanceId: string; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'Draft data as a JSON object. Structure is not validated.', | ||
| type: Object, | ||
| }) | ||
| @Expose() | ||
| @IsNotEmpty() | ||
| @IsObject() | ||
| data: object; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'Time the draft was created.', | ||
| type: Date, | ||
| }) | ||
| @Expose() | ||
| createdAt: Date; | ||
|
|
||
| @ApiProperty({ | ||
| description: 'Time the draft was last updated.', | ||
| type: Date, | ||
| }) | ||
| @Expose() | ||
| updatedAt: Date; | ||
| } |
172 changes: 172 additions & 0 deletions
172
redisinsight/api/src/modules/rdi/pipeline-draft.controller.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| import { Test, TestingModule } from '@nestjs/testing'; | ||
| import { faker } from '@faker-js/faker'; | ||
| import { NotFoundException, ValidationPipe } from '@nestjs/common'; | ||
| import { mockSessionMetadata } from 'src/__mocks__'; | ||
| import { PipelineDraftController } from './pipeline-draft.controller'; | ||
| import { PipelineDraftService } from './pipeline-draft.service'; | ||
| import { pipelineDraftFactory } from './__tests__/pipeline-draft.factory'; | ||
| import { CreatePipelineDraftDto, UpdatePipelineDraftDto } from './dto'; | ||
|
|
||
| const mockRdiInstanceId = faker.string.uuid(); | ||
|
|
||
| const mockRdiClientMetadata = { | ||
| sessionMetadata: mockSessionMetadata, | ||
| id: mockRdiInstanceId, | ||
| }; | ||
|
|
||
| const mockPipelineDraftService = () => ({ | ||
| create: jest.fn(), | ||
| list: jest.fn(), | ||
| get: jest.fn(), | ||
| update: jest.fn(), | ||
| delete: jest.fn(), | ||
| }); | ||
|
|
||
| describe('PipelineDraftController', () => { | ||
| let controller: PipelineDraftController; | ||
| let service: ReturnType<typeof mockPipelineDraftService>; | ||
|
|
||
| beforeEach(async () => { | ||
| const module: TestingModule = await Test.createTestingModule({ | ||
| controllers: [PipelineDraftController], | ||
| providers: [ | ||
| { | ||
| provide: PipelineDraftService, | ||
| useFactory: mockPipelineDraftService, | ||
| }, | ||
| ], | ||
| }).compile(); | ||
|
|
||
| controller = module.get(PipelineDraftController); | ||
| service = module.get(PipelineDraftService); | ||
| }); | ||
|
|
||
| describe('create', () => { | ||
| it('should create a pipeline draft', async () => { | ||
| const draft = pipelineDraftFactory.build({ | ||
| rdiInstanceId: mockRdiInstanceId, | ||
| }); | ||
| service.create.mockResolvedValueOnce(draft); | ||
|
|
||
| const result = await controller.create(mockRdiClientMetadata as any, { | ||
| data: draft.data, | ||
| }); | ||
|
|
||
| expect(result).toEqual(draft); | ||
| expect(service.create).toHaveBeenCalledWith( | ||
| mockSessionMetadata, | ||
| mockRdiInstanceId, | ||
| { data: draft.data }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should strip fields not declared on the create dto, such as an injected id', async () => { | ||
| const pipe = new ValidationPipe({ transform: true, whitelist: true }); | ||
| const draft = pipelineDraftFactory.build(); | ||
|
|
||
| const transformed = await pipe.transform( | ||
| { data: draft.data, id: faker.string.uuid() }, | ||
| { type: 'body', metatype: CreatePipelineDraftDto }, | ||
| ); | ||
|
|
||
| expect(transformed).toEqual({ data: draft.data }); | ||
| expect(transformed.id).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('list', () => { | ||
| it('should return list of drafts', async () => { | ||
| const drafts = pipelineDraftFactory.buildList(3, { | ||
| rdiInstanceId: mockRdiInstanceId, | ||
| }); | ||
| service.list.mockResolvedValueOnce(drafts); | ||
|
|
||
| const result = await controller.list(mockRdiClientMetadata as any); | ||
|
|
||
| expect(result).toEqual(drafts); | ||
| expect(service.list).toHaveBeenCalledWith( | ||
| mockSessionMetadata, | ||
| mockRdiInstanceId, | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('get', () => { | ||
| it('should return a single draft', async () => { | ||
| const draft = pipelineDraftFactory.build({ | ||
| rdiInstanceId: mockRdiInstanceId, | ||
| }); | ||
| service.get.mockResolvedValueOnce(draft); | ||
|
|
||
| const result = await controller.get( | ||
| mockRdiClientMetadata as any, | ||
| draft.id, | ||
| ); | ||
|
|
||
| expect(result).toEqual(draft); | ||
| }); | ||
|
|
||
| it('should throw NotFoundException when draft not found', async () => { | ||
| service.get.mockRejectedValueOnce(new NotFoundException()); | ||
|
|
||
| await expect( | ||
| controller.get(mockRdiClientMetadata as any, faker.string.uuid()), | ||
| ).rejects.toThrow(NotFoundException); | ||
| }); | ||
| }); | ||
|
|
||
| describe('update', () => { | ||
| it('should update a pipeline draft', async () => { | ||
| const draft = pipelineDraftFactory.build({ | ||
| rdiInstanceId: mockRdiInstanceId, | ||
| }); | ||
| const updatedData = { updated: true }; | ||
| const updatedDraft = { ...draft, data: updatedData }; | ||
| service.update.mockResolvedValueOnce(updatedDraft); | ||
|
|
||
| const result = await controller.update( | ||
| mockRdiClientMetadata as any, | ||
| draft.id, | ||
| { data: updatedData }, | ||
| ); | ||
|
|
||
| expect(result).toEqual(updatedDraft); | ||
| }); | ||
|
|
||
| it('should reject null data instead of treating it as an omitted field', async () => { | ||
| const pipe = new ValidationPipe({ transform: true, whitelist: true }); | ||
|
|
||
| await expect( | ||
| pipe.transform( | ||
| { data: null }, | ||
| { type: 'body', metatype: UpdatePipelineDraftDto }, | ||
| ), | ||
| ).rejects.toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('delete', () => { | ||
| it('should delete a pipeline draft', async () => { | ||
| const draft = pipelineDraftFactory.build({ | ||
| rdiInstanceId: mockRdiInstanceId, | ||
| }); | ||
| service.delete.mockResolvedValueOnce(undefined); | ||
|
|
||
| await controller.delete(mockRdiClientMetadata as any, draft.id); | ||
|
|
||
| expect(service.delete).toHaveBeenCalledWith( | ||
| mockSessionMetadata, | ||
| mockRdiInstanceId, | ||
| draft.id, | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw NotFoundException when draft not found', async () => { | ||
| service.delete.mockRejectedValueOnce(new NotFoundException()); | ||
|
|
||
| await expect( | ||
| controller.delete(mockRdiClientMetadata as any, faker.string.uuid()), | ||
| ).rejects.toThrow(NotFoundException); | ||
| }); | ||
| }); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.