Skip to content

Resource Module Architecture

DeepVision employs a consistent architecture for NestJS resource modules to ensure better support and maintainability across all applications within our ecosystem. NestKit provides numerous utilities to minimize boilerplate code in module development.

Module Structure

Each resource module should be placed in the src/modules folder and follow this structure:

plaintext
📦resource-name
 ┣ 📂types
 ┃ ┣ 📂resolver
 ┃ ┃ ┣ 📜create-resource.ts
 ┃ ┃ ┣ 📜delete-resource.ts
 ┃ ┃ ┣ 📜fetch-resources.ts
 ┃ ┃ ┣ 📜index.ts
 ┃ ┃ ┗ 📜update-resource.ts
 ┃ ┣ 📂service
 ┃ ┃ ┣ 📜create-resource.ts
 ┃ ┃ ┣ 📜get-many-resources.ts
 ┃ ┃ ┣ 📜index.ts
 ┃ ┃ ┗ 📜update-resource.ts
 ┃ ┗ 📜common.ts
 ┣ 📜resource.entity.ts
 ┣ 📜resource.module.ts
 ┣ 📜resource.resolver.ts
 ┗ 📜resource.service.ts

Replace resource-name and resource with the specific name of the resource module you are creating. This structure ensures a consistent approach to resource module development, making it easier to maintain and support the applications within the DeepVision ecosystem.

Using the SeasonGroups example, the module structure would look like this:

plaintext
📦season-groups
 ┣ 📂types
 ┃ ┣ 📂resolver
 ┃ ┃ ┣ 📜create-season-group.ts
 ┃ ┃ ┣ 📜delete-season-group.ts
 ┃ ┃ ┣ 📜fetch-season-groups.ts
 ┃ ┃ ┣ 📜index.ts
 ┃ ┃ ┗ 📜update-season-group.ts
 ┃ ┣ 📂service
 ┃ ┃ ┣ 📜create-season-group.ts
 ┃ ┃ ┣ 📜get-many-season-groups.ts
 ┃ ┃ ┣ 📜index.ts
 ┃ ┃ ┗ 📜update-season-group.ts
 ┃ ┗ 📜common.ts
 ┣ 📜season-group.entity.ts
 ┣ 📜season-groups.module.ts
 ┣ 📜season-groups.resolver.ts
 ┗ 📜season-groups.service.ts

In this example, the resource-name and resource placeholders from the previous structure have been replaced with season-groups and season-group, respectively.

In the types directory, we store type definitions related to the entity properties, options, and inputs used by service methods and GraphQL resolvers. This helps maintain a clear separation of concerns and keeps the codebase organized, making it easier to work with and understand.

The updated SeasonGroups module structure, including the purpose of the types directory, is as follows:

plaintext
📦season-groups
 ┣ 📂types
 ┃ ┣ 📂resolver
 ┃ ┃ ┣ 📜create-season-group.ts       // Input types for creating a season group
 ┃ ┃ ┣ 📜delete-season-group.ts       // Input types for deleting a season group
 ┃ ┃ ┣ 📜fetch-season-groups.ts       // Input types for fetching season groups
 ┃ ┃ ┣ 📜index.ts                     // Exports all resolver types
 ┃ ┃ ┗ 📜update-season-group.ts       // Input types for updating a season group
 ┃ ┣ 📂service
 ┃ ┃ ┣ 📜create-season-group.ts       // Options for creating a season group
 ┃ ┃ ┣ 📜get-many-season-groups.ts    // Options for fetching season groups
 ┃ ┃ ┣ 📜index.ts                     // Exports all service types
 ┃ ┃ ┗ 📜update-season-group.ts       // Options for updating a season group
 ┃ ┗ 📜common.ts                      // Shared types for the entity properties

By organizing type definitions in the types directory, it becomes easier to navigate and understand the relationships between various components in the resource module.

For each GraphQL mutation or query, it's a good practice to create separate files where you define the input types and the response payloads. This approach helps to keep your codebase clean, modular, and easy to maintain.

