@rytass/cms-base-nestjs-module
Version:
Rytass Content Management System NestJS Base Module
321 lines (318 loc) • 14.1 kB
JavaScript
import { Injectable, Inject, InternalServerErrorException, BadRequestException } from '@nestjs/common';
import { In } 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 { 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_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 multiLanguageName = category.multiLanguageNames.find((multiLanguageName)=>multiLanguageName.language === language);
return {
id: category.id,
bindable: category.bindable,
createdAt: category.createdAt,
updatedAt: category.updatedAt,
deletedAt: category.deletedAt,
language: multiLanguageName.language,
name: multiLanguageName.name,
children: category.children?.map((childCategory)=>this.parseSingleLanguageCategory(childCategory)) ?? []
};
}
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));
}
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;
}
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 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 category;
} 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 category;
} 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))
], CategoryBaseService);
export { CategoryBaseService };