mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:45:18 +00:00
Feature/sort (#36)
* feat: add sort value initialization via beforeCreate hook * fix: after reinitialization, hooks are lost * test: temp test for hook * fix: hooks defined in the table options does not work * refactor: change sort config into sort type field and fix updateAssociations to create with foreignKey * refactor: abstract utility functions * fix: type definition * fix: type and where value type Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
6b84446697
commit
841249f58c
@ -47,7 +47,7 @@ describe('create', () => {
|
||||
{ content: 'comment2', status: 'draft' },
|
||||
]
|
||||
});
|
||||
expect(response.body.sort).toBe(null);
|
||||
expect(response.body.sort).toBe(1);
|
||||
expect(response.body.user_id).toBe(1);
|
||||
|
||||
const postWithUser = await agent
|
||||
|
@ -8,7 +8,7 @@ describe('list', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
resourcer.define({
|
||||
name: 'posts',
|
||||
name: 'articles',
|
||||
middlewares: [
|
||||
jsonReponse,
|
||||
],
|
||||
@ -16,8 +16,8 @@ describe('list', () => {
|
||||
});
|
||||
db = await initDatabase();
|
||||
db.table({
|
||||
name: 'posts',
|
||||
tableName: 'actions__m__posts',
|
||||
name: 'articles',
|
||||
tableName: 'actions__articles',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
@ -48,7 +48,7 @@ describe('list', () => {
|
||||
|
||||
it('create', async () => {
|
||||
const response = await agent
|
||||
.post('/posts')
|
||||
.post('/articles')
|
||||
.send({
|
||||
title: 'title1',
|
||||
});
|
||||
@ -56,7 +56,7 @@ describe('list', () => {
|
||||
});
|
||||
|
||||
it('list', async () => {
|
||||
const response = await agent.get('/posts?fields=title&page=1');
|
||||
const response = await agent.get('/articles?fields=title&page=1');
|
||||
expect(response.body).toEqual({
|
||||
data: [ { title: 'title1' } ],
|
||||
meta: { count: 1, page: 1, per_page: 20 }
|
||||
|
@ -6,13 +6,12 @@ describe('get', () => {
|
||||
beforeEach(async () => {
|
||||
db = await initDatabase();
|
||||
const User = db.getModel('users');
|
||||
const users = await User.bulkCreate('abcdefg'.split('').map(name => ({ name })));
|
||||
const users = await User.bulkCreate(Array.from('abcdefg').map(name => ({ name })));
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
const posts = await Post.bulkCreate(Array(22).fill(null).map((_, i) => ({
|
||||
const posts = await Post.bulkCreate(Array(10).fill(null).map((_, i) => ({
|
||||
title: `title_${i}`,
|
||||
status: i % 2 ? 'publish' : 'draft',
|
||||
sort: i,
|
||||
user_id: users[i % users.length].id
|
||||
})));
|
||||
|
||||
@ -20,58 +19,76 @@ describe('get', () => {
|
||||
comments: Array(post.sort % 5).fill(null).map((_, index) => ({
|
||||
content: `content_${index}`,
|
||||
status: index % 2 ? 'published' : 'draft',
|
||||
user_id: users[index % users.length].id,
|
||||
sort: index
|
||||
user_id: users[index % users.length].id
|
||||
}))
|
||||
})), Promise.resolve());
|
||||
// [
|
||||
// { id: 1, post_id: 2, sort: 0 },
|
||||
// { id: 2, post_id: 3, sort: 0 },
|
||||
// { id: 3, post_id: 3, sort: 1 },
|
||||
// { id: 4, post_id: 4, sort: 0 },
|
||||
// { id: 5, post_id: 4, sort: 1 },
|
||||
// { id: 6, post_id: 4, sort: 2 },
|
||||
// { id: 7, post_id: 5, sort: 0 },
|
||||
// { id: 8, post_id: 5, sort: 1 },
|
||||
// { id: 9, post_id: 5, sort: 2 },
|
||||
// { id: 10, post_id: 5, sort: 3 },
|
||||
// { id: 11, post_id: 7, sort: 0 },
|
||||
// { id: 12, post_id: 8, sort: 0 },
|
||||
// { id: 13, post_id: 8, sort: 1 },
|
||||
// { id: 14, post_id: 9, sort: 0 },
|
||||
// { id: 15, post_id: 9, sort: 1 },
|
||||
// { id: 16, post_id: 9, sort: 2 },
|
||||
// { id: 17, post_id: 10, sort: 0 },
|
||||
// { id: 18, post_id: 10, sort: 1 },
|
||||
// { id: 19, post_id: 10, sort: 2 },
|
||||
// { id: 20, post_id: 10, sort: 3 },
|
||||
// { id: 21, post_id: 15, sort: 0 },
|
||||
// { id: 22, post_id: 15, sort: 1 },
|
||||
// { id: 23, post_id: 15, sort: 2 },
|
||||
// { id: 24, post_id: 15, sort: 3 },
|
||||
// { id: 25, post_id: 12, sort: 0 },
|
||||
// { id: 26, post_id: 13, sort: 0 },
|
||||
// { id: 27, post_id: 13, sort: 1 },
|
||||
// { id: 28, post_id: 14, sort: 0 },
|
||||
// { id: 29, post_id: 14, sort: 1 },
|
||||
// { id: 30, post_id: 14, sort: 2 },
|
||||
// { id: 31, post_id: 17, sort: 0 },
|
||||
// { id: 32, post_id: 18, sort: 0 },
|
||||
// { id: 33, post_id: 18, sort: 1 },
|
||||
// { id: 34, post_id: 19, sort: 0 },
|
||||
// { id: 35, post_id: 19, sort: 1 },
|
||||
// { id: 36, post_id: 19, sort: 2 },
|
||||
// { id: 37, post_id: 20, sort: 0 },
|
||||
// { id: 38, post_id: 20, sort: 1 },
|
||||
// { id: 39, post_id: 20, sort: 2 },
|
||||
// { id: 40, post_id: 20, sort: 3 },
|
||||
// { id: 41, post_id: 22, sort: 0 }
|
||||
// ]
|
||||
});
|
||||
|
||||
afterAll(() => db.close());
|
||||
|
||||
describe.only('sort value initialization', () => {
|
||||
it('initialization by bulkCreate', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const posts = await Post.findAll({
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
expect(posts.map(({ id, sort, sort_in_status, sort_in_user }) => ({ id, sort, sort_in_status, sort_in_user }))).toEqual([
|
||||
{ id: 1, sort: 1, sort_in_status: 1, sort_in_user: 1 },
|
||||
{ id: 2, sort: 2, sort_in_status: 1, sort_in_user: 1 },
|
||||
{ id: 3, sort: 3, sort_in_status: 2, sort_in_user: 1 },
|
||||
{ id: 4, sort: 4, sort_in_status: 2, sort_in_user: 1 },
|
||||
{ id: 5, sort: 5, sort_in_status: 3, sort_in_user: 1 },
|
||||
{ id: 6, sort: 6, sort_in_status: 3, sort_in_user: 1 },
|
||||
{ id: 7, sort: 7, sort_in_status: 4, sort_in_user: 1 },
|
||||
{ id: 8, sort: 8, sort_in_status: 4, sort_in_user: 2 },
|
||||
{ id: 9, sort: 9, sort_in_status: 5, sort_in_user: 2 },
|
||||
{ id: 10, sort: 10, sort_in_status: 5, sort_in_user: 2 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('initialization by updateAssociations', async () => {
|
||||
const Comment = db.getModel('comments');
|
||||
const comments = await Comment.findAll({
|
||||
order: [['id', 'ASC']]
|
||||
});
|
||||
expect(comments.map(({ id, sort, sort_in_status, sort_in_post }) => ({ id, sort, sort_in_status, sort_in_post }))).toEqual([
|
||||
{ id: 1, sort: 1, sort_in_status: 1, sort_in_post: 1 },
|
||||
{ id: 2, sort: 2, sort_in_status: 2, sort_in_post: 1 },
|
||||
{ id: 3, sort: 3, sort_in_status: 1, sort_in_post: 2 },
|
||||
{ id: 4, sort: 4, sort_in_status: 3, sort_in_post: 1 },
|
||||
{ id: 5, sort: 5, sort_in_status: 2, sort_in_post: 2 },
|
||||
{ id: 6, sort: 6, sort_in_status: 4, sort_in_post: 3 },
|
||||
{ id: 7, sort: 7, sort_in_status: 5, sort_in_post: 1 },
|
||||
{ id: 8, sort: 8, sort_in_status: 3, sort_in_post: 2 },
|
||||
{ id: 9, sort: 9, sort_in_status: 6, sort_in_post: 3 },
|
||||
{ id: 10, sort: 10, sort_in_status: 4, sort_in_post: 4 },
|
||||
{ id: 11, sort: 11, sort_in_status: 7, sort_in_post: 1 },
|
||||
{ id: 12, sort: 12, sort_in_status: 8, sort_in_post: 1 },
|
||||
{ id: 13, sort: 13, sort_in_status: 5, sort_in_post: 2 },
|
||||
{ id: 14, sort: 14, sort_in_status: 9, sort_in_post: 1 },
|
||||
{ id: 15, sort: 15, sort_in_status: 6, sort_in_post: 2 },
|
||||
{ id: 16, sort: 16, sort_in_status: 10, sort_in_post: 3 },
|
||||
{ id: 17, sort: 17, sort_in_status: 11, sort_in_post: 1 },
|
||||
{ id: 18, sort: 18, sort_in_status: 7, sort_in_post: 2 },
|
||||
{ id: 19, sort: 19, sort_in_status: 12, sort_in_post: 3 },
|
||||
{ id: 20, sort: 20, sort_in_status: 8, sort_in_post: 4 }
|
||||
]);
|
||||
});
|
||||
|
||||
it('sort value of append item', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create({ user_id: 1 });
|
||||
expect(post.sort).toBe(11);
|
||||
expect(post.sort_in_status).toBe(6);
|
||||
expect(post.sort_in_user).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort in whole table', () => {
|
||||
it('init sort value', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
});
|
||||
|
||||
it('move id=1 by offset=1', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
await agent
|
||||
@ -165,10 +182,10 @@ describe('get', () => {
|
||||
expect(post1.get('sort')).toBe(0);
|
||||
|
||||
const post2 = await Post.findByPk(2);
|
||||
expect(post2.get('sort')).toBe(22);
|
||||
expect(post2.get('sort')).toBe(10);
|
||||
|
||||
const post22 = await Post.findByPk(22);
|
||||
expect(post22.get('sort')).toBe(21);
|
||||
const post10 = await Post.findByPk(10);
|
||||
expect(post10.get('sort')).toBe(9);
|
||||
});
|
||||
|
||||
it('move id=2 by offset=-Infinity', async () => {
|
||||
@ -222,7 +239,7 @@ describe('get', () => {
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
const post2 = await Post.findByPk(2);
|
||||
expect(post2.get('sort')).toBe(22);
|
||||
expect(post2.get('sort')).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -21,8 +21,18 @@ export default {
|
||||
name: 'user',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
type: 'sort',
|
||||
name: 'sort'
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort_in_status',
|
||||
scope: ['status']
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort_in_post',
|
||||
scope: ['post']
|
||||
}
|
||||
],
|
||||
scopes: {
|
||||
@ -31,6 +41,5 @@ export default {
|
||||
status: 'published'
|
||||
}
|
||||
}
|
||||
},
|
||||
sortable: true
|
||||
}
|
||||
} as TableOptions;
|
||||
|
@ -30,18 +30,24 @@ export default {
|
||||
name: 'tags',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
type: 'sort',
|
||||
name: 'sort'
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort_in_status',
|
||||
scope: ['status']
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort_in_user',
|
||||
scope: ['user']
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'meta'
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
beforeCreate(model, options) {
|
||||
},
|
||||
},
|
||||
scopes: {
|
||||
customTitle: (title, ctx) => {
|
||||
return {
|
||||
@ -49,7 +55,6 @@ export default {
|
||||
title: title,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
sortable: true
|
||||
}
|
||||
}
|
||||
} as TableOptions;
|
||||
|
@ -158,7 +158,7 @@ describe('hooks', () => {
|
||||
expect(test.get('arr')).toEqual([ 1, 3, 2, 4 ]);
|
||||
});
|
||||
|
||||
it.only('add hook in custom field', async () => {
|
||||
it('add hook in custom field', async () => {
|
||||
const table = db.table({
|
||||
name: 'test3',
|
||||
fields: [
|
||||
|
38
packages/database/src/__tests__/model/getScopeWhere.test.ts
Normal file
38
packages/database/src/__tests__/model/getScopeWhere.test.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { getDatabase } from '..';
|
||||
import Database from '../../database';
|
||||
|
||||
|
||||
|
||||
describe('getScopeWhere', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = getDatabase();
|
||||
db.table({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'status'
|
||||
}
|
||||
]
|
||||
});
|
||||
await db.sync({ force: true });
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
|
||||
it('exist column', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create({ status: 'published' });
|
||||
const where = post.getScopeWhere(['status']);
|
||||
expect(where).toEqual({ status: 'published' });
|
||||
});
|
||||
|
||||
it('non-exist column', async () => {
|
||||
const Post = db.getModel('posts');
|
||||
const post = await Post.create({});
|
||||
const where = post.getScopeWhere(['whatever']);
|
||||
expect(where).toEqual({});
|
||||
});
|
||||
});
|
@ -367,7 +367,6 @@ describe('model', () => {
|
||||
}))
|
||||
});
|
||||
const updatedComments = await Comment.findAll();
|
||||
console.log(updatedComments);
|
||||
const post1CommentsCount = await Comment.count({
|
||||
where: { post_id: post.id }
|
||||
});
|
||||
|
@ -9,13 +9,15 @@ import {
|
||||
BelongsToManyOptions,
|
||||
ThroughOptions,
|
||||
} from 'sequelize';
|
||||
import { template, get, toNumber } from 'lodash';
|
||||
import bcrypt from 'bcrypt';
|
||||
|
||||
import * as Options from './option-types';
|
||||
import { getDataTypeKey } from '.';
|
||||
import Table from '../table';
|
||||
import Database from '../database';
|
||||
import Model, { ModelCtor } from '../model';
|
||||
import { template, isArray, map, get, toNumber } from 'lodash';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { whereCompare } from '../utils';
|
||||
|
||||
export interface IField {
|
||||
|
||||
@ -689,3 +691,74 @@ export class BELONGSTOMANY extends Relation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class SORT extends NUMBER {
|
||||
|
||||
public readonly options: Options.SortOptions;
|
||||
|
||||
static async beforeCreateHook(this: SORT, model, options) {
|
||||
const { transaction } = options;
|
||||
const Model = model.constructor;
|
||||
const { name, scope = [], next = 'max' } = this.options;
|
||||
const where = model.getScopeWhere(scope);
|
||||
const extremum: number = await Model[next](name, { where, transaction }) || 0;
|
||||
model.set(name, extremum + (next === 'max' ? 1 : -1));
|
||||
}
|
||||
|
||||
static async beforeBulkCreateHook(this: SORT, models, options) {
|
||||
const { transaction } = options;
|
||||
const table = this.context.sourceTable;
|
||||
const Model = table.getModel();
|
||||
const { name, scope = [], next = 'max' } = this.options;
|
||||
// 如果未配置范围限定,则可以进行性能优化处理(常用情况)。
|
||||
if (!scope.length) {
|
||||
const extremum: number = await Model[next](name, { transaction }) || 0;
|
||||
models.forEach((model, i: number) => {
|
||||
model.setDataValue(name, extremum + (i + 1) * (next === 'max' ? 1 : -1));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 用于存放 where 条件与计算极值
|
||||
const groups = new Map<{ [key: string]: any }, number>();
|
||||
await models.reduce((promise, model) => promise.then(async () => {
|
||||
const where = model.getScopeWhere(scope);
|
||||
|
||||
let extremum: number;
|
||||
// 以 map 作为 key
|
||||
let combo;
|
||||
// 查找与 where 值相等的组合
|
||||
for (combo of groups.keys()) {
|
||||
if (whereCompare(combo, where)) {
|
||||
// 如果找到的话则以之前储存的值作为基础极值
|
||||
extremum = groups.get(combo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如未找到组合
|
||||
if (typeof extremum === 'undefined') {
|
||||
// 则使用 where 条件查询极值
|
||||
extremum = await Model[next](name, { where, transaction }) || 0;
|
||||
// 且使用 where 条件创建组合
|
||||
combo = where;
|
||||
}
|
||||
const nextValue = extremum + (next === 'max' ? 1 : -1);
|
||||
// 设置数据行的排序值
|
||||
model.setDataValue(name, nextValue);
|
||||
// 保存新的排序值为对应 where 组合的极值,以供下次计算
|
||||
groups.set(combo, nextValue);
|
||||
}), Promise.resolve());
|
||||
}
|
||||
|
||||
constructor(options: Options.SortOptions, context: FieldContext) {
|
||||
super(options, context);
|
||||
const Model = context.sourceTable.getModel();
|
||||
// TODO(feature): 可考虑策略模式,以在需要时对外提供接口
|
||||
Model.addHook('beforeCreate', SORT.beforeCreateHook.bind(this));
|
||||
Model.addHook('beforeBulkCreate', SORT.beforeBulkCreateHook.bind(this));
|
||||
}
|
||||
|
||||
public getDataType(): Function {
|
||||
return DataTypes.INTEGER;
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +174,25 @@ export interface BelongsToManyOptions extends Omit<SequelizeBelongsToManyOptions
|
||||
otherKey?: string;
|
||||
}
|
||||
|
||||
export interface SortOptions extends NumberOptions {
|
||||
type: 'sort';
|
||||
/**
|
||||
* 排序限定范围
|
||||
*
|
||||
* 在同表的限定范围内的字段值相等的数据行中排序
|
||||
*/
|
||||
scope?: string[];
|
||||
/**
|
||||
* 新值创建策略
|
||||
*
|
||||
* max: 使用最大值
|
||||
* min: 使用最小值
|
||||
*
|
||||
* Defaults to 'max'
|
||||
*/
|
||||
next?: 'min' | 'max';
|
||||
}
|
||||
|
||||
export type ColumnOptions = AbstractFieldOptions
|
||||
| BooleanOptions
|
||||
| NumberOptions
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {
|
||||
Model as SequelizeModel, Op, Sequelize, ProjectionAlias, Utils, SaveOptions,
|
||||
Model as SequelizeModel, Op, Sequelize, ProjectionAlias, Utils, SaveOptions
|
||||
} from 'sequelize';
|
||||
import Database from './database';
|
||||
import {
|
||||
@ -259,6 +259,27 @@ export abstract class Model extends SequelizeModel {
|
||||
return data;
|
||||
}
|
||||
|
||||
getScopeWhere(scope: string[]) {
|
||||
const Model = this.constructor as ModelCtor<Model>;
|
||||
const table = this.database.getTable(this.constructor.name);
|
||||
const associations = table.getAssociations();
|
||||
const where = {};
|
||||
scope.forEach(col => {
|
||||
const association = associations.get(col);
|
||||
const dataKey = association && association instanceof BELONGSTO
|
||||
? association.options.foreignKey
|
||||
: col;
|
||||
if (!Model.rawAttributes[dataKey]) {
|
||||
return;
|
||||
}
|
||||
const value = this.getDataValue(dataKey);
|
||||
if (typeof value !== 'undefined') {
|
||||
where[dataKey] = value;
|
||||
}
|
||||
});
|
||||
return where;
|
||||
}
|
||||
|
||||
async updateSingleAssociation(key: string, data: any, options: SaveOptions<any> & { context?: any; } = {}) {
|
||||
const {
|
||||
fields,
|
||||
@ -399,19 +420,22 @@ export abstract class Model extends SequelizeModel {
|
||||
for (const item of toUpsertObjects) {
|
||||
let target;
|
||||
if (typeof item[targetKey] === 'undefined') {
|
||||
// TODO(optimize): 不确定 bulkCreate 的结果是否能保证顺序,能保证的话这里可以优化为批量处理
|
||||
target = await Target.create(item, opts);
|
||||
target = await this[accessors.create](item, opts);
|
||||
} else {
|
||||
let created: boolean;
|
||||
[target, created] = await Target.findOrCreate({
|
||||
target = await Target.findOne({
|
||||
...opts,
|
||||
where: { [targetKey]: item[targetKey] },
|
||||
defaults: item,
|
||||
transaction
|
||||
});
|
||||
if (!created) {
|
||||
if (!target) {
|
||||
target = await this[accessors.create](item, opts);
|
||||
} else {
|
||||
await target.update(item, opts);
|
||||
}
|
||||
}
|
||||
// TODO(optimize): 此处添加的对象其实已经创建了关联,
|
||||
// 但考虑到单条 create 的 hook 要求带上关联键,且后面的 set,
|
||||
// 所以仍然交给 set 再调用关联一次。
|
||||
toSetItems.add(target);
|
||||
|
||||
if (association instanceof BELONGSTOMANY) {
|
||||
belongsToManyList.push({
|
||||
@ -419,7 +443,6 @@ export abstract class Model extends SequelizeModel {
|
||||
target
|
||||
});
|
||||
}
|
||||
toSetItems.add(target);
|
||||
|
||||
await target.updateAssociations(item, opts);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
Relation,
|
||||
BELONGSTO,
|
||||
BELONGSTOMANY,
|
||||
SORT
|
||||
} from './fields';
|
||||
import Database from './database';
|
||||
import { Model, ModelCtor } from './model';
|
||||
@ -127,6 +128,10 @@ export class Table {
|
||||
|
||||
public relationTables = new Set<string>();
|
||||
|
||||
get sortable(): boolean {
|
||||
return Array.from(this.fields.values()).some(field => field instanceof SORT);
|
||||
}
|
||||
|
||||
constructor(options: TableOptions, context: TabelContext) {
|
||||
const { database } = context;
|
||||
const {
|
||||
|
@ -290,6 +290,10 @@ export function toInclude(options: any, context: ToIncludeContext = {}) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export function whereCompare(a: any, b: any): boolean {
|
||||
return _.isEqual(a, b);
|
||||
}
|
||||
|
||||
export function requireModule(module: any) {
|
||||
if (typeof module === 'string') {
|
||||
module = require(module);
|
||||
|
Loading…
Reference in New Issue
Block a user