In the SeasonGroups example, the GraphQL mutation and query inputs and payloads can be organized within the types/resolver directory.

Resource Entity Definition

We use TypeORM entities to describe the resource schema in the database. TypeORM entities provide a convenient way to define and interact with database tables in an object-oriented manner.

Here's an example of a SeasonGroup entity definition:

ts
import { Field, ObjectType } from '@nestjs/graphql';
import {
  BaseEntity, Column, Entity, PrimaryColumn, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, ManyToOne,
} from 'typeorm';
import { User } from '@modules/users/user.entity';

import { SeasonGroupStatus } from './types/common';
import { Image } from '@/modules/images/image.entity.ts';

@ObjectType()
@Entity()
export class SeasonGroup extends BaseEntity {
  @Field()
  @PrimaryColumn()
  id!: string;

  @Field(() => SeasonGroupStatus)
  @Column({
    type: 'enum',
    enum: SeasonGroupStatus,
    default: SeasonGroupStatus.ACTIVE,
  })
  status!: SeasonGroupStatus;

  @Field()
  @Column()
  title!: string;
  
  @Field()
  @Column()
  imageId!: string;

  @Field(() => Image)
  @OneToOne(() => Image, {
    lazy: true,
    onDelete: 'RESTRICT', // DELETE, SET NULL, rely on business logic
  })
  @JoinColumn()
  image!: Promise<Image>;

  @Field()
  @Column()
  creatorId!: string;

  @ManyToOne(() => User, {
    lazy: true,
    onDelete: 'RESTRICT',
    nullable: true,
  })
  @JoinColumn()
  creator!: Promise<User>;

  @CreateDateColumn({
    type: 'timestamptz',
    precision: 3,
  })
  createdAt!: Date;

  @UpdateDateColumn({
    type: 'timestamptz',
    precision: 3,
  })
  updatedAt!: Date;
}

In this example, the SeasonGroup entity is defined using TypeORM decorators such as @Entity,@PrimaryColumn, @Column, @OneToOne,@ManyToOne, @CreateDateColumn, and @UpdateDateColumn. The corresponding GraphQL schema is automatically generated using NestJS GraphQL decorators like @ObjectType and @Field. This combination allows you to define the database schema and GraphQL schema in one place, making it easier to manage and maintain.

We expose the creatorId with @Field() decorator to declare the creatorId field directly in your entity class. This will allow clients to request just the creatorId without needing to fetch the entire User object.

graphql
query {
  seasonGroup(id: "xxx") {
    id
    creatorId
  }
}

Resource GraphQL Resolver

The GraphQL Resolver for a resource provides a CRUD interface for the end user, utilizing the resource service to perform operations with the database.

ts
import {
  Args, Mutation, Query, Resolver,
} from '@nestjs/graphql';
import { NotFoundException, UseGuards } from '@nestjs/common';
import { ErrorKeys } from '@/enums/error-keys';
import {
  GraphQLAuthGuard,
  GraphQLPermissionsGuard,
  createOffsetPaginationOptions,
  MaybeNull,
  offsetPaginatedOutput,
} from '@deeepvision/nest-kit';
import { SeasonGroup } from './season-group.entity';
import { SeasonGroupsService } from './season-groups.service';
import { ActionContext, IActionContext } from '@/decorators';
import {
  CreateSeasonGroupInput,
  DeleteSeasonGroupPayload, FetchSeasonGroupsInput,
  PaginatedSeasonGroups, UpdateSeasonGroupInput,
} from './types/resolver';
import { UsePermission } from '@/decorators';

@Resolver(() => SeasonGroup)
// Guards are required. For shared endpoints use service accounts
@UseGuards(GraphQLAuthGuard, GraphQLPermissionsGuard)
export class SeasonGroupsResolver {
  constructor(private readonly seasonGroupsService: SeasonGroupsService) {}

