* refactor: remove useless code * feat: should always show user variable * style: optimize style of variable input * feat: change maxDepth of user varialbe to 3 * refactor: migrate getFieldDefaultValue to SchemaSettings.tsx * feat: support to parse variables * feat: support to parse array fields * refactor: make testing more stable * chore: fix CI * feat: support to parse current user * fix: fix tests * feat: support to register variable * feat: support to parse "$context" * refactor: remove useless code * fix: fix build * fix: fix build * test: make stable * docs: add doc * fix: fix T-1267 * fix: fix T-1268 * fix: fix T-1269 * fix: fix T-1271 * fix: fix T-1274 * refactor: improve code * feat(variable): support local variable * fix: fix T-1280 * refactor: close T-881 * refactor: remove useLazyLoadAssociation * fix: fix T-1289,T-1290 * fix: fix T-1296 * refactor(AssignedField): replace user variable * refactor: extract useBaseVariable * refactor(AssignedField): replace variables * feat(useVariables): support to use array as local variables * refactor: improve variables (T-1295) * feat: limit user choice (T-1297) * fix: fix T-1305 * refactor: extract as SchemaSettings.DataScope * refactor: improve linkage rules * refactor: improve linkage rules of Action * chore: fix build * refactor: improve data scope of association fields * chore: fix build * feat: select limit * refactor: use VariableInput to replace other * fix: fix can not input * fix(AssignedField): fix cannot close variables list * test: fix errors * refactor: migrate to new file * fix: uniq * fix: filter empty values * fix: transform variables value * fix: should sum numbers (T-1330) * fix: fix Select (T-1337) * refactor: use useMemo * refactor: use hooks * refactor: use hooks * refactor: use hooks * refactor: remove useless code * refactor: rename `$form` to `$nForm` and `$record` to `$nRecord` * fix: fix T-1334 * refactor(LinkageRuleActionGroup): use hooks * refactor(FormButtonLinkageRuleAction): use hooks * refactor: add type * fix: avoid crashing * refactor: rename `$date` to `$nDate` * fix: fix T-1348 * perf: avoid duplicate requests * test: fix error * fix: fix T-1355 * fix: fix T-1355 * fix: fix T-1345 * fix: fix T-1346 * refactor(useVariableOptions): replace query with parameter * fix: fix T-1335 * chore: fix build * fix: fix T-1453 * refactor: remove useless code * fix: fix T-1447 * fix: should update when seting default value * fix: fix T-1454 * chore(eslint): ignore docker * fix: fix T-1440 * refactor: use hooks * fix: fix T-1332 * fix(defaultValue): should use Select mode * fix(defaultValue): fix Checkbox * refactor: rename and comment * refactor: rename file name and hook name * feat(subForm): support lazy load association fields * fix: filter empty values * fix(defaultValue): schema type of Checkbox should be void * fix: fix T-1479 * fix: fix T-1491 * fix(ToOneNester): should use RecordProvider * fix: fix T-1493 * chore: add comment * fix: fix T-1495 * chore: fix build * test: make pass * fix: fix T-1503 * fix: fix T-1504 * fix: fix transformVariableValue * fix: disable unsupport options * refactor(useVariableOptions): add noDisabled params * fix: data scope support to select to-many fields * chore: add comment * refactor: improve default value * refactor: extract as useIsAllowToSetDefaultValue * fix: should not support to set default value in edit mode * fix: only the default values for sub-forms and sub-tables are valid * fix: limit default value of association fields * fix: fix '$nForm' variable * fix: allow all inherit chain * fix: optimize default value * fix: optimize json field * fix(currentObject): avoid error * fix: should not show variable string * fix: active fields * fix: avoid errors * feat: support special case * fix: fix CI * chore: fix build * fix: json * refactor: rename flag name * fix: fix T-1559 * fix: fix T-1515 * fix: avoid crash * fix: fix T-1520 * fix: fix T-1521 * fix: fix T-1523 * fix: fix T-1563 * fix: fix T-1565 * fix: fix T-1525 * fix: fix T-1524 * fix: fix T-1603 * fix: fix T-1332 * fix: fix T-1559 * fix: fix T-1602 * fix: fix T-1608 * fix: fix T-1612 T-1609 * fix: fix T-1681 * fix: fix T-1711 * fix: fix T-1713 * fix: fix T-1680 * chore: close T-1550 * fix: fix T-1507 * chore: avoid crashing * fix: fix T-1557 * perf: close T-1726 * fix: fix T-1727 * fix: fix T-1741 * fix: fix T-1740 * chore(workflow): compact * chore: should not to support to set default value in assign field values * perf: compat and perf (T-1736) * fix: fix T-1872 * fix: fix T-1877 * fix: fix quarter variables * chore: add README * chore: front-end should not depend on back-end code * chore: should disable all variable options if no field is selected * chore: should be a leaf if it has no children * chore: fix build error * fix: compat $form * fix: compat $form * chore: fix build * chore: fix build * fix: typeError: Cannot read properties of undefined (reading 'template') * chore: fix test * chore: add test * fix: fix T-2072 * chore: optimize eslint * perf: reduce number of requests * fix: fix JSON * test: fix error * fix(e2e): avoid error --------- Co-authored-by: chenos <chenlinxh@gmail.com>
		
			
				
	
	
		
			727 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			727 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import merge from 'deepmerge';
 | |
