Feature field for set default (#49)

* feat: add AsDefault field type

* fix: association definition in test case

* fix: logic of field as default in bulkCreate

* fix: change to asDefault field

* refactor: rename to radio and move unit test cases back to database package

* change to radio

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2020-12-28 23:13:17 +08:00 committed by GitHub
parent b2fe087fc2
commit 3e75bbe6c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 290 additions and 5 deletions

View File

@ -0,0 +1,174 @@
import { getDatabase } from '..';
describe('radio', () => {
let db;
beforeEach(async () => {
db = getDatabase();
db.table({
name: 'users',
fields: [
{
type: 'string',
name: 'name'
},
{
type: 'hasMany',
name: 'posts'
}
]
});
db.table({
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'string',
name: 'status',
defaultValue: 'published',
},
{
type: 'radio',
name: 'pinned'
},
{
type: 'radio',
name: 'latest',
defaultValue: true
},
{
type: 'radio',
name: 'pinned_in_status',
scope: ['status']
},
{
type: 'radio',
name: 'pinned_in_user',
scope: ['user'],
defaultValue: true
}
]
});
await db.sync({ force: true });
});
afterEach(() => db.close());
describe('create', () => {
it('undefined value as defaultValue', async () => {
const Post = db.getModel('posts');
const created1 = await Post.create({ title: 'title1', pinned: true });
expect(created1.pinned).toBe(true);
expect(created1.latest).toBe(true);
const created2 = await Post.create({ title: 'title2' });
expect(created2.pinned).toBe(false);
expect(created2.latest).toBe(true);
const posts = await Post.findAll({ order: [['id', 'ASC']] });
expect(posts.map(({ pinned, latest }) => ({ pinned, latest }))).toEqual([
{ pinned: true, latest: false },
{ pinned: false, latest: true }
]);
});
it('true value set', async () => {
const Post = db.getModel('posts');
const created1 = await Post.create({ title: 'title1', pinned: true });
expect(created1.pinned).toBe(true);
const created2 = await Post.create({ title: 'title2', pinned: true });
expect(created2.pinned).toBe(true);
const posts = await Post.findAll({ order: [['id', 'ASC']] });
expect(posts.map(({ pinned }) => pinned)).toEqual([false, true]);
});
it('bulkCreate', async () => {
const Post = db.getModel('posts');
const posts = await Post.bulkCreate([
{ title: 'title1' },
{ title: 'title2', pinned: true },
{ title: 'title3' },
]);
expect(posts.map(({ pinned, latest }) => ({ pinned, latest }))).toEqual([
{ pinned: false, latest: false },
{ pinned: true, latest: false },
{ pinned: false, latest: true }
]);
});
it('create with scopes', async () => {
const User = db.getModel('users');
const users = await User.bulkCreate([{}, {}]);
const Post = db.getModel('posts');
const bulkCreated = await Post.bulkCreate([
{ title: 'title1', status: 'published', user_id: 1},
{ title: 'title2', status: 'published', user_id: 2, pinned_in_status: true },
{ title: 'title3', status: 'draft', user_id: 1, pinned_in_status: true },
]);
expect(bulkCreated.map(({ pinned_in_status, pinned_in_user }) => ({ pinned_in_status, pinned_in_user }))).toEqual([
{ pinned_in_status: false, pinned_in_user: false },
{ pinned_in_status: true, pinned_in_user: true },
{ pinned_in_status: true, pinned_in_user: true }
]);
const user1Post = await users[1].createPost({ title: 'title4', status: 'draft', pinned_in_status: true });
expect(user1Post.pinned_in_status).toBe(true);
expect(user1Post.pinned_in_user).toBe(true);
const posts = await Post.findAll({ order: [['id', 'ASC']] });
expect(posts.map(({ title, pinned_in_status, pinned_in_user }) => ({ title, pinned_in_status, pinned_in_user }))).toMatchObject([
{ title: 'title1', pinned_in_status: false, pinned_in_user: false },
{ title: 'title2', pinned_in_status: true, pinned_in_user: false },
{ title: 'title3', pinned_in_status: false, pinned_in_user: true },
{ title: 'title4', pinned_in_status: true, pinned_in_user: true }
]);
});
});
describe('update', () => {
it('update one to false effect nothing else', async () => {
const Post = db.getModel('posts');
await Post.bulkCreate([
{ title: 'title1', pinned: true },
{ title: 'title2' }
]);
const created = await Post.create({ pinned: false });
expect(created.pinned).toBe(false);
const posts = await Post.findAll({ order: [['title', 'ASC']] });
expect(posts.map(({ pinned }) => pinned)).toEqual([true, false, false]);
});
it('update one to true makes others to false', async () => {
const Post = db.getModel('posts');
await Post.bulkCreate([
{ title: 'title1', pinned: true },
{ title: 'title2' }
]);
const created = await Post.create({ pinned: true });
expect(created.pinned).toBe(true);
const posts = await Post.findAll({ order: [['title', 'ASC']] });
expect(posts.map(({ pinned }) => pinned)).toEqual([false, false, true]);
});
});
});

