From 2da2367a40a05a7c77add05b90ac4cd684794dc2 Mon Sep 17 00:00:00 2001 From: Collin Bolles Date: Thu, 5 Sep 2024 12:36:27 -0400 Subject: [PATCH] feat: Basic Crud (#33) # Description * GraphQL updates for basic CRUD on * Service * Category * Bundle * Service list validation before update ## Checklist - [x] This PR can be reviewed in under 30 minutes - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] I have assigned reviewers to this PR. --- src/bundles/bundles.module.ts | 3 ++- src/bundles/bundles.pipe.ts | 17 +++++++++++++++++ src/bundles/bundles.resolver.ts | 10 +++++++++- src/bundles/bundles.service.ts | 10 ++++++++++ src/bundles/dtos/update.dto.ts | 8 ++++++++ src/bundles/update.pipe.ts | 17 +++++++++++++++++ src/categories/categories.module.ts | 4 +++- src/categories/categories.pipe.ts | 19 +++++++++++++++++++ src/categories/categories.resolver.ts | 13 ++++++++++++- src/categories/categories.service.ts | 10 ++++++++++ src/categories/dtos/update.dto.ts | 8 ++++++++ src/categories/update.pipe.ts | 17 +++++++++++++++++ src/services/damplab-services.module.ts | 3 ++- src/services/damplab-services.resolver.ts | 13 ++++++++++++- src/services/damplab-services.services.ts | 6 ++++++ src/services/dtos/update.dto.ts | 8 ++++++++ src/services/update.pipe.ts | 17 +++++++++++++++++ 17 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 src/bundles/bundles.pipe.ts create mode 100644 src/bundles/dtos/update.dto.ts create mode 100644 src/bundles/update.pipe.ts create mode 100644 src/categories/categories.pipe.ts create mode 100644 src/categories/dtos/update.dto.ts create mode 100644 src/categories/update.pipe.ts create mode 100644 src/services/dtos/update.dto.ts create mode 100644 src/services/update.pipe.ts diff --git a/src/bundles/bundles.module.ts b/src/bundles/bundles.module.ts index c28eee8..d8af307 100644 --- a/src/bundles/bundles.module.ts +++ b/src/bundles/bundles.module.ts @@ -4,9 +4,10 @@ import { DampLabServicesModule } from '../services/damplab-services.module'; import { Bundle, BundleSchema } from './bundles.model'; import { BundlesResolver } from './bundles.resolver'; import { BundlesService } from './bundles.service'; +import { BundleUpdatePipe } from './update.pipe'; @Module({ imports: [MongooseModule.forFeature([{ name: Bundle.name, schema: BundleSchema }]), DampLabServicesModule], - providers: [BundlesService, BundlesResolver] + providers: [BundlesService, BundlesResolver, BundleUpdatePipe] }) export class BundlesModule {} diff --git a/src/bundles/bundles.pipe.ts b/src/bundles/bundles.pipe.ts new file mode 100644 index 0000000..466fbc4 --- /dev/null +++ b/src/bundles/bundles.pipe.ts @@ -0,0 +1,17 @@ +import { Injectable, NotFoundException, PipeTransform } from '@nestjs/common'; +import { Bundle } from './bundles.model'; +import { BundlesService } from './bundles.service'; + +@Injectable() +export class BundlesPipe implements PipeTransform> { + constructor(private readonly bundleService: BundlesService) {} + + async transform(value: string): Promise { + const bundle = await this.bundleService.find(value); + + if (!bundle) { + throw new NotFoundException(`Bundle with id ${value} not found`); + } + return bundle; + } +} diff --git a/src/bundles/bundles.resolver.ts b/src/bundles/bundles.resolver.ts index bbee738..ea21235 100644 --- a/src/bundles/bundles.resolver.ts +++ b/src/bundles/bundles.resolver.ts @@ -1,8 +1,11 @@ -import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Parent, Query, ResolveField, Resolver, Args, Mutation, ID } from '@nestjs/graphql'; import { DampLabServices } from '../services/damplab-services.services'; import { DampLabService } from '../services/models/damplab-service.model'; import { Bundle } from './bundles.model'; import { BundlesService } from './bundles.service'; +import { BundlesPipe } from './bundles.pipe'; +import { BundleChange } from './dtos/update.dto'; +import { BundleUpdatePipe } from './update.pipe'; @Resolver(() => Bundle) export class BundlesResolver { @@ -13,6 +16,11 @@ export class BundlesResolver { return this.bundlesService.findAll(); } + @Mutation(() => Bundle) + async updateBundle(@Args('bundle', { type: () => ID }, BundlesPipe) bundle: Bundle, @Args('changes', { type: () => BundleChange }, BundleUpdatePipe) changes: BundleChange): Promise { + return this.bundlesService.update(bundle, changes); + } + @ResolveField() async services(@Parent() bundle: Bundle): Promise { return this.dampLabServices.findByIds(bundle.services); diff --git a/src/bundles/bundles.service.ts b/src/bundles/bundles.service.ts index ff24a39..2d06d16 100644 --- a/src/bundles/bundles.service.ts +++ b/src/bundles/bundles.service.ts @@ -2,12 +2,22 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Bundle } from './bundles.model'; import { Model } from 'mongoose'; +import { BundleChange } from './dtos/update.dto'; @Injectable() export class BundlesService { constructor(@InjectModel(Bundle.name) private readonly bundleModel: Model) {} + async find(id: string): Promise { + return this.bundleModel.findById(id); + } + async findAll(): Promise { return this.bundleModel.find().exec(); } + + async update(bundle: Bundle, changes: BundleChange): Promise { + await this.bundleModel.updateOne({ _id: bundle.id }, changes); + return (await this.find(bundle.id))!; + } } diff --git a/src/bundles/dtos/update.dto.ts b/src/bundles/dtos/update.dto.ts new file mode 100644 index 0000000..712adc2 --- /dev/null +++ b/src/bundles/dtos/update.dto.ts @@ -0,0 +1,8 @@ +import { Bundle } from '../bundles.model'; +import { ID, InputType, OmitType, PartialType, Field } from '@nestjs/graphql'; + +@InputType() +export class BundleChange extends PartialType(OmitType(Bundle, ['id', 'services'] as const), InputType) { + @Field(() => [ID], { nullable: true }) + services: string[]; +} diff --git a/src/bundles/update.pipe.ts b/src/bundles/update.pipe.ts new file mode 100644 index 0000000..b6e2453 --- /dev/null +++ b/src/bundles/update.pipe.ts @@ -0,0 +1,17 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; +import { DampLabServicePipe } from '../services/damplab-services.pipe'; +import { BundleChange } from './dtos/update.dto'; + +@Injectable() +export class BundleUpdatePipe implements PipeTransform> { + constructor(private readonly damplabServicePipe: DampLabServicePipe) {} + + async transform(value: BundleChange): Promise { + // If services is includes, make sure they are all valid + if (value.services) { + await Promise.all(value.services.map((service) => this.damplabServicePipe.transform(service))); + } + + return value; + } +} diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts index c3f7b37..46a604e 100644 --- a/src/categories/categories.module.ts +++ b/src/categories/categories.module.ts @@ -4,9 +4,11 @@ import { Category, CategorySchema } from './category.model'; import { CategoryService } from './categories.service'; import { CategoryResolver } from './categories.resolver'; import { DampLabServicesModule } from '../services/damplab-services.module'; +import { CategoryPipe } from './categories.pipe'; +import { CategoryUpdatePipe } from './update.pipe'; @Module({ imports: [MongooseModule.forFeature([{ name: Category.name, schema: CategorySchema }]), DampLabServicesModule], - providers: [CategoryService, CategoryResolver] + providers: [CategoryService, CategoryResolver, CategoryPipe, CategoryUpdatePipe] }) export class CategoriesModule {} diff --git a/src/categories/categories.pipe.ts b/src/categories/categories.pipe.ts new file mode 100644 index 0000000..f495ea1 --- /dev/null +++ b/src/categories/categories.pipe.ts @@ -0,0 +1,19 @@ +import { NotFoundException, Injectable, PipeTransform } from '@nestjs/common'; +import { Category } from './category.model'; +import { CategoryService } from './categories.service'; + +@Injectable() +export class CategoryPipe implements PipeTransform> { + constructor(private readonly categoryService: CategoryService) {} + + async transform(value: string): Promise { + try { + const category = await this.categoryService.find(value); + if (category) { + return category; + } + } catch (e) {} + + throw new NotFoundException(`Category with id ${value} not found`); + } +} diff --git a/src/categories/categories.resolver.ts b/src/categories/categories.resolver.ts index f0e46e1..6d2a35a 100644 --- a/src/categories/categories.resolver.ts +++ b/src/categories/categories.resolver.ts @@ -1,8 +1,11 @@ -import { Query, ResolveField, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, ResolveField, Resolver, ID } from '@nestjs/graphql'; import { Category } from './category.model'; import { CategoryService } from './categories.service'; import { DampLabServices } from '../services/damplab-services.services'; import { DampLabService } from '../services/models/damplab-service.model'; +import { CategoryPipe } from './categories.pipe'; +import { CategoryChange } from './dtos/update.dto'; +import { CategoryUpdatePipe } from './update.pipe'; @Resolver(() => Category) export class CategoryResolver { @@ -13,6 +16,14 @@ export class CategoryResolver { return this.categoryService.findAll(); } + @Mutation(() => Category) + async updateCategory( + @Args('category', { type: () => ID }, CategoryPipe) category: Category, + @Args('changes', { type: () => CategoryChange }, CategoryUpdatePipe) changes: CategoryChange + ): Promise { + return this.categoryService.update(category, changes); + } + /** * Resolver for the services field of the Category type */ diff --git a/src/categories/categories.service.ts b/src/categories/categories.service.ts index e0d14c1..6f65c24 100644 --- a/src/categories/categories.service.ts +++ b/src/categories/categories.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Category, CategoryDocument } from './category.model'; import { Model } from 'mongoose'; +import { CategoryChange } from './dtos/update.dto'; @Injectable() export class CategoryService { @@ -10,4 +11,13 @@ export class CategoryService { async findAll(): Promise { return this.categoryModel.find().exec(); } + + async find(id: string): Promise { + return this.categoryModel.findById(id); + } + + async update(category: Category, change: CategoryChange): Promise { + await this.categoryModel.updateOne({ _id: category._id }, change); + return (await this.find(category._id))!; + } } diff --git a/src/categories/dtos/update.dto.ts b/src/categories/dtos/update.dto.ts new file mode 100644 index 0000000..d8742d3 --- /dev/null +++ b/src/categories/dtos/update.dto.ts @@ -0,0 +1,8 @@ +import { Category } from '../category.model'; +import { ID, OmitType, PartialType, Field, InputType } from '@nestjs/graphql'; + +@InputType() +export class CategoryChange extends PartialType(OmitType(Category, ['_id', 'services'] as const), InputType) { + @Field(() => [ID], { nullable: true }) + services: string[]; +} diff --git a/src/categories/update.pipe.ts b/src/categories/update.pipe.ts new file mode 100644 index 0000000..6ff1fd8 --- /dev/null +++ b/src/categories/update.pipe.ts @@ -0,0 +1,17 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; +import { DampLabServicePipe } from '../services/damplab-services.pipe'; +import { CategoryChange } from './dtos/update.dto'; + +@Injectable() +export class CategoryUpdatePipe implements PipeTransform> { + constructor(private readonly damplabServicePipe: DampLabServicePipe) {} + + async transform(value: CategoryChange): Promise { + // If services is includes, make sure they are all valid + if (value.services) { + await Promise.all(value.services.map((service) => this.damplabServicePipe.transform(service))); + } + + return value; + } +} diff --git a/src/services/damplab-services.module.ts b/src/services/damplab-services.module.ts index f5717f1..8011782 100644 --- a/src/services/damplab-services.module.ts +++ b/src/services/damplab-services.module.ts @@ -4,10 +4,11 @@ import { DampLabServicePipe } from './damplab-services.pipe'; import { DampLabServicesResolver } from './damplab-services.resolver'; import { DampLabServices } from './damplab-services.services'; import { DampLabService, DampLabServiceSchema } from './models/damplab-service.model'; +import { ServiceUpdatePipe } from './update.pipe'; @Module({ imports: [MongooseModule.forFeature([{ name: DampLabService.name, schema: DampLabServiceSchema }])], - providers: [DampLabServicesResolver, DampLabServices, DampLabServicePipe], + providers: [DampLabServicesResolver, DampLabServices, DampLabServicePipe, ServiceUpdatePipe], exports: [DampLabServices, DampLabServicePipe] }) export class DampLabServicesModule {} diff --git a/src/services/damplab-services.resolver.ts b/src/services/damplab-services.resolver.ts index 6a2e02d..5013466 100644 --- a/src/services/damplab-services.resolver.ts +++ b/src/services/damplab-services.resolver.ts @@ -1,6 +1,9 @@ -import { Resolver, Query, ResolveField, Parent } from '@nestjs/graphql'; +import { Resolver, Query, ResolveField, Parent, ID, Args, Mutation } from '@nestjs/graphql'; +import { DampLabServicePipe } from './damplab-services.pipe'; import { DampLabServices } from './damplab-services.services'; +import { ServiceChange } from './dtos/update.dto'; import { DampLabService } from './models/damplab-service.model'; +import { ServiceUpdatePipe } from './update.pipe'; @Resolver(() => DampLabService) export class DampLabServicesResolver { @@ -11,6 +14,14 @@ export class DampLabServicesResolver { return this.dampLabServices.findAll(); } + @Mutation(() => DampLabService) + async updateService( + @Args('service', { type: () => ID }, DampLabServicePipe) service: DampLabService, + @Args('changes', { type: () => ServiceChange }, ServiceUpdatePipe) changes: ServiceChange + ): Promise { + return this.dampLabServices.update(service, changes); + } + /** * Resolver which the `allowedConnections` field of the `DampLabService` * type. Allows for the recursive search on possible connections. diff --git a/src/services/damplab-services.services.ts b/src/services/damplab-services.services.ts index 3b15030..5d9e6ed 100644 --- a/src/services/damplab-services.services.ts +++ b/src/services/damplab-services.services.ts @@ -3,6 +3,7 @@ import { DampLabService, DampLabServiceDocument } from './models/damplab-service import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import mongoose from 'mongoose'; +import { ServiceChange } from './dtos/update.dto'; @Injectable() export class DampLabServices { @@ -22,4 +23,9 @@ export class DampLabServices { async findOne(id: string): Promise { return this.dampLabServiceModel.findById(id).exec(); } + + async update(service: DampLabService, changes: ServiceChange): Promise { + await this.dampLabServiceModel.updateOne({ _id: service._id }, changes); + return (await this.dampLabServiceModel.findById(service._id))!; + } } diff --git a/src/services/dtos/update.dto.ts b/src/services/dtos/update.dto.ts new file mode 100644 index 0000000..2084491 --- /dev/null +++ b/src/services/dtos/update.dto.ts @@ -0,0 +1,8 @@ +import { DampLabService } from '../models/damplab-service.model'; +import { ID, InputType, OmitType, PartialType, Field } from '@nestjs/graphql'; + +@InputType() +export class ServiceChange extends PartialType(OmitType(DampLabService, ['_id', 'allowedConnections'] as const), InputType) { + @Field(() => [ID], { nullable: true }) + allowedConnections: string[]; +} diff --git a/src/services/update.pipe.ts b/src/services/update.pipe.ts new file mode 100644 index 0000000..8341ed4 --- /dev/null +++ b/src/services/update.pipe.ts @@ -0,0 +1,17 @@ +import { Injectable, PipeTransform } from '@nestjs/common'; +import { DampLabServicePipe } from '../services/damplab-services.pipe'; +import { ServiceChange } from './dtos/update.dto'; + +@Injectable() +export class ServiceUpdatePipe implements PipeTransform> { + constructor(private readonly damplabServicePipe: DampLabServicePipe) {} + + async transform(value: ServiceChange): Promise { + // If services is includes, make sure they are all valid + if (value.allowedConnections) { + await Promise.all(value.allowedConnections.map((service) => this.damplabServicePipe.transform(service))); + } + + return value; + } +}