  @Query(() => SeasonGroup)
  @UsePermission('js:core:season-groups:get') // project:module:resource:action
  async seasonGroup(
    @Args('id') id: string,
    @ActionContext() ctx: IActionContext,
  ): Promise<SeasonGroup> {
    return await this.seasonGroupsService.getOnOrFail(id, ctx);
  }

  @Query(() => PaginatedSeasonGroups)
  @UsePermission('js:core:season-groups:list')
  async seasonGroups(
    @Args() input: FetchSeasonGroupsInput,
    @ActionContext() ctx: IActionContext,
  ): Promise<PaginatedSeasonGroups> {
    const [seasonGroups, meta] = await this.seasonGroupsService.getMany(
      {
        filter: input.filter ?? {
        },
        orderBy: input.orderBy,
        ...createOffsetPaginationOptions(input),
      },
      ctx,
    );

    return offsetPaginatedOutput(seasonGroups, meta);
  }

  @Mutation(() => SeasonGroup)
  @UsePermission('js:core:season-groups:create')
  async createSeasonGroup(
    @Args('input') input: CreateSeasonGroupInput,
    @ActionContext() ctx: IActionContext,
  ): Promise<SeasonGroup> {
    return await this.seasonGroupsService.create(input, ctx);
  }

  @Mutation(() => SeasonGroup)
  async updateSeasonGroup(
    @Args('input') input: UpdateSeasonGroupInput,
    @ActionContext() ctx: IActionContext,
  ): Promise<SeasonGroup> {
    return await this.seasonGroupsService.update(input, ctx);
  }

  @Mutation(() => DeleteSeasonGroupPayload)
  @UsePermission('js:core:season-groups:delete')
  async deleteSeasonGroup(
    @Args('input') input: DeleteSeasonGroupInput,
    @ActionContext() ctx: IActionContext,
  ): Promise<DeleteSeasonGroupPayload> {
    await this.seasonGroupsService.delete(id, ctx);

    return {
      id,
    };
  }
}

This example demonstrates a SeasonGroupsResolver for the SeasonGroup resource. It includes the necessary imports and decorators to protect the resolver with guards and permissions. The resolver uses the SeasonGroupsService to perform CRUD operations, such as creating, updating, and deleting season groups, as well as fetching a single season group or a paginated list of season groups.

The GraphQLAuthGuard from NestKit is used to authenticate the user making the request. For each query and mutation, the @ActionContext decorator is defined, which provides the ctx decorator that contains the resolved user making the request. This makes it easy to attach the creator to the SeasonGroup.

The GraphQLPermissionsGuard from NestKit is used for access control to queries and mutations. It works in conjunction with the @UsePermission decorator to manage and enforce role-based permissions for the API.

When you apply the @UsePermission decorator to a query or mutation, it takes an argument that represents the required permission string for the user to access that specific endpoint. This permission string is typically in the format of project:module:resource:action. The GraphQLPermissionsGuard then checks whether the authenticated user has the necessary permission to perform the requested operation.

Each query and mutation, except for simple queries, has input parameters. These input parameters are typed in separate files for better organization and maintainability.

Create mutation

Create input is stored in a file named types/resolver/create-season-group.ts.

Here's an example of a create input for the SeasonGroup resource:

ts
import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class CreateSeasonGroupInput {
  @Field()
  title!: string;

  @Field()
  imageId!: string;
}

Create mutation returns the created entity as a result. This allows the client to receive the latest state of the resource after performing the mutation.

Update mutation

Update input is stored in a file named types/resolver/update-season-group.ts. You must provide the id of the resource you want to update. Other fields are optional, allowing for atomic updates.

ts
import { Field, InputType } from '@nestjs/graphql';

@InputType()
export class UpdateSeasonGroupInput {
  @Field()
  id!: string;

  @Field({ nullable: true })
  title?: string;

  @Field({ nullable: true })
  imageId?: string;
}

Update mutation returns the updated entity as a result. This allows the client to receive the latest state of the resource after performing the mutation.

