/** * This file is part of the NocoBase (R) project. * Copyright (c) 2020-2024 NocoBase Co., Ltd. * Authors: NocoBase Team. * * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. * For more information, please refer to: https://www.nocobase.com/agreement. */ import _ from 'lodash'; import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions, SyncOptions, Transactionable } from 'sequelize'; import { Collection } from '../collection'; import { Database } from '../database'; import { ModelEventTypes } from '../types'; import { snakeCase } from '../utils'; export interface FieldContext { database: Database; collection: Collection; } export interface BaseFieldOptions { name?: string; hidden?: boolean; translation?: boolean; [key: string]: any; } export interface BaseColumnFieldOptions extends BaseFieldOptions, Omit { dataType?: DataType; index?: boolean | ModelIndexesOptions; } export abstract class Field { options: any; context: FieldContext; database: Database; collection: Collection; [key: string]: any; constructor(options?: any, context?: FieldContext) { this.context = context as any; this.database = this.context.database; this.collection = this.context.collection; this.options = options || {}; this.init(); } get name() { return this.options.name; } get type() { return this.options.type; } abstract get dataType(): any; isRelationField() { return false; } async sync(syncOptions: SyncOptions) { await this.collection.sync({ ...syncOptions, force: false, alter: { drop: false, }, }); } init() { // code } on(eventName: ModelEventTypes, listener: (...args: any[]) => void) { this.database.on(`${this.collection.name}.${eventName}`, listener); return this; } off(eventName: string, listener: (...args: any[]) => void) { this.database.off(`${this.collection.name}.${eventName}`, listener); return this; } get(name: string) { return this.options[name]; } remove() { this.collection.removeIndex([this.name]); return this.collection.removeField(this.name); } columnName() { if (this.options.field) { return this.options.field; } if (this.database.options.underscored) { return snakeCase(this.name); } return this.name; } async existsInDb(options?: Transactionable) { const opts = { transaction: options?.transaction, }; let sql; if (this.database.sequelize.getDialect() === 'sqlite') { sql = `SELECT * from pragma_table_info('${this.collection.model.tableName}') WHERE name = '${this.columnName()}'`; } else if (this.database.inDialect('mysql', 'mariadb')) { sql = ` select column_name from INFORMATION_SCHEMA.COLUMNS where TABLE_SCHEMA = '${this.database.options.database}' AND TABLE_NAME = '${this.collection.model.tableName}' AND column_name = '${this.columnName()}' `; } else { sql = ` select column_name from INFORMATION_SCHEMA.COLUMNS where TABLE_NAME = '${this.collection.model.tableName}' AND column_name = '${this.columnName()}' AND table_schema = '${this.collection.collectionSchema() || 'public'}' `; } const [rows] = await this.database.sequelize.query(sql, opts); return rows.length > 0; } merge(obj: any) { Object.assign(this.options, obj); } bind() { const { model } = this.context.collection; model.rawAttributes[this.name] = this.toSequelize(); // @ts-ignore model.refreshAttributes(); if (this.options.index) { this.context.collection.addIndex([this.name]); } } unbind() { const { model } = this.context.collection; delete model.prototype[this.name]; model.removeAttribute(this.name); if (this.options.index || this.options.unique) { this.context.collection.removeIndex([this.name]); } } toSequelize(): any { const opts = _.omit(this.options, ['name']); if (this.dataType) { // @ts-ignore Object.assign(opts, { type: this.database.sequelize.normalizeDataType(this.dataType) }); } Object.assign(opts, this.additionalSequelizeOptions()); return opts; } additionalSequelizeOptions() { return {}; } typeToString() { return this.dataType.toString(); } }