Merge branch 'main' into next

This commit is contained in:
katherinehhh 2025-01-24 23:05:50 +08:00
commit cae5d68211
22 changed files with 406 additions and 98 deletions

View File

@ -2,9 +2,7 @@
"version": "1.5.0-beta.33",
"npmClient": "yarn",
"useWorkspaces": true,
"npmClientArgs": [
"--ignore-engines"
],
"npmClientArgs": ["--ignore-engines"],
"command": {
"version": {
"forcePublish": true,

View File

@ -10,10 +10,11 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
import { DatePickerProvider, ActionBarProvider } from '../schema-component';
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
import { DefaultValueProvider } from '../schema-settings';
import { CollectOperators } from './CollectOperators';
import { FormBlockProvider } from './FormBlockProvider';
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const filedSchema = useFieldSchema();
@ -21,22 +22,24 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const deprecatedOperators = filedSchema['x-filter-operators'] || {};
return (
<CollectOperators defaultOperators={deprecatedOperators}>
<DatePickerProvider value={{ utc: false }}>
<ActionBarProvider
forceProps={{
style: {
overflowX: 'auto',
maxWidth: '100%',
float: 'right',
},
}}
>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
</DefaultValueProvider>
</ActionBarProvider>
</DatePickerProvider>
</CollectOperators>
<SchemaComponentOptions components={{ CollectionField: FilterCollectionField }}>
<CollectOperators defaultOperators={deprecatedOperators}>
<DatePickerProvider value={{ utc: false }}>
<ActionBarProvider
forceProps={{
style: {
overflowX: 'auto',
maxWidth: '100%',
float: 'right',
},
}}
>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
</DefaultValueProvider>
</ActionBarProvider>
</DatePickerProvider>
</CollectOperators>
</SchemaComponentOptions>
);
});

View File

@ -164,8 +164,49 @@ export const time = [
];
export const boolean = [
{ label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true },
{ label: '{{t("No")}}', value: '$isFalsy', noValue: true },
{
label: '{{t("Yes")}}',
value: '$isTruly',
selected: true,
noValue: true,
schema: {
'x-component': 'Select',
'x-component-props': {
multiple: false,
options: [
{
label: '{{t("Yes")}}',
value: true,
},
{
label: '{{t("No")}}',
value: false,
},
],
},
},
},
{
label: '{{t("No")}}',
value: '$isFalsy',
noValue: true,
schema: {
'x-component': 'Select',
'x-component-props': {
multiple: false,
options: [
{
label: '{{t("Yes")}}',
value: true,
},
{
label: '{{t("No")}}',
value: false,
},
],
},
},
},
];
export const tableoid = [

View File

@ -0,0 +1,119 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Field } from '@formily/core';
import { connect, Schema, useField, useFieldSchema } from '@formily/react';
import { untracked } from '@formily/reactive';
import { merge } from '@formily/shared';
import { concat } from 'lodash';
import React, { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../../hoc/withDynamicSchemaProps';
import { ErrorFallback, useCompile, useComponent } from '../../../schema-component';
import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useCollectionManager_deprecated } from '../../../';
import {
CollectionFieldProvider,
useCollectionField,
} from '../../../data-source/collection-field/CollectionFieldProvider';
type Props = {
component: any;
children?: React.ReactNode;
};
const setFieldProps = (field: Field, key: string, value: any) => {
untracked(() => {
if (field[key] === undefined) {
field[key] = value;
}
});
};
const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
};
/**
* TODO: 初步适配
* @internal
*/
export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getInterface } = useCollectionManager_deprecated();
const { uiSchema: uiSchemaOrigin, defaultValue, interface: collectionInterface } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const targetInterface = getInterface(collectionInterface);
const operator = targetInterface?.filterable?.operators?.find(
(v, index) => v.value === fieldSchema['x-filter-operator'] || index === 0,
);
const Component = useComponent(
operator?.schema?.['x-component'] ||
fieldSchema['x-component-props']?.['component'] ||
uiSchemaOrigin?.['x-component'] ||
'Input',
);
const ctx = useFormBlockContext();
const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props);
// TODO: 初步适配
useEffect(() => {
if (!uiSchemaOrigin) {
return;
}
const uiSchema = compile(uiSchemaOrigin);
setFieldProps(field, 'content', uiSchema['x-content']);
setFieldProps(field, 'title', uiSchema.title);
setFieldProps(field, 'description', uiSchema.description);
if (ctx?.form) {
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal);
}
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []);
field.validator = concatSchema;
}
if (fieldSchema['x-disabled'] === true) {
field.disabled = true;
}
if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true;
}
setRequired(field, fieldSchema, uiSchema);
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps =
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchemaOrigin]);
if (!uiSchemaOrigin) return null;
return <Component {...props} {...dynamicProps} />;
};
export const FilterCollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
const field = useField<Field>();
return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
<CollectionFieldProvider name={fieldSchema.name}>
<FilterCollectionFieldInternalField {...props} />
</CollectionFieldProvider>
</ErrorBoundary>
);
});
FilterCollectionField.displayName = 'FilterCollectionField';