Delete mutation

Delete input and response payload are stored in a file named types/resolver/delete-season-group.ts.

ts
import { Field, InputType, ObjectType } from '@nestjs/graphql';

@InputType()
export class DeleteSeasonGroupInput {
  @Field()
  id!: string;
}

@ObjectType()
export class DeleteSeasonGroupPayload {
  @Field()
  id!: string;
}

Delete mutations can use input instead of a simple ID to provide additional data for performing optional actions. For example, you can include a deleteFileFromRemoteStorage field in the input to specify whether a file should be deleted from remote storage when the associated resource is deleted. As a result, delete mutations return a DeletePayload that contains information about the deleted resource and any additional actions performed.

Here's an example of a delete mutation with additional input:

ts
import { Field, ObjectType } from '@nestjs/graphql';

@InputType()
export class DeleteResourceInput {
  @Field()
  id!: string;

  @Field()
  deleteFileFromRemoteStorage!: boolean;
}

Fetch items

The seasonGroups query is a complex query that returns a list of offset-paginated items. This list supports the offset and limit parameters and can be filtered by special parameters.

The types for this query are stored in the types/resolver/fetch-season-groups.ts file:

ts
import { SeasonGroup } from '../../season-group.entity';
import {
  ExtractSortFields,
  OffsetPaginated,
  OffsetPaginationInput,
} from '@deeepvision/nest-kit';
import {
  ArgsType,
  Field,
  InputType,
  ObjectType,
  registerEnumType,
} from '@nestjs/graphql';

import { SeasonGroupStatus } from '../common';

@InputType()
export class SeasonGroupsFilter {
  @Field(() => [String], {
    nullable: true,
  })
  ids?: SeasonGroupStatus[];

  @Field(() => String, {
    nullable: true,
  })
  search?: String;

  @Field(() => [SeasonGroupStatus], {
    nullable: true,
  })
  statuses?: SeasonGroupStatus[];
}

export enum SeasonGroupsOrderBy {
  createdAt_ASC = 'createdAt_ASC',
  createdAt_DESC = 'createdAt_DESC',
  status_ASC = 'status_ASC',
  status_DESC = 'status_DESC',
}

export type SeasonGroupsOrderFields = ExtractSortFields<SeasonGroupsOrderBy>;

registerEnumType(SeasonGroupsOrderBy, {
  name: 'SeasonGroupsOrderBy',
});

@ArgsType()
export class FetchSeasonGroupsInput extends OffsetPaginationInput {
  @Field(() => SeasonGroupsOrderBy, {
    defaultValue: SeasonGroupsOrderBy.createdAt_DESC,
  })
  orderBy!: SeasonGroupsOrderBy;

  @Field(() => SeasonGroupsFilter, {
    nullable: true,
  })
  filter?: SeasonGroupsFilter;
}

@ObjectType()
export class PaginatedSeasonGroups extends OffsetPaginated(SeasonGroup) {}

In the FetchSeasonGroupsInput, the @Args() decorator is used. This means that all the properties within the class will be treated as individual arguments for the seasonGroups query.

Here's an example GraphQL query for fetching seasonGroups using the defined arguments:

graphql
query SeasonGroups($limit: Int!, $offset: Int!, $orderBy: SeasonGroupsOrderBy!, $filter: SeasonGroupsFilter) {
  seasonGroups(limit: $limit, offset: $offset, orderBy: $orderBy, filter: $filter) {
    ...
  }
}

In this query, the limit, offset, orderBy, and filter variables are passed as arguments to the seasonGroups query.

Filtering

SeasonGroupsFilter is an input type used to define the filter criteria for fetching season groups. It allows you to specify the params you want to filter the season groups by.

Yes, you can extend the filter based on your specific business logic requirements. To add more filter parameters, update the SeasonGroupsFilter input type by adding new fields with appropriate types and decorators. For example, if you want to add a filter parameter for filtering season groups by brandId, you can do so like this:

