UNPKG

@rytass/cms-base-nestjs-module

Version:

Rytass Content Management System NestJS Base Module

341 lines (338 loc) 15.4 kB
import { Injectable, Inject, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { In, Repository, DataSource } from 'typeorm'; import { RESOLVED_CATEGORY_MULTI_LANGUAGE_NAME_REPO, RESOLVED_CATEGORY_REPO, MULTIPLE_LANGUAGE_MODE, MULTIPLE_CATEGORY_PARENT_MODE, CIRCULAR_CATEGORY_MODE, CATEGORY_DATA_LOADER } from '../typings/cms-base-providers.js'; import { DEFAULT_LANGUAGE } from '../constants/default-language.js'; import { InjectDataSource } from '@nestjs/typeorm'; import { CategoryDataLoader } from '../data-loaders/category.dataloader.js'; import { CategorySorter } from '../typings/category-sorter.enum.js'; import { CircularCategoryNotAllowedError, CategoryNotFoundError, MultipleParentCategoryNotAllowedError, ParentCategoryNotFoundError } from '../constants/errors/category.errors.js'; function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } function _ts_param(paramIndex, decorator) { return function(target, key) { decorator(target, key, paramIndex); }; } class CategoryBaseService { baseCategoryMultiLanguageNameRepo; baseCategoryRepo; multipleLanguageMode; allowMultipleParentCategories; allowCircularCategories; dataSource; categoryDataLoader; constructor(baseCategoryMultiLanguageNameRepo, baseCategoryRepo, multipleLanguageMode, allowMultipleParentCategories, allowCircularCategories, dataSource, categoryDataLoader){ this.baseCategoryMultiLanguageNameRepo = baseCategoryMultiLanguageNameRepo; this.baseCategoryRepo = baseCategoryRepo; this.multipleLanguageMode = multipleLanguageMode; this.allowMultipleParentCategories = allowMultipleParentCategories; this.allowCircularCategories = allowCircularCategories; this.dataSource = dataSource; this.categoryDataLoader = categoryDataLoader; } getDefaultQueryBuilder(alias = 'categories') { const qb = this.baseCategoryRepo.createQueryBuilder(alias); qb.innerJoinAndSelect(`${alias}.multiLanguageNames`, 'multiLanguageNames'); qb.leftJoinAndSelect(`${alias}.children`, 'children'); qb.leftJoinAndSelect('children.multiLanguageNames', 'childrenMultiLanguageNames'); return qb; } parseSingleLanguageCategory(category, language = DEFAULT_LANGUAGE) { const { children, multiLanguageNames, parents, articles, ...columns } = category; const multiLanguageName = category.multiLanguageNames.find((multiLanguageName)=>multiLanguageName.language === language); return { ...columns, ...multiLanguageName, children: (children ?? []).map((child)=>this.parseSingleLanguageCategory(child)), parents: (parents ?? []).map(/* istanbul ignore next: recursive parent parsing */ (parent)=>this.parseSingleLanguageCategory(parent)) }; } parseToMultiLanguageCategory(category) { const { parents, children, articles, multiLanguageNames, ...columns } = category; return { ...columns, children: (children ?? []).map(/* istanbul ignore next: recursive child parsing */ (child)=>this.parseToMultiLanguageCategory(child)), parents: (parents ?? []).map(/* istanbul ignore next: recursive parent parsing */ (child)=>this.parseToMultiLanguageCategory(child)), multiLanguageNames }; } async getParentCategoryIdSet(id, givenSet = new Set()) { const foundCategory = await this.categoryDataLoader.withParentsLoader.load(id); givenSet.add(id); if (foundCategory.parents.length) { return foundCategory.parents.map((parent)=>(set)=>this.getParentCategoryIdSet(parent.id, set)).reduce((prev, next)=>prev.then(next), Promise.resolve(givenSet)); } return givenSet; } async checkCircularCategories(category, targetParents) { const allParentIdSet = await targetParents.map((parent)=>(set)=>this.getParentCategoryIdSet(parent.id, set)).reduce((prev, next)=>prev.then(next), Promise.resolve(new Set())); if (allParentIdSet.has(category.id)) { throw new CircularCategoryNotAllowedError(); } } async findAll(options) { const qb = this.getDefaultQueryBuilder('categories'); if (options?.ids) { qb.andWhere('categories.id IN (:...ids)', { ids: options.ids }); } if (options?.fromTop) { qb.leftJoin('categories.parents', 'fromTopParents'); qb.andWhere('fromTopParents.id IS NULL'); } if (options?.parentIds?.length) { qb.innerJoin('categories.parents', 'parentForFilters'); qb.andWhere('parentForFilters.id IN (:...parentIds)', { parentIds: options.parentIds }); } const categories = await qb.getMany(); if (options?.language || !this.multipleLanguageMode) { return categories.map((category)=>this.parseSingleLanguageCategory(category, options?.language ?? undefined)); } switch(options?.sorter){ case CategorySorter.CREATED_AT_ASC: qb.addOrderBy('articles.createdAt', 'ASC'); break; case CategorySorter.CREATED_AT_DESC: default: qb.addOrderBy('articles.createdAt', 'DESC'); break; } qb.skip(options?.offset ?? 0); qb.take(Math.min(options?.limit ?? 20, 100)); return categories.map((category)=>this.parseToMultiLanguageCategory(category)); } async findById(id, language) { const qb = this.getDefaultQueryBuilder('categories'); qb.andWhere('categories.id = :id', { id }); const category = await qb.getOne(); if (!category) { throw new CategoryNotFoundError(); } if (language || !this.multipleLanguageMode) { return this.parseSingleLanguageCategory(category, language); } return this.parseToMultiLanguageCategory(category); } async archive(id) { const category = await this.baseCategoryRepo.findOne({ where: { id } }); if (!category) { throw new CategoryNotFoundError(); } await this.baseCategoryRepo.softDelete(id); } async update(id, options, multiLanguageOptions) { if (!this.allowMultipleParentCategories && options.parentIds?.length) { throw new MultipleParentCategoryNotAllowedError(); } const qb = this.getDefaultQueryBuilder('categories'); qb.leftJoinAndSelect('categories.parents', 'parents'); qb.andWhere('categories.id = :id', { id }); const category = await qb.getOne(); if (!category) { throw new CategoryNotFoundError(); } let parentCategories = []; if ((options.parentIds?.length || options.parentId) && this.allowMultipleParentCategories) { parentCategories = await this.baseCategoryRepo.find({ where: { id: In(options.parentIds?.length ? options.parentIds : [ options.parentId ]) }, relations: [ 'parents' ] }); if (parentCategories.length !== (options.parentIds?.length ?? 1)) { throw new ParentCategoryNotFoundError(); } } if (options.parentId && !this.allowMultipleParentCategories) { const parentCategory = await this.baseCategoryRepo.findOne({ where: { id: options.parentId }, relations: [ 'parents' ] }); if (!parentCategory) { throw new ParentCategoryNotFoundError(); } parentCategories = [ parentCategory ]; } if (!this.allowCircularCategories) { await this.checkCircularCategories(category, parentCategories); } let willRemoveLanguages = []; let willCreateOrUpdateLanguages = []; if ('multiLanguageNames' in options) { if (!this.multipleLanguageMode) throw new InternalServerErrorException('Multiple language mode is not enabled'); const existedLanguage = new Map(category.multiLanguageNames.map((multiLanguageName)=>[ multiLanguageName.language, multiLanguageName ])); const nextLanguageSet = new Set(Object.keys(options.multiLanguageNames ?? {})); willRemoveLanguages = category.multiLanguageNames.filter((multiLanguageName)=>!nextLanguageSet.has(multiLanguageName.language)); willCreateOrUpdateLanguages = Object.entries(options.multiLanguageNames ?? {}).map(([language, name])=>{ const existed = existedLanguage.get(language); if (existed) { existed.name = name; return existed; } return this.baseCategoryMultiLanguageNameRepo.create({ ...multiLanguageOptions ?? {}, categoryId: category.id, language, name }); }); } else { const defaultLanguage = category.multiLanguageNames.find((multiLanguageName)=>multiLanguageName.language === DEFAULT_LANGUAGE); if (defaultLanguage) { willCreateOrUpdateLanguages = [ this.baseCategoryMultiLanguageNameRepo.create({ ...multiLanguageOptions ?? {}, ...defaultLanguage, name: options.name }) ]; } else { willCreateOrUpdateLanguages = [ this.baseCategoryMultiLanguageNameRepo.create({ ...multiLanguageOptions ?? {}, categoryId: category.id, language: DEFAULT_LANGUAGE, name: options.name }) ]; } } const runner = this.dataSource.createQueryRunner(); await runner.connect(); await runner.startTransaction(); try { await runner.manager.save(this.baseCategoryRepo.create({ ...category, ...options, bindable: options.bindable ?? true, parents: parentCategories })); await runner.manager.remove(willRemoveLanguages); await runner.manager.save(willCreateOrUpdateLanguages); await runner.commitTransaction(); return this.findById(category.id); } catch (ex) { await runner.rollbackTransaction(); throw new BadRequestException(ex); } finally{ await runner.release(); } } async create(options, multiLanguageOptions) { let parentCategories = []; if (!this.allowMultipleParentCategories && options.parentIds?.length) { throw new MultipleParentCategoryNotAllowedError(); } if ((options.parentIds?.length || options.parentId) && this.allowMultipleParentCategories) { parentCategories = await this.baseCategoryRepo.find({ where: { id: In(options.parentIds?.length ? options.parentIds : [ options.parentId ]) } }); if (parentCategories.length !== (options.parentIds?.length ?? 1)) { throw new ParentCategoryNotFoundError(); } } if (options.parentId && !this.allowMultipleParentCategories) { const parentCategory = await this.baseCategoryRepo.findOne({ where: { id: options.parentId } }); if (!parentCategory) { throw new ParentCategoryNotFoundError(); } parentCategories = [ parentCategory ]; } const category = this.baseCategoryRepo.create({ ...options, bindable: options.bindable ?? true, parents: parentCategories }); const runner = this.dataSource.createQueryRunner(); await runner.connect(); await runner.startTransaction(); try { await runner.manager.save(category); if ('multiLanguageNames' in options) { if (!this.multipleLanguageMode) throw new InternalServerErrorException('Multiple language mode is not enabled'); await runner.manager.save(Object.entries(options.multiLanguageNames ?? {}).map(([language, name])=>this.baseCategoryMultiLanguageNameRepo.create({ ...multiLanguageOptions ?? {}, categoryId: category.id, language, name }))); } else { await runner.manager.save(this.baseCategoryMultiLanguageNameRepo.create({ ...multiLanguageOptions ?? {}, categoryId: category.id, language: DEFAULT_LANGUAGE, name: options.name })); } await runner.commitTransaction(); return this.findById(category.id); } catch (ex) { await runner.rollbackTransaction(); throw new BadRequestException(ex); } finally{ await runner.release(); } } } CategoryBaseService = _ts_decorate([ Injectable(), _ts_param(0, Inject(RESOLVED_CATEGORY_MULTI_LANGUAGE_NAME_REPO)), _ts_param(1, Inject(RESOLVED_CATEGORY_REPO)), _ts_param(2, Inject(MULTIPLE_LANGUAGE_MODE)), _ts_param(3, Inject(MULTIPLE_CATEGORY_PARENT_MODE)), _ts_param(4, Inject(CIRCULAR_CATEGORY_MODE)), _ts_param(5, InjectDataSource()), _ts_param(6, Inject(CATEGORY_DATA_LOADER)), _ts_metadata("design:type", Function), _ts_metadata("design:paramtypes", [ typeof Repository === "undefined" ? Object : Repository, typeof Repository === "undefined" ? Object : Repository, Boolean, Boolean, Boolean, typeof DataSource === "undefined" ? Object : DataSource, typeof CategoryDataLoader === "undefined" ? Object : CategoryDataLoader ]) ], CategoryBaseService); export { CategoryBaseService };