@nestjsx/crud-typeorm
Version:
NestJs CRUD for RESTful APIs - TypeORM
693 lines • 28.7 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
const crud_1 = require("@nestjsx/crud");
const util_1 = require("@nestjsx/util");
const o0_1 = require("@zmotivat0r/o0");
const class_transformer_1 = require("class-transformer");
const typeorm_1 = require("typeorm");
class TypeOrmCrudService extends crud_1.CrudService {
constructor(repo) {
super();
this.repo = repo;
this.entityHasDeleteColumn = false;
this.entityColumnsHash = {};
this.entityRelationsHash = new Map();
this.sqlInjectionRegEx = [
/(%27)|(\')|(--)|(%23)|(#)/gi,
/((%3D)|(=))[^\n]*((%27)|(\')|(--)|(%3B)|(;))/gi,
/w*((%27)|(\'))((%6F)|o|(%4F))((%72)|r|(%52))/gi,
/((%27)|(\'))union/gi,
];
this.dbName = this.repo.metadata.connection.options.type;
this.onInitMapEntityColumns();
}
get findOne() {
return this.repo.findOne.bind(this.repo);
}
get find() {
return this.repo.find.bind(this.repo);
}
get count() {
return this.repo.count.bind(this.repo);
}
get entityType() {
return this.repo.target;
}
get alias() {
return this.repo.metadata.targetName;
}
async getMany(req) {
const { parsed, options } = req;
const builder = await this.createBuilder(parsed, options);
return this.doGetMany(builder, parsed, options);
}
async getOne(req) {
return this.getOneOrFail(req);
}
async createOne(req, dto) {
const { returnShallow } = req.options.routes.createOneBase;
const entity = this.prepareEntityBeforeSave(dto, req.parsed);
if (!entity) {
this.throwBadRequestException(`Empty data. Nothing to save.`);
}
const saved = await this.repo.save(entity);
if (returnShallow) {
return saved;
}
else {
const primaryParams = this.getPrimaryParams(req.options);
if (!primaryParams.length && primaryParams.some((p) => util_1.isNil(saved[p]))) {
return saved;
}
else {
req.parsed.search = primaryParams.reduce((acc, p) => ({ ...acc, [p]: saved[p] }), {});
return this.getOneOrFail(req);
}
}
}
async createMany(req, dto) {
if (!util_1.isObject(dto) || !util_1.isArrayFull(dto.bulk)) {
this.throwBadRequestException(`Empty data. Nothing to save.`);
}
const bulk = dto.bulk
.map((one) => this.prepareEntityBeforeSave(one, req.parsed))
.filter((d) => !util_1.isUndefined(d));
if (!util_1.hasLength(bulk)) {
this.throwBadRequestException(`Empty data. Nothing to save.`);
}
return this.repo.save(bulk, { chunk: 50 });
}
async updateOne(req, dto) {
const { allowParamsOverride, returnShallow } = req.options.routes.updateOneBase;
const paramsFilters = this.getParamFilters(req.parsed);
const found = await this.getOneOrFail(req, returnShallow);
const toSave = !allowParamsOverride
? { ...found, ...dto, ...paramsFilters, ...req.parsed.authPersist }
: { ...found, ...dto, ...req.parsed.authPersist };
const updated = await this.repo.save(class_transformer_1.plainToClass(this.entityType, toSave));
if (returnShallow) {
return updated;
}
else {
req.parsed.paramsFilter.forEach((filter) => {
filter.value = updated[filter.field];
});
return this.getOneOrFail(req);
}
}
async recoverOne(req) {
const found = await this.getOneOrFail(req, false, true);
return this.repo.recover(found);
}
async replaceOne(req, dto) {
const { allowParamsOverride, returnShallow } = req.options.routes.replaceOneBase;
const paramsFilters = this.getParamFilters(req.parsed);
const [_, found] = await o0_1.oO(this.getOneOrFail(req, returnShallow));
const toSave = !allowParamsOverride
? { ...(found || {}), ...dto, ...paramsFilters, ...req.parsed.authPersist }
: {
...(found || {}),
...paramsFilters,
...dto,
...req.parsed.authPersist,
};
const replaced = await this.repo.save(class_transformer_1.plainToClass(this.entityType, toSave));
if (returnShallow) {
return replaced;
}
else {
const primaryParams = this.getPrimaryParams(req.options);
if (!primaryParams.length) {
return replaced;
}
req.parsed.search = primaryParams.reduce((acc, p) => ({ ...acc, [p]: replaced[p] }), {});
return this.getOneOrFail(req);
}
}
async deleteOne(req) {
const { returnDeleted } = req.options.routes.deleteOneBase;
const found = await this.getOneOrFail(req, returnDeleted);
const toReturn = returnDeleted
? class_transformer_1.plainToClass(this.entityType, { ...found })
: undefined;
const deleted = req.options.query.softDelete === true
? await this.repo.softRemove(found)
: await this.repo.remove(found);
return toReturn;
}
getParamFilters(parsed) {
let filters = {};
if (util_1.hasLength(parsed.paramsFilter)) {
for (const filter of parsed.paramsFilter) {
filters[filter.field] = filter.value;
}
}
return filters;
}
async createBuilder(parsed, options, many = true, withDeleted = false) {
const builder = this.repo.createQueryBuilder(this.alias);
const select = this.getSelect(parsed, options.query);
builder.select(select);
this.setSearchCondition(builder, parsed.search);
const joinOptions = options.query.join || {};
const allowedJoins = util_1.objKeys(joinOptions);
if (util_1.hasLength(allowedJoins)) {
const eagerJoins = {};
for (let i = 0; i < allowedJoins.length; i++) {
if (joinOptions[allowedJoins[i]].eager) {
const cond = parsed.join.find((j) => j && j.field === allowedJoins[i]) || {
field: allowedJoins[i],
};
this.setJoin(cond, joinOptions, builder);
eagerJoins[allowedJoins[i]] = true;
}
}
if (util_1.isArrayFull(parsed.join)) {
for (let i = 0; i < parsed.join.length; i++) {
if (!eagerJoins[parsed.join[i].field]) {
this.setJoin(parsed.join[i], joinOptions, builder);
}
}
}
}
if (this.entityHasDeleteColumn && options.query.softDelete) {
if (parsed.includeDeleted === 1 || withDeleted) {
builder.withDeleted();
}
}
if (many) {
const sort = this.getSort(parsed, options.query);
builder.orderBy(sort);
const take = this.getTake(parsed, options.query);
if (isFinite(take)) {
builder.take(take);
}
const skip = this.getSkip(parsed, take);
if (isFinite(skip)) {
builder.skip(skip);
}
}
if (options.query.cache && parsed.cache !== 0) {
builder.cache(builder.getQueryAndParameters(), options.query.cache);
}
return builder;
}
async doGetMany(builder, query, options) {
if (this.decidePagination(query, options)) {
const [data, total] = await builder.getManyAndCount();
const limit = builder.expressionMap.take;
const offset = builder.expressionMap.skip;
return this.createPageInfo(data, total, limit || total, offset || 0);
}
return builder.getMany();
}
onInitMapEntityColumns() {
this.entityColumns = this.repo.metadata.columns.map((prop) => {
if (prop.embeddedMetadata) {
this.entityColumnsHash[prop.propertyPath] = prop.databasePath;
return prop.propertyPath;
}
this.entityColumnsHash[prop.propertyName] = prop.databasePath;
return prop.propertyName;
});
this.entityPrimaryColumns = this.repo.metadata.columns
.filter((prop) => prop.isPrimary)
.map((prop) => prop.propertyName);
this.entityHasDeleteColumn =
this.repo.metadata.columns.filter((prop) => prop.isDeleteDate).length > 0;
}
async getOneOrFail(req, shallow = false, withDeleted = false) {
const { parsed, options } = req;
const builder = shallow
? this.repo.createQueryBuilder(this.alias)
: await this.createBuilder(parsed, options, true, withDeleted);
if (shallow) {
this.setSearchCondition(builder, parsed.search);
}
const found = withDeleted
? await builder.withDeleted().getOne()
: await builder.getOne();
if (!found) {
this.throwNotFoundException(this.alias);
}
return found;
}
prepareEntityBeforeSave(dto, parsed) {
if (!util_1.isObject(dto)) {
return undefined;
}
if (util_1.hasLength(parsed.paramsFilter)) {
for (const filter of parsed.paramsFilter) {
dto[filter.field] = filter.value;
}
}
if (!util_1.hasLength(util_1.objKeys(dto))) {
return undefined;
}
return dto instanceof this.entityType
? Object.assign(dto, parsed.authPersist)
: class_transformer_1.plainToClass(this.entityType, { ...dto, ...parsed.authPersist });
}
getAllowedColumns(columns, options) {
return (!options.exclude || !options.exclude.length) &&
(!options.allow || !options.allow.length)
? columns
: columns.filter((column) => (options.exclude && options.exclude.length
? !options.exclude.some((col) => col === column)
: true) &&
(options.allow && options.allow.length
? options.allow.some((col) => col === column)
: true));
}
getEntityColumns(entityMetadata) {
const columns = entityMetadata.columns.map((prop) => prop.propertyPath) ||
[];
const primaryColumns = entityMetadata.primaryColumns.map((prop) => prop.propertyPath) ||
[];
return { columns, primaryColumns };
}
getRelationMetadata(field, options) {
try {
let allowedRelation;
let nested = false;
if (this.entityRelationsHash.has(field)) {
allowedRelation = this.entityRelationsHash.get(field);
}
else {
const fields = field.split('.');
let relationMetadata;
let name;
let path;
let parentPath;
if (fields.length === 1) {
const found = this.repo.metadata.relations.find((one) => one.propertyName === fields[0]);
if (found) {
name = fields[0];
path = `${this.alias}.${fields[0]}`;
relationMetadata = found.inverseEntityMetadata;
}
}
else {
nested = true;
parentPath = '';
const reduced = fields.reduce((res, propertyName, i) => {
const found = res.relations.length
? res.relations.find((one) => one.propertyName === propertyName)
: null;
const relationMetadata = found ? found.inverseEntityMetadata : null;
const relations = relationMetadata ? relationMetadata.relations : [];
name = propertyName;
if (i !== fields.length - 1) {
parentPath = !parentPath
? propertyName
: `${parentPath}.${propertyName}`;
}
return {
relations,
relationMetadata,
};
}, {
relations: this.repo.metadata.relations,
relationMetadata: null,
});
relationMetadata = reduced.relationMetadata;
}
if (relationMetadata) {
const { columns, primaryColumns } = this.getEntityColumns(relationMetadata);
if (!path && parentPath) {
const parentAllowedRelation = this.entityRelationsHash.get(parentPath);
if (parentAllowedRelation) {
path = parentAllowedRelation.alias
? `${parentAllowedRelation.alias}.${name}`
: field;
}
}
allowedRelation = {
alias: options.alias,
name,
path,
columns,
nested,
primaryColumns,
};
}
}
if (allowedRelation) {
const allowedColumns = this.getAllowedColumns(allowedRelation.columns, options);
const toSave = { ...allowedRelation, allowedColumns };
this.entityRelationsHash.set(field, toSave);
if (options.alias) {
this.entityRelationsHash.set(options.alias, toSave);
}
return toSave;
}
}
catch (_) {
return null;
}
}
setJoin(cond, joinOptions, builder) {
const options = joinOptions[cond.field];
if (!options) {
return true;
}
const allowedRelation = this.getRelationMetadata(cond.field, options);
if (!allowedRelation) {
return true;
}
const relationType = options.required ? 'innerJoin' : 'leftJoin';
const alias = options.alias ? options.alias : allowedRelation.name;
builder[relationType](allowedRelation.path, alias);
if (options.select !== false) {
const columns = util_1.isArrayFull(cond.select)
? cond.select.filter((column) => allowedRelation.allowedColumns.some((allowed) => allowed === column))
: allowedRelation.allowedColumns;
const select = [
...allowedRelation.primaryColumns,
...(util_1.isArrayFull(options.persist) ? options.persist : []),
...columns,
].map((col) => `${alias}.${col}`);
builder.addSelect(select);
}
}
setAndWhere(cond, i, builder) {
const { str, params } = this.mapOperatorsToQuery(cond, `andWhere${i}`);
builder.andWhere(str, params);
}
setOrWhere(cond, i, builder) {
const { str, params } = this.mapOperatorsToQuery(cond, `orWhere${i}`);
builder.orWhere(str, params);
}
setSearchCondition(builder, search, condition = '$and') {
if (util_1.isObject(search)) {
const keys = util_1.objKeys(search);
if (keys.length) {
if (util_1.isArrayFull(search.$and)) {
if (search.$and.length === 1) {
this.setSearchCondition(builder, search.$and[0], condition);
}
else {
this.builderAddBrackets(builder, condition, new typeorm_1.Brackets((qb) => {
search.$and.forEach((item) => {
this.setSearchCondition(qb, item, '$and');
});
}));
}
}
else if (util_1.isArrayFull(search.$or)) {
if (keys.length === 1) {
if (search.$or.length === 1) {
this.setSearchCondition(builder, search.$or[0], condition);
}
else {
this.builderAddBrackets(builder, condition, new typeorm_1.Brackets((qb) => {
search.$or.forEach((item) => {
this.setSearchCondition(qb, item, '$or');
});
}));
}
}
else {
this.builderAddBrackets(builder, condition, new typeorm_1.Brackets((qb) => {
keys.forEach((field) => {
if (field !== '$or') {
const value = search[field];
if (!util_1.isObject(value)) {
this.builderSetWhere(qb, '$and', field, value);
}
else {
this.setSearchFieldObjectCondition(qb, '$and', field, value);
}
}
else {
if (search.$or.length === 1) {
this.setSearchCondition(builder, search.$or[0], '$and');
}
else {
this.builderAddBrackets(qb, '$and', new typeorm_1.Brackets((qb2) => {
search.$or.forEach((item) => {
this.setSearchCondition(qb2, item, '$or');
});
}));
}
}
});
}));
}
}
else {
if (keys.length === 1) {
const field = keys[0];
const value = search[field];
if (!util_1.isObject(value)) {
this.builderSetWhere(builder, condition, field, value);
}
else {
this.setSearchFieldObjectCondition(builder, condition, field, value);
}
}
else {
this.builderAddBrackets(builder, condition, new typeorm_1.Brackets((qb) => {
keys.forEach((field) => {
const value = search[field];
if (!util_1.isObject(value)) {
this.builderSetWhere(qb, '$and', field, value);
}
else {
this.setSearchFieldObjectCondition(qb, '$and', field, value);
}
});
}));
}
}
}
}
}
builderAddBrackets(builder, condition, brackets) {
if (condition === '$and') {
builder.andWhere(brackets);
}
else {
builder.orWhere(brackets);
}
}
builderSetWhere(builder, condition, field, value, operator = '$eq') {
const time = process.hrtime();
const index = `${field}${time[0]}${time[1]}`;
const args = [
{ field, operator: util_1.isNull(value) ? '$isnull' : operator, value },
index,
builder,
];
const fn = condition === '$and' ? this.setAndWhere : this.setOrWhere;
fn.apply(this, args);
}
setSearchFieldObjectCondition(builder, condition, field, object) {
if (util_1.isObject(object)) {
const operators = util_1.objKeys(object);
if (operators.length === 1) {
const operator = operators[0];
const value = object[operator];
if (util_1.isObject(object.$or)) {
const orKeys = util_1.objKeys(object.$or);
this.setSearchFieldObjectCondition(builder, orKeys.length === 1 ? condition : '$or', field, object.$or);
}
else {
this.builderSetWhere(builder, condition, field, value, operator);
}
}
else {
if (operators.length > 1) {
this.builderAddBrackets(builder, condition, new typeorm_1.Brackets((qb) => {
operators.forEach((operator) => {
const value = object[operator];
if (operator !== '$or') {
this.builderSetWhere(qb, condition, field, value, operator);
}
else {
const orKeys = util_1.objKeys(object.$or);
if (orKeys.length === 1) {
this.setSearchFieldObjectCondition(qb, condition, field, object.$or);
}
else {
this.builderAddBrackets(qb, condition, new typeorm_1.Brackets((qb2) => {
this.setSearchFieldObjectCondition(qb2, '$or', field, object.$or);
}));
}
}
});
}));
}
}
}
}
getSelect(query, options) {
const allowed = this.getAllowedColumns(this.entityColumns, options);
const columns = query.fields && query.fields.length
? query.fields.filter((field) => allowed.some((col) => field === col))
: allowed;
const select = [
...(options.persist && options.persist.length ? options.persist : []),
...columns,
...this.entityPrimaryColumns,
].map((col) => `${this.alias}.${col}`);
return select;
}
getSort(query, options) {
return query.sort && query.sort.length
? this.mapSort(query.sort)
: options.sort && options.sort.length
? this.mapSort(options.sort)
: {};
}
getFieldWithAlias(field, sort = false) {
const i = this.dbName === 'mysql' ? '`' : '"';
const cols = field.split('.');
switch (cols.length) {
case 1:
if (sort) {
return `${this.alias}.${field}`;
}
const dbColName = this.entityColumnsHash[field] !== field ? this.entityColumnsHash[field] : field;
return `${i}${this.alias}${i}.${i}${dbColName}${i}`;
case 2:
return field;
default:
return cols.slice(cols.length - 2, cols.length).join('.');
}
}
mapSort(sort) {
const params = {};
for (let i = 0; i < sort.length; i++) {
const field = this.getFieldWithAlias(sort[i].field, true);
const checkedFiled = this.checkSqlInjection(field);
params[checkedFiled] = sort[i].order;
}
return params;
}
mapOperatorsToQuery(cond, param) {
const field = this.getFieldWithAlias(cond.field);
const likeOperator = this.dbName === 'postgres' ? 'ILIKE' : 'LIKE';
let str;
let params;
if (cond.operator[0] !== '$') {
cond.operator = ('$' + cond.operator);
}
switch (cond.operator) {
case '$eq':
str = `${field} = :${param}`;
break;
case '$ne':
str = `${field} != :${param}`;
break;
case '$gt':
str = `${field} > :${param}`;
break;
case '$lt':
str = `${field} < :${param}`;
break;
case '$gte':
str = `${field} >= :${param}`;
break;
case '$lte':
str = `${field} <= :${param}`;
break;
case '$starts':
str = `${field} LIKE :${param}`;
params = { [param]: `${cond.value}%` };
break;
case '$ends':
str = `${field} LIKE :${param}`;
params = { [param]: `%${cond.value}` };
break;
case '$cont':
str = `${field} LIKE :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$excl':
str = `${field} NOT LIKE :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$in':
this.checkFilterIsArray(cond);
str = `${field} IN (:...${param})`;
break;
case '$notin':
this.checkFilterIsArray(cond);
str = `${field} NOT IN (:...${param})`;
break;
case '$isnull':
str = `${field} IS NULL`;
params = {};
break;
case '$notnull':
str = `${field} IS NOT NULL`;
params = {};
break;
case '$between':
this.checkFilterIsArray(cond, cond.value.length !== 2);
str = `${field} BETWEEN :${param}0 AND :${param}1`;
params = {
[`${param}0`]: cond.value[0],
[`${param}1`]: cond.value[1],
};
break;
case '$eqL':
str = `LOWER(${field}) = :${param}`;
break;
case '$neL':
str = `LOWER(${field}) != :${param}`;
break;
case '$startsL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `${cond.value}%` };
break;
case '$endsL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}` };
break;
case '$contL':
str = `LOWER(${field}) ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$exclL':
str = `LOWER(${field}) NOT ${likeOperator} :${param}`;
params = { [param]: `%${cond.value}%` };
break;
case '$inL':
this.checkFilterIsArray(cond);
str = `LOWER(${field}) IN (:...${param})`;
break;
case '$notinL':
this.checkFilterIsArray(cond);
str = `LOWER(${field}) NOT IN (:...${param})`;
break;
default:
str = `${field} = :${param}`;
break;
}
if (typeof params === 'undefined') {
params = { [param]: cond.value };
}
return { str, params };
}
checkFilterIsArray(cond, withLength) {
if (!Array.isArray(cond.value) ||
!cond.value.length ||
(!util_1.isNil(withLength) ? withLength : false)) {
this.throwBadRequestException(`Invalid column '${cond.field}' value`);
}
}
checkSqlInjection(field) {
if (this.sqlInjectionRegEx.length) {
for (let i = 0; i < this.sqlInjectionRegEx.length; i++) {
if (this.sqlInjectionRegEx[0].test(field)) {
this.throwBadRequestException(`SQL injection detected: "${field}"`);
}
}
}
return field;
}
}
exports.TypeOrmCrudService = TypeOrmCrudService;
//# sourceMappingURL=typeorm-crud.service.js.map
;