| import { EventEmitter } from 'events';
 | |
| import { default as _, default as lodash } from 'lodash';
 | |
| import {
 | |
|   ModelOptions,
 | |
|   ModelStatic,
 | |
|   QueryInterfaceDropTableOptions,
 | |
|   SyncOptions,
 | |
|   Transactionable,
 | |
|   Utils,
 | |
| } from 'sequelize';
 | |
| import { Database } from './database';
 | |
| import { BelongsToField, Field, FieldOptions, HasManyField } from './fields';
 | |
| import { Model } from './model';
 | |
| import { AdjacencyListRepository } from './repositories/tree-repository/adjacency-list-repository';
 | |
| import { Repository } from './repository';
 | |
| import { checkIdentifier, md5, snakeCase } from './utils';
 | |
| 
 | |
| export type RepositoryType = typeof Repository;
 | |
| 
 | |
| export type CollectionSortable = string | boolean | { name?: string; scopeKey?: string };
 | |
| 
 | |
| type dumpable = 'required' | 'optional' | 'skip';
 | |
| 
 | |
| function EnsureAtomicity(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 | |
|   const originalMethod = descriptor.value;
 | |
| 
 | |
|   descriptor.value = function (...args: any[]) {
 | |
|     const model = this.model;
 | |
|     const beforeAssociationKeys = Object.keys(model.associations);
 | |
|     const beforeRawAttributes = Object.keys(model.rawAttributes);
 | |
| 
 | |
|     try {
 | |
|       return originalMethod.apply(this, args);
 | |
|     } catch (error) {
 | |
|       // remove associations created in this method
 | |
|       const afterAssociationKeys = Object.keys(model.associations);
 | |
|       const createdAssociationKeys = lodash.difference(afterAssociationKeys, beforeAssociationKeys);
 | |
|       for (const key of createdAssociationKeys) {
 | |
|         delete this.model.associations[key];
 | |
|       }
 | |
| 
 | |
|       const afterRawAttributes = Object.keys(model.rawAttributes);
 | |
|       const createdRawAttributes = lodash.difference(afterRawAttributes, beforeRawAttributes);
 | |
|       console.log({
 | |
|         beforeRawAttributes,
 | |
|         afterRawAttributes,
 | |
|       });
 | |
|       for (const key of createdRawAttributes) {
 | |
|         delete this.model.rawAttributes[key];
 | |
|       }
 | |
|       throw error;
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return descriptor;
 | |
| }
 | |
| 
 | |
| export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
 | |
|   name: string;
 | |
|   title?: string;
 | |
|   namespace?: string;
 | |
|   /**
 | |
|    * Used for @nocobase/plugin-duplicator
 | |
|    * @see packages/core/database/src/collection-group-manager.tss
 | |
|    *
 | |
|    * @prop {'required' | 'optional' | 'skip'} dumpable - Determine whether the collection is dumped
 | |
|    * @prop {string[] | string} [with] - Collections dumped with this collection
 | |
|    * @prop {any} [delayRestore] - A function to execute after all collections are restored
 | |
|    */
 | |
|   duplicator?:
 | |
|     | dumpable
 | |
|     | {
 | |
|         dumpable: dumpable;
 | |
|         with?: string[] | string;
 | |
|         delayRestore?: any;
 | |
|       };
 | |
| 
 | |
|   tableName?: string;
 | |
|   inherits?: string[] | string;
 | |
|   viewName?: string;
 | |
|   writableView?: boolean;
 | |
| 
 | |
|   filterTargetKey?: string;
 | |
|   fields?: FieldOptions[];
 | |
|   model?: string | ModelStatic<Model>;
 | |
|   repository?: string | RepositoryType;
 | |
|   sortable?: CollectionSortable;
 | |
|   /**
 | |
|    * @default true
 | |
|    */
 | |
|   autoGenId?: boolean;
 | |
|   /**
 | |
|    * @default 'options'
 | |
|    */
 | |
|   magicAttribute?: string;
 | |
| 
 | |
|   tree?: string;
 | |
| 
 | |
|   template?: string;
 | |
| 
 | |
|   [key: string]: any;
 | |
| }
 | |
| 
 | |
| export interface CollectionContext {
 | |
|   database: Database;
 | |
| }
 | |
| 
 | |
| export class Collection<
 | |
|   TModelAttributes extends {} = any,
 | |
|   TCreationAttributes extends {} = TModelAttributes
 | |
| > extends EventEmitter {
 | |
|   options: CollectionOptions;
 | |
|   context: CollectionContext;
 | |
|   isThrough?: boolean;
 | |
|   fields: Map<string, any> = new Map<string, any>();
 | |
|   model: ModelStatic<Model>;
 | |
|   repository: Repository<TModelAttributes, TCreationAttributes>;
 | |
| 
 | |
|   constructor(options: CollectionOptions, context: CollectionContext) {
 | |
|     super();
 | |
|     this.context = context;
 | |
|     this.options = options;
 | |
| 
 | |
|     this.checkOptions(options);
 | |
| 
 | |
|     this.bindFieldEventListener();
 | |
|     this.modelInit();
 | |
| 
 | |
|     this.db.modelCollection.set(this.model, this);
 | |
| 
 | |
|     // set tableName to collection map
 | |
|     // the form of key is `${schema}.${tableName}` if schema exists
 | |
|     // otherwise is `${tableName}`
 | |
|     this.db.tableNameCollectionMap.set(this.getTableNameWithSchemaAsString(), this);
 | |
| 
 | |
|     if (!options.inherits) {
 | |
|       this.setFields(options.fields);
 | |
|     }
 | |
| 
 | |
|     this.setRepository(options.repository);
 | |
|     this.setSortable(options.sortable);
 | |
|   }
 | |
| 
 | |
|   get filterTargetKey() {
 | |
|     const targetKey = lodash.get(this.options, 'filterTargetKey', this.model.primaryKeyAttribute);
 | |
|     if (!targetKey && this.model.rawAttributes['id']) {
 | |
|       return 'id';
 | |
|     }
 | |
| 
 | |
|     return targetKey;
 | |
|   }
 | |
| 
 | |
|   get name() {
 | |
|     return this.options.name;
 | |
|   }
 | |
| 
 | |
|   get titleField() {
 | |
|     return (this.options.titleField as string) || this.model.primaryKeyAttribute;
 | |
|   }
 | |
| 
 | |
|   get db() {
 | |
|     return this.context.database;
 | |
|   }
 | |
| 
 | |
|   get treeParentField(): BelongsToField | null {
 | |
|     for (const [_, field] of this.fields) {
 | |
|       if (field.options.treeParent) {
 | |
|         return field;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   get treeChildrenField(): HasManyField | null {
 | |
|     for (const [_, field] of this.fields) {
 | |
|       if (field.options.treeChildren) {
 | |
|         return field;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   tableName() {
 | |
|     const { name, tableName } = this.options;
 | |
|     const tName = tableName || name;
 | |
|     return this.options.underscored ? snakeCase(tName) : tName;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * TODO
 | |
|    */
 | |
|   modelInit() {
 | |
|     if (this.model) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const { name, model, autoGenId = true } = this.options;
 | |
|     let M: ModelStatic<Model> = Model;
 | |
| 
 | |
|     if (this.context.database.sequelize.isDefined(name)) {
 | |
|       const m = this.context.database.sequelize.model(name);
 | |
|       if ((m as any).isThrough) {
 | |
|         // @ts-ignore
 | |
|         this.model = m;
 | |
|         // @ts-ignore
 | |
|         this.model.database = this.context.database;
 | |
|         // @ts-ignore
 | |
|         this.model.collection = this;
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (typeof model === 'string') {
 | |
|       M = this.context.database.models.get(model) || Model;
 | |
|     } else if (model) {
 | |
|       M = model;
 | |
|     }
 | |
| 
 | |
|     // @ts-ignore
 | |
|     this.model = class extends M {};
 | |
|     this.model.init(null, this.sequelizeModelOptions());
 | |
| 
 | |
|     if (!autoGenId) {
 | |
|       this.model.removeAttribute('id');
 | |
|     }
 | |
| 
 | |
|     // @ts-ignore
 | |
|     this.model.database = this.context.database;
 | |
|     // @ts-ignore
 | |
|     this.model.collection = this;
 | |
|   }
 | |
| 
 | |
|   setRepository(repository?: RepositoryType | string) {
 | |
|     let repo = Repository;
 | |
|     if (typeof repository === 'string') {
 | |
|       repo = this.context.database.repositories.get(repository) || Repository;
 | |
|     }
 | |
| 
 | |
|     if (this.options.tree == 'adjacency-list' || this.options.tree == 'adjacencyList') {
 | |
|       repo = AdjacencyListRepository;
 | |
|     }
 | |
| 
 | |
|     this.repository = new repo(this);
 | |
|   }
 | |
| 
 | |
|   forEachField(callback: (field: Field) => void) {
 | |
|     return [...this.fields.values()].forEach(callback);
 | |
|   }
 | |
| 
 | |
|   findField(callback: (field: Field) => boolean) {
 | |
|     return [...this.fields.values()].find(callback);
 | |
|   }
 | |
| 
 | |
|   hasField(name: string) {
 | |
|     return this.fields.has(name);
 | |
|   }
 | |
| 
 | |
|   getField<F extends Field>(name: string): F {
 | |
|     return this.fields.get(name);
 | |
|   }
 | |
| 
 | |
|   addField(name: string, options: FieldOptions): Field {
 | |
|     return this.setField(name, options);
 | |
|   }
 | |
| 
 | |
|   checkFieldType(name: string, options: FieldOptions) {
 | |
|     if (!this.options.underscored) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const fieldName = options.field || snakeCase(name);
 | |
| 
 | |
|     const field = this.findField((f) => {
 | |
|       if (f.name === name) {
 | |
|         return false;
 | |
|       }
 | |
|       if (f.field) {
 | |
|         return f.field === fieldName;
 | |
|       }
 | |
|       return snakeCase(f.name) === fieldName;
 | |
|     });
 | |
| 
 | |
|     if (!field) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (options.type !== field.type) {
 | |
|       throw new Error(`fields with same column must be of the same type ${JSON.stringify(options)}`);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   @EnsureAtomicity
 | |
|   setField(name: string, options: FieldOptions): Field {
 | |
|     checkIdentifier(name);
 | |
|     this.checkFieldType(name, options);
 | |
| 
 | |
|     const { database } = this.context;
 | |
| 
 | |
|     if (options.source) {
 | |
|       const [sourceCollectionName, sourceFieldName] = options.source.split('.');
 | |
|       const sourceCollection = this.db.collections.get(sourceCollectionName);
 | |
|       if (!sourceCollection) {
 | |
|         this.db.logger.warn(
 | |
|           `source collection "${sourceCollectionName}" not found for field "${name}" at collection "${this.name}"`,
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       const sourceField = sourceCollection.fields.get(sourceFieldName);
 | |
| 
 | |
|       if (!sourceField) {
 | |
|         this.db.logger.warn(
 | |
|           `source field "${sourceFieldName}" not found for field "${name}" at collection "${this.name}"`,
 | |
|         );
 | |
|       } else {
 | |
|         options = { ...lodash.omit(sourceField.options, 'name'), ...options };
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     this.emit('field.beforeAdd', name, options, { collection: this });
 | |
| 
 | |
|     const field = database.buildField(
 | |
|       { name, ...options },
 | |
|       {
 | |
|         ...this.context,
 | |
|         collection: this,
 | |
|       },
 | |
|     );
 | |
| 
 | |
|     const oldField = this.fields.get(name);
 | |
| 
 | |
|     if (oldField && oldField.options.inherit && field.typeToString() != oldField.typeToString()) {
 | |
|       throw new Error(
 | |
|         `Field type conflict: cannot set "${name}" on "${this.name}" to ${options.type}, parent "${name}" type is ${oldField.options.type}`,
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     if (this.options.autoGenId !== false && options.primaryKey) {
 | |
|       this.model.removeAttribute('id');
 | |
|     }
 | |
| 
 | |
|     this.removeField(name);
 | |
|     this.fields.set(name, field);
 | |
|     this.emit('field.afterAdd', field);
 | |
| 
 | |
|     // refresh children models
 | |
|     if (this.isParent()) {
 | |
|       for (const child of this.context.database.inheritanceMap.getChildren(this.name, {
 | |
|         deep: false,
 | |
|       })) {
 | |
|         const childCollection = this.db.getCollection(child);
 | |
|         const existField = childCollection.getField(name);
 | |
| 
 | |
|         if (!existField || existField.options.inherit) {
 | |
|           childCollection.setField(name, {
 | |
|             ...options,
 | |
|             inherit: true,
 | |
|           });
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return field;
 | |
|   }
 | |
| 
 | |
|   setFields(fields: FieldOptions[], resetFields = true) {
 | |
|     if (!Array.isArray(fields)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     if (resetFields) {
 | |
|       this.resetFields();
 | |
|     }
 | |
| 
 | |
|     for (const { name, ...options } of fields) {
 | |
|       this.addField(name, options);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   resetFields() {
 | |
|     const fieldNames = this.fields.keys();
 | |
|     for (const fieldName of fieldNames) {
 | |
|       this.removeField(fieldName);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   remove() {
 | |
|     this.context.database.removeCollection(this.name);
 | |
|   }
 | |
| 
 | |
|   async removeFromDb(options?: QueryInterfaceDropTableOptions) {
 | |
|     if (
 | |
|       !this.isView() &&
 | |
|       (await this.existsInDb({
 | |
|         transaction: options?.transaction,
 | |
|       }))
 | |
|     ) {
 | |
|       const queryInterface = this.db.sequelize.getQueryInterface();
 | |
|       await queryInterface.dropTable(this.getTableNameWithSchema(), options);
 | |
|     }
 | |
|     this.remove();
 | |
|   }
 | |
| 
 | |
|   async existsInDb(options?: Transactionable) {
 | |
|     return this.db.queryInterface.collectionTableExists(this, options);
 | |
|   }
 | |
| 
 | |
|   removeField(name: string): void | Field {
 | |
|     if (!this.fields.has(name)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const field = this.fields.get(name);
 | |
| 
 | |
|     const bool = this.fields.delete(name);
 | |
| 
 | |
|     if (bool) {
 | |
|       if (this.isParent()) {
 | |
|         for (const child of this.db.inheritanceMap.getChildren(this.name, {
 | |
|           deep: false,
 | |
|         })) {
 | |
|           const childCollection = this.db.getCollection(child);
 | |
|           const existField = childCollection.getField(name);
 | |
|           if (existField && existField.options.inherit) {
 | |
|             childCollection.removeField(name);
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       this.emit('field.afterRemove', field);
 | |
|     }
 | |
| 
 | |
|     return field as Field;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * TODO
 | |
|    */
 | |
|   updateOptions(options: CollectionOptions, mergeOptions?: any) {
 | |
|     let newOptions = lodash.cloneDeep(options);
 | |
|     newOptions = merge(this.options, newOptions, mergeOptions);
 | |
|     this.context.database.emit('beforeUpdateCollection', this, newOptions);
 | |
|     this.options = newOptions;
 | |
| 
 | |
|     this.setFields(options.fields, false);
 | |
|     if (options.repository) {
 | |
|       this.setRepository(options.repository);
 | |
|     }
 | |
| 
 | |
|     this.context.database.emit('afterUpdateCollection', this);
 | |
|     return this;
 | |
|   }
 | |
| 
 | |
|   setSortable(sortable) {
 | |
|     if (!sortable) {
 | |
|       return;
 | |
|     }
 | |
|     if (sortable === true) {
 | |
|       this.setField('sort', {
 | |
|         type: 'sort',
 | |
|         hidden: true,
 | |
|       });
 | |
|     }
 | |
|     if (typeof sortable === 'string') {
 | |
|       this.setField(sortable, {
 | |
|         type: 'sort',
 | |
|         hidden: true,
 | |
|       });
 | |
|     } else if (typeof sortable === 'object') {
 | |
|       const { name, ...opts } = sortable;
 | |
|       this.setField(name || 'sort', { type: 'sort', hidden: true, ...opts });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * TODO
 | |
|    *
 | |
|    * @param name
 | |
|    * @param options
 | |
|    */
 | |
|   updateField(name: string, options: FieldOptions) {
 | |
|     if (!this.hasField(name)) {
 | |
|       throw new Error(`field ${name} not exists`);
 | |
|     }
 | |
| 
 | |
|     if (options.name && options.name !== name) {
 | |
|       this.removeField(name);
 | |
|     }
 | |
| 
 | |
|     this.setField(options.name || name, options);
 | |
|   }
 | |
| 
 | |
|   addIndex(index: string | string[] | { fields: string[]; unique?: boolean; [key: string]: any }) {
 | |
|     if (!index) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // collection defined indexes
 | |
|     const indexes: any = this.model.options.indexes || [];
 | |
| 
 | |
|     let indexName = [];
 | |
|     let indexItem;
 | |
| 
 | |
|     if (typeof index === 'string') {
 | |
|       indexItem = {
 | |
|         fields: [index],
 | |
|       };
 | |
|       indexName = [index];
 | |
|     } else if (Array.isArray(index)) {
 | |
|       indexItem = {
 | |
|         fields: index,
 | |
|       };
 | |
|       indexName = index;
 | |
|     } else if (index?.fields) {
 | |
|       indexItem = index;
 | |
|       indexName = index.fields;
 | |
|     }
 | |
| 
 | |
|     if (lodash.isEqual(this.model.primaryKeyAttributes, indexName)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const name: string = this.model.primaryKeyAttributes.join(',');
 | |
| 
 | |
|     if (name.startsWith(`${indexName.join(',')},`)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (const item of indexes) {
 | |
|       if (lodash.isEqual(item.fields, indexName)) {
 | |
|         return;
 | |
|       }
 | |
|       const name: string = item.fields.join(',');
 | |
|       if (name.startsWith(`${indexName.join(',')},`)) {
 | |
|         return;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (!indexItem) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     indexes.push(indexItem);
 | |
| 
 | |
|     const tableName = this.model.getTableName();
 | |
|     // @ts-ignore
 | |
|     this.model._indexes = this.model.options.indexes
 | |
|       // @ts-ignore
 | |
|       .map((index) => Utils.nameIndex(this.model._conformIndex(index), tableName))
 | |
|       .map((item) => {
 | |
|         if (item.name && item.name.length > 63) {
 | |
|           item.name = 'i_' + md5(item.name);
 | |
|         }
 | |
|         return item;
 | |
|       });
 | |
| 
 | |
|     this.refreshIndexes();
 | |
|   }
 | |
| 
 | |
|   removeIndex(fields: any) {
 | |
|     if (!fields) {
 | |
|       return;
 | |
|     }
 | |
|     // @ts-ignore
 | |
|     const indexes: any[] = this.model._indexes;
 | |
|     // @ts-ignore
 | |
|     this.model._indexes = indexes.filter((item) => {
 | |
|       return !lodash.isEqual(item.fields, fields);
 | |
|     });
 | |
|     this.refreshIndexes();
 | |
|   }
 | |
| 
 | |
|   refreshIndexes() {
 | |
|     // @ts-ignore
 | |
|     const indexes: any[] = this.model._indexes;
 | |
| 
 | |
|     // @ts-ignore
 | |
|     this.model._indexes = lodash.uniqBy(
 | |
|       indexes
 | |
|         .filter((item) => {
 | |
|           return item.fields.every((field) =>
 | |
|             Object.values(this.model.rawAttributes).find((fieldVal) => fieldVal.field === field),
 | |
|           );
 | |
|         })
 | |
|         .map((item) => {
 | |
|           if (this.options.underscored) {
 | |
|             item.fields = item.fields.map((field) => snakeCase(field));
 | |
|           }
 | |
|           return item;
 | |
|         }),
 | |
|       'name',
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async sync(syncOptions?: SyncOptions) {
 | |
|     const modelNames = new Set([this.model.name]);
 | |
| 
 | |
|     const { associations } = this.model;
 | |
| 
 | |
|     for (const associationKey in associations) {
 | |
|       const association = associations[associationKey];
 | |
|       modelNames.add(association.target.name);
 | |
| 
 | |
|       if ((<any>association).through) {
 | |
|         modelNames.add((<any>association).through.model.name);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const models: ModelStatic<Model>[] = [];
 | |
|     // @ts-ignore
 | |
|     this.context.database.sequelize.modelManager.forEachModel((model) => {
 | |
|       if (modelNames.has(model.name)) {
 | |
|         models.push(model);
 | |
|       }
 | |
|     });
 | |
| 
 | |
|     for (const model of models) {
 | |
|       await model.sync(syncOptions);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public isInherited() {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   public isParent() {
 | |
|     return this.context.database.inheritanceMap.isParentNode(this.name);
 | |
|   }
 | |
| 
 | |
|   public getTableNameWithSchema() {
 | |
|     const tableName = this.model.tableName;
 | |
| 
 | |
|     if (this.collectionSchema() && this.db.inDialect('postgres')) {
 | |
|       return this.db.utils.addSchema(tableName, this.collectionSchema());
 | |
|     }
 | |
| 
 | |
|     return tableName;
 | |
|   }
 | |
| 
 | |
|   public tableNameAsString(options?: { ignorePublicSchema: boolean }) {
 | |
|     const tableNameWithSchema = this.getTableNameWithSchema();
 | |
|     if (lodash.isString(tableNameWithSchema)) {
 | |
|       return tableNameWithSchema;
 | |
|     }
 | |
| 
 | |
|     const schema = tableNameWithSchema.schema;
 | |
|     const tableName = tableNameWithSchema.tableName;
 | |
| 
 | |
|     if (options?.ignorePublicSchema && schema === 'public') {
 | |
|       return tableName;
 | |
|     }
 | |
| 
 | |
|     return `${schema}.${tableName}`;
 | |
|   }
 | |
| 
 | |
|   public getTableNameWithSchemaAsString() {
 | |
|     const tableName = this.model.tableName;
 | |
| 
 | |
|     if (this.collectionSchema() && this.db.inDialect('postgres')) {
 | |
|       return `${this.collectionSchema()}.${tableName}`;
 | |
|     }
 | |
| 
 | |
|     return tableName;
 | |
|   }
 | |
| 
 | |
|   public quotedTableName() {
 | |
|     return this.db.utils.quoteTable(this.getTableNameWithSchema());
 | |
|   }
 | |
| 
 | |
|   public collectionSchema() {
 | |
|     if (this.options.schema) {
 | |
|       return this.options.schema;
 | |
|     }
 | |
| 
 | |
|     if (this.db.options.schema) {
 | |
|       return this.db.options.schema;
 | |
|     }
 | |
| 
 | |
|     if (this.db.inDialect('postgres')) {
 | |
|       return 'public';
 | |
|     }
 | |
| 
 | |
|     return undefined;
 | |
|   }
 | |
| 
 | |
|   public isView() {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   protected sequelizeModelOptions() {
 | |
|     const { name } = this.options;
 | |
|     return {
 | |
|       ..._.omit(this.options, ['name', 'fields', 'model', 'targetKey']),
 | |
|       modelName: name,
 | |
|       sequelize: this.context.database.sequelize,
 | |
|       tableName: this.tableName(),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private checkOptions(options: CollectionOptions) {
 | |
|     checkIdentifier(options.name);
 | |
|     this.checkTableName();
 | |
|   }
 | |
| 
 | |
|   private checkTableName() {
 | |
|     const tableName = this.tableName();
 | |
|     for (const [k, collection] of this.db.collections) {
 | |
|       if (
 | |
|         collection.name != this.options.name &&
 | |
|         tableName === collection.tableName() &&
 | |
|         collection.collectionSchema() === this.collectionSchema()
 | |
|       ) {
 | |
|         throw new Error(`collection ${collection.name} and ${this.name} have same tableName "${tableName}"`);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private bindFieldEventListener() {
 | |
|     this.on('field.afterAdd', (field: Field) => {
 | |
|       field.bind();
 | |
|     });
 | |
| 
 | |
|     this.on('field.afterRemove', (field: Field) => {
 | |
|       field.unbind();
 | |
|       this.db.emit('field.afterRemove', field);
 | |
|     });
 | |
|   }
 | |
| }
 |