nocobase/packages/database/src/model.ts

433 lines
12 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;
}
/**
*
*
* TODO: 暂不支持除主键以外关联字段的更新
*
* @param data
*/
async updateAssociations(data: any, options?: SaveOptions & { context?: any }) {
const model = this;
const name = this.constructor.name;
const table = this.database.getTable(name);
for (const [key, association] of table.getAssociations()) {
if (!data[key]) {
continue;
}
let item = data[key];
const accessors = association.getAccessors();
if (association instanceof BelongsTo || association instanceof HasOne) {
if (typeof item === 'number' || typeof item === 'string') {
await model[accessors.set](item, options);
continue;
}
if (item instanceof SequelizeModel) {
await model[accessors.set](item, options);
continue;
}
if (typeof item !== 'object') {
continue;
}
const Target = association.getTargetModel();
const targetAttribute = association instanceof BelongsTo
? association.options.targetKey
: association.options.sourceKey;
if (item[targetAttribute]) {
await model[accessors.set](item[targetAttribute], options);
if (Object.keys(item).length > 1) {
const target = await Target.findOne({
where: {
[targetAttribute]: item[targetAttribute],
},
});
await target.update(item, options);
// @ts-ignore
await target.updateAssociations(item, options);
}
continue;
}
const t = await model[accessors.create](item, options);
await t.updateAssociations(item, options);
}
if (association instanceof HasMany || association instanceof BelongsToMany) {
if (!Array.isArray(item)) {
item = [item];
}
if (item.length === 0) {
continue;
}
await model[accessors.set](null, options);
const Target = association.getTargetModel();
await Promise.all(item.map(async value => {
let target: SequelizeModel;
let targetKey: string;
// 支持 number 和 string 类型的字段作为关联字段
if (typeof value === 'number' || typeof value === 'string') {
targetKey = (association instanceof BelongsToMany ? association.options.targetKey : Target.primaryKeyAttribute) as string;
let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
if (targetKeyType === 'integer') {
targetKeyType = 'number';
}
let primaryKeyType = getDataTypeKey(Target.rawAttributes[Target.primaryKeyAttribute].type).toLocaleLowerCase();
if (primaryKeyType === 'integer') {
primaryKeyType = 'number';
}
if (typeof value === targetKeyType) {
target = await Target.findOne({
where: {
[targetKey] : value,
},
});
}
if (Target.primaryKeyAttribute !== targetKey && !target && typeof value === primaryKeyType) {
target = await Target.findOne({
where: {
[Target.primaryKeyAttribute] : value,
},
});
}
if (!target) {
console.log(targetKey);
throw new Error(`target [${value}] does not exist`);
}
return await model[accessors.add](target, options);
}
if (value instanceof SequelizeModel) {
if (association instanceof HasMany) {
return await model[accessors.add](value.getDataValue(Target.primaryKeyAttribute), options);
}
return await model[accessors.add](value, options);
}
if (typeof value !== 'object') {
return;
}
targetKey = association.options.targetKey as string;
// 如果有主键,直接查询主键
if (value[Target.primaryKeyAttribute]) {
target = await Target.findOne({
where: {
[Target.primaryKeyAttribute]: value[Target.primaryKeyAttribute],
},
});
}
// 如果主键和关系字段配置的不一样
else if (Target.primaryKeyAttribute !== targetKey && value[targetKey]) {
target = await Target.findOne({
where: {
[targetKey]: value[targetKey],
},
});
}
if (target) {
await model[accessors.add](target, options);
if (Object.keys(value).length > 1) {
await target.update(value, options);
// @ts-ignore
await target.updateAssociations(value, options);
}
if (association instanceof BelongsToMany) {
const ThroughModel = association.getThroughModel();
const throughName = association.getThroughName();
if (typeof value[throughName] === 'object') {
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: this.get(sourceKey),
[otherKey]: target.get(targetKey),
},
});
const throughValues = value[throughName];
await through.update(throughValues);
await through.updateAssociations(throughValues);
}
}
return;
}
const t = await model[accessors.create](value, options);
// console.log(t);
await model[accessors.add](t, options);
await t.updateAssociations(value, options);
if (association instanceof BelongsToMany) {
const ThroughModel = association.getThroughModel();
const throughName = association.getThroughName();
if (typeof value[throughName] === 'object') {
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: this.get(sourceKey),
[otherKey]: t.get(targetKey),
},
});
const throughValues = value[throughName];
await through.update(throughValues);
await through.updateAssociations(throughValues);
}
}
return;
}));
}
}
}
}
/**
* ModelCtor Model
*/
export type ModelCtor<M extends Model> = typeof Model & { new(): M } & { [key: string]: any };
export default Model;