View File

@ -20,11 +20,11 @@ import {
JSON as Json,
JSONB as Jsonb,
PASSWORD as Password,
} from '../fields';
} from '../../fields';
import { DataTypes } from 'sequelize';
import { ABSTRACT } from 'sequelize/lib/data-types';
import { getDatabase } from '.';
import Database from '..';
import { getDatabase } from '..';
import Database from '../..';
describe('field types', () => {
const assertTypeInstanceOf = (expected, actual) => {

View File

@ -788,3 +788,102 @@ export class SORT extends NUMBER {
return extremum + (next === 'max' ? 1 : -1);
}
}
export class Radio extends BOOLEAN {
public readonly options: Options.RadioOptions;
static async beforeSaveHook(this: Radio, model, options) {
const { name, defaultValue = false, scope = [] } = this.options;
const { transaction } = options;
const value = model.get(name) || defaultValue;
model.set(name, value);
if (value) {
const where = model.getValuesByFieldNames(scope);
await this.setOthers({ where, transaction });
}
}
static async beforeBulkCreateHook(this: Radio, models, options) {
const { name, defaultValue = false, scope = [] } = this.options;
const { transaction } = options;
// 如果未配置范围限定,则可以进行性能优化处理(常用情况)。
if (!scope.length) {
await this.makeGroup(models, { transaction });
return;
}
const groups = new Map<{ [key: string]: any }, any[]>();
// 按 scope 先分组
models.forEach(model => {
const where = model.getValuesByFieldNames(scope);
// 以 map 作为 key
let combo;
let group;
// 查找与 where 值相等的组合
for (combo of groups.keys()) {
if (whereCompare(combo, where)) {
group = groups.get(combo);
break;
}
}
if (!group) {
group = [];
groups.set(where, group);
}
group.push(model);
});
for (const [where, group] of groups) {
await this.makeGroup(group, { where, transaction });
}
}
constructor({ type, ...options }: Options.RadioOptions, context: FieldContext) {
super({ ...options, type: 'boolean' }, context);
const Model = context.sourceTable.getModel();
// TODO(feature): 可考虑策略模式,以在需要时对外提供接口
const beforeSaveHook = Radio.beforeSaveHook.bind(this);
Model.addHook('beforeCreate', beforeSaveHook);
Model.addHook('beforeUpdate', beforeSaveHook);
// Model.addHook('beforeUpsert', beforeSaveHook);
Model.addHook('beforeBulkCreate', Radio.beforeBulkCreateHook.bind(this));
// TODO(optimize): bulkUpdate 的 hooks 参数不一样,没有对象列表,考虑到很少用,暂时不实现
// Model.addHook('beforeBulkUpdate', beforeBulkCreateHook);
}
public getDataType() {
return DataTypes.BOOLEAN;
}
public async setOthers(this: Radio, { where = {}, transaction }) {
const { name } = this.options;
const table = this.context.sourceTable;
const Model = table.getModel();
// 防止 beforeBulkUpdate hook 死循环,因外层 bulkUpdate 并不禁用,正常更新无影响。
await Model.update({ [name]: false }, { where, transaction, hooks: false });
}
async makeGroup(this: Radio, models, { where = {}, transaction }) {
const { name, defaultValue = false } = this.options;
let lastTrue;
let lastNull;
models.forEach(model => {
const value = model.get(name);
if (value) {
lastTrue = model;
} else if (value == null) {
lastNull = model;
}
model.set(name, false);
});
if (lastTrue) {
lastTrue.set(name, true);
} else if (defaultValue && lastNull) {
lastNull.set(name, true);
}
await this.setOthers({ where, transaction });
}
}

View File

@ -193,6 +193,16 @@ export interface SortOptions extends NumberOptions {
next?: 'min' | 'max';
}
export interface RadioOptions extends Omit<BooleanOptions, 'type'> {
type: 'radio';
/**
*
*
*
*/
scope?: string[];
}
export type ColumnOptions = AbstractFieldOptions
| BooleanOptions
| NumberOptions

View File

@ -176,10 +176,11 @@ export default {
},
{
interface: 'boolean',
type: 'boolean',
type: 'radio',
name: 'default',
title: '作为默认标签页',
defaultValue: false,
scope: ['collection'],
component: {
type: 'checkbox',
showInTable: true,

View File

@ -139,10 +139,11 @@ export default {
},
{
interface: 'boolean',
type: 'boolean',
type: 'radio',
name: 'default',
title: '作为默认试图',
defaultValue: false,
scope: ['collection'],
component: {
type: 'checkbox',
showInTable: true,