nocobase/packages/core/database/src/model.ts
ChengLei Shao 4f87de7da5
feat: database view collection (#1587)
* test: create view collection

* feat: view collection class

* feat: list view

* chore: skip sync view collection

* test: should create view collection in difference schema

* test: create view collection in collection manager

* feat: create view collection by user sql

* test: view resourcer

* feat: view collection

* feat: view collection cannot be added, deleted, or modified

* feat: view collection cannot be added, deleted, or modified

* feat: view collection cannot be added, deleted, or modified

* feat: view collection cannot be added, deleted, or modified

* refactor: connect to database view

* refactor: sync from database

* chore: rename list view sql

* chore: list view fields api

* chore: create collection without viewName

* feat: bring out fields when selecting a view

* chore: bring out fields when selecting a view

* feat: view field inference class

* chore: bring out fields when selecting a view

* chore: sync form database view

* chore: sync form database view

* refactor: view collection local

* feat: view get api

* feat: database type infer

* feat: integer map

* chore: remove from in view list

* chore: build error

* chore: uniq collection

* fix: typo

* chore: replace collection list source field

* fix: destroy view collection

* chore: timestamp field map

* refactor: interface avalableTypes

* refactor: interface avalableTypes

* chore: list fields test

* refactor: interface avalableTypes

* chore: uiSchema response in field source

* fix: view query

* chore: collection snippet

* refactor: view collection support preview

* fix: handle field source

* fix: typo

* fix: configure fileds title

* fix: configure fileds title

* fix: configure fileds title

* fix: sync from databse interface

* fix: sync from databse interface

* feat: set fields api

* fix: sync from databse fix

* feat: possibleTypes

* chore: fields get

* fix: sync from databse

* fix: list view test

* fix: view test in difference schema

* chore: comment

* feat: when there is only one source  collection, the view is a subset of a Collection

* feat: view collection add field

* fix: inherit query with schema

* fix: test

* fix: ci test

* fix: test with schema

* chore: set pg default search path

* chore: mysql test

* fix: test with schema

* chore: test

* chore: action test

* chore: view column usage return type

* feat: mysql field inference

* fix: tableName

* chore: node sql parser

* fix: sql build

* fix: database build

* fix: mysql test

* feat: view collection uiSchema title

* fix: incorrect field source display  when switching views

* refactor: view collection not allow modify

* fix: view collection is allow add, delete, and modify

* fix: mysql test

* fix: sqlite test

* fix: sqlite test

* fix: sqlite test

* fix: sqlite test

* chore: add id field as default target key

* style: style improve

* feat: load source field options

* style: style improve

* chore: disable remove column in view collection

* chore: support creating view collection with different schemas with the same name

* chore: support creating view collection with different schemas with the same name

* fix: query view in difference schema

* refactor: view collection viewname

* fix: query view collection in difference schema

* fix: field load

* chore: field options

* fix: mysql test

* fix: uiSchema component error when using a view field in a block

* fix: sqlite test

* chore: test

* fix: dump user views

* fix: view collection can be updated and edited in table block

* chore: sync from database display last field configuration

* chore: loadCollections

* chore: sync from database display last field configuration

* fix: field options merge issues

* style: preview table

* fix: view collection is allow using in kanban blocks

* refactor: code improve

* fix: view collection can be updated an edited in calendar block

* chore: disable infer field without interface

* feat: preview only shows source or interface fields

* fix: test

* refactor: locale

* feat: sql parser

* chore: remove node-sql-parser

* fix: yarn.lock

* test: view repository

* fix: view repository test

* chore: console.log

* chore: console.log

* fix: mysql without schema

* fix: mysql without schema

* chore: preview with field schema

* chore: tableActionInitializers

* style: preview style improve

* chore:  parameter is filter when there is no filterByTk

* fix: preview pagination

* fix: preview pagination

* style: preview table style improve

* fix: sync from database loading

* chore: preview performance optimization

* chore: preview performance optimization

* feat: limit & offset

* chore: preview performance optimization

* test: field with dot column

* fix: datetime interface display

* fix: missing boolean type

* fix: sync

* fix: sync from database

* style: style improve

* style: style improve

* style: style improve

* chore: preview table

* chore: preview table

* chore: preview table

* fix: styling

---------

Co-authored-by: katherinehhh <katherine_15995@163.com>
Co-authored-by: chenos <chenlinxh@gmail.com>
2023-04-01 21:56:01 +08:00

203 lines
5.7 KiB
TypeScript

import lodash from 'lodash';
import { Model as SequelizeModel, ModelStatic } from 'sequelize';
import { Collection } from './collection';
import { Database } from './database';
import { Field } from './fields';
import { SyncRunner } from './sync-runner';
const _ = lodash;
interface IModel {
[key: string]: any;
}
interface JSONTransformerOptions {
model: ModelStatic<any>;
collection: Collection;
db: Database;
key?: string;
field?: Field;
}
export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
extends SequelizeModel<TModelAttributes, TCreationAttributes>
implements IModel
{
public static database: Database;
public static collection: Collection;
[key: string]: any;
protected _changedWithAssociations = new Set();
protected _previousDataValuesWithAssociations = {};
// TODO
public toChangedWithAssociations() {
// @ts-ignore
this._changedWithAssociations = new Set([...this._changedWithAssociations, ...this._changed]);
// @ts-ignore
this._previousDataValuesWithAssociations = this._previousDataValues;
}
public changedWithAssociations(key?: string, value?: any) {
if (key === undefined) {
if (this._changedWithAssociations.size > 0) {
return Array.from(this._changedWithAssociations);
}
return false;
}
if (value === true) {
this._changedWithAssociations.add(key);
return this;
}
if (value === false) {
this._changedWithAssociations.delete(key);
return this;
}
return this._changedWithAssociations.has(key);
}
public clearChangedWithAssociations() {
this._changedWithAssociations = new Set();
}
public toJSON<T extends TModelAttributes>(): T {
const handleObj = (obj, options: JSONTransformerOptions) => {
const handles = [
(data) => {
if (data instanceof Model) {
return data.toJSON();
}
return data;
},
this.hiddenObjKey,
];
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), obj);
};
const handleArray = (arrayOfObj, options: JSONTransformerOptions) => {
const handles = [this.sortAssociations];
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), arrayOfObj || []);
};
const opts = {
model: this.constructor as ModelStatic<any>,
collection: (this.constructor as any).collection,
db: (this.constructor as any).database as Database,
};
const traverseJSON = (data: T, options: JSONTransformerOptions): T => {
const { model, db, collection } = options;
// handle Object
data = handleObj(data, options);
const result = {};
for (const key of Object.keys(data)) {
// @ts-ignore
if (model.hasAlias(key)) {
const association = model.associations[key];
const opts = {
model: association.target,
collection: db.getCollection(association.target.name),
db,
key,
field: collection.getField(key),
};
if (['HasMany', 'BelongsToMany'].includes(association.associationType)) {
result[key] = handleArray(data[key], opts).map((item) => traverseJSON(item, opts));
} else {
result[key] = data[key] ? traverseJSON(data[key], opts) : null;
}
} else {
result[key] = data[key];
}
}
return result as T;
};
return traverseJSON(super.toJSON(), opts);
}
private hiddenObjKey(obj, options: JSONTransformerOptions) {
const hiddenFields = Array.from(options.collection.fields.values())
.filter((field) => field.options.hidden)
.map((field) => field.options.name);
return lodash.omit(obj, hiddenFields);
}
private sortAssociations(data, { field }: JSONTransformerOptions): any {
const sortBy = field.options.sortBy;
return sortBy ? this.sortArray(data, sortBy) : data;
}
private sortArray(data, sortBy: string | string[]) {
if (!lodash.isArray(sortBy)) {
sortBy = [sortBy];
}
const orderItems = [];
const orderDirections = [];
sortBy.forEach((sortItem) => {
orderDirections.push(sortItem.startsWith('-') ? 'desc' : 'asc');
orderItems.push(sortItem.replace('-', ''));
});
return lodash.orderBy(data, orderItems, orderDirections);
}
static async sync(options) {
if (this.collection.isView()) {
return;
}
const model = this as any;
const _schema = model._schema;
if (_schema && _schema != 'public') {
await this.sequelize.query(`CREATE SCHEMA IF NOT EXISTS "${_schema}";`, {
raw: true,
transaction: options?.transaction,
});
}
// fix sequelize sync with model that not have any column
if (Object.keys(model.tableAttributes).length === 0) {
if (this.database.inDialect('sqlite', 'mysql')) {
console.error(`Zero-column tables aren't supported in ${this.database.sequelize.getDialect()}`);
return;
}
// @ts-ignore
const queryInterface = this.sequelize.queryInterface;
if (!queryInterface.patched) {
const oldDescribeTable = queryInterface.describeTable;
queryInterface.describeTable = async function (...args) {
try {
return await oldDescribeTable.call(this, ...args);
} catch (err) {
if (err.message.includes('No description found for')) {
return [];
} else {
throw err;
}
}
};
queryInterface.patched = true;
}
}
if (this.collection.isInherited()) {
return SyncRunner.syncInheritModel(model, options);
}
return SequelizeModel.sync.call(this, options);
}
}