View File

@ -443,7 +443,13 @@ export const filterSelectComponentFieldSettings = new SchemaSettings({
return isSelectFieldMode && !isFieldReadPretty;
},
},
getAllowMultiple({ title: 'Allow multiple selection' }),
{
...getAllowMultiple({ title: 'Allow multiple selection' }),
useVisible() {
const field = useField();
return field.componentProps.multiple !== false;
},
},
{
...titleField,
useVisible: useIsAssociationField,

View File

@ -9,6 +9,7 @@
import React, { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { useForm } from '@formily/react';
import { useCollectionRecordData } from '../../../data-source/collection-record/CollectionRecordProvider';
import { Collection } from '../../../data-source/collection/Collection';
import { useCollection } from '../../../data-source/collection/CollectionProvider';

View File

@ -13,17 +13,18 @@ import { useActionContext } from '.';
import { NocoBaseRecursionField } from '../../../formily/NocoBaseRecursionField';
import { useOpenModeContext } from '../../../modules/popup/OpenModeProvider';
import { ComposedActionDrawer } from './types';
import { ActionDrawer } from './Action.Drawer';
const PopupLevelContext = React.createContext(0);
export const ActionContainer: ComposedActionDrawer = observer(
(props: any) => {
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext();
const { getComponentByOpenMode, defaultOpenMode } = useOpenModeContext() || {};
const { openMode = defaultOpenMode } = useActionContext();
const popupLevel = React.useContext(PopupLevelContext);
const currentLevel = popupLevel + 1;
const Component = getComponentByOpenMode(openMode);
const Component = getComponentByOpenMode(openMode) || ActionDrawer;
return (
<PopupLevelContext.Provider value={currentLevel}>

View File

@ -28,6 +28,7 @@ import { isVariable } from '../../../variables/utils/isVariable';
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
import { RemoteSelect, RemoteSelectProps } from '../remote-select';
import useServiceOptions, { useAssociationFieldContext } from './hooks';
import { VariablePopupRecordProvider } from '../../../modules/variable/variablesProvider/VariablePopupRecordProvider';
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
addMode?: 'quickAdd' | 'modalAdd';
@ -160,17 +161,19 @@ const InternalAssociationSelect = observer(
{addMode === 'modalAdd' && (
<SchemaComponentContext.Provider value={{ ...schemaComponentCtxValue, draggable: false }}>
<RecordProvider isNew={true} record={null} parent={recordData}>
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
<ClearCollectionFieldContext>
<NocoBaseRecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</ClearCollectionFieldContext>
<VariablePopupRecordProvider>
{/* 快捷添加按钮添加的添加的是一个普通的 form 区块(非关系区块),不应该与任何字段有关联,所以在这里把字段相关的上下文给清除掉 */}
<ClearCollectionFieldContext>
<NocoBaseRecursionField
onlyRenderProperties
basePath={field.address}
schema={fieldSchema}
filterProperties={(s) => {
return s['x-component'] === 'Action';
}}
/>
</ClearCollectionFieldContext>
</VariablePopupRecordProvider>
</RecordProvider>
</SchemaComponentContext.Provider>
)}

View File

@ -17,6 +17,7 @@ import React from 'react';
import { ReadPretty } from './ReadPretty';
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
import { useCompile } from '../../';
export type SelectProps<
ValueType = any,
@ -120,6 +121,7 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
const InternalSelect = connect(
(props: SelectProps) => {
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
const compile = useCompile();
let mode: any = props.multiple ? 'multiple' : props.mode;
if (mode && !['multiple', 'tags'].includes(mode)) {
mode = undefined;
@ -172,7 +174,7 @@ const InternalSelect = connect(
</Tag>
);
}}
{...others}
{...compile(others)}
onChange={(changed) => {
props.onChange?.(changed === undefined ? null : changed);
}}

View File

@ -10,7 +10,7 @@
import { Schema } from '@formily/json-schema';
import { useTranslation } from 'react-i18next';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
import { useCollection } from '../../../data-source';
import { useCollection, useCollectionField } from '../../../data-source';
import { useCollectionRecord } from '../../../data-source/collection-record/CollectionRecordProvider';
import { useParentCollection } from '../../../data-source/collection/AssociationProvider';
import { useFlag } from '../../../flag-provider/hooks/useFlag';
@ -59,12 +59,14 @@ export const useCurrentParentRecordContext = () => {
const collection = useCollection();
const { isInSubForm, isInSubTable } = useFlag() || {};
const dataSource = parentCollectionName ? parentDataSource : collection?.dataSource;
const associationCollectionField = useCollectionField();
return {
// 当该变量使用在区块数据范围的时候,由于某些区块(如 Table是在 DataBlockProvider 之前解析 filter 的,
// 导致此时 record.parentRecord 的值还是空的,此时正确的值应该是 record所以在后面加了 record?.data 来防止这种情况
currentParentRecordCtx: record?.parentRecord?.data || record?.data,
shouldDisplayCurrentParentRecord: !!record?.parentRecord?.data && !isInSubForm && !isInSubTable,
shouldDisplayCurrentParentRecord:
!!record?.parentRecord?.data && !isInSubForm && !isInSubTable && associationCollectionField,
// 在后面加上 collection?.name 的原因如上面的变量一样
collectionName: parentCollectionName || collection?.name,
dataSource,

View File

@ -7,14 +7,15 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { omit } from 'lodash';
import _ from 'lodash';
import { Transactionable } from 'sequelize/types';
import { Collection } from '../collection';
import { transactionWrapperBuilder } from '../decorators/transaction-decorator';
import { FindOptions } from '../repository';
import { MultipleRelationRepository } from './multiple-relation-repository';
import { MultipleRelationRepository } from '../relation-repository/multiple-relation-repository';
import Database from '../database';
import { Model } from '../model';
import { UpdateAssociationOptions } from '../update-associations';
const transaction = transactionWrapperBuilder(function () {
return this.collection.model.sequelize.transaction();
@ -68,6 +69,21 @@ export class BelongsToArrayAssociation {
on: this.db.sequelize.literal(`${left}=any(${right})`),
};
}
async update(instance: Model, value: any, options: UpdateAssociationOptions = {}) {
// @ts-ignore
await instance.update(
{
[this.as]: value,
},
{
values: {
[this.as]: value,
},
transaction: options?.transaction,
},
);
}
}
export class BelongsToArrayRepository extends MultipleRelationRepository {
@ -101,7 +117,7 @@ export class BelongsToArrayRepository extends MultipleRelationRepository {
}
const findOptions = {
...omit(options, ['filterByTk', 'where', 'values', 'attributes']),
..._.omit(options, ['filterByTk', 'where', 'values', 'attributes']),
filter: {
$and: [options.filter || {}, addFilter],
},

View File

@ -13,7 +13,7 @@ import { ModelStatic } from 'sequelize';
import { Collection } from './collection';
import { Database } from './database';
import { Model } from './model';
import { BelongsToArrayAssociation } from './relation-repository/belongs-to-array-repository';
import { BelongsToArrayAssociation } from './belongs-to-array/belongs-to-array-repository';
const debug = require('debug')('noco-database');

View File

@ -44,7 +44,7 @@ export * from './relation-repository/belongs-to-repository';
export * from './relation-repository/hasmany-repository';
export * from './relation-repository/multiple-relation-repository';
export * from './relation-repository/single-relation-repository';
export * from './relation-repository/belongs-to-array-repository';
export * from './belongs-to-array/belongs-to-array-repository';
export * from './repository';
export * from './update-associations';
export { snakeCase } from './utils';

View File

@ -10,7 +10,26 @@
import { Op } from 'sequelize';
export default {
$isFalsy() {
$isFalsy(value) {
if (value === true || value === 'true') {
return {
[Op.or]: {
[Op.is]: null,
[Op.eq]: false,
},
};
}
return {
[Op.eq]: true,
};
},
$isTruly(value) {
if (value === true || value === 'true') {
return {
[Op.eq]: true,
};
}
return {
[Op.or]: {
[Op.is]: null,
@ -18,10 +37,4 @@ export default {
},
};
},
$isTruly() {
return {
[Op.eq]: true,
};
},
} as Record<string, any>;

View File

@ -13,6 +13,7 @@ import { Model } from '../model';
import { FindOptions, TargetKey, UpdateOptions } from './types';
import { updateModelByValues } from '../update-associations';
import { RelationRepository, transaction } from './relation-repository';
import lodash from 'lodash';
interface SetOption extends Transactionable {
tk?: TargetKey;
@ -100,7 +101,7 @@ export abstract class SingleRelationRepository extends RelationRepository {
}
await updateModelByValues(target, options?.values, {
...options,
...lodash.omit(options, 'values'),
transaction,
});

View File

@ -38,7 +38,7 @@ import FilterParser from './filter-parser';
import { Model } from './model';
import operators from './operators';
import { OptionsParser } from './options-parser';
import { BelongsToArrayRepository } from './relation-repository/belongs-to-array-repository';
import { BelongsToArrayRepository } from './belongs-to-array/belongs-to-array-repository';
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
import { HasManyRepository } from './relation-repository/hasmany-repository';

View File

@ -22,18 +22,7 @@ import { Model } from './model';
import { UpdateGuard } from './update-guard';
import { TargetKey } from './repository';
import Database from './database';
function isUndefinedOrNull(value: any) {
return typeof value === 'undefined' || value === null;
}
function isStringOrNumber(value: any) {
return typeof value === 'string' || typeof value === 'number';
}
function getKeysByPrefix(keys: string[], prefix: string) {
return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
}
import { getKeysByPrefix, isStringOrNumber, isUndefinedOrNull } from './utils';
export function modelAssociations(instance: Model) {
return (<typeof Model>instance.constructor).associations;
@ -51,7 +40,12 @@ export function belongsToManyAssociations(instance: Model): Array<BelongsToMany>
});
}
export function modelAssociationByKey(instance: Model, key: string): Association {
export function modelAssociationByKey(
instance: Model,
key: string,
): Association & {
update?: (instance: Model, value: any, options: UpdateAssociationOptions) => Promise<any>;
} {
return modelAssociations(instance)[key] as Association;
}
@ -71,7 +65,7 @@ interface UpdateOptions extends Transactionable {
sourceModel?: Model;
}
interface UpdateAssociationOptions extends Transactionable, Hookable {
export interface UpdateAssociationOptions extends Transactionable, Hookable {
updateAssociationValues?: string[];
sourceModel?: Model;
context?: any;
@ -243,6 +237,10 @@ export async function updateAssociation(
return false;
}
if (association.update) {
return association.update(instance, value, options);
}
switch (association.associationType) {
case 'HasOne':
case 'BelongsTo':

View File

@ -116,3 +116,15 @@ export function percent2float(value: string) {
const v = parseInt('1' + '0'.repeat(repeat));
return (parseFloat(value) * v) / (100 * v);
}
export function isUndefinedOrNull(value: any) {
return typeof value === 'undefined' || value === null;
}
export function isStringOrNumber(value: any) {
return typeof value === 'string' || typeof value === 'number';
}
export function getKeysByPrefix(keys: string[], prefix: string) {
return keys.filter((key) => key.startsWith(`${prefix}.`)).map((key) => key.substring(prefix.length + 1));
}

View File

@ -12515,7 +12515,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneBelongsTo',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@ -12562,7 +12562,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneHasOne',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@ -12656,7 +12656,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.manyToOne',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',

View File

@ -113,7 +113,7 @@ describe('issues', () => {
expect(res.status).toBe(200);
});
test('update m2m array field in single realtion collection', async () => {
test('update m2m array field in single relation collection, a.b', async () => {
await db.getRepository('collections').create({
values: {
name: 'tags',
@ -216,4 +216,112 @@ describe('issues', () => {
}
expect(res.status).toBe(200);
});
test('update m2m array field in single relation collection', async () => {
await db.getRepository('collections').create({
values: {
name: 'tags',
fields: [
{
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
{
name: 'title',
type: 'string',
},
],
},
});
await db.getRepository('collections').create({
values: {
name: 'users',
fields: [
{
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
{
name: 'username',
type: 'string',
},
{
name: 'tags',
type: 'belongsToArray',
foreignKey: 'tag_ids',
target: 'tags',
targetKey: 'id',
},
],
},
});
await db.getRepository('collections').create({
values: {
name: 'projects',
fields: [
{
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
},
{
name: 'title',
type: 'string',
},
{
name: 'users',
type: 'belongsTo',
foreignKey: 'user_id',
target: 'users',
},
],
},
});
// @ts-ignore
await db.getRepository('collections').load();
await db.sync();
await db.getRepository('tags').create({
values: [{ title: 'a' }, { title: 'b' }, { title: 'c' }],
});
await db.getRepository('users').create({
values: { id: 1, username: 'a' },
});
let user = await db.getRepository('users').findOne({
filterByTk: 1,
});
expect(user.tag_ids).toEqual(null);
await db.getRepository('projects').create({
values: { id: 1, title: 'p1', user_id: 1 },
});
const res = await agent.resource('projects').update({
filterByTk: 1,
updateAssociationValues: ['users', 'users.tags'],
values: {
users: {
id: 1,
tags: [
{ id: 1, title: 'a' },
{ id: 2, title: 'b' },
],
},
},
});
user = await db.getRepository('users').findOne({
filterByTk: 1,
});
if (db.sequelize.getDialect() === 'postgres') {
expect(user.tag_ids).toMatchObject(['1', '2']);
} else {
expect(user.tag_ids).toMatchObject([1, 2]);
}
expect(res.status).toBe(200);
});
});

View File

@ -239,11 +239,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: [{ id: 1 }, { id: 3 }],
},
});
if (db.sequelize.getDialect() === 'postgres') {
expect(user.tag_ids).toMatchObject(['1', '3']);
} else {
expect(user.tag_ids).toMatchObject([1, 3]);
}
expect(user.tag_ids).toMatchObject([1, 3]);
const user2 = await db.getRepository('users').create({
values: {
id: 4,
@ -251,11 +247,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: [1, 3],
},
});
if (db.sequelize.getDialect() === 'postgres') {
expect(user2.tag_ids).toMatchObject(['1', '3']);
} else {
expect(user2.tag_ids).toMatchObject([1, 3]);
}
expect(user2.tag_ids).toMatchObject([1, 3]);
const user3 = await db.getRepository('users').create({
values: {
id: 5,
@ -263,11 +255,7 @@ describe('m2m array api, bigInt targetKey', () => {
tags: { id: 1 },
},
});
if (db.sequelize.getDialect() === 'postgres') {
expect(user3.tag_ids).toMatchObject(['1']);
} else {
expect(user3.tag_ids).toMatchObject([1]);
}
expect(user3.tag_ids).toMatchObject([1]);
});
it('should create target when creating belongsToArray', async () => {

View File

@ -54,17 +54,6 @@ export class BelongsToArrayField extends RelationField {
model.set(foreignKey, tks);
};
init() {
super.init();
const { name, ...opts } = this.options;
this.collection.model.associations[name] = new BelongsToArrayAssociation({
db: this.database,
source: this.collection.model,
as: name,
...opts,
}) as any;
}
checkTargetCollection() {
const { target } = this.options;
if (!target) {
@ -115,6 +104,13 @@ export class BelongsToArrayField extends RelationField {
return false;
}
this.checkAssociationKeys();
const { name, ...opts } = this.options;
this.collection.model.associations[name] = new BelongsToArrayAssociation({
db: this.database,
source: this.collection.model,
as: name,
...opts,
}) as any;
this.on('beforeSave', this.setForeignKeyArray);
}