refactor(sequence-field): move to plugin and use table to record (#1209)
* refactor(sequence-field): move to plugin and use table to record * fix(database): skip test case which not in core * fix(plugin-sequence): add migration * fix(plugin-sequence): fix types * test(plugin-sequence): fix test cases * fix(plugin-sequence): fix configuration ui * fix(plugin-sequence): fix merge * fix(plugin-sequence): fix schema and error message
This commit is contained in:
parent
720cbc76e2
commit
e3e352ffeb
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
@ -34,6 +34,24 @@
|
|||||||
],
|
],
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"internalConsoleOptions": "neverOpen"
|
"internalConsoleOptions": "neverOpen"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug Migration",
|
||||||
|
"runtimeExecutable": "yarn",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"--inspect-brk",
|
||||||
|
"nocobase",
|
||||||
|
"migrator",
|
||||||
|
"up",
|
||||||
|
],
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
],
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"internalConsoleOptions": "neverOpen"
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
1
packages/app/client/src/plugins/sequence-field.ts
Normal file
1
packages/app/client/src/plugins/sequence-field.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from '@nocobase/plugin-sequence-field/client';
|
@ -7,7 +7,6 @@ export * from './createdBy';
|
|||||||
export * from './datetime';
|
export * from './datetime';
|
||||||
export * from './email';
|
export * from './email';
|
||||||
export * from './formula';
|
export * from './formula';
|
||||||
export * from './sequence';
|
|
||||||
export * from './icon';
|
export * from './icon';
|
||||||
export * from './id';
|
export * from './id';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
|
@ -155,27 +155,6 @@ export default {
|
|||||||
"Advanced type": "高级类型",
|
"Advanced type": "高级类型",
|
||||||
"Formula": "公式",
|
"Formula": "公式",
|
||||||
"Formula description": "基于同一条记录中的其他字段计算出一个值。",
|
"Formula description": "基于同一条记录中的其他字段计算出一个值。",
|
||||||
"Sequence": "自动编码",
|
|
||||||
"Sequence rules": "编号规则",
|
|
||||||
"Add rule": "添加规则",
|
|
||||||
"Inputable": "可输入",
|
|
||||||
"Match rules": "输入必须匹配规则",
|
|
||||||
"Type": "类型",
|
|
||||||
"Autoincrement": "自增数字",
|
|
||||||
"Fixed text": "固定文本",
|
|
||||||
"Text content": "文本内容",
|
|
||||||
"Rule content": "规则内容",
|
|
||||||
"{{value}} Digits": "{{value}} 位数字",
|
|
||||||
"Digits": "位数",
|
|
||||||
"Start from": "起始于",
|
|
||||||
"Starts from {{value}}": "从 {{value}} 开始",
|
|
||||||
"Reset cycle": "重置周期",
|
|
||||||
"No reset": "不重置",
|
|
||||||
"Daily": "每天",
|
|
||||||
"Every Monday": "每周一",
|
|
||||||
"Monthly": "每月",
|
|
||||||
"Yearly": "每年",
|
|
||||||
"Operations": "操作",
|
|
||||||
"Choices": "选择类型",
|
"Choices": "选择类型",
|
||||||
"Checkbox": "勾选",
|
"Checkbox": "勾选",
|
||||||
"Single select": "下拉菜单(单选)",
|
"Single select": "下拉菜单(单选)",
|
||||||
|
@ -96,7 +96,7 @@ pgOnly()('collection inherits', () => {
|
|||||||
await db.sync();
|
await db.sync();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not conflict when fields have same DateType', async () => {
|
it.skip('should not conflict when fields have same DateType', async () => {
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'parent',
|
name: 'parent',
|
||||||
fields: [{ name: 'field1', type: 'string' }],
|
fields: [{ name: 'field1', type: 'string' }],
|
||||||
|
@ -25,7 +25,6 @@ import { UidFieldOptions } from './uid-field';
|
|||||||
import { UUIDFieldOptions } from './uuid-field';
|
import { UUIDFieldOptions } from './uuid-field';
|
||||||
import { VirtualFieldOptions } from './virtual-field';
|
import { VirtualFieldOptions } from './virtual-field';
|
||||||
import { FormulaFieldOptions } from './formula-field';
|
import { FormulaFieldOptions } from './formula-field';
|
||||||
import { SequenceFieldOptions } from './sequence-field';
|
|
||||||
import { SetFieldOptions } from './set-field';
|
import { SetFieldOptions } from './set-field';
|
||||||
|
|
||||||
export * from './array-field';
|
export * from './array-field';
|
||||||
@ -51,7 +50,6 @@ export * from './uid-field';
|
|||||||
export * from './uuid-field';
|
export * from './uuid-field';
|
||||||
export * from './virtual-field';
|
export * from './virtual-field';
|
||||||
export * from './formula-field';
|
export * from './formula-field';
|
||||||
export { SequenceField } from './sequence-field';
|
|
||||||
|
|
||||||
export type FieldOptions =
|
export type FieldOptions =
|
||||||
| BaseFieldOptions
|
| BaseFieldOptions
|
||||||
@ -80,5 +78,4 @@ export type FieldOptions =
|
|||||||
| BelongsToFieldOptions
|
| BelongsToFieldOptions
|
||||||
| HasOneFieldOptions
|
| HasOneFieldOptions
|
||||||
| HasManyFieldOptions
|
| HasManyFieldOptions
|
||||||
| BelongsToManyFieldOptions
|
| BelongsToManyFieldOptions;
|
||||||
| SequenceFieldOptions;
|
|
||||||
|
@ -1,202 +0,0 @@
|
|||||||
import { DataTypes, Transactionable } from 'sequelize';
|
|
||||||
import parser from 'cron-parser';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { escapeRegExp } from 'lodash';
|
|
||||||
|
|
||||||
import { Registry } from '@nocobase/utils';
|
|
||||||
|
|
||||||
import { Model } from '..';
|
|
||||||
import { BaseColumnFieldOptions, Field, FieldContext } from './field';
|
|
||||||
|
|
||||||
interface Pattern {
|
|
||||||
validate?(options): string | null;
|
|
||||||
generate(this: SequenceField, instance: Model, index: number): string;
|
|
||||||
getLength(options): number;
|
|
||||||
getMatcher(options): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sequencePatterns = new Registry<Pattern>();
|
|
||||||
|
|
||||||
sequencePatterns.register('string', {
|
|
||||||
validate(options) {
|
|
||||||
if (!options?.value) {
|
|
||||||
return 'options.value should be configured as a non-empty string';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
generate(instance, index) {
|
|
||||||
const { options } = this.options.patterns[index];
|
|
||||||
return options.value;
|
|
||||||
},
|
|
||||||
getLength(options) {
|
|
||||||
return options.value.length;
|
|
||||||
},
|
|
||||||
getMatcher(options) {
|
|
||||||
return escapeRegExp(options.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sequencePatterns.register('integer', {
|
|
||||||
generate(instance: Model, index) {
|
|
||||||
const { options = {} } = this.options.patterns[index];
|
|
||||||
const { digits = 1, start = 0, base = 10, cycle } = options;
|
|
||||||
const max = Math.pow(base, digits) - 1;
|
|
||||||
const { lastRecord = null } = this.options;
|
|
||||||
|
|
||||||
if (typeof options.current === 'undefined') {
|
|
||||||
if (lastRecord && lastRecord.get(this.options.name)) {
|
|
||||||
// if match current pattern
|
|
||||||
const matcher = this.match(lastRecord.get(this.options.name));
|
|
||||||
if (matcher) {
|
|
||||||
const lastNumber = Number.parseInt(matcher[index + 1], base);
|
|
||||||
options.current = Number.isNaN(lastNumber) ? start : lastNumber + 1;
|
|
||||||
} else {
|
|
||||||
options.current = start;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.current = start;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.current += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cycle as cron string
|
|
||||||
if (cycle && lastRecord) {
|
|
||||||
const interval = parser.parseExpression(cycle, { currentDate: <Date>lastRecord.get('createdAt') });
|
|
||||||
const next = interval.next();
|
|
||||||
if ((<Date>instance.get('createdAt')).getTime() >= next.getTime()) {
|
|
||||||
options.current = start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.current > max) {
|
|
||||||
options.current = start;
|
|
||||||
}
|
|
||||||
|
|
||||||
// update options
|
|
||||||
Object.assign(this.options.patterns[index], { options });
|
|
||||||
|
|
||||||
return options.current.toString(base).padStart(digits, '0');
|
|
||||||
},
|
|
||||||
|
|
||||||
getLength({ digits = 1 } = {}) {
|
|
||||||
return digits;
|
|
||||||
},
|
|
||||||
|
|
||||||
getMatcher(options = {}) {
|
|
||||||
const { digits = 1, start = 0, base = 10 } = options;
|
|
||||||
const startLen = start ? start.toString(base).length : 1;
|
|
||||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base);
|
|
||||||
return `[${chars}]{${digits}}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
sequencePatterns.register('date', {
|
|
||||||
generate(instance, index) {
|
|
||||||
const { options } = this.options.patterns[index];
|
|
||||||
return moment(instance.get(options?.field ?? 'createdAt')).format(options?.format ?? 'YYYYMMDD');
|
|
||||||
},
|
|
||||||
getLength(options) {
|
|
||||||
return options.format?.length ?? 8;
|
|
||||||
},
|
|
||||||
getMatcher(options = {}) {
|
|
||||||
return `.{${options?.format?.length ?? 8}}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
interface PatternConfig {
|
|
||||||
type: string;
|
|
||||||
title?: string;
|
|
||||||
options?: any;
|
|
||||||
}
|
|
||||||
export interface SequenceFieldOptions extends BaseColumnFieldOptions {
|
|
||||||
type: 'sequence';
|
|
||||||
patterns: PatternConfig[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SequenceField extends Field {
|
|
||||||
matcher: RegExp;
|
|
||||||
|
|
||||||
get dataType() {
|
|
||||||
return DataTypes.STRING;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(options: SequenceFieldOptions, context: FieldContext) {
|
|
||||||
super(options, context);
|
|
||||||
if (!options.patterns || !options.patterns.length) {
|
|
||||||
throw new Error('at least one pattern should be defined for sequence type');
|
|
||||||
}
|
|
||||||
options.patterns.forEach(pattern => {
|
|
||||||
const P = sequencePatterns.get(pattern.type);
|
|
||||||
if (!P) {
|
|
||||||
throw new Error(`pattern type ${pattern.type} is not registered`);
|
|
||||||
}
|
|
||||||
if (P.validate) {
|
|
||||||
const error = P.validate(pattern.options);
|
|
||||||
if (error) {
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const patterns = options.patterns
|
|
||||||
.map(({ type, options }) => sequencePatterns.get(type).getMatcher(options));
|
|
||||||
this.matcher = new RegExp(`^${patterns.map(p => `(${p})`).join('')}$`, 'i');
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue = async (instance: Model, options) => {
|
|
||||||
const { name, patterns } = this.options;
|
|
||||||
// NOTE: only load when value is not set, if null stand for no last record
|
|
||||||
if (typeof this.options.lastRecord === 'undefined') {
|
|
||||||
const model = <typeof Model>instance.constructor;
|
|
||||||
this.options.lastRecord = await model.findOne({
|
|
||||||
attributes: [model.primaryKeyAttribute, this.options.name, 'createdAt'],
|
|
||||||
order: [
|
|
||||||
['createdAt', 'DESC'],
|
|
||||||
// TODO(bug): will cause problem if no auto-increment id
|
|
||||||
[model.primaryKeyAttribute, 'DESC']
|
|
||||||
],
|
|
||||||
transaction: options.transaction
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = patterns.reduce((result, p, i) => {
|
|
||||||
const item = sequencePatterns.get(p.type).generate.call(this, instance, i, options);
|
|
||||||
return result.concat(item);
|
|
||||||
}, []);
|
|
||||||
instance.set(name, results.join(''));
|
|
||||||
};
|
|
||||||
|
|
||||||
setLast = (instance: Model, options) => {
|
|
||||||
this.options.lastRecord = instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
match(value) {
|
|
||||||
return typeof value === 'string' ? value.match(this.matcher) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
parse(value: string, patternIndex: number): string {
|
|
||||||
for (let i = 0, index = 0; i < this.options.patterns.length; i += 1) {
|
|
||||||
const { type, options } = this.options.patterns[i];
|
|
||||||
const { getLength } = sequencePatterns.get(type);
|
|
||||||
const length = getLength(options);
|
|
||||||
if (i === patternIndex) {
|
|
||||||
return value.substring(index, index + length);
|
|
||||||
}
|
|
||||||
index += length;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
bind() {
|
|
||||||
super.bind();
|
|
||||||
this.on('beforeCreate', this.setValue);
|
|
||||||
this.on('afterCreate', this.setLast);
|
|
||||||
}
|
|
||||||
|
|
||||||
unbind() {
|
|
||||||
super.unbind();
|
|
||||||
this.off('beforeCreate', this.setValue);
|
|
||||||
this.off('afterCreate', this.setLast);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
'unique violation': '{{field}} 字段值是唯一的',
|
'unique violation': '{{field}} 字段值是唯一的',
|
||||||
'notNull Violation': '{{field}} 字段不能为空',
|
'notNull violation': '{{field}} 字段不能为空',
|
||||||
|
'Validation error': '{{field}} 字段规则验证失败',
|
||||||
};
|
};
|
||||||
|
4
packages/plugins/sequence-field/client.d.ts
vendored
Normal file
4
packages/plugins/sequence-field/client.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/client';
|
||||||
|
export { default } from './lib/client';
|
||||||
|
|
30
packages/plugins/sequence-field/client.js
Normal file
30
packages/plugins/sequence-field/client.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||||
|
|
||||||
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||||
|
|
||||||
|
var _index = _interopRequireWildcard(require("./lib/client"));
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
var _exportNames = {};
|
||||||
|
Object.defineProperty(exports, "default", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function get() {
|
||||||
|
return _index.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(_index).forEach(function (key) {
|
||||||
|
if (key === "default" || key === "__esModule") return;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||||
|
if (key in exports && exports[key] === _index[key]) return;
|
||||||
|
Object.defineProperty(exports, key, {
|
||||||
|
enumerable: true,
|
||||||
|
get: function get() {
|
||||||
|
return _index[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
27
packages/plugins/sequence-field/package.json
Normal file
27
packages/plugins/sequence-field/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/plugin-sequence-field",
|
||||||
|
"version": "0.8.0-alpha.13",
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"type": "Apache-2.0",
|
||||||
|
"url": "http://www.apache.org/licenses/LICENSE-2.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@nocobase/actions": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/client": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/database": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/plugin-collection-manager": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/server": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/utils": "0.8.0-alpha.13",
|
||||||
|
"classnames": "^2.3.1",
|
||||||
|
"cron-parser": "4.4.0",
|
||||||
|
"react-js-cron": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nocobase/test": "0.8.0-alpha.13"
|
||||||
|
},
|
||||||
|
"gitHead": "ce588eefb0bfc50f7d5bbee575e0b5e843bf6644"
|
||||||
|
}
|
4
packages/plugins/sequence-field/server.d.ts
vendored
Normal file
4
packages/plugins/sequence-field/server.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
export * from './lib/server';
|
||||||
|
export { default } from './lib/server';
|
||||||
|
|
30
packages/plugins/sequence-field/server.js
Normal file
30
packages/plugins/sequence-field/server.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
|
||||||
|
|
||||||
|
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
|
||||||
|
|
||||||
|
var _index = _interopRequireWildcard(require("./lib/server"));
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
var _exportNames = {};
|
||||||
|
Object.defineProperty(exports, "default", {
|
||||||
|
enumerable: true,
|
||||||
|
get: function get() {
|
||||||
|
return _index.default;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(_index).forEach(function (key) {
|
||||||
|
if (key === "default" || key === "__esModule") return;
|
||||||
|
if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return;
|
||||||
|
if (key in exports && exports[key] === _index[key]) return;
|
||||||
|
Object.defineProperty(exports, key, {
|
||||||
|
enumerable: true,
|
||||||
|
get: function get() {
|
||||||
|
return _index[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
23
packages/plugins/sequence-field/src/client/index.tsx
Normal file
23
packages/plugins/sequence-field/src/client/index.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
import { registerField, CollectionManagerContext, SchemaComponentOptions } from '@nocobase/client';
|
||||||
|
|
||||||
|
import { RuleConfigForm, sequence } from './sequence';
|
||||||
|
|
||||||
|
registerField(sequence.group, 'sequence', sequence);
|
||||||
|
|
||||||
|
export default function (props) {
|
||||||
|
const ctx = useContext(CollectionManagerContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SchemaComponentOptions
|
||||||
|
components={{
|
||||||
|
RuleConfigForm,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, sequence } }}>
|
||||||
|
{props.children}
|
||||||
|
</CollectionManagerContext.Provider>
|
||||||
|
</SchemaComponentOptions>
|
||||||
|
);
|
||||||
|
}
|
16
packages/plugins/sequence-field/src/client/locale/index.ts
Normal file
16
packages/plugins/sequence-field/src/client/locale/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { i18n } from '@nocobase/client';
|
||||||
|
|
||||||
|
import zhCN from './zh-CN';
|
||||||
|
|
||||||
|
export const NAMESPACE = 'sequence_field';
|
||||||
|
|
||||||
|
i18n.addResources('zh-CN', NAMESPACE, zhCN);
|
||||||
|
|
||||||
|
export function lang(key: string, options = {}) {
|
||||||
|
return i18n.t(key, { ...options, ns: NAMESPACE });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePluginTranslation() {
|
||||||
|
return useTranslation(NAMESPACE);
|
||||||
|
}
|
24
packages/plugins/sequence-field/src/client/locale/zh-CN.ts
Normal file
24
packages/plugins/sequence-field/src/client/locale/zh-CN.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export default {
|
||||||
|
"Sequence": "自动编码",
|
||||||
|
"Sequence rules": "编号规则",
|
||||||
|
"Add rule": "添加规则",
|
||||||
|
"Inputable": "可输入",
|
||||||
|
"Match rules": "输入必须匹配规则",
|
||||||
|
"Type": "类型",
|
||||||
|
"Autoincrement": "自增数字",
|
||||||
|
"Fixed text": "固定文本",
|
||||||
|
"Text content": "文本内容",
|
||||||
|
"Rule content": "规则内容",
|
||||||
|
"{{value}} Digits": "{{value}} 位数字",
|
||||||
|
"Digits": "位数",
|
||||||
|
"Start from": "起始于",
|
||||||
|
"Starts from {{value}}": "从 {{value}} 开始",
|
||||||
|
"Reset cycle": "重置周期",
|
||||||
|
"No reset": "不重置",
|
||||||
|
"Daily": "每天",
|
||||||
|
"Every Monday": "每周一",
|
||||||
|
"Monthly": "每月",
|
||||||
|
"Yearly": "每年",
|
||||||
|
"Operations": "操作",
|
||||||
|
"Customize": "自定义",
|
||||||
|
};
|
@ -1,14 +1,13 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
|
import { ArrayTable, FormButtonGroup, FormDrawer, FormLayout, Submit } from '@formily/antd';
|
||||||
import { onFieldValueChange } from '@formily/core';
|
import { onFieldValueChange } from '@formily/core';
|
||||||
import { SchemaOptionsContext, useForm, useFormEffects } from '@formily/react';
|
import { SchemaOptionsContext, useForm, useFormEffects, ISchema } from '@formily/react';
|
||||||
import { Button, Select } from 'antd';
|
import { Button, Select } from 'antd';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Cron, SchemaComponent, SchemaComponentOptions, useCompile } from '../../schema-component';
|
import { Cron, SchemaComponent, SchemaComponentOptions, useCompile, interfacesProperties, IField, useCollectionField } from '@nocobase/client';
|
||||||
import { defaultProps, operators, unique } from './properties';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import { IField } from './types';
|
|
||||||
|
|
||||||
function RuleTypeSelect(props) {
|
function RuleTypeSelect(props) {
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
@ -42,10 +41,10 @@ function RuleOptions() {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`}>
|
`}>
|
||||||
{Object.keys(options)
|
{Object.keys(options)
|
||||||
.filter(key => typeof options[key] !== 'undefined')
|
.filter(key => typeof options[key] !== 'undefined' && ruleType.optionRenders[key])
|
||||||
.map(key => {
|
.map(key => {
|
||||||
const Component = ruleType.optionRenders[key];
|
const Component = ruleType.optionRenders[key];
|
||||||
const { title } = ruleType.fieldset[key]
|
const { title } = ruleType.fieldset[key];
|
||||||
return Component
|
return Component
|
||||||
? (
|
? (
|
||||||
<dl key={key} className={css`
|
<dl key={key} className={css`
|
||||||
@ -70,7 +69,7 @@ function RuleOptions() {
|
|||||||
|
|
||||||
const RuleTypes = {
|
const RuleTypes = {
|
||||||
string: {
|
string: {
|
||||||
title: '{{t("Fixed text")}}',
|
title: `{{t("Fixed text", { ns: "${NAMESPACE}" })}}`,
|
||||||
optionRenders: {
|
optionRenders: {
|
||||||
value(options = { value: '' }) {
|
value(options = { value: '' }) {
|
||||||
return <code>{options.value}</code>;
|
return <code>{options.value}</code>;
|
||||||
@ -79,20 +78,20 @@ const RuleTypes = {
|
|||||||
fieldset: {
|
fieldset: {
|
||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '{{t("Text content")}}',
|
title: `{{t("Text content", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Input'
|
'x-component': 'Input'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
integer: {
|
integer: {
|
||||||
title: '{{t("Autoincrement")}}',
|
title: `{{t("Autoincrement", { ns: "${NAMESPACE}" })}}`,
|
||||||
optionRenders: {
|
optionRenders: {
|
||||||
digits({ value }) {
|
digits({ value }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{t('{{value}} Digits', { value })}
|
{t('{{value}} Digits', { ns: NAMESPACE, value })}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -100,7 +99,7 @@ const RuleTypes = {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{t('Starts from {{value}}', { value })}
|
{t('Starts from {{value}}', { ns: NAMESPACE, value })}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -120,7 +119,7 @@ const RuleTypes = {
|
|||||||
fieldset: {
|
fieldset: {
|
||||||
digits: {
|
digits: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
title: '{{t("Digits")}}',
|
title: `{{t("Digits", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'InputNumber',
|
'x-component': 'InputNumber',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
@ -128,25 +127,40 @@ const RuleTypes = {
|
|||||||
max: 10
|
max: 10
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
default: 1
|
default: 1,
|
||||||
|
'x-reactions': {
|
||||||
|
target: 'start',
|
||||||
|
fulfill: {
|
||||||
|
schema: {
|
||||||
|
'x-component-props.max': '{{ 10 ** $self.value - 1 }}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
start: {
|
start: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
title: '{{t("Start from")}}',
|
title: `{{t("Start from", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'InputNumber',
|
'x-component': 'InputNumber',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
min: 0
|
min: 0
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
default: 0
|
default: 0,
|
||||||
|
// 'x-reactions': {
|
||||||
|
// dependencies: ['.start', '.base'],
|
||||||
|
// fulfill: {
|
||||||
|
// schema: {
|
||||||
|
// 'x-component-props.max': '{{ ($deps[1] ?? 10) ** ($deps[0] ?? 1) - 1 }}'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
},
|
},
|
||||||
cycle: {
|
cycle: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: '{{t("Reset cycle")}}',
|
title: `{{t("Reset cycle", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
['x-component']({ value, onChange }) {
|
['x-component']({ value, onChange }) {
|
||||||
const { t } = useTranslation();
|
|
||||||
const shortValues = [
|
const shortValues = [
|
||||||
{ label: 'No reset', value: 0 },
|
{ label: 'No reset', value: 0 },
|
||||||
{ label: 'Daily', value: 1, cron: '0 0 * * *' },
|
{ label: 'Daily', value: 1, cron: '0 0 * * *' },
|
||||||
@ -164,7 +178,7 @@ const RuleTypes = {
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<Select value={option.value} onChange={(v) => onChange(shortValues[v].cron)}>
|
<Select value={option.value} onChange={(v) => onChange(shortValues[v].cron)}>
|
||||||
{shortValues.map(item => (
|
{shortValues.map(item => (
|
||||||
<Select.Option key={item.value} value={item.value}>{t(item.label)}</Select.Option>
|
<Select.Option key={item.value} value={item.value}>{lang(item.label)}</Select.Option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{option.value === 5
|
{option.value === 5
|
||||||
@ -184,7 +198,7 @@ const RuleTypes = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
title: '{{t("Date")}}',
|
title: `{{t("Date", { ns: "${NAMESPACE}" })}}`,
|
||||||
optionRenders: {
|
optionRenders: {
|
||||||
format(options = { value: 'YYYYMMDD' }) {
|
format(options = { value: 'YYYYMMDD' }) {
|
||||||
return <code>{options.value}</code>;
|
return <code>{options.value}</code>;
|
||||||
@ -193,7 +207,7 @@ const RuleTypes = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function RuleConfigForm() {
|
export function RuleConfigForm() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const schemaOptions = useContext(SchemaOptionsContext);
|
const schemaOptions = useContext(SchemaOptionsContext);
|
||||||
@ -253,7 +267,7 @@ export const sequence: IField = {
|
|||||||
type: 'object',
|
type: 'object',
|
||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
order: 3,
|
order: 3,
|
||||||
title: '{{t("Sequence")}}',
|
title: `{{t("Sequence", { ns: "${NAMESPACE}" })}}`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
default: {
|
default: {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
@ -261,19 +275,24 @@ export const sequence: IField = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
'x-component': 'Input',
|
'x-component': 'Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
readOnly: true,
|
|
||||||
disabled: true
|
|
||||||
},
|
},
|
||||||
'x-read-pretty': true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hasDefaultValue: false,
|
hasDefaultValue: false,
|
||||||
|
schemaInitialize(schema: ISchema, { block, field }) {
|
||||||
|
if (block === 'Form') {
|
||||||
|
Object.assign(schema['x-component-props'], {
|
||||||
|
disabled: !field.inputable,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return schema;
|
||||||
|
},
|
||||||
properties: {
|
properties: {
|
||||||
...defaultProps,
|
...interfacesProperties.defaultProps,
|
||||||
unique,
|
unique: interfacesProperties.unique,
|
||||||
patterns: {
|
patterns: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
title: '{{t("Sequence rules")}}',
|
title: `{{t("Sequence rules", { ns: "${NAMESPACE}" })}}`,
|
||||||
required: true,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'ArrayTable',
|
'x-component': 'ArrayTable',
|
||||||
@ -294,7 +313,7 @@ export const sequence: IField = {
|
|||||||
type: {
|
type: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'ArrayTable.Column',
|
'x-component': 'ArrayTable.Column',
|
||||||
'x-component-props': { title: '{{t("Type")}}' },
|
'x-component-props': { title: `{{t("Type", { ns: "${NAMESPACE}" })}}` },
|
||||||
// 'x-hidden': true,
|
// 'x-hidden': true,
|
||||||
properties: {
|
properties: {
|
||||||
type: {
|
type: {
|
||||||
@ -309,7 +328,7 @@ export const sequence: IField = {
|
|||||||
options: {
|
options: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'ArrayTable.Column',
|
'x-component': 'ArrayTable.Column',
|
||||||
'x-component-props': { title: '{{t("Rule content")}}' },
|
'x-component-props': { title: `{{t("Rule content", { ns: "${NAMESPACE}" })}}` },
|
||||||
properties: {
|
properties: {
|
||||||
options: {
|
options: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -322,7 +341,7 @@ export const sequence: IField = {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'ArrayTable.Column',
|
'x-component': 'ArrayTable.Column',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
title: '{{t("Operations")}}',
|
title: `{{t("Operations", { ns: "${NAMESPACE}" })}}`,
|
||||||
dataIndex: 'operations',
|
dataIndex: 'operations',
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
className: css`
|
className: css`
|
||||||
@ -337,7 +356,6 @@ export const sequence: IField = {
|
|||||||
properties: {
|
properties: {
|
||||||
config: {
|
config: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
// 'x-component': 'span',
|
|
||||||
properties: {
|
properties: {
|
||||||
options: {
|
options: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
@ -345,55 +363,6 @@ export const sequence: IField = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// configure: {
|
|
||||||
// type: 'void',
|
|
||||||
// title: '{{t("Configure")}}',
|
|
||||||
// 'x-component': 'Action.Link',
|
|
||||||
// properties: {
|
|
||||||
// drawer: {
|
|
||||||
// type: 'void',
|
|
||||||
// 'x-component': 'Action.Drawer',
|
|
||||||
// 'x-decorator': 'Form',
|
|
||||||
// 'x-decorator-props': {
|
|
||||||
// useValues: useRowOptions
|
|
||||||
// },
|
|
||||||
// title: '{{t("Configure")}}',
|
|
||||||
// properties: {
|
|
||||||
// options: {
|
|
||||||
// type: 'void',
|
|
||||||
// 'x-component': RuleConfig
|
|
||||||
// },
|
|
||||||
// actions: {
|
|
||||||
// type: 'void',
|
|
||||||
// 'x-component': 'Action.Drawer.Footer',
|
|
||||||
// properties: {
|
|
||||||
// cancel: {
|
|
||||||
// title: '{{t("Cancel")}}',
|
|
||||||
// 'x-component': 'Action',
|
|
||||||
// 'x-component-props': {
|
|
||||||
// // useAction: '{{ cm.useCancelAction }}',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// submit: {
|
|
||||||
// title: '{{t("Submit")}}',
|
|
||||||
// 'x-component': 'Action',
|
|
||||||
// 'x-component-props': {
|
|
||||||
// type: 'primary',
|
|
||||||
// async useAction() {
|
|
||||||
// const form = useForm();
|
|
||||||
// const ctx = useActionContext();
|
|
||||||
// await form.submit();
|
|
||||||
// console.log(form);
|
|
||||||
// ctx.setVisible(false);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
remove: {
|
remove: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'ArrayTable.Remove',
|
'x-component': 'ArrayTable.Remove',
|
||||||
@ -407,14 +376,35 @@ export const sequence: IField = {
|
|||||||
type: 'void',
|
type: 'void',
|
||||||
'x-component': 'ArrayTable.Addition',
|
'x-component': 'ArrayTable.Addition',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
defaultValue: { type: 'integer' }
|
defaultValue: { type: 'integer', options: { digits: 1, start: 0 } }
|
||||||
},
|
},
|
||||||
title: "{{t('Add rule')}}",
|
title: `{{t("Add rule", { ns: "${NAMESPACE}" })}}`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
inputable: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `{{t("Inputable", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
},
|
||||||
|
match: {
|
||||||
|
type: 'boolean',
|
||||||
|
title: `{{t("Match rules", { ns: "${NAMESPACE}" })}}`,
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
'x-reactions': {
|
||||||
|
dependencies: ['inputable'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
value: '{{$deps[0] && $self.value}}',
|
||||||
|
visible: '{{$deps[0] === true}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
filterable: {
|
filterable: {
|
||||||
operators: operators.string,
|
operators: interfacesProperties.operators.string,
|
||||||
}
|
}
|
||||||
};
|
};
|
1
packages/plugins/sequence-field/src/index.ts
Normal file
1
packages/plugins/sequence-field/src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './server';
|
99
packages/plugins/sequence-field/src/server/Plugin.ts
Normal file
99
packages/plugins/sequence-field/src/server/Plugin.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { randomInt } from 'crypto';
|
||||||
|
|
||||||
|
import { Registry } from '@nocobase/utils';
|
||||||
|
import { Plugin } from '@nocobase/server';
|
||||||
|
import { Pattern, SequenceField } from './fields/sequence-field';
|
||||||
|
|
||||||
|
const asyncRandomInt = promisify(randomInt);
|
||||||
|
|
||||||
|
export default class SequenceFieldPlugin extends Plugin {
|
||||||
|
patternTypes = new Registry<Pattern>();
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
const { app, db, options } = this;
|
||||||
|
|
||||||
|
db.registerFieldTypes({
|
||||||
|
sequence: SequenceField
|
||||||
|
});
|
||||||
|
|
||||||
|
db.addMigrations({
|
||||||
|
namespace: 'sequence-field',
|
||||||
|
directory: path.resolve(__dirname, 'migrations'),
|
||||||
|
context: {
|
||||||
|
plugin: this,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.import({
|
||||||
|
directory: path.resolve(__dirname, 'collections'),
|
||||||
|
});
|
||||||
|
|
||||||
|
db.on('fields.beforeSave', async (field, { transaction }) => {
|
||||||
|
if (field.get('type') !== 'sequence') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const patterns = (field.get('patterns') || []).filter(p => p.type === 'integer');
|
||||||
|
if (!patterns.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceRepo = db.getRepository('sequences');
|
||||||
|
await patterns.reduce((promise: Promise<any>, p) => promise.then(async () => {
|
||||||
|
if (p.options?.key == null) {
|
||||||
|
Object.assign(p, {
|
||||||
|
options: {
|
||||||
|
...p.options,
|
||||||
|
key: await asyncRandomInt(1 << 16)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}), Promise.resolve());
|
||||||
|
const sequences = await SequenceRepo.find({
|
||||||
|
filter: {
|
||||||
|
field: field.get('name'),
|
||||||
|
collection: field.get('collectionName'),
|
||||||
|
key: patterns.map(p => p.options.key),
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
await patterns.reduce((promise: Promise<any>, p) => promise.then(async () => {
|
||||||
|
if (!sequences.find(s => s.get('key') === p.options.key)) {
|
||||||
|
await SequenceRepo.create({
|
||||||
|
values: {
|
||||||
|
field: field.get('name'),
|
||||||
|
collection: field.get('collectionName'),
|
||||||
|
key: p.options.key,
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
await field.load({ transaction });
|
||||||
|
}
|
||||||
|
}), Promise.resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
db.on('fields.afterDestroy', async (field, { transaction }) => {
|
||||||
|
if (field.get('type') !== 'sequence') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const patterns = (field.get('patterns') || []).filter(p => p.type === 'integer');
|
||||||
|
if (!patterns.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SequenceRepo = db.getRepository('sequences');
|
||||||
|
await SequenceRepo.destroy({
|
||||||
|
filter: {
|
||||||
|
field: field.get('name'),
|
||||||
|
collection: field.get('collectionName'),
|
||||||
|
key: patterns.map(p => p.key),
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async install() {}
|
||||||
|
}
|
@ -1,13 +1,27 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
import { Database } from '../../database';
|
import { Application } from '@nocobase/server';
|
||||||
import { mockDatabase } from '..';
|
import { Database } from '@nocobase/database';
|
||||||
|
import { mockServer, mockDatabase } from '@nocobase/test';
|
||||||
|
|
||||||
describe('string field', () => {
|
import Plugin, { SequenceField, SequenceFieldOptions } from '..';
|
||||||
|
|
||||||
|
describe('sequence field', () => {
|
||||||
|
let app: Application;
|
||||||
let db: Database;
|
let db: Database;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
db = mockDatabase();
|
app = mockServer();
|
||||||
|
app.plugin(Plugin);
|
||||||
|
db = app.db;
|
||||||
|
await db.sync({
|
||||||
|
force: true,
|
||||||
|
alter: {
|
||||||
|
drop: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await app.load();
|
||||||
|
await app.start();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -69,6 +83,15 @@ describe('string field', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('integer pattern', () => {
|
describe('integer pattern', () => {
|
||||||
|
it.skip('no key', async () => {
|
||||||
|
expect(() => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'sequence', name: 'name', patterns: [{ type: 'integer' }] }],
|
||||||
|
});
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('default start from 0, digits as 1, no cycle', async () => {
|
it('default start from 0, digits as 1, no cycle', async () => {
|
||||||
db.collection({
|
db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
@ -78,7 +101,8 @@ describe('string field', () => {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
type: 'integer'
|
type: 'integer',
|
||||||
|
options: { key: 1 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -105,7 +129,8 @@ describe('string field', () => {
|
|||||||
{
|
{
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
options: {
|
options: {
|
||||||
start: 9
|
start: 9,
|
||||||
|
key: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -131,7 +156,8 @@ describe('string field', () => {
|
|||||||
name: 'name',
|
name: 'name',
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
type: 'integer'
|
type: 'integer',
|
||||||
|
options: { key: 1 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -139,8 +165,10 @@ describe('string field', () => {
|
|||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
const field = collection.getField('name');
|
const field = collection.getField('name');
|
||||||
// set current option in memory
|
const SeqRepo = db.getRepository('sequences');
|
||||||
field.options.patterns[0].options = { current: 9 };
|
await SeqRepo.create({
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
const TestModel = db.getModel('tests');
|
const TestModel = db.getModel('tests');
|
||||||
const item1 = await TestModel.create();
|
const item1 = await TestModel.create();
|
||||||
@ -159,7 +187,8 @@ describe('string field', () => {
|
|||||||
type: 'integer',
|
type: 'integer',
|
||||||
options: {
|
options: {
|
||||||
digits: 2,
|
digits: 2,
|
||||||
start: 9
|
start: 9,
|
||||||
|
key: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -187,7 +216,8 @@ describe('string field', () => {
|
|||||||
{
|
{
|
||||||
type: 'integer',
|
type: 'integer',
|
||||||
options: {
|
options: {
|
||||||
cycle: '0 0 * * * *'
|
cycle: '0 0 0 * * *',
|
||||||
|
key: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -201,19 +231,21 @@ describe('string field', () => {
|
|||||||
|
|
||||||
const TestModel = db.getModel('tests');
|
const TestModel = db.getModel('tests');
|
||||||
const item1 = await TestModel.create({
|
const item1 = await TestModel.create({
|
||||||
|
id: 1,
|
||||||
createdAt: yesterday
|
createdAt: yesterday
|
||||||
});
|
});
|
||||||
expect(item1.get('name')).toBe('0');
|
expect(item1.get('name')).toBe('0');
|
||||||
|
|
||||||
const item2 = await TestModel.create({
|
const item2 = await TestModel.create({
|
||||||
|
id: 2,
|
||||||
createdAt: yesterday
|
createdAt: yesterday
|
||||||
});
|
});
|
||||||
expect(item2.get('name')).toBe('1');
|
expect(item2.get('name')).toBe('1');
|
||||||
|
|
||||||
const item3 = await TestModel.create();
|
const item3 = await TestModel.create({ id: 3 });
|
||||||
expect(item3.get('name')).toBe('0');
|
expect(item3.get('name')).toBe('0');
|
||||||
|
|
||||||
const item4 = await TestModel.create();
|
const item4 = await TestModel.create({ id: 4 });
|
||||||
expect(item4.get('name')).toBe('1');
|
expect(item4.get('name')).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -232,7 +264,8 @@ describe('string field', () => {
|
|||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{
|
{
|
||||||
type: 'integer'
|
type: 'integer',
|
||||||
|
options: { key: 1 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -241,6 +274,64 @@ describe('string field', () => {
|
|||||||
const item2 = await TestModel.create();
|
const item2 = await TestModel.create();
|
||||||
expect(item2.get('name')).toBe('0');
|
expect(item2.get('name')).toBe('0');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('deleted sequence should be skipped', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: { key: 1 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('0');
|
||||||
|
|
||||||
|
await item1.destroy();
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple integer in same field', async () => {
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: { key: 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'integer',
|
||||||
|
options: { key: 2 }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.get('name')).toBe('00');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create();
|
||||||
|
expect(item2.get('name')).toBe('11');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('date pattern', () => {
|
describe('date pattern', () => {
|
||||||
@ -331,7 +422,7 @@ describe('string field', () => {
|
|||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'A' } },
|
{ type: 'string', options: { value: 'A' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer' }
|
{ type: 'integer', options: { key: 1 } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -359,7 +450,7 @@ describe('string field', () => {
|
|||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'A' } },
|
{ type: 'string', options: { value: 'A' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer' }
|
{ type: 'integer', options: { key: 1 } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -373,53 +464,66 @@ describe('string field', () => {
|
|||||||
const item1 = await TestModel.create();
|
const item1 = await TestModel.create();
|
||||||
expect(item1.get('name')).toBe(`A${YYYYMMDD}0`);
|
expect(item1.get('name')).toBe(`A${YYYYMMDD}0`);
|
||||||
|
|
||||||
testsCollection.setField('name', {
|
const f2 = testsCollection.setField('name', {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'A' } },
|
{ type: 'string', options: { value: 'A' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
// change options but no difference with default
|
// change options but no difference with default with new key
|
||||||
{ type: 'integer', options: { digits: 1 } }
|
{ type: 'integer', options: { digits: 1, key: 2 } }
|
||||||
]
|
]
|
||||||
});
|
}) as SequenceField;
|
||||||
|
|
||||||
const item2 = await TestModel.create();
|
const item2 = await TestModel.create();
|
||||||
expect(item2.get('name')).toBe(`A${YYYYMMDD}1`);
|
expect(item2.get('name')).toBe(`A${YYYYMMDD}0`);
|
||||||
|
|
||||||
testsCollection.setField('name', {
|
const f3 = testsCollection.setField('name', {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'A' } },
|
{ type: 'string', options: { value: 'A' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer', options: { digits: 2 } }
|
// change options but no difference with default with key
|
||||||
|
{ type: 'integer', options: { digits: 1, key: f2.options.patterns[2].options.key } }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const item3 = await TestModel.create();
|
const item3 = await TestModel.create();
|
||||||
expect(item3.get('name')).toBe(`A${YYYYMMDD}00`);
|
expect(item3.get('name')).toBe(`A${YYYYMMDD}1`);
|
||||||
|
|
||||||
|
const f4 = testsCollection.setField('name', {
|
||||||
|
type: 'sequence',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { digits: 2, key: 3 } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const item4 = await TestModel.create();
|
||||||
|
expect(item4.get('name')).toBe(`A${YYYYMMDD}00`);
|
||||||
|
|
||||||
testsCollection.setField('name', {
|
testsCollection.setField('name', {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'a' } },
|
{ type: 'string', options: { value: 'a' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer', options: { digits: 2 } }
|
{ type: 'integer', options: { digits: 2, key: f4.options.patterns[2].options.key } }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const item4 = await TestModel.create();
|
const item5 = await TestModel.create();
|
||||||
expect(item4.get('name')).toBe(`a${YYYYMMDD}01`);
|
expect(item5.get('name')).toBe(`a${YYYYMMDD}01`);
|
||||||
|
|
||||||
testsCollection.setField('name', {
|
testsCollection.setField('name', {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer', options: { digits: 2 } }
|
{ type: 'integer', options: { digits: 2, key: 4 } }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const item5 = await TestModel.create();
|
const item6 = await TestModel.create();
|
||||||
expect(item5.get('name')).toBe(`${YYYYMMDD}00`);
|
expect(item6.get('name')).toBe(`${YYYYMMDD}00`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -434,7 +538,7 @@ describe('string field', () => {
|
|||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'A' } },
|
{ type: 'string', options: { value: 'A' } },
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer', options: { digits: 2, cycle: '0 0 * * *' } }
|
{ type: 'integer', options: { digits: 2, cycle: '0 0 * * *', key: 1 } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -442,7 +546,7 @@ describe('string field', () => {
|
|||||||
name: 'code',
|
name: 'code',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'C' } },
|
{ type: 'string', options: { value: 'C' } },
|
||||||
{ type: 'integer', options: { digits: 4 }}
|
{ type: 'integer', options: { digits: 4, key: 1 }}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -466,15 +570,188 @@ describe('string field', () => {
|
|||||||
testsCollection.setField('name', {
|
testsCollection.setField('name', {
|
||||||
type: 'sequence',
|
type: 'sequence',
|
||||||
patterns: [
|
patterns: [
|
||||||
{ type: 'string', options: { value: 'a' } },
|
|
||||||
{ type: 'date' },
|
{ type: 'date' },
|
||||||
{ type: 'integer', options: { digits: 1 } }
|
{ type: 'integer', options: { digits: 1, key: 1 } },
|
||||||
|
{ type: 'string', options: { value: 'a' } },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
const item3 = await TestModel.create();
|
const item3 = await TestModel.create();
|
||||||
expect(item3.get('name')).toBe(`a${NOW}0`);
|
expect(item3.get('name')).toBe(`${NOW}1a`);
|
||||||
expect(item3.get('code')).toBe(`C0002`);
|
expect(item3.get('code')).toBe(`C0002`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('inputable', () => {
|
||||||
|
it('not inputable', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { key: 1 } }
|
||||||
|
],
|
||||||
|
inputable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
const name = `BB${YYYYMMDD}11`;
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const result = await TestModel.create({ name });
|
||||||
|
expect(result.name).toBe(`A${YYYYMMDD}0`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inputable without match', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { key: 1 } }
|
||||||
|
],
|
||||||
|
inputable: true,
|
||||||
|
match: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
const name = `BB${YYYYMMDD}11`;
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const result = await TestModel.create({ name });
|
||||||
|
expect(result.name).toBe(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('inputable with match', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'string', options: { value: 'A' } },
|
||||||
|
{ type: 'date' },
|
||||||
|
{ type: 'integer', options: { key: 1 } }
|
||||||
|
],
|
||||||
|
inputable: true,
|
||||||
|
match: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const YYYYMMDD = moment(now).format('YYYYMMDD');
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
await expect(TestModel.create({ name: `BB${YYYYMMDD}11` })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input value within generated sequence', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'integer', options: { key: 1 } }
|
||||||
|
],
|
||||||
|
inputable: true,
|
||||||
|
match: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.name).toBe('0');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create({ name: '0' });
|
||||||
|
expect(item2.name).toBe('0');
|
||||||
|
|
||||||
|
const item3 = await TestModel.create();
|
||||||
|
expect(item3.name).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input value beyond generated sequence', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'integer', options: { key: 1 } }
|
||||||
|
],
|
||||||
|
inputable: true,
|
||||||
|
match: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create();
|
||||||
|
expect(item1.name).toBe('0');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create({ name: '2' });
|
||||||
|
expect(item2.name).toBe('2');
|
||||||
|
|
||||||
|
const item3 = await TestModel.create();
|
||||||
|
expect(item3.name).toBe('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('input value with cycle', async () => {
|
||||||
|
const testsCollection = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'sequence',
|
||||||
|
name: 'name',
|
||||||
|
patterns: [
|
||||||
|
{ type: 'integer', options: { key: 1, cycle: '0 0 0 * * *' } }
|
||||||
|
],
|
||||||
|
inputable: true,
|
||||||
|
match: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const TestModel = db.getModel('tests');
|
||||||
|
const item1 = await TestModel.create({ createdAt: yesterday });
|
||||||
|
expect(item1.name).toBe('0');
|
||||||
|
|
||||||
|
const item2 = await TestModel.create({ name: '0', createdAt: yesterday });
|
||||||
|
expect(item2.name).toBe('0');
|
||||||
|
|
||||||
|
const item3 = await TestModel.create({ createdAt: yesterday });
|
||||||
|
expect(item3.name).toBe('1');
|
||||||
|
|
||||||
|
const item4 = await TestModel.create();
|
||||||
|
expect(item4.name).toBe('0');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
@ -0,0 +1,25 @@
|
|||||||
|
export default {
|
||||||
|
name: 'sequences',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'collection',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'field',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'key',
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'current',
|
||||||
|
type: 'bigInt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lastGeneratedAt',
|
||||||
|
type: 'date'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
@ -0,0 +1,280 @@
|
|||||||
|
import { DataTypes, Transactionable, ValidationError, ValidationErrorItem } from 'sequelize';
|
||||||
|
import parser from 'cron-parser';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { escapeRegExp } from 'lodash';
|
||||||
|
|
||||||
|
import { Registry } from '@nocobase/utils';
|
||||||
|
import { Model, BaseColumnFieldOptions, Field, FieldContext } from '@nocobase/database';
|
||||||
|
|
||||||
|
|
||||||
|
export interface Pattern {
|
||||||
|
validate?(options): string | null;
|
||||||
|
generate(this: SequenceField, instance: Model, index: number, options: Transactionable): Promise<string> | string;
|
||||||
|
getLength(options): number;
|
||||||
|
getMatcher(options): string;
|
||||||
|
update?(this: SequenceField, instance: Model, value: string, options, transactionable: Transactionable): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sequencePatterns = new Registry<Pattern>();
|
||||||
|
|
||||||
|
sequencePatterns.register('string', {
|
||||||
|
validate(options) {
|
||||||
|
if (!options?.value) {
|
||||||
|
return 'options.value should be configured as a non-empty string';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
generate(instance, index) {
|
||||||
|
const { options } = this.options.patterns[index];
|
||||||
|
return options.value;
|
||||||
|
},
|
||||||
|
getLength(options) {
|
||||||
|
return options.value.length;
|
||||||
|
},
|
||||||
|
getMatcher(options) {
|
||||||
|
return escapeRegExp(options.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencePatterns.register('integer', {
|
||||||
|
// validate(options) {
|
||||||
|
// if (!options?.key) {
|
||||||
|
// return 'options.key should be configured as an integer';
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// },
|
||||||
|
async generate(this: SequenceField, instance: Model, index, { transaction }) {
|
||||||
|
const recordTime = <Date>instance.get('createdAt');
|
||||||
|
const { options = {} } = this.options.patterns[index];
|
||||||
|
const { digits = 1, start = 0, base = 10, cycle, key } = options;
|
||||||
|
const max = Math.pow(base, digits) - 1;
|
||||||
|
const SeqRepo = this.database.getRepository('sequences');
|
||||||
|
const lastSeq = await SeqRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
collection: this.collection.name,
|
||||||
|
field: this.name,
|
||||||
|
key
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
|
||||||
|
let next;
|
||||||
|
if (lastSeq && lastSeq.get('current') != null) {
|
||||||
|
next = lastSeq.get('current') + 1;
|
||||||
|
|
||||||
|
// cycle as cron string
|
||||||
|
if (cycle) {
|
||||||
|
const interval = parser.parseExpression(cycle, { currentDate: <Date>lastSeq.get('lastGeneratedAt') });
|
||||||
|
const nextTime = interval.next();
|
||||||
|
if (recordTime.getTime() >= nextTime.getTime()) {
|
||||||
|
next = start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next > max) {
|
||||||
|
next = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update options
|
||||||
|
if (lastSeq) {
|
||||||
|
await lastSeq.update({ current: next, lastGeneratedAt: recordTime }, { transaction });
|
||||||
|
} else {
|
||||||
|
await SeqRepo.create({
|
||||||
|
values: {
|
||||||
|
collection: this.collection.name,
|
||||||
|
field: this.name,
|
||||||
|
key,
|
||||||
|
current: next,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.toString(base).padStart(digits, '0');
|
||||||
|
},
|
||||||
|
|
||||||
|
getLength({ digits = 1 } = {}) {
|
||||||
|
return digits;
|
||||||
|
},
|
||||||
|
|
||||||
|
getMatcher(options = {}) {
|
||||||
|
const { digits = 1, start = 0, base = 10 } = options;
|
||||||
|
const chars = '0123456789abcdefghijklmnopqrstuvwxyz'.slice(0, base);
|
||||||
|
return `[${chars}]{${digits}}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async update(instance, value, options, { transaction }) {
|
||||||
|
const recordTime = <Date>instance.get('createdAt');
|
||||||
|
const { digits = 1, start = 0, base = 10, cycle, key } = options;
|
||||||
|
const SeqRepo = this.database.getRepository('sequences');
|
||||||
|
const lastSeq = await SeqRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
collection: this.collection.name,
|
||||||
|
field: this.name,
|
||||||
|
key
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
const current = Number.parseInt(value, base);
|
||||||
|
if (!lastSeq) {
|
||||||
|
return SeqRepo.create({
|
||||||
|
values: {
|
||||||
|
collection: this.collection.name,
|
||||||
|
field: this.name,
|
||||||
|
key,
|
||||||
|
current,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (lastSeq.get('current') == null) {
|
||||||
|
return lastSeq.update({
|
||||||
|
current,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
}, { transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cycle) {
|
||||||
|
const interval = parser.parseExpression(cycle, { currentDate: <Date>lastSeq.get('lastGeneratedAt') });
|
||||||
|
const nextTime = interval.next();
|
||||||
|
if (recordTime.getTime() >= nextTime.getTime()) {
|
||||||
|
lastSeq.set({
|
||||||
|
current,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (current > lastSeq.get('current')) {
|
||||||
|
lastSeq.set({
|
||||||
|
current,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (current > lastSeq.get('current')) {
|
||||||
|
lastSeq.set({
|
||||||
|
current,
|
||||||
|
lastGeneratedAt: recordTime
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastSeq.save({ transaction });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sequencePatterns.register('date', {
|
||||||
|
generate(this: SequenceField, instance, index) {
|
||||||
|
const { options } = this.options.patterns[index];
|
||||||
|
return moment(instance.get(options?.field ?? 'createdAt')).format(options?.format ?? 'YYYYMMDD');
|
||||||
|
},
|
||||||
|
getLength(options) {
|
||||||
|
return options.format?.length ?? 8;
|
||||||
|
},
|
||||||
|
getMatcher(options = {}) {
|
||||||
|
return `.{${options?.format?.length ?? 8}}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PatternConfig {
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
options?: any;
|
||||||
|
}
|
||||||
|
export interface SequenceFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'sequence';
|
||||||
|
patterns: PatternConfig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SequenceField extends Field {
|
||||||
|
matcher: RegExp;
|
||||||
|
|
||||||
|
get dataType() {
|
||||||
|
return DataTypes.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: SequenceFieldOptions, context: FieldContext) {
|
||||||
|
super(options, context);
|
||||||
|
if (!options.patterns || !options.patterns.length) {
|
||||||
|
throw new Error('at least one pattern should be defined for sequence type');
|
||||||
|
}
|
||||||
|
options.patterns.forEach(pattern => {
|
||||||
|
const P = sequencePatterns.get(pattern.type);
|
||||||
|
if (!P) {
|
||||||
|
throw new Error(`pattern type ${pattern.type} is not registered`);
|
||||||
|
}
|
||||||
|
if (P.validate) {
|
||||||
|
const error = P.validate(pattern.options);
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const patterns = options.patterns
|
||||||
|
.map(({ type, options }) => sequencePatterns.get(type).getMatcher(options));
|
||||||
|
this.matcher = new RegExp(`^${patterns.map(p => `(${p})`).join('')}$`, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
validate = (instance: Model) => {
|
||||||
|
const { name, inputable, match } = this.options;
|
||||||
|
const value = instance.get(name);
|
||||||
|
if (value != null && inputable && match && !this.match(value)) {
|
||||||
|
throw new ValidationError('sequence pattern not match', [
|
||||||
|
new ValidationErrorItem(
|
||||||
|
`input value of ${name} field not match the sequence pattern (${this.matcher.toString()}) which is required`,
|
||||||
|
'Validation error', // NOTE: type should only be this which in sequelize enum set
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
instance,
|
||||||
|
'sequence_pattern_not_match'
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setValue = async (instance: Model, options) => {
|
||||||
|
const { name, patterns, inputable, match } = this.options;
|
||||||
|
const value = instance.get(name);
|
||||||
|
if (value != null && inputable) {
|
||||||
|
return this.update(instance, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await patterns.reduce((promise, p, i) => promise.then(async (result) => {
|
||||||
|
const item = await (sequencePatterns.get(p.type)).generate.call(this, instance, i, options);
|
||||||
|
return result.concat(item);
|
||||||
|
}), Promise.resolve([]));
|
||||||
|
instance.set(name, results.join(''));
|
||||||
|
};
|
||||||
|
|
||||||
|
match(value) {
|
||||||
|
return typeof value === 'string' ? value.match(this.matcher) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(instance: Model, options) {
|
||||||
|
const { name, patterns } = this.options;
|
||||||
|
const matched = this.match(instance.get(name));
|
||||||
|
if (matched) {
|
||||||
|
await matched.slice(1)
|
||||||
|
.map((_, i) => sequencePatterns.get(patterns[i].type).update).filter(Boolean)
|
||||||
|
.reduce((promise, update, i) => promise.then(() => update!.call(this, instance, matched[i + 1], patterns[i].options, options)), Promise.resolve());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind() {
|
||||||
|
super.bind();
|
||||||
|
this.on('beforeValidate', this.validate);
|
||||||
|
this.on('beforeCreate', this.setValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind() {
|
||||||
|
super.unbind();
|
||||||
|
this.off('beforeValidate', this.validate);
|
||||||
|
this.off('beforeCreate', this.setValue);
|
||||||
|
}
|
||||||
|
}
|
2
packages/plugins/sequence-field/src/server/index.ts
Normal file
2
packages/plugins/sequence-field/src/server/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './fields/sequence-field';
|
||||||
|
export { default } from './Plugin';
|
@ -0,0 +1,65 @@
|
|||||||
|
import { Migration } from '@nocobase/server';
|
||||||
|
|
||||||
|
export default class extends Migration {
|
||||||
|
async up() {
|
||||||
|
const match = await this.app.version.satisfies('<=0.8.0-alpha.13');
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { db } = this.context;
|
||||||
|
|
||||||
|
const fieldRepo = db.getRepository('fields');
|
||||||
|
if (!fieldRepo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pluginRepo = db.getRepository('applicationPlugins');
|
||||||
|
await db.sequelize.transaction(async (transaction) => {
|
||||||
|
const seqPlugin = await pluginRepo.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'sequence-field'
|
||||||
|
},
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!seqPlugin) {
|
||||||
|
await pluginRepo.create({
|
||||||
|
values: {
|
||||||
|
name: 'sequence-field',
|
||||||
|
version: '0.8.0-alpha.13',
|
||||||
|
enabled: true,
|
||||||
|
installed: true,
|
||||||
|
builtIn: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = await fieldRepo.find({
|
||||||
|
filter: {
|
||||||
|
type: 'sequence'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await fields.reduce((promise, field) => promise.then(async () => {
|
||||||
|
const options = field.get('options');
|
||||||
|
const fieldName = field.get('name');
|
||||||
|
const collectionName = field.get('collectionName');
|
||||||
|
// NOTE: cannot use .update because no changes are made, only for forcing to trigger beforeSave hook.
|
||||||
|
field.set('patterns', options.patterns);
|
||||||
|
await field.save({ transaction });
|
||||||
|
|
||||||
|
const repo = db.getRepository(collectionName);
|
||||||
|
const item = await repo.findOne({
|
||||||
|
sort: ['-createdAt'],
|
||||||
|
transaction
|
||||||
|
});
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const collection = db.getCollection(collectionName);
|
||||||
|
const memField = collection.getField(fieldName);
|
||||||
|
await memField.update(item, { transaction });
|
||||||
|
}), Promise.resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async down() {}
|
||||||
|
}
|
@ -29,7 +29,7 @@ function migrateConfig(config) {
|
|||||||
return Object.keys(config).reduce((memo, key) => ({ ...memo, [key]: migrateConfig(config[key]) }), {});
|
return Object.keys(config).reduce((memo, key) => ({ ...memo, [key]: migrateConfig(config[key]) }), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddUsersPhoneMigration extends Migration {
|
export default class extends Migration {
|
||||||
async up() {
|
async up() {
|
||||||
const match = await this.app.version.satisfies('<=0.8.0-alpha.13');
|
const match = await this.app.version.satisfies('<=0.8.0-alpha.13');
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"@nocobase/plugin-saml": "0.8.0-alpha.13",
|
"@nocobase/plugin-saml": "0.8.0-alpha.13",
|
||||||
"@nocobase/plugin-import": "0.8.0-alpha.13",
|
"@nocobase/plugin-import": "0.8.0-alpha.13",
|
||||||
"@nocobase/plugin-sample-hello": "0.8.0-alpha.13",
|
"@nocobase/plugin-sample-hello": "0.8.0-alpha.13",
|
||||||
|
"@nocobase/plugin-sequence-field": "0.8.0-alpha.13",
|
||||||
"@nocobase/plugin-system-settings": "0.8.0-alpha.13",
|
"@nocobase/plugin-system-settings": "0.8.0-alpha.13",
|
||||||
"@nocobase/plugin-ui-routes-storage": "0.8.0-alpha.13",
|
"@nocobase/plugin-ui-routes-storage": "0.8.0-alpha.13",
|
||||||
"@nocobase/plugin-ui-schema-storage": "0.8.0-alpha.13",
|
"@nocobase/plugin-ui-schema-storage": "0.8.0-alpha.13",
|
||||||
|
@ -12,6 +12,7 @@ export class PresetNocoBase extends Plugin {
|
|||||||
'ui-routes-storage',
|
'ui-routes-storage',
|
||||||
'file-manager',
|
'file-manager',
|
||||||
'system-settings',
|
'system-settings',
|
||||||
|
'sequence-field',
|
||||||
'verification',
|
'verification',
|
||||||
'users',
|
'users',
|
||||||
'acl',
|
'acl',
|
||||||
|
Loading…
Reference in New Issue
Block a user