ts
import { Field, InputType } from '@nestjs/graphql';
import { SeasonGroupStatus } from '../common';

@InputType()
export class SeasonGroupsFilter {
  @Field(() => [SeasonGroupStatus], {
    nullable: true,
  })
  statuses?: SeasonGroupStatus[];

  // Add a new filter parameter for brandId
  @Field({ nullable: true })
  brandId?: string;
}

Remember to update your service to handle the new filter parameter in your filtering logic as well. Here's an example of how you can update your service to incorporate the new brandId filter parameter.

Ordering

SeasonGroupsOrderBy is an enumeration that defines the possible sorting options for the seasonGroups query. It allows the user to sort the fetched season groups based on different criteria, such as creation date or status.

In the given example, the SeasonGroupsOrderBy enum has four possible values:

ts
export enum SeasonGroupsOrderBy {
  createdAt_ASC = 'createdAt_ASC',
  createdAt_DESC = 'createdAt_DESC',
  status_ASC = 'status_ASC',
  status_DESC = 'status_DESC',
}

These values represent the different ways the user can order the fetched season groups:

  1. createdAt_ASC: Sort season groups by creation date in ascending order (from oldest to newest).
  2. createdAt_DESC: Sort season groups by creation date in descending order (from newest to oldest).
  3. status_ASC: Sort season groups by status in ascending order (e.g., from ACTIVE to INACTIVE).
  4. status_DESC: Sort season groups by status in descending order (e.g., from INACTIVE to ACTIVE).

By providing this enumeration, you allow users to easily customize the order in which the season groups are fetched.

The SeasonGroupsOrderBy enum follows a special format {param}_{order}. This format makes it easy to understand and extend the sorting options. The format consists of two parts:

  1. {param}: This represents the field or property of the resource.

  2. {order}: This indicates the sorting order, either ascending (ASC) or descending (DESC).

Pagination

PaginatedSeasonGroups utilizes the OffsetPaginated utility from NestKit to create an ObjectType of paginated items. This makes it easy to handle pagination in GraphQL queries, while providing a consistent output structure for paginated data.

Here's a sample GraphQL query using PaginatedSeasonGroups:

graphql
query SeasonGroups($limit: Int!, $offset: Int!, $orderBy: SeasonGroupsOrderBy!, $filter: SeasonGroupsFilter) {
  seasonGroups(limit: $limit, offset: $offset, orderBy: $orderBy, filter: $filter) {
    items {
      title
    }
    pageInfo {
      limit
      page
      totalItems
      totalPages
    }
  }
}

This query accepts arguments such as limit, offset, orderBy, and filter to control pagination, sorting, and filtering. The response is structured with two main properties:

  1. items: This contains an array of the actual SeasonGroup items. You can request specific fields (e.g., title) within the items.
  2. pageInfo: This provides metadata about the pagination, including the limit, page, totalItems, and totalPages.

By using PaginatedSeasonGroups and the OffsetPaginated utility, you can easily manage pagination for any resource while maintaining a consistent response structure across your application.

Fetch item by id

The season query is a simple query that retrieves a specific SeasonGroup entity based on its id. This query is useful when you want to fetch detailed information about a single SeasonGroup.

Here's an example of the season query in the GraphQL Resolver:

ts
@Query(() => SeasonGroup)
@UsePermission('js:core:season-groups:get') // project:module:resource:action
async seasonGroup(
  @Args('id') id: string,
  @ActionContext() ctx: IActionContext,
): Promise<SeasonGroup> {
  return await this.seasonGroupsService.getOnOrFail(id, ctx);
}

In the SeasonGroupsResolver, the seasonGroup method is decorated with the @Query decorator, which indicates that it's a GraphQL query. It also uses the @UsePermission decorator to handle access control.

The seasonGroup method accepts two arguments:

  1. id: The id of the SeasonGroup you want to fetch.
  2. ctx: The ActionContext, which provides context about the user who made the request.

