feat(db): field value parser
This commit is contained in:
parent
0ac351bbd2
commit
5805b69455
@ -15,7 +15,7 @@ import {
|
||||
Sequelize,
|
||||
SyncOptions,
|
||||
Transactionable,
|
||||
Utils,
|
||||
Utils
|
||||
} from 'sequelize';
|
||||
import { SequelizeStorage, Umzug } from 'umzug';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
@ -58,11 +58,12 @@ import {
|
||||
SyncListener,
|
||||
UpdateListener,
|
||||
UpdateWithAssociationsListener,
|
||||
ValidateListener,
|
||||
ValidateListener
|
||||
} from './types';
|
||||
import { patchSequelizeQueryInterface, snakeCase } from './utils';
|
||||
|
||||
import DatabaseUtils from './database-utils';
|
||||
import { BaseValueParser, registerFieldValueParsers } from './value-parsers';
|
||||
|
||||
export interface MergeOptions extends merge.Options {}
|
||||
|
||||
@ -150,6 +151,7 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
migrator: Umzug;
|
||||
migrations: Migrations;
|
||||
fieldTypes = new Map();
|
||||
fieldValueParsers = new Map();
|
||||
options: IDatabaseOptions;
|
||||
models = new Map<string, ModelStatic<Model>>();
|
||||
repositories = new Map<string, RepositoryType>();
|
||||
@ -227,6 +229,8 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
registerFieldValueParsers(this);
|
||||
|
||||
this.initOperators();
|
||||
|
||||
const migratorOptions: any = this.options.migrator || {};
|
||||
@ -473,6 +477,20 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
registerFieldValueParsers(parsers: MapOf<any>) {
|
||||
for (const [type, parser] of Object.entries(parsers)) {
|
||||
this.fieldValueParsers.set(type, parser);
|
||||
}
|
||||
}
|
||||
|
||||
buildFieldValueParser<T extends BaseValueParser>(field: Field, ctx: any) {
|
||||
const Parser = this.fieldValueParsers.has(field.type)
|
||||
? this.fieldValueParsers.get(field.type)
|
||||
: this.fieldValueParsers.get('default');
|
||||
const parser = new Parser(field, ctx);
|
||||
return parser as T;
|
||||
}
|
||||
|
||||
registerModels(models: MapOf<ModelStatic<any>>) {
|
||||
for (const [type, schemaType] of Object.entries(models)) {
|
||||
this.models.set(type, schemaType);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class ArrayField extends Field {
|
||||
get dataType() {
|
||||
@ -28,51 +28,6 @@ export class ArrayField extends Field {
|
||||
super.unbind();
|
||||
this.off('beforeSave', this.sortValue);
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ArrayValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class ArrayValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
const { map, set } = this.getOptions();
|
||||
const values = this.toArr(value);
|
||||
if (set.size > 0) {
|
||||
const filtered = values.map((v) => (map.has(v) ? map.get(v) : v)).filter((v) => set.has(v));
|
||||
if (values.length === filtered.length) {
|
||||
this.value = filtered;
|
||||
} else {
|
||||
this.errors.push('No matching option found');
|
||||
}
|
||||
} else {
|
||||
this.value = values;
|
||||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||
const map = new Map();
|
||||
const set = new Set();
|
||||
for (const option of options) {
|
||||
set.add(option.value);
|
||||
set.add(option.label);
|
||||
map.set(option.label, option.value);
|
||||
}
|
||||
return { map, set };
|
||||
}
|
||||
|
||||
toArr(value) {
|
||||
let values = [];
|
||||
if (!value) {
|
||||
values = [];
|
||||
} else if (typeof value === 'string') {
|
||||
values = value.split(',');
|
||||
} else if (Array.isArray(value)) {
|
||||
values = value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ArrayFieldOptions extends BaseColumnFieldOptions {
|
||||
|
@ -2,7 +2,6 @@ import { omit } from 'lodash';
|
||||
import { BelongsToOptions as SequelizeBelongsToOptions, Utils } from 'sequelize';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { ToOneValueParser } from './has-one-field';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToField extends RelationField {
|
||||
@ -116,10 +115,6 @@ export class BelongsToField extends RelationField {
|
||||
collection.model.refreshAttributes();
|
||||
// this.collection.removeIndex([this.options.foreignKey]);
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ToOneValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export interface BelongsToFieldOptions extends BaseRelationFieldOptions, SequelizeBelongsToOptions {
|
||||
|
@ -2,10 +2,8 @@ import { omit } from 'lodash';
|
||||
import { BelongsToManyOptions as SequelizeBelongsToManyOptions, Utils } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { Repository } from '../repository';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { BelongsToField } from './belongs-to-field';
|
||||
import { ValueParser } from './field';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToManyField extends RelationField {
|
||||
@ -125,48 +123,6 @@ export class BelongsToManyField extends RelationField {
|
||||
this.clearAccessors();
|
||||
delete collection.model.associations[this.name];
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ToManyValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class ToManyValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
const fieldNames = this.getFileNames();
|
||||
console.log('fieldNames', fieldNames, this.ctx.column);
|
||||
if (this.isInterface('chinaRegion')) {
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
this.value = await Promise.all(
|
||||
value.split('/').map(async (v) => {
|
||||
const instance = await repository.findOne({ filter: { [fieldNames.label]: v.trim() } });
|
||||
return instance ? instance.get(fieldNames.value) : v;
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||
return;
|
||||
}
|
||||
const field = this.ctx.column.dataIndex[1];
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
this.value = await Promise.all(
|
||||
value.split(',').map(async (v) => {
|
||||
const instance = await repository.findOne({ filter: { [field]: v.trim() } });
|
||||
return instance ? instance.get(fieldNames.value) : v;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {};
|
||||
return { label: 'id', value: 'id', ...fieldNames };
|
||||
}
|
||||
|
||||
isInterface(name) {
|
||||
return this.field.options.interface === name;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BelongsToManyFieldOptions
|
||||
|
@ -1,42 +1,10 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class BooleanField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.BOOLEAN;
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new BooleanValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class BooleanValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
// Boolean
|
||||
if (typeof value === 'boolean') {
|
||||
this.value = value;
|
||||
}
|
||||
// Number
|
||||
else if (typeof value === 'number' && [0, 1].includes(value)) {
|
||||
this.value = value === 1;
|
||||
}
|
||||
// String
|
||||
else if (typeof value === 'string') {
|
||||
if (!value) {
|
||||
this.value = null;
|
||||
}
|
||||
if (['1', 'y', 'yes', 'true', '是'].includes(value.toLowerCase())) {
|
||||
this.value = true;
|
||||
} else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
|
||||
this.value = false;
|
||||
} else {
|
||||
this.errors.push(`${JSON.stringify(this.value)} value invalid`);
|
||||
}
|
||||
} else {
|
||||
this.errors.push(`${JSON.stringify(this.value)} value invalid`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface BooleanFieldOptions extends BaseColumnFieldOptions {
|
||||
|
@ -1,38 +1,10 @@
|
||||
import { moment2str } from '@nocobase/utils';
|
||||
import moment, { isDate, isMoment } from 'moment';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class DateField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.DATE(3);
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new DateValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class DateValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
if (isMoment(value)) {
|
||||
this.value = value;
|
||||
} else if (isDate(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
const props = this.getProps();
|
||||
const m = moment(value);
|
||||
if (m.isValid()) {
|
||||
this.value = moment2str(m, props);
|
||||
} else {
|
||||
this.errors.push('Invalid date');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.field.options?.uiSchema?.['x-component-props'] || {};
|
||||
}
|
||||
}
|
||||
|
||||
export interface DateFieldOptions extends BaseColumnFieldOptions {
|
||||
|
@ -238,33 +238,4 @@ export abstract class Field {
|
||||
typeToString() {
|
||||
return this.dataType.toString();
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueParser {
|
||||
ctx: any;
|
||||
field: any;
|
||||
value: any;
|
||||
errors = [];
|
||||
|
||||
constructor(field: any, ctx: any) {
|
||||
this.field = field;
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
async setValue(value: any) {
|
||||
console.log(this.field.name, value);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { ToManyValueParser } from './belongs-to-many-field';
|
||||
import { MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export interface HasManyFieldOptions extends HasManyOptions {
|
||||
@ -186,10 +185,6 @@ export class HasManyField extends RelationField {
|
||||
// @ts-ignore
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ToManyValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions {
|
||||
|
@ -9,9 +9,7 @@ import {
|
||||
} from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { Reference } from '../features/ReferencesMap';
|
||||
import { Repository } from '../repository';
|
||||
import { checkIdentifier } from '../utils';
|
||||
import { ValueParser } from './field';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export interface HasOneFieldOptions extends HasOneOptions {
|
||||
@ -193,36 +191,6 @@ export class HasOneField extends RelationField {
|
||||
// @ts-ignore
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new ToOneValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class ToOneValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
const fieldNames = this.getFileNames();
|
||||
console.log('fieldNames', fieldNames, this.ctx.column);
|
||||
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||
return;
|
||||
}
|
||||
const field = this.ctx.column.dataIndex[1];
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
const instance = await repository.findOne({ filter: { [field]: value.trim() } });
|
||||
if (instance) {
|
||||
this.value = instance.get(fieldNames.value);
|
||||
}
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {};
|
||||
return { label: 'id', value: 'id', ...fieldNames };
|
||||
}
|
||||
|
||||
isInterface(name) {
|
||||
return this.field.options.interface === name;
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasOneFieldOptions extends BaseRelationFieldOptions, SequelizeHasOneOptions {
|
||||
|
@ -1,41 +1,8 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
abstract class NumberField extends Field {
|
||||
buildValueParser(ctx: any) {
|
||||
return new NumberValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function percent2float(value: string) {
|
||||
const index = value.indexOf('.');
|
||||
if (index === -1) {
|
||||
return parseFloat(value) / 100;
|
||||
}
|
||||
const repeat = value.length - index - 2;
|
||||
const v = parseInt('1' + '0'.repeat(repeat));
|
||||
return (parseFloat(value) * v) / (100 * v);
|
||||
}
|
||||
|
||||
export class NumberValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
if (value === null || value === undefined || typeof value === 'number') {
|
||||
this.value = value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
if (!value) {
|
||||
this.value = null;
|
||||
} else if (['n/a', '-'].includes(value.toLowerCase())) {
|
||||
this.value = null;
|
||||
} else if (value.endsWith('%')) {
|
||||
this.value = percent2float(value);
|
||||
console.log(value, this.value);
|
||||
} else {
|
||||
const val = +value;
|
||||
this.value = isNaN(val) ? null : val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class IntegerField extends NumberField {
|
||||
|
@ -1,44 +1,10 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field, ValueParser } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class StringField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
|
||||
buildValueParser(ctx: any) {
|
||||
return new StringValueParser(this, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
export class StringValueParser extends ValueParser {
|
||||
async setValue(value: any) {
|
||||
const { map, set } = this.getOptions();
|
||||
if (set.size > 0) {
|
||||
if (map.has(value)) {
|
||||
value = map.get(value);
|
||||
}
|
||||
if (set.has(value)) {
|
||||
this.value = value;
|
||||
} else {
|
||||
this.errors.push('No matching option found');
|
||||
}
|
||||
} else {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||
const map = new Map();
|
||||
const set = new Set();
|
||||
for (const option of options) {
|
||||
set.add(option.value);
|
||||
set.add(option.label);
|
||||
map.set(option.label, option.value);
|
||||
}
|
||||
return { map, set };
|
||||
}
|
||||
}
|
||||
|
||||
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
||||
|
@ -1,9 +1,12 @@
|
||||
export { DataTypes, ModelStatic, Op, SyncOptions } from 'sequelize';
|
||||
export * from './collection';
|
||||
export * from './inherited-collection';
|
||||
export * from './collection-importer';
|
||||
export * from './database';
|
||||
export { Database as default } from './database';
|
||||
export * from './field-repository/array-field-repository';
|
||||
export * from './fields';
|
||||
export * from './filter-match';
|
||||
export * from './inherited-collection';
|
||||
export * from './magic-attribute-model';
|
||||
export * from './migration';
|
||||
export * from './mock-database';
|
||||
@ -15,7 +18,6 @@ export * from './relation-repository/multiple-relation-repository';
|
||||
export * from './relation-repository/single-relation-repository';
|
||||
export * from './repository';
|
||||
export * from './update-associations';
|
||||
export * from './collection-importer';
|
||||
export * from './filter-match';
|
||||
export * from './field-repository/array-field-repository';
|
||||
export { snakeCase } from './utils';
|
||||
export * from './value-parsers';
|
||||
|
||||
|
@ -1,8 +1,7 @@
|
||||
import crypto from 'crypto';
|
||||
import Database from './database';
|
||||
import { IdentifierError } from './errors/identifier-error';
|
||||
import { Model } from './model';
|
||||
import lodash from 'lodash';
|
||||
import Database from './database';
|
||||
|
||||
type HandleAppendsQueryOptions = {
|
||||
templateModel: any;
|
||||
@ -116,3 +115,13 @@ export function patchSequelizeQueryInterface(db: Database) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function percent2float(value: string) {
|
||||
const index = value.indexOf('.');
|
||||
if (index === -1) {
|
||||
return parseFloat(value) / 100;
|
||||
}
|
||||
const repeat = value.length - index - 2;
|
||||
const v = parseInt('1' + '0'.repeat(repeat));
|
||||
return (parseFloat(value) * v) / (100 * v);
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class ArrayValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
const { map, set } = this.getOptions();
|
||||
const values = this.toArr(value);
|
||||
if (set.size > 0) {
|
||||
const filtered = values.map((v) => (map.has(v) ? map.get(v) : v)).filter((v) => set.has(v));
|
||||
if (values.length === filtered.length) {
|
||||
this.value = filtered;
|
||||
} else {
|
||||
this.errors.push(`No matching option found - ${JSON.stringify(value)}`);
|
||||
}
|
||||
} else {
|
||||
this.value = values;
|
||||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||
const map = new Map();
|
||||
const set = new Set();
|
||||
for (const option of options) {
|
||||
set.add(option.value);
|
||||
set.add(option.label);
|
||||
map.set(option.label, option.value);
|
||||
}
|
||||
return { map, set };
|
||||
}
|
||||
|
||||
toArr(value) {
|
||||
let values: string[] = [];
|
||||
if (!value) {
|
||||
values = [];
|
||||
} else if (typeof value === 'string') {
|
||||
values = value.split(',');
|
||||
} else if (Array.isArray(value)) {
|
||||
values = value;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
export class BaseValueParser {
|
||||
ctx: any;
|
||||
field: any;
|
||||
value: any;
|
||||
errors: string[] = [];
|
||||
|
||||
constructor(field: any, ctx: any) {
|
||||
this.field = field;
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
async setValue(value: any) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class BooleanValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
// Boolean
|
||||
if (typeof value === 'boolean') {
|
||||
this.value = value;
|
||||
}
|
||||
// Number
|
||||
else if (typeof value === 'number' && [0, 1].includes(value)) {
|
||||
this.value = value === 1;
|
||||
}
|
||||
// String
|
||||
else if (typeof value === 'string') {
|
||||
if (!value) {
|
||||
this.value = null;
|
||||
}
|
||||
if (['1', 'y', 'yes', 'true', '是'].includes(value.toLowerCase())) {
|
||||
this.value = true;
|
||||
} else if (['0', 'n', 'no', 'false', '否'].includes(value.toLowerCase())) {
|
||||
this.value = false;
|
||||
} else {
|
||||
this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`);
|
||||
}
|
||||
} else {
|
||||
this.errors.push(`Value invalid - ${JSON.stringify(this.value)}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import { moment2str } from '@nocobase/utils';
|
||||
import moment, { isDate, isMoment } from 'moment';
|
||||
import { BaseValueParser } from "./base-value-parser";
|
||||
|
||||
export class DateValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
if (isMoment(value)) {
|
||||
this.value = value;
|
||||
} else if (isDate(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
const props = this.getProps();
|
||||
const m = moment(value);
|
||||
if (m.isValid()) {
|
||||
this.value = moment2str(m, props);
|
||||
} else {
|
||||
this.errors.push('Invalid date');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getProps() {
|
||||
return this.field.options?.uiSchema?.['x-component-props'] || {};
|
||||
}
|
||||
}
|
46
packages/core/database/src/value-parsers/index.ts
Normal file
46
packages/core/database/src/value-parsers/index.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Database } from '../database';
|
||||
import { ArrayValueParser } from './array-value-parser';
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
import { BooleanValueParser } from './boolean-value-parser';
|
||||
import { DateValueParser } from './date-value-parser';
|
||||
import { JsonValueParser } from './json-value-parser';
|
||||
import { NumberValueParser } from './number-value-parser';
|
||||
import { StringValueParser } from './string-value-parser';
|
||||
import { ToManyValueParser } from './to-many-value-parser';
|
||||
import { ToOneValueParser } from './to-one-value-parser';
|
||||
|
||||
export function registerFieldValueParsers(db: Database) {
|
||||
db.registerFieldValueParsers({
|
||||
default: BaseValueParser,
|
||||
array: ArrayValueParser,
|
||||
set: ArrayValueParser,
|
||||
boolean: BooleanValueParser,
|
||||
date: DateValueParser,
|
||||
json: JsonValueParser,
|
||||
jsonb: JsonValueParser,
|
||||
number: NumberValueParser,
|
||||
integer: NumberValueParser,
|
||||
bigInt: NumberValueParser,
|
||||
float: NumberValueParser,
|
||||
double: NumberValueParser,
|
||||
real: NumberValueParser,
|
||||
decimal: NumberValueParser,
|
||||
string: StringValueParser,
|
||||
hasOne: ToOneValueParser,
|
||||
hasMany: ToManyValueParser,
|
||||
belongsTo: ToOneValueParser,
|
||||
belongsToMany: ToManyValueParser,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
ArrayValueParser,
|
||||
BaseValueParser,
|
||||
BooleanValueParser,
|
||||
DateValueParser,
|
||||
JsonValueParser,
|
||||
NumberValueParser,
|
||||
StringValueParser,
|
||||
ToManyValueParser,
|
||||
ToOneValueParser,
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class JsonValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
import { percent2float } from '../utils';
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class NumberValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
if (value === null || value === undefined || typeof value === 'number') {
|
||||
this.value = value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
if (!value) {
|
||||
this.value = null;
|
||||
} else if (['n/a', '-'].includes(value.toLowerCase())) {
|
||||
this.value = null;
|
||||
} else if (value.endsWith('%')) {
|
||||
this.value = percent2float(value);
|
||||
console.log(value, this.value);
|
||||
} else {
|
||||
const val = +value;
|
||||
this.value = isNaN(val) ? null : val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class StringValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
const { map, set } = this.getOptions();
|
||||
if (set.size > 0) {
|
||||
if (map.has(value)) {
|
||||
value = map.get(value);
|
||||
}
|
||||
if (set.has(value)) {
|
||||
this.value = value;
|
||||
} else {
|
||||
this.errors.push(`No matching option found - ${JSON.stringify(value)}`);
|
||||
}
|
||||
} else {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
const options = this.field.options?.['uiSchema']?.enum || [];
|
||||
const map = new Map();
|
||||
const set = new Set();
|
||||
for (const option of options) {
|
||||
set.add(option.value);
|
||||
set.add(option.label);
|
||||
map.set(option.label, option.value);
|
||||
}
|
||||
return { map, set };
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
import { Repository } from '../repository';
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class ToManyValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
const fieldNames = this.getFileNames();
|
||||
if (this.isInterface('chinaRegion')) {
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
try {
|
||||
this.value = await Promise.all(
|
||||
value.split('/').map(async (v) => {
|
||||
const instance = await repository.findOne({ filter: { [fieldNames.label]: v.trim() } });
|
||||
if (!instance) {
|
||||
throw new Error(`"${v}" does not exist`);
|
||||
}
|
||||
return instance.get(fieldNames.value);
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.errors.push(error.message);
|
||||
}
|
||||
} else {
|
||||
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||
this.errors.push(`data index invalid`);
|
||||
return;
|
||||
}
|
||||
const key = this.ctx.column.dataIndex[1];
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
try {
|
||||
this.value = await Promise.all(
|
||||
value.split(',').map(async (v) => {
|
||||
const instance = await repository.findOne({ filter: { [key]: v.trim() } });
|
||||
if (!instance) {
|
||||
throw new Error(`"${v}" does not exist`);
|
||||
}
|
||||
return instance.get(fieldNames.value);
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
this.errors.push(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {};
|
||||
return { label: 'id', value: 'id', ...fieldNames };
|
||||
}
|
||||
|
||||
isInterface(name) {
|
||||
return this.field.options.interface === name;
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
import { Repository } from '../repository';
|
||||
import { BaseValueParser } from './base-value-parser';
|
||||
|
||||
export class ToOneValueParser extends BaseValueParser {
|
||||
async setValue(value: any) {
|
||||
const fieldNames = this.getFileNames();
|
||||
const dataIndex = this.ctx?.column?.dataIndex || [];
|
||||
if (Array.isArray(dataIndex) && dataIndex.length < 2) {
|
||||
return;
|
||||
}
|
||||
const field = this.ctx.column.dataIndex[1];
|
||||
const repository = this.field.database.getRepository(this.field.target) as Repository;
|
||||
const instance = await repository.findOne({ filter: { [field]: value.trim() } });
|
||||
if (instance) {
|
||||
this.value = instance.get(fieldNames.value);
|
||||
} else {
|
||||
this.errors.push(`"${value}" does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
getFileNames() {
|
||||
const fieldNames = this.field.options?.uiSchema?.['x-component-props']?.['fieldNames'] || {};
|
||||
return { label: 'id', value: 'id', ...fieldNames };
|
||||
}
|
||||
|
||||
isInterface(name) {
|
||||
return this.field.options.interface === name;
|
||||
}
|
||||
}
|
@ -40,9 +40,6 @@ export const useDownloadXlsxTemplateAction = () => {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
if (field.interface === 'chinaRegion') {
|
||||
column.dataIndex.push('name');
|
||||
}
|
||||
column.defaultTitle = compile(field?.uiSchema?.title) || field.name;
|
||||
if (column.dataIndex.length > 1) {
|
||||
const subField = getCollectionJoinField(`${name}.${column.dataIndex.join('.')}`);
|
||||
@ -51,6 +48,9 @@ export const useDownloadXlsxTemplateAction = () => {
|
||||
}
|
||||
column.defaultTitle = column.defaultTitle + '/' + compile(subField?.uiSchema?.title) || subField.name;
|
||||
}
|
||||
if (field.interface === 'chinaRegion') {
|
||||
column.dataIndex.push('name');
|
||||
}
|
||||
return column;
|
||||
})
|
||||
.filter(Boolean);
|
||||
@ -94,9 +94,6 @@ export const useImportStartAction = () => {
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
if (field.interface === 'chinaRegion') {
|
||||
column.dataIndex.push('name');
|
||||
}
|
||||
column.defaultTitle = compile(field?.uiSchema?.title) || field.name;
|
||||
if (column.dataIndex.length > 1) {
|
||||
const subField = getCollectionJoinField(`${name}.${column.dataIndex.join('.')}`);
|
||||
@ -105,6 +102,9 @@ export const useImportStartAction = () => {
|
||||
}
|
||||
column.defaultTitle = column.defaultTitle + '/' + compile(subField?.uiSchema?.title) || subField.name;
|
||||
}
|
||||
if (field.interface === 'chinaRegion') {
|
||||
column.dataIndex.push('name');
|
||||
}
|
||||
return column;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
@ -77,7 +77,7 @@ class Importer {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
const parser = field.buildValueParser({ ...this.context, column });
|
||||
const parser = this.context.db.buildFieldValueParser(field, { ...this.context, column });
|
||||
await parser.setValue(typeof value === 'string' ? value.trim() : value);
|
||||
value = parser.getValue();
|
||||
if (parser.errors.length > 0) {
|
||||
@ -127,6 +127,7 @@ class Importer {
|
||||
result[0].push(instance);
|
||||
} catch (error) {
|
||||
this.context.log.error(error, row);
|
||||
row.push(error.message);
|
||||
result[1].push(row);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { getConfiguration, setConfiguration } from './actions';
|
||||
import { CircleField, LineStringField, PointField, PolygonField } from './fields';
|
||||
import { CircleValueParser, LineStringValueParser, PointValueParser, PolygonValueParser } from './value-parsers';
|
||||
|
||||
export class MapPlugin extends Plugin {
|
||||
afterAdd() {}
|
||||
@ -13,8 +14,13 @@ export class MapPlugin extends Plugin {
|
||||
lineString: LineStringField,
|
||||
circle: CircleField,
|
||||
};
|
||||
|
||||
this.db.registerFieldTypes(fields);
|
||||
this.db.registerFieldValueParsers({
|
||||
point: PointValueParser,
|
||||
polygon: PolygonValueParser,
|
||||
lineString: LineStringValueParser,
|
||||
circle: CircleValueParser,
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
55
packages/plugins/map/src/server/value-parsers/index.ts
Normal file
55
packages/plugins/map/src/server/value-parsers/index.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { BaseValueParser } from '@nocobase/database';
|
||||
|
||||
export class PointValueParser extends BaseValueParser {
|
||||
async setValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
this.value = value.split(',');
|
||||
} else {
|
||||
this.errors.push('Value invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PolygonValueParser extends BaseValueParser {
|
||||
async setValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
this.value = value
|
||||
.substring(1, value.length - 1)
|
||||
.split('),(')
|
||||
.map((v) => v.split(','));
|
||||
} else {
|
||||
this.errors.push('Value invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class LineStringValueParser extends BaseValueParser {
|
||||
async setValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
this.value = value
|
||||
.substring(1, value.length - 1)
|
||||
.split('),(')
|
||||
.map((v) => v.split(','));
|
||||
} else {
|
||||
this.errors.push('Value invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CircleValueParser extends BaseValueParser {
|
||||
async setValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
this.value = value;
|
||||
} else if (typeof value === 'string') {
|
||||
this.value = value.split(',');
|
||||
} else {
|
||||
this.errors.push('Value invalid');
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user