diff --git a/packages/core/database/src/database.ts b/packages/core/database/src/database.ts index 18e62090a..1f9b2f024 100644 --- a/packages/core/database/src/database.ts +++ b/packages/core/database/src/database.ts @@ -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>(); repositories = new Map(); @@ -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) { + for (const [type, parser] of Object.entries(parsers)) { + this.fieldValueParsers.set(type, parser); + } + } + + buildFieldValueParser(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>) { for (const [type, schemaType] of Object.entries(models)) { this.models.set(type, schemaType); diff --git a/packages/core/database/src/fields/array-field.ts b/packages/core/database/src/fields/array-field.ts index 9e7ff6b7f..2cf264589 100644 --- a/packages/core/database/src/fields/array-field.ts +++ b/packages/core/database/src/fields/array-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/belongs-to-field.ts b/packages/core/database/src/fields/belongs-to-field.ts index 5d0c71afc..b4d552d88 100644 --- a/packages/core/database/src/fields/belongs-to-field.ts +++ b/packages/core/database/src/fields/belongs-to-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/belongs-to-many-field.ts b/packages/core/database/src/fields/belongs-to-many-field.ts index 7f670e8a3..b0fc12ac2 100644 --- a/packages/core/database/src/fields/belongs-to-many-field.ts +++ b/packages/core/database/src/fields/belongs-to-many-field.ts @@ -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 diff --git a/packages/core/database/src/fields/boolean-field.ts b/packages/core/database/src/fields/boolean-field.ts index 946c39ef5..ee97a43b6 100644 --- a/packages/core/database/src/fields/boolean-field.ts +++ b/packages/core/database/src/fields/boolean-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/date-field.ts b/packages/core/database/src/fields/date-field.ts index 448886365..1a07d04bc 100644 --- a/packages/core/database/src/fields/date-field.ts +++ b/packages/core/database/src/fields/date-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/field.ts b/packages/core/database/src/fields/field.ts index e8c0d1a13..9f1b3eba0 100644 --- a/packages/core/database/src/fields/field.ts +++ b/packages/core/database/src/fields/field.ts @@ -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; - } } diff --git a/packages/core/database/src/fields/has-many-field.ts b/packages/core/database/src/fields/has-many-field.ts index 99183f1b1..939ddc357 100644 --- a/packages/core/database/src/fields/has-many-field.ts +++ b/packages/core/database/src/fields/has-many-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/has-one-field.ts b/packages/core/database/src/fields/has-one-field.ts index 486c14bd0..6ca8d495f 100644 --- a/packages/core/database/src/fields/has-one-field.ts +++ b/packages/core/database/src/fields/has-one-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/number-field.ts b/packages/core/database/src/fields/number-field.ts index 6e80ebe8f..2b11e25d4 100644 --- a/packages/core/database/src/fields/number-field.ts +++ b/packages/core/database/src/fields/number-field.ts @@ -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 { diff --git a/packages/core/database/src/fields/string-field.ts b/packages/core/database/src/fields/string-field.ts index 92ad3c609..db7e60617 100644 --- a/packages/core/database/src/fields/string-field.ts +++ b/packages/core/database/src/fields/string-field.ts @@ -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 { diff --git a/packages/core/database/src/index.ts b/packages/core/database/src/index.ts index 35f8a2bce..32324cfb6 100644 --- a/packages/core/database/src/index.ts +++ b/packages/core/database/src/index.ts @@ -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'; + diff --git a/packages/core/database/src/utils.ts b/packages/core/database/src/utils.ts index c11c98cf3..78709d713 100644 --- a/packages/core/database/src/utils.ts +++ b/packages/core/database/src/utils.ts @@ -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); +} diff --git a/packages/core/database/src/value-parsers/array-value-parser.ts b/packages/core/database/src/value-parsers/array-value-parser.ts new file mode 100644 index 000000000..278010d38 --- /dev/null +++ b/packages/core/database/src/value-parsers/array-value-parser.ts @@ -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; + } +} diff --git a/packages/core/database/src/value-parsers/base-value-parser.ts b/packages/core/database/src/value-parsers/base-value-parser.ts new file mode 100644 index 000000000..48666d6ac --- /dev/null +++ b/packages/core/database/src/value-parsers/base-value-parser.ts @@ -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; + } +} diff --git a/packages/core/database/src/value-parsers/boolean-value-parser.ts b/packages/core/database/src/value-parsers/boolean-value-parser.ts new file mode 100644 index 000000000..45b680dd5 --- /dev/null +++ b/packages/core/database/src/value-parsers/boolean-value-parser.ts @@ -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)}`); + } + } +} diff --git a/packages/core/database/src/value-parsers/date-value-parser.ts b/packages/core/database/src/value-parsers/date-value-parser.ts new file mode 100644 index 000000000..32631e4ba --- /dev/null +++ b/packages/core/database/src/value-parsers/date-value-parser.ts @@ -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'] || {}; + } +} diff --git a/packages/core/database/src/value-parsers/index.ts b/packages/core/database/src/value-parsers/index.ts new file mode 100644 index 000000000..c7882c4c1 --- /dev/null +++ b/packages/core/database/src/value-parsers/index.ts @@ -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, +}; diff --git a/packages/core/database/src/value-parsers/json-value-parser.ts b/packages/core/database/src/value-parsers/json-value-parser.ts new file mode 100644 index 000000000..792eb154b --- /dev/null +++ b/packages/core/database/src/value-parsers/json-value-parser.ts @@ -0,0 +1,7 @@ +import { BaseValueParser } from './base-value-parser'; + +export class JsonValueParser extends BaseValueParser { + async setValue(value: any) { + + } +} diff --git a/packages/core/database/src/value-parsers/number-value-parser.ts b/packages/core/database/src/value-parsers/number-value-parser.ts new file mode 100644 index 000000000..75e3f0e2f --- /dev/null +++ b/packages/core/database/src/value-parsers/number-value-parser.ts @@ -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; + } + } + } +} diff --git a/packages/core/database/src/value-parsers/string-value-parser.ts b/packages/core/database/src/value-parsers/string-value-parser.ts new file mode 100644 index 000000000..c12cc7aa1 --- /dev/null +++ b/packages/core/database/src/value-parsers/string-value-parser.ts @@ -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 }; + } +} diff --git a/packages/core/database/src/value-parsers/to-many-value-parser.ts b/packages/core/database/src/value-parsers/to-many-value-parser.ts new file mode 100644 index 000000000..991cea895 --- /dev/null +++ b/packages/core/database/src/value-parsers/to-many-value-parser.ts @@ -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; + } +} diff --git a/packages/core/database/src/value-parsers/to-one-value-parser.ts b/packages/core/database/src/value-parsers/to-one-value-parser.ts new file mode 100644 index 000000000..0585dd84e --- /dev/null +++ b/packages/core/database/src/value-parsers/to-one-value-parser.ts @@ -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; + } +} diff --git a/packages/plugins/import/src/client/useImportAction.ts b/packages/plugins/import/src/client/useImportAction.ts index f8fa29081..f8ddf27bb 100644 --- a/packages/plugins/import/src/client/useImportAction.ts +++ b/packages/plugins/import/src/client/useImportAction.ts @@ -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); diff --git a/packages/plugins/import/src/server/actions/importXlsx.ts b/packages/plugins/import/src/server/actions/importXlsx.ts index 377ec1d13..65b4f6fe4 100644 --- a/packages/plugins/import/src/server/actions/importXlsx.ts +++ b/packages/plugins/import/src/server/actions/importXlsx.ts @@ -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); } } diff --git a/packages/plugins/map/src/server/plugin.ts b/packages/plugins/map/src/server/plugin.ts index 857251ac6..0fe68d6d2 100644 --- a/packages/plugins/map/src/server/plugin.ts +++ b/packages/plugins/map/src/server/plugin.ts @@ -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() { diff --git a/packages/plugins/map/src/server/value-parsers/index.ts b/packages/plugins/map/src/server/value-parsers/index.ts new file mode 100644 index 000000000..920dd8143 --- /dev/null +++ b/packages/plugins/map/src/server/value-parsers/index.ts @@ -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'); + } + } +}