The method calls the getOnOrFail function from the seasonGroupsService, passing the id and ctx as arguments. This function returns the SeasonGroup entity if found; otherwise, it throws an error.

Here's a sample GraphQL query for the season query:

graphql
query SeasonGroup($id: String!) {
  seasonGroup(id: $id) {
    id
    title
    status
    createdAt
    updatedAt
  }
}

This query accepts the id parameter and requests specific fields of the SeasonGroup, such as id, title, status, createdAt, and updatedAt.

Resource Service

The SeasonGroupsService class in the code below defines a CRUD (Create, Read, Update, Delete) interface for working with SeasonGroup entities. This service is meant for internal use within your application, allowing for easy interaction with the SeasonGroup entities in your database.

ts
import {
  Injectable,
  InternalServerErrorException,
  NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ErrorKeys } from '@/enums/error-keys';
import {
  createListMeta,
  defineStatuses,
  extractSortParams,
  InjectWinstonLoggerFactory,
  ListMeta,
  MaybeNull,
  WinstonLogger,
  WinstonLoggerFactory,
} from '@deeepvision/nest-kit';
import { SeasonGroup } from './season-group.entity';
import { SeasonGroupsOrderBy, SeasonGroupsOrderFields } from './types/resolver';
import {
  CreateSeasonGroupOptions,
  GetManySeasonGroupsOptions,
  UpdateSeasonGroupOptions,
} from './types/service';
import { SeasonGroupStatus, defaultSeasonGroupStatuses } from './types/common';
import { IdPrefix } from '@/enums/id-prefix';
import { ServiceMethodContext } from '@/types';

@Injectable()
export class SeasonGroupsService {
  private readonly logger = this.loggerFactory.create({
    scope: SeasonGroupsService.name,
  });

  constructor(
    @InjectRepository(SeasonGroup) private readonly seasonGroupRepository: Repository<SeasonGroup>,
    @InjectWinstonLoggerFactory() private readonly loggerFactory: WinstonLoggerFactory,
    private readonly idService: IdService,
  ) {}

  async getOneBy(opts: GetSeasonGroupBy) {
    return await this.seasonGroupRepository.findOneBy({
      ...opts,
      status: Not(SeasonGroupStatus.DELETED),
    })
  }

  async getOne(id: string): Promise<MaybeNull<SeasonGroup>> {
    return await this.getOneBy({
      id,
    });
  }

  async getOneByOrFail(opts: GetSeasonGroupBy, ctx: ServiceMethodContext) {
    const logger = this.logger.forMethod(this.getOneByOrFail.name, ctx, {
      id,
    });

    const seasonGroup = await this.getOneBy(opts);

    if (!seasonGroup) {
      throw new NotFoundException({
        message: `Season group with ${stringifyOpts(opts)} not found`,
        key: ErrorKeys.JS_SEASON_GROUPS_NOT_FOUND,
        context: logger.getContext(),
      });
    }

    return seasonGroup;
  }

  async getOneOrFile(id: string, ctx: ServiceMethodContext): Promise<MaybeNull<SeasonGroup>> {
    return await this.getOneByOrFail({
      id,
    }, ctx);
  }

  async getMany(
    {
      filter,
      orderBy = SeasonGroupsOrderBy.createdAt_DESC,
      limit,
      offset,
      needCountTotal,
    }: GetManySeasonGroupsOptions,
  ): Promise<[SeasonGroup[], ListMeta]> {
    const sort = extractSortParams<SeasonGroupsOrderFields>(orderBy);
    const statuses = defineStatuses(filter.statuses, defaultSeasonGroupStatuses);

    const alias = 'sg';

    const query = this.seasonGroupRepository
      .createQueryBuilder(alias)
      .andWhere(`${alias}.status IN(:...statuses)`, {
        statuses,
      });

    query.addOrderBy(`${alias}.${sort.columnName}`, sort.direction);

    query.limit(limit);
    query.offset(offset);

    return Promise.all([
      query.getMany(),
      createListMeta<SeasonGroup>({
        query,
        needCountTotal,
        limit,
        offset,
      }),
    ]);
  }

