tachybase_todo/packages/database-next/src/options-parser.ts

274 lines
7.4 KiB
TypeScript
Raw Normal View History

feat: database next (#130) * FIX: database test with sqlite * more types * filter test * split filter parser * filter test * filter test: hasMany * define inverse association for belongsTo & hasMany * chore: console.log * repository count method * chore: Collection * repository filter & appends & fields & expect * repository: sort option * chore: test * add: test * find & findAndCount * chore: test * database-next: update guard * database-next: update guard associationKeysToBeUpdate * chore: comment * update-guard OneToOne Association * has one repository * support through table value * belongs to many repository * has many repository * has many repository find * fix: has many find and count * clean code * add count method * chore: multiple relation * chore: single relation * repository find * relation repository builder * repository count * repository count test * fix test * close db afterEach test * sort with associations * repository update * has many repository: destroy * belongs to many repository: destroy * add transaction decorator * belongs to many with transaction * has many with transaction * clean types * clean types * clean types * repository transaction * fix test * single relation repository with transaction * single relation repository with transaction * fix: test * fix: option parser fields append * fix: typo * fix: string type * fix: import * collection field methods * cleanup * collection sync * fix: import * fix: test * collection update field * collection update options * database hook * database test * database event test * update database event * add async emmit mixin * async model event * database import * fix: model hook type * fix: collection event * recall model.init on collection update * skip redefine collection test * skip collection model update * add model hook class * global model event support * chore * chore * change utils import * add field types * database import * more import test * test case * fix: through model init... * bugfix * fix * update database import * collection sync by foreachModel * fix collection model sync * update * add field types * custom operator * sqlite array field * postgresql array field * array query escape * mysql array operators * date operators * array field sqlite fix * association operator * date operator empty & notEmpty * fix: fields import * fix array field nested association * filter parse prepare * fix test * string field empty * add date operator test * field option types * fix typo * fix: operator name conflict * rename function Co-authored-by: Chareice <chareice@live.com>
2021-12-06 21:12:54 +08:00
import { Appends, Except, FindOptions } from './repository';
import FilterParser from './filter-parser';
import { FindAttributeOptions, ModelCtor } from 'sequelize';
import { Database } from './database';
const debug = require('debug')('noco-database');
export class OptionsParser {
options: FindOptions;
database: Database;
model: ModelCtor<any>;
filterParser: FilterParser;
constructor(model: ModelCtor<any>, database: Database, options: FindOptions) {
this.model = model;
this.options = options;
this.database = database;
this.filterParser = new FilterParser(model, this.database, options?.filter);
}
isAssociation(key: string) {
return this.model.associations[key] !== undefined;
}
isAssociationPath(path: string) {
return this.isAssociation(path.split('.')[0]);
}
parseFilterByPk() {
if (this.options?.filterByPk) {
return {
where: {
[this.model.primaryKeyAttribute]: this.options.filterByPk,
},
};
}
return null;
}
toSequelizeParams() {
const filterParams = this.options?.filterByPk
? this.parseFilterByPk()
: this.filterParser.toSequelizeParams();
return this.parseSort(this.parseFields(filterParams));
}
/**
* parser sort options
* @param filterParams
* @protected
*/
protected parseSort(filterParams) {
const sort = this.options?.sort || [];
const orderParams = sort.map((sortKey: string) => {
const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
const sortField: Array<any> = sortKey.replace('-', '').split('.');
// handle sort by association
if (sortField.length > 1) {
let associationModel = this.model;
for (let i = 0; i < sortField.length - 1; i++) {
const associationKey = sortField[i];
sortField[i] = associationModel.associations[associationKey].target;
associationModel = sortField[i];
}
}
sortField.push(direction);
return sortField;
});
if (orderParams.length > 0) {
return {
order: orderParams,
...filterParams,
};
}
return filterParams;
}
protected parseFields(filterParams: any) {
const appends = this.options?.appends || [];
const except = [];
let attributes: FindAttributeOptions = {
include: [],
exclude: [],
}; // out put all fields by default
if (this.options?.fields) {
// 将fields拆分为 attributes 和 appends
for (const field of this.options.fields) {
if (this.isAssociationPath(field)) {
// field is association field
appends.push(field);
} else {
// field is model attribute, change attributes to array type
if (!Array.isArray(attributes)) attributes = [];
attributes.push(field);
}
}
}
if (this.options?.except) {
for (const exceptKey of this.options.except) {
if (this.isAssociationPath(exceptKey)) {
// except association field
except.push(exceptKey);
} else {
// if attributes is array form, ignore except
if (Array.isArray(attributes)) continue;
attributes.exclude.push(exceptKey);
}
}
}
return {
attributes,
...this.parseExcept(except, this.parseAppends(appends, filterParams)),
};
}
protected parseExcept(except: Except, filterParams: any) {
if (!except) return filterParams;
const setExcept = (queryParams: any, except: string) => {
// split exceptKey to path form
// posts.comments.content => ['posts', 'comments', 'content']
// then set except on include attributes
const exceptPath = except.split('.');
const association = exceptPath[0];
const lastLevel = exceptPath.length <= 2;
let existIncludeIndex = queryParams['include'].findIndex(
(include) => include['association'] == association,
);
if (existIncludeIndex == -1) {
// if include not exists, ignore this except
return;
}
if (lastLevel) {
// if it not have exclude form
if (
Array.isArray(queryParams['include'][existIncludeIndex]['attributes'])
) {
return;
} else {
if (
!queryParams['include'][existIncludeIndex]['attributes']['exclude']
) {
queryParams['include'][existIncludeIndex]['attributes']['exclude'] =
[];
}
queryParams['include'][existIncludeIndex]['attributes'][
'exclude'
].push(exceptPath[1]);
}
} else {
setExcept(
queryParams['include'][existIncludeIndex],
exceptPath.filter((_, index) => index !== 0).join('.'),
);
}
};
for (const exceptKey of except) {
setExcept(filterParams, exceptKey);
}
return filterParams;
}
protected parseAppends(appends: Appends, filterParams: any) {
if (!appends) return filterParams;
const associations = this.model.associations;
/**
* set include params
* @param includeRoot
* @param appends
*/
const setInclude = (queryParams: any, append: string) => {
const appendFields = append.split('.');
const appendAssociation = appendFields[0];
// if append length less or equal 2
// example:
// appends: ['posts']
// appends: ['posts.title']
// All of these can be seen as last level
const lastLevel = appendFields.length <= 2;
// find association index
if (queryParams['include'] == undefined) {
queryParams['include'] = [];
}
let existIncludeIndex = queryParams['include'].findIndex(
(include) => include['association'] == appendAssociation,
);
// if association not exist, create it
if (existIncludeIndex == -1) {
// association not exists
queryParams['include'].push({
association: appendAssociation,
});
existIncludeIndex = 0;
}
// end appends
// without nests association
if (lastLevel) {
// get exist association attributes
let attributes = queryParams['include'][existIncludeIndex][
'attributes'
] || {
include: [], // all fields are output by default
};
// if need set attribute
if (appendFields.length == 2) {
if (!Array.isArray(attributes)) {
attributes = [];
}
// push field to it
attributes.push(appendFields[1]);
} else {
// if attributes is empty array, change it to object
if (Array.isArray(attributes) && attributes.length == 0) {
attributes = {
include: [],
};
}
}
// set new attributes
queryParams['include'][existIncludeIndex] = {
...queryParams['include'][existIncludeIndex],
attributes,
};
} else {
setInclude(
queryParams['include'][existIncludeIndex],
appendFields.filter((_, index) => index !== 0).join('.'),
);
}
};
// handle every appends
for (const append of appends) {
const appendFields = append.split('.');
if (!associations[appendFields[0]]) {
throw new Error(`${append} is not a valid association`);
}
setInclude(filterParams, append);
}
debug('filter params: %o', filterParams);
return filterParams;
}
}