feat: register collection sync logic (#3055)

This commit is contained in:
ChengLei Shao 2023-11-17 11:22:46 +08:00 committed by GitHub
parent cd76ae7e63
commit 76a225e354
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 73 deletions

View File

@ -20,8 +20,8 @@ describe('collection factory', function () {
static type = 'child'; static type = 'child';
} }
db.collectionFactory.registerCollectionType(ChildCollection, (options) => { db.collectionFactory.registerCollectionType(ChildCollection, {
return options.child == true; condition: (options) => options.child,
}); });
const collection = db.collectionFactory.createCollection({ const collection = db.collectionFactory.createCollection({
@ -37,4 +37,27 @@ describe('collection factory', function () {
expect(collection2).toBeInstanceOf(Collection); expect(collection2).toBeInstanceOf(Collection);
}); });
it('should register collection type with sync logic', async () => {
class ChildCollection extends Collection {
static type = 'child';
}
const fn = jest.fn();
db.collectionFactory.registerCollectionType(ChildCollection, {
condition: (options) => options.child,
onSync(model, options) {
fn();
},
});
const collection = db.collectionFactory.createCollection({
name: 'child',
child: true,
});
await collection.sync();
expect(fn).toHaveBeenCalledTimes(1);
});
}); });

View File

@ -1,28 +1,38 @@
import { Collection, CollectionOptions } from './collection'; import { Collection, CollectionOptions } from './collection';
import Database from './database'; import Database from './database';
import { Model } from './model';
type CollectionTypeOptions = {
condition: (options: CollectionOptions) => boolean;
onSync?: (model: typeof Model, options: any) => Promise<void>;
};
export class CollectionFactory { export class CollectionFactory {
private collectionTypes: Array<{ // Using a Map with the collection subclass as the key and options as the value
ctor: typeof Collection; public collectionTypes: Map<typeof Collection, CollectionTypeOptions> = new Map();
condition: (options: CollectionOptions) => boolean;
}> = [];
constructor(private database: Database) {} constructor(private database: Database) {}
registerCollectionType(collectionClass: typeof Collection, condition: (options: CollectionOptions) => boolean) { registerCollectionType(
this.collectionTypes.push({ ctor: collectionClass, condition }); collectionClass: typeof Collection, // Using the collection class as the key
options: CollectionTypeOptions,
) {
// Storing the options associated with the collection class
this.collectionTypes.set(collectionClass, options);
} }
createCollection<T extends Collection>(options: CollectionOptions): T { createCollection<T extends Collection>(collectionOptions: CollectionOptions): T {
let klass = Collection; let klass: typeof Collection = Collection;
for (const { ctor, condition } of this.collectionTypes) {
if (condition(options)) { // Iterating over the map to find the right class based on the condition
for (const [ctor, options] of this.collectionTypes) {
if (options.condition(collectionOptions)) {
klass = ctor; klass = ctor;
break; break;
} }
} }
return new klass(options, { return new klass(collectionOptions, {
database: this.database, database: this.database,
}) as T; }) as T;
} }

View File

@ -313,16 +313,22 @@ export class Database extends EventEmitter implements AsyncEmitter {
} }
registerCollectionType() { registerCollectionType() {
this.collectionFactory.registerCollectionType(InheritedCollection, (options) => { this.collectionFactory.registerCollectionType(InheritedCollection, {
return options.inherits && lodash.castArray(options.inherits).length > 0; condition: (options) => {
return options.inherits && lodash.castArray(options.inherits).length > 0;
},
}); });
this.collectionFactory.registerCollectionType(ViewCollection, (options) => { this.collectionFactory.registerCollectionType(ViewCollection, {
return options.viewName || options.view; condition: (options) => {
return options.viewName || options.view;
},
}); });
this.collectionFactory.registerCollectionType(SqlCollection, (options) => { this.collectionFactory.registerCollectionType(SqlCollection, {
return options.sql; condition: (options) => {
return options.sql;
},
}); });
} }

View File

@ -31,6 +31,69 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
protected _changedWithAssociations = new Set(); protected _changedWithAssociations = new Set();
protected _previousDataValuesWithAssociations = {}; protected _previousDataValuesWithAssociations = {};
static async sync(options) {
if (this.collection.isView()) {
return;
}
if (this.collection.options.sync === false) {
return;
}
// @ts-ignore
const collectionSyncOptions = this.database.collectionFactory.collectionTypes.get(this.collection.constructor)
?.onSync;
if (collectionSyncOptions) {
await collectionSyncOptions(this, options);
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);
}
// TODO // TODO
public toChangedWithAssociations() { public toChangedWithAssociations() {
// @ts-ignore // @ts-ignore
@ -149,58 +212,4 @@ export class Model<TModelAttributes extends {} = any, TCreationAttributes extend
return lodash.orderBy(data, orderItems, orderDirections); return lodash.orderBy(data, orderItems, orderDirections);
} }
static async sync(options) {
if (this.collection.isView()) {
return;
}
if (this.collection.options.sync === false) {
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);
}
} }