  async create(
    opts: CreateSeasonGroupOptions,
    ctx: ServiceMethodContext,
  ): Promise<SeasonGroup> {
    const logger = this.logger.forMethod(this.create.name, ctx);

    const seasonGroup = this.seasonGroupRepository.create({
      id: this.idService.generateEntityId(IdPrefix.SEASON_GROUP),
      creatorId: ctx.user.id,
      ...opts,
    });

    try {
      return await this.seasonGroupRepository.save(seasonGroup);
    } catch (error) {
      throw new InternalServerErrorException({
        message: 'Failed to create season group',
        key: ErrorKeys.JS_SEASON_GROUPS_FAILED_TO_CREATE,
        context: logger.getContext(),
        error,
      });
    }
  }

  async update(
    {
      id,
      ...opts
    }: UpdateSeasonGroupOptions,
    ctx: ServiceMethodContext,
  ): Promise<SeasonGroup> {
    const logger = this.logger.forMethod(this.update.name, ctx, {
      id,
    });

    const seasonGroup = await this.getOneOrFail(id, ctx);

    this.seasonGroupRepository.merge(seasonGroup, opts);

    try {
      return await this.seasonGroupRepository.save(seasonGroup);
    } catch (error) {
      throw new InternalServerErrorException({
        message: `Failed to update season group with id "${id}"`,
        key: ErrorKeys.JS_SEASON_GROUPS_FAILED_TO_UPDATE,
        context: logger.getContext(),
        error,
      });
    }
  }

  async delete(id: string, ctx: ServiceMethodContext): Promise<void> {
    const logger = this.logger.forMethod(this.delete.name, ctx, {
      id,
    });

    const seasonGroup = await this.getOne(id);

    if (!seasonGroup || seasonGroup.isDeleted) {
      return;
    }

    seasonGroup.status = SeasonGroupStatus.DELETED;

    try {
      await this.seasonGroupRepository.save(seasonGroup);
    } catch (error) {
      throw new InternalServerErrorException({
        message: `Failed to delete season group with id ${id}`,
        key: ErrorKeys.JS_SEASON_GROUPS_FAILED_TO_DELETE,
        context: logger.getContext(),
        error,
      });
    }
  }
}

Here's a brief overview of the methods provided by this service:

  1. getOneBy(opts: GetSeasonGroupBy): Fetches a single SeasonGroup entity based on the specified options. The method filters out the deleted items.

  2. getOne(id: string): Fetches a single SeasonGroup entity by its id.

  3. getOneByOrFail(opts: GetSeasonGroupBy, ctx: ServiceMethodContext): Fetches a single SeasonGroup entity based on the specified options, throwing a NotFoundException if the item is not found.

  4. getOneOrFile(id: string, ctx: ServiceMethodContext): Fetches a single SeasonGroup entity by its id, throwing a NotFoundException if the item is not found.

  5. getMany(options: GetManySeasonGroupsOptions): Fetches a list of SeasonGroup entities based on the specified options, such as filters, ordering, limit, and offset. It returns the list of items along with pagination metadata.

  6. create(opts: CreateSeasonGroupOptions, ctx: ServiceMethodContext): Creates a new SeasonGroup entity based on the specified options and the user's context.

  7. update(options: UpdateSeasonGroupOptions, ctx: ServiceMethodContext): Updates an existing SeasonGroup entity based on the specified options and the user's context.

  8. delete(id: string, ctx: ServiceMethodContext): Deletes a SeasonGroup entity by its id. The method marks the entity as deleted by setting its status to SeasonGroupStatus.DELETED.

These methods can be used within your application's resolvers to implement the GraphQL queries and mutations that interact with the SeasonGroup entities. The service utilizes the TypeORM repository for SeasonGroup to perform database operations, and it also uses a logger for error handling and debugging purposes.

Created by DeepVision Software.