nocobase/packages/database/src/model.ts

497 lines
13 KiB
TypeScript
Raw Normal View History

2020-10-24 07:34:43 +00:00
import {
Model as SequelizeModel, Op, Sequelize, ProjectionAlias, Utils, SaveOptions,
} from 'sequelize';
import Database from './database';
import { HasOne, HasMany, BelongsTo, BelongsToMany, getDataTypeKey } from './fields';
import { toInclude } from './utils';
export interface ApiJsonOptions {
/**
*
*
*
* ['col', 'association.col1', 'association_count'],
*
*
* {
* only: ['col1'],
* appends: ['association_count'],
* }
*
*
* {
* except: ['col1'],
* appends: ['association_count'],
* }
*/
fields?: string[] | {
only?: string[];
appends?: string[];
} | {
except?: string[];
appends?: string[];
};
/**
*
*
*
* {
* col1: {
* $eq: 'val1'
* },
* }
*
* scope scope col scope
* {
* scope1: value
* }
*
* json &
* {
* 'association.col1': {
* $eq: 'val1'
* },
* }
*
* meta json
* {
* 'meta.key': {
* $eq: 'val1'
* },
* }
*
* json &
* {
* association: {
* col1: {
* $eq: 'val1'
* },
* },
* }
*/
filter?: any;
/**
*
*
* TODO
*
* ['col1', '-col2', 'association.col1', '-association.col2']
*/
sort?: any;
/**
*
*/
page?: number;
perPage?: number;
2020-10-24 07:34:43 +00:00
context?: any;
[key: string]: any;
}
export interface WithCountAttributeOptions {
/**
*
*/
association: string;
/**
* SourceModel
*
* include 使 include association
*
* include: {
* association: 'user', // Post.belongsTo(User)
* attributes: [
* User.withCountAttribute({
* association: 'posts',
* sourceAlias: 'user', // 内嵌时,需要指定 source 别名
* })
* ]
* }
*/
sourceAlias?: string;
where?: any;
/**
* association_count
*/
alias?: string;
[key: string]: any;
}
export const DEFAULT_OFFSET = 0;
export const DEFAULT_LIMIT = 100;
export const MAX_LIMIT = 500;
2020-10-24 07:34:43 +00:00
/**
* Model
*
* TODO: 自定义 model
*/
// @ts-ignore
export abstract class Model extends SequelizeModel {
/**
* ts
*/
[key: string]: any;
/**
* Model database
*
* Model.sequelize database public static readonly
*/
public static database: Database;
/**
* model 访 database
*/
get database(): Database {
// @ts-ignore
return this.constructor.database;
}
/**
* sub query
*
* TODO: 关联字段暂不支持主键以外的字段
*
* @param options
*/
static withCountAttribute(options?: string | WithCountAttributeOptions): (string | ProjectionAlias) {
if (typeof options === 'string') {
options = { association: options };
}
const { sourceAlias, association, where = {}, alias, ...restOptions } = options;
const associator = this.associations[association];
const table = this.database.getTable(this.name);
const field = table.getField(association);
const { targetKey, otherKey, foreignKey, sourceKey } = field.options as any;
if (associator.associationType === 'HasMany') {
where[foreignKey as string] = {
[Op.eq]: Sequelize.col(`${sourceAlias||this.name}.${sourceKey}`),
};
} else if (associator.associationType === 'BelongsToMany') {
where[targetKey] = {
// @ts-ignore
[Op.in]: Sequelize.literal(`(${associator.through.model.selectQuery({
attributes: [otherKey],
where: {
[foreignKey]: {
[Op.eq]: Sequelize.col(`${sourceAlias||this.name}.${sourceKey}`),
},
// @ts-ignore
...(associator.through.scope||{}),
},
})})`),
};
}
let countLiteral = 'count(*)';
if (this.database.sequelize.getDialect() === 'postgres') {
countLiteral = 'cast(count(*) as integer)';
}
const attribute = [
Sequelize.literal(
// @ts-ignore
`(${associator.target.selectQuery({
...restOptions,
attributes: [[Sequelize.literal(countLiteral), 'count']],
where: {
// @ts-ignore
...where, ...(associator.scope||{}),
},
})})`
),
alias || Utils.underscoredIf(`${association}Count`, this.options.underscored),
].filter(Boolean);
return attribute as ProjectionAlias;
}
/**
* Model SQL
*
* @param options
*/
static selectQuery(options = {}): string {
// @ts-ignore
return this.queryGenerator.selectQuery(
this.getTableName(),
options,
this,
).replace(/;$/, '');
}
static parseApiJson(options: ApiJsonOptions) {
const { fields, filter, sort, context, page, perPage } = options;
2020-10-24 07:34:43 +00:00
const data = toInclude({fields, filter, sort}, {
model: this,
2020-10-24 07:34:43 +00:00
associations: this.associations,
dialect: this.sequelize.getDialect(),
ctx: context,
});
if (page || perPage) {
data.limit = perPage === -1 ? MAX_LIMIT : Math.min(perPage || DEFAULT_LIMIT, MAX_LIMIT);
data.offset = data.limit * (page > 0 ? page - 1 : DEFAULT_OFFSET);
}
2020-10-24 07:34:43 +00:00
if (data.attributes && data.attributes.length === 0) {
delete data.attributes;
}
return data;
}
async updateSingleAssociation(key: string, data: any, options: SaveOptions<any> & { context?: any; } = {}) {
const {
fields,
transaction = await this.sequelize.transaction(),
...opts
} = options;
Object.assign(opts, { transaction });
const table = this.database.getTable(this.constructor.name);
const association = table.getAssociations().get(key);
const accessors = association.getAccessors();
if (typeof data === 'number' || typeof data === 'string' || data instanceof SequelizeModel) {
await this[accessors.set](data, opts);
} else if (typeof data === 'object') {
const Target = association.getTargetModel();
const targetAttribute = association instanceof BelongsTo
? association.options.targetKey
: association.options.sourceKey;
if (data[targetAttribute]) {
await this[accessors.set](data[targetAttribute], opts);
if (Object.keys(data).length > 1) {
const target = await Target.findOne({
where: {
[targetAttribute]: data[targetAttribute],
},
transaction
});
await target.update(data, opts);
// @ts-ignore
await target.updateAssociations(data, opts);
}
} else {
const t = await this[accessors.create](data, opts);
await t.updateAssociations(data, opts);
}
}
if (!options.transaction) {
await transaction.commit();
}
}
async updateMultipleAssociation(associationName: string, data: any, options: SaveOptions<any> & { context?: any; } = {}) {
const items = Array.isArray(data) ? data : [data];
if (!items.length) {
return;
}
const {
fields,
transaction = await this.sequelize.transaction(),
...opts
} = options;
Object.assign(opts, { transaction });
const table = this.database.getTable(this.constructor.name);
const association = table.getAssociations().get(associationName);
const accessors = association.getAccessors();
const Target = association.getTargetModel();
// 当前表关联 target 表的外键(大部分情况与 target 表主键相同,但可以设置为不同的,要考虑)
const { targetKey = Target.primaryKeyAttribute } = association.options;
// target 表的主键
const targetPk = Target.primaryKeyAttribute;
const targetKeyIsPk = targetKey === targetPk;
// 准备设置的关联主键
const toSetPks = new Set();
const toSetUks = new Set();
// 筛选后准备设置的关联主键
const toSetItems = new Set();
// 准备添加的关联对象
const toUpsertObjects = [];
// 遍历所有值成员准备数据
items.forEach(item => {
if (item instanceof SequelizeModel) {
if (targetKeyIsPk) {
toSetPks.add(item.getDataValue(targetPk));
} else {
toSetUks.add(item.getDataValue(targetKey));
}
return;
}
if (typeof item === 'number' || typeof item === 'string') {
let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
if (targetKeyType === 'integer') {
targetKeyType = 'number';
}
// 如果传值类型与之前在 Model 上定义的 targetKey 不同,则报错。
// 不应兼容定义的 targetKey 不是 primaryKey 却传了 primaryKey 的值的情况。
if (typeof item !== targetKeyType) {
throw new Error(`target key type [${typeof item}] does not match to [${targetKeyType}]`);
}
if (targetKeyIsPk) {
toSetPks.add(item);
} else {
toSetUks.add(item);
}
return;
}
if (typeof item === 'object') {
toUpsertObjects.push(item);
}
});
/* 仅传关联键处理开始 */
// 查找已存在的数据
const byPkExistItems = toSetPks.size ? await Target.findAll({
...opts,
// @ts-ignore
where: {
[targetPk]: {
[Op.in]: Array.from(toSetPks)
}
},
attributes: [targetPk]
}) : [];
byPkExistItems.forEach(item => {
toSetItems.add(item);
});
const byUkExistItems = toSetUks.size ? await Target.findAll({
...opts,
// @ts-ignore
where: {
[targetKey]: {
[Op.in]: Array.from(toSetUks)
}
},
attributes: [targetPk, targetKey]
}) : [];
byUkExistItems.forEach(item => {
toSetItems.add(item);
});
/* 仅传关联键处理结束 */
const belongsToManyList = [];
/* 值为对象处理开始 */
for (const item of toUpsertObjects) {
let target;
if (typeof item[targetKey] === 'undefined') {
// TODO(optimize): 不确定 bulkCreate 的结果是否能保证顺序,能保证的话这里可以优化为批量处理
target = await Target.create(item, opts);
} else {
let created: boolean;
[target, created] = await Target.findOrCreate({
where: { [targetKey]: item[targetKey] },
defaults: item,
transaction
});
if (!created) {
await target.update(item, opts);
}
}
if (association instanceof BelongsToMany) {
belongsToManyList.push({
item,
target
});
}
toSetItems.add(target);
await target.updateAssociations(item, opts);
}
/* 值为对象处理结束 */
// 添加所有计算后的关联
await this[accessors.set](Array.from(toSetItems), opts);
// 后处理 belongsToMany 的更新内容
if (belongsToManyList.length) {
const ThroughModel = (association as BelongsToMany).getThroughModel();
const throughName = (association as BelongsToMany).getThroughName();
for (const { item, target } of belongsToManyList) {
const throughValues = item[throughName];
if (typeof throughValues === 'object') {
const { foreignKey, sourceKey, otherKey } = association.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: this.get(sourceKey),
[otherKey]: target.get(targetKey),
},
transaction
});
await through.update(throughValues, opts);
await through.updateAssociations(throughValues, opts);
}
}
}
if (!options.transaction) {
await transaction.commit();
}
}
async updateAssociation(key: string, data: any, options: SaveOptions<any> & { context?: any; }) {
const table = this.database.getTable(this.constructor.name);
const association = table.getAssociations().get(key);
switch (true) {
case association instanceof BelongsTo:
case association instanceof HasOne:
return this.updateSingleAssociation(key, data, options);
case association instanceof HasMany:
case association instanceof BelongsToMany:
return this.updateMultipleAssociation(key, data, options);
}
}
2020-10-24 07:34:43 +00:00
/**
*
*
* TODO: 暂不支持除主键以外关联字段的更新
*
* @param data
*/
async updateAssociations(data: any, options: SaveOptions & { context?: any } = {}) {
const { transaction = await this.sequelize.transaction() } = options;
const table = this.database.getTable(this.constructor.name);
for (const key of table.getAssociations().keys()) {
2020-10-24 07:34:43 +00:00
if (!data[key]) {
continue;
}
await this.updateAssociation(key, data[key], {
...options,
transaction
});
}
if (!options.transaction) {
await transaction.commit();
2020-10-24 07:34:43 +00:00
}
}
}
/**
* ModelCtor Model
*/
export type ModelCtor<M extends Model> = typeof Model & { new(): M } & { [key: string]: any };
export default Model;