refactor(db): add batch logic to update for better performance (#2070)
* refactor(db): add batch logic to update for better performance * test(plugin-workflow): fix test cases * fix(db): treat belongsTo field in update values as foreignKey * fix(db): also handle object with id for belongsTo field * fix(db): avoid 0 as falsy * fix(db): fix test case
This commit is contained in:
parent
c240228a69
commit
6a589543f9
@ -398,6 +398,7 @@ describe('repository.update', () => {
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
],
|
||||
});
|
||||
Comment = db.collection({
|
||||
@ -411,7 +412,7 @@ describe('repository.update', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('update1', async () => {
|
||||
it('update with filterByTk and with associations', async () => {
|
||||
const user = await User.model.create<any>({
|
||||
name: 'user1',
|
||||
});
|
||||
@ -454,7 +455,7 @@ describe('repository.update', () => {
|
||||
expect(updated2.posts.length).toBe(2);
|
||||
});
|
||||
|
||||
it('update2', async () => {
|
||||
it('update with filterByTk', async () => {
|
||||
const user = await User.model.create<any>({
|
||||
name: 'user1',
|
||||
});
|
||||
@ -463,6 +464,9 @@ describe('repository.update', () => {
|
||||
name: 'user2',
|
||||
});
|
||||
|
||||
const hook = jest.fn();
|
||||
db.on('users.afterUpdate', hook);
|
||||
|
||||
await User.repository.update({
|
||||
filterByTk: user.id,
|
||||
values: {
|
||||
@ -470,6 +474,8 @@ describe('repository.update', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(hook).toBeCalledTimes(1);
|
||||
|
||||
const updated = await User.model.findByPk(user.id);
|
||||
|
||||
expect(updated.get('name')).toEqual('user11');
|
||||
@ -477,6 +483,119 @@ describe('repository.update', () => {
|
||||
const u2 = await User.model.findByPk(user2.id);
|
||||
expect(u2.get('name')).toEqual('user2');
|
||||
});
|
||||
|
||||
it('update with filter one by one when individualHooks is not set', async () => {
|
||||
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||
|
||||
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
|
||||
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
|
||||
const p3 = await Post.repository.create({ values: { name: 'p3' } });
|
||||
|
||||
const hook = jest.fn();
|
||||
db.on('posts.afterUpdate', hook);
|
||||
|
||||
await Post.repository.update({
|
||||
filter: {
|
||||
userId: u1.id,
|
||||
},
|
||||
values: {
|
||||
name: 'pp',
|
||||
},
|
||||
});
|
||||
|
||||
const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
|
||||
expect(postsAfterUpdated[0].name).toBe('pp');
|
||||
expect(postsAfterUpdated[1].name).toBe('pp');
|
||||
expect(postsAfterUpdated[2].name).toBe('p3');
|
||||
|
||||
expect(hook).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('update in batch when individualHooks is false', async () => {
|
||||
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||
|
||||
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
|
||||
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
|
||||
const p3 = await Post.repository.create({ values: { name: 'p3' } });
|
||||
|
||||
const hook = jest.fn();
|
||||
db.on('posts.afterUpdate', hook);
|
||||
|
||||
await Post.repository.update({
|
||||
filter: {
|
||||
userId: u1.id,
|
||||
},
|
||||
values: {
|
||||
name: 'pp',
|
||||
},
|
||||
individualHooks: false,
|
||||
});
|
||||
|
||||
const postsAfterUpdated = await Post.repository.find({ order: [['id', 'ASC']] });
|
||||
expect(postsAfterUpdated[0].name).toBe('pp');
|
||||
expect(postsAfterUpdated[1].name).toBe('pp');
|
||||
expect(postsAfterUpdated[2].name).toBe('p3');
|
||||
|
||||
expect(hook).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
it('update in batch with belongsTo field as foreignKey', async () => {
|
||||
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||
const u2 = await User.repository.create({ values: { name: 'u2' } });
|
||||
const p1 = await Post.repository.create({ values: { name: 'p1', userId: u1.id } });
|
||||
const p2 = await Post.repository.create({ values: { name: 'p2', userId: u1.id } });
|
||||
|
||||
const r1 = await Post.repository.update({
|
||||
filter: {
|
||||
name: p1.name,
|
||||
},
|
||||
values: {
|
||||
user: u2.id,
|
||||
},
|
||||
individualHooks: false,
|
||||
});
|
||||
|
||||
expect(r1).toEqual(1);
|
||||
|
||||
const p1Updated = await Post.repository.findOne({
|
||||
filterByTk: p1.id,
|
||||
});
|
||||
expect(p1Updated.userId).toBe(u2.id);
|
||||
|
||||
const r2 = await Post.repository.update({
|
||||
filter: {
|
||||
id: p2.id,
|
||||
},
|
||||
values: {
|
||||
user: null,
|
||||
},
|
||||
individualHooks: false,
|
||||
});
|
||||
|
||||
expect(r2).toEqual(1);
|
||||
|
||||
const p2Updated = await Post.repository.findOne({
|
||||
filterByTk: p2.id,
|
||||
});
|
||||
expect(p2Updated.userId).toBe(null);
|
||||
|
||||
const r3 = await Post.repository.update({
|
||||
filter: {
|
||||
id: p1.id,
|
||||
},
|
||||
values: {
|
||||
user: { id: u1.id },
|
||||
},
|
||||
individualHooks: false,
|
||||
});
|
||||
|
||||
expect(r3).toEqual(1);
|
||||
|
||||
const p1Updated2 = await Post.repository.findOne({
|
||||
filterByTk: p1.id,
|
||||
});
|
||||
expect(p1Updated2.userId).toBe(u1.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('repository.destroy', () => {
|
||||
|
@ -370,6 +370,7 @@ describe('One2One Association', () => {
|
||||
uid: 1,
|
||||
name: '123',
|
||||
},
|
||||
userId: 1,
|
||||
};
|
||||
|
||||
const guard = new UpdateGuard();
|
||||
@ -381,6 +382,7 @@ describe('One2One Association', () => {
|
||||
user: {
|
||||
uid: 1,
|
||||
},
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
guard.setAssociationKeysToBeUpdate(['user']);
|
||||
|
@ -609,6 +609,45 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
const queryOptions = this.buildQueryOptions(options);
|
||||
|
||||
// NOTE:
|
||||
// 1. better to be moved to separated API like bulkUpdate/updateMany
|
||||
// 2. strictly `false` comparing for compatibility of legacy api invoking
|
||||
if (options.individualHooks === false) {
|
||||
const { model: Model } = this.collection;
|
||||
// @ts-ignore
|
||||
const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
|
||||
// NOTE:
|
||||
// 1. find ids first for reusing `queryOptions` logic
|
||||
// 2. estimation memory usage will be N * M bytes (N = rows, M = model object memory)
|
||||
// 3. would be more efficient up to 100000 ~ 1000000 rows
|
||||
const rows = await Model.findAll({
|
||||
...queryOptions,
|
||||
attributes: [primaryKeyField],
|
||||
group: `${Model.name}.${primaryKeyField}`,
|
||||
include: queryOptions.include.filter((include) => {
|
||||
return (
|
||||
Object.keys(include.where || {}).length > 0 ||
|
||||
JSON.stringify(queryOptions?.filter)?.includes(include.association)
|
||||
);
|
||||
}),
|
||||
transaction,
|
||||
});
|
||||
const [result] = await Model.update(values, {
|
||||
where: {
|
||||
[primaryKeyField]: rows.map((row) => row.get(primaryKeyField)),
|
||||
},
|
||||
fields: options.fields,
|
||||
hooks: options.hooks,
|
||||
validate: options.validate,
|
||||
sideEffects: options.sideEffects,
|
||||
limit: options.limit,
|
||||
silent: options.silent,
|
||||
transaction,
|
||||
});
|
||||
// TODO: not support association fields except belongsTo
|
||||
return result;
|
||||
}
|
||||
|
||||
const instances = await this.find({
|
||||
...queryOptions,
|
||||
transaction,
|
||||
|
@ -93,6 +93,8 @@ export class UpdateGuard {
|
||||
Object.keys(associationsValues).forEach((association) => {
|
||||
let associationValues = associationsValues[association];
|
||||
|
||||
const associationObj = associations[association];
|
||||
|
||||
const filterAssociationToBeUpdate = (value) => {
|
||||
if (value === null) {
|
||||
return value;
|
||||
@ -104,8 +106,6 @@ export class UpdateGuard {
|
||||
return value;
|
||||
}
|
||||
|
||||
const associationObj = associations[association];
|
||||
|
||||
const associationKeyName =
|
||||
associationObj.associationType == 'BelongsTo' || associationObj.associationType == 'HasOne'
|
||||
? (<any>associationObj).targetKey
|
||||
@ -143,6 +143,16 @@ export class UpdateGuard {
|
||||
|
||||
// set association values to sanitized value
|
||||
values[association] = associationValues;
|
||||
|
||||
if (associationObj.associationType === 'BelongsTo') {
|
||||
if (typeof associationValues === 'object' && associationValues !== null) {
|
||||
if (associationValues[(associationObj as any).targetKey] != null) {
|
||||
values[(associationObj as any).foreignKey] = associationValues[(associationObj as any).targetKey];
|
||||
}
|
||||
} else {
|
||||
values[(associationObj as any).foreignKey] = associationValues;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (values instanceof Model) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { observer, useForm, useField } from '@formily/react';
|
||||
import { Input, Button, Dropdown, Menu, Form } from 'antd';
|
||||
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
@ -32,25 +32,28 @@ function AssociationInput(props) {
|
||||
return <Input {...props} value={value} onChange={onChange} />;
|
||||
}
|
||||
|
||||
export function useCollectionUIFields(collection) {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
|
||||
return getCollectionFields(collection).filter(
|
||||
(field) => !field.hidden && (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false),
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: observer for watching useProps
|
||||
const CollectionFieldSet = observer(
|
||||
({ value, disabled, onChange }: any) => {
|
||||
({ value, disabled, onChange, filter }: any) => {
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
const form = useForm();
|
||||
const { getCollection, getCollectionFields } = useCollectionManager();
|
||||
const { values: config } = useForm();
|
||||
const { getCollection } = useCollectionManager();
|
||||
const scope = useWorkflowVariableOptions();
|
||||
const { values: config } = form;
|
||||
const collectionName = config?.collection;
|
||||
const fields = getCollectionFields(collectionName).filter(
|
||||
(field) =>
|
||||
!field.hidden &&
|
||||
(field.uiSchema ? !field.uiSchema['x-read-pretty'] : false) &&
|
||||
// TODO: should use some field option but not type to control this
|
||||
!['formula'].includes(field.type),
|
||||
);
|
||||
const collectionFields = useCollectionUIFields(collectionName);
|
||||
const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields;
|
||||
|
||||
const unassignedFields = fields.filter((field) => !value || !(field.name in value));
|
||||
const scope = useWorkflowVariableOptions();
|
||||
const mergedDisabled = disabled || form.disabled;
|
||||
|
||||
return (
|
||||
|
@ -171,6 +171,13 @@ export default {
|
||||
'Update record': '更新数据',
|
||||
'Update records of a collection. You can use variables from upstream nodes as query conditions and field values.':
|
||||
'更新一个数据表中的数据。可以使用上游节点里的变量作为查询条件和数据值。',
|
||||
'Update mode': '更新模式',
|
||||
'Update in a batch': '批量更新',
|
||||
'Update one by one': '逐条更新',
|
||||
'Update all eligible data at one time, which has better performance when the amount of data is large. But the updated data will not trigger other workflows, and will not record audit logs.':
|
||||
'一次性更新所有符合条件的数据,在数据量较大时有比较好的性能;但被更新的数据不会触发其他工作流,也不会记录更新日志。',
|
||||
'The updated data can trigger other workflows, and the audit log will also be recorded. But it is usually only applicable to several or dozens of pieces of data, otherwise there will be performance problems.':
|
||||
'被更新的数据可以再次触发其他工作流,也会记录更新日志;但通常只适用于数条或数十条数据,否则会有性能问题。',
|
||||
'Query record': '查询数据',
|
||||
'Query records from a collection. You can use variables from upstream nodes as query conditions.':
|
||||
'查询一个数据表中的数据。可以使用上游节点里的变量作为查询条件。',
|
||||
|
@ -1,11 +1,43 @@
|
||||
import React from 'react';
|
||||
import { onFieldInputValueChange } from '@formily/core';
|
||||
import { useForm, useField } from '@formily/react';
|
||||
|
||||
import { useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
import CollectionFieldset, { useCollectionUIFields } from '../components/CollectionFieldset';
|
||||
|
||||
import { isValidFilter } from '../utils';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { collection, filter, values } from '../schemas/collection';
|
||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
|
||||
function IndividualHooksRadioWithTooltip({ onChange, ...props }) {
|
||||
const form = useForm();
|
||||
const { collection } = form.values;
|
||||
const fields = useCollectionUIFields(collection);
|
||||
const field = useField<any>();
|
||||
|
||||
function onValueChange({ target }) {
|
||||
const valuesField = field.query('.values').take();
|
||||
if (!valuesField) {
|
||||
return;
|
||||
}
|
||||
const filteredValues = fields.reduce((result, item) => {
|
||||
if (
|
||||
item.name in valuesField.value &&
|
||||
(target.value || !['hasOne', 'hasMany', 'belongsToMany'].includes(item.type))
|
||||
) {
|
||||
result[item.name] = valuesField.value[item.name];
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
form.setValuesIn('params.values', filteredValues);
|
||||
|
||||
onChange(target.value);
|
||||
}
|
||||
return <RadioWithTooltip {...props} onChange={onValueChange} />;
|
||||
}
|
||||
|
||||
export default {
|
||||
title: `{{t("Update record", { ns: "${NAMESPACE}" })}}`,
|
||||
@ -17,6 +49,27 @@ export default {
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
individualHooks: {
|
||||
type: 'boolean',
|
||||
title: `{{t("Update mode", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'IndividualHooksRadioWithTooltip',
|
||||
'x-component-props': {
|
||||
options: [
|
||||
{
|
||||
label: `{{t("Update in a batch", { ns: "${NAMESPACE}" })}}`,
|
||||
value: false,
|
||||
tooltip: `{{t("Update all eligible data at one time, which has better performance when the amount of data is large. But the updated data will not trigger other workflows, and will not record audit logs.", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
{
|
||||
label: `{{t("Update one by one", { ns: "${NAMESPACE}" })}}`,
|
||||
value: true,
|
||||
tooltip: `{{t("The updated data can trigger other workflows, and the audit log will also be recorded. But it is usually only applicable to several or dozens of pieces of data, otherwise there will be performance problems.", { ns: "${NAMESPACE}" })}}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
default: false,
|
||||
},
|
||||
filter: {
|
||||
...filter,
|
||||
title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`,
|
||||
@ -24,7 +77,14 @@ export default {
|
||||
return isValidFilter(value) ? '' : `{{t("Please add at least one condition", { ns: "${NAMESPACE}" })}}`;
|
||||
},
|
||||
},
|
||||
values,
|
||||
values: {
|
||||
...values,
|
||||
'x-component-props': {
|
||||
filter(this, field) {
|
||||
return this.params?.individualHooks || !['hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -35,5 +95,6 @@ export default {
|
||||
components: {
|
||||
FilterDynamicComponent,
|
||||
CollectionFieldset,
|
||||
IndividualHooksRadioWithTooltip,
|
||||
},
|
||||
};
|
||||
|
@ -201,8 +201,9 @@ export default class WorkflowPlugin extends Plugin {
|
||||
|
||||
this.events.push([workflow, context, options]);
|
||||
|
||||
this.getLogger(workflow.id).debug(`new event triggered, now events: ${this.events.length}`, {
|
||||
data: workflow.config,
|
||||
this.getLogger(workflow.id).info(`new event triggered, now events: ${this.events.length}`);
|
||||
this.getLogger(workflow.id).debug(`event data:`, {
|
||||
data: context,
|
||||
});
|
||||
|
||||
if (this.events.length > 1) {
|
||||
|
@ -18,7 +18,6 @@ describe('workflow > instructions > update', () => {
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
title: 'test workflow',
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
@ -54,51 +53,137 @@ describe('workflow > instructions > update', () => {
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.published).toBe(true);
|
||||
expect(job.result).toBe(1);
|
||||
|
||||
const updatedPost = await PostRepo.findById(post.id);
|
||||
expect(updatedPost.published).toBe(true);
|
||||
});
|
||||
|
||||
it('params: from job of node', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
title: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'update',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
id: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
},
|
||||
values: {
|
||||
title: 'changed',
|
||||
},
|
||||
},
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
await n1.setDownstream(n2);
|
||||
|
||||
// NOTE: the result of post immediately created will not be changed by workflow
|
||||
const { id } = await PostRepo.create({ values: { title: 'test' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
// should get from db
|
||||
const post = await PostRepo.findById(id);
|
||||
expect(post.title).toBe('changed');
|
||||
});
|
||||
});
|
||||
|
||||
it('params: from job of node', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
title: 'test',
|
||||
describe('update batch', () => {
|
||||
it('individualHooks off should not trigger other workflow', async () => {
|
||||
const w2 = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 2,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'update',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
id: '{{$context.data.id}}',
|
||||
},
|
||||
values: {
|
||||
published: true,
|
||||
},
|
||||
individualHooks: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
expect(post.published).toBe(false);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result).toBe(1);
|
||||
|
||||
const updatedPost = await PostRepo.findById(post.id);
|
||||
expect(updatedPost.published).toBe(true);
|
||||
|
||||
const w2Exes = await w2.getExecutions();
|
||||
expect(w2Exes.length).toBe(0);
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'update',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
id: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
},
|
||||
values: {
|
||||
title: 'changed',
|
||||
it('individualHooks on should trigger other workflow', async () => {
|
||||
const w2 = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 2,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'update',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
filter: {
|
||||
id: '{{$context.data.id}}',
|
||||
},
|
||||
values: {
|
||||
published: true,
|
||||
},
|
||||
individualHooks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
upstreamId: n1.id,
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
expect(post.published).toBe(false);
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result).toBe(1);
|
||||
|
||||
const updatedPost = await PostRepo.findById(post.id);
|
||||
expect(updatedPost.published).toBe(true);
|
||||
|
||||
const w2Exes = await w2.getExecutions();
|
||||
expect(w2Exes.length).toBe(1);
|
||||
});
|
||||
|
||||
await n1.setDownstream(n2);
|
||||
|
||||
// NOTE: the result of post immediately created will not be changed by workflow
|
||||
const { id } = await PostRepo.create({ values: { title: 'test' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
// should get from db
|
||||
const post = await PostRepo.findById(id);
|
||||
expect(post.title).toBe('changed');
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,7 @@ import { JOB_STATUS } from '../constants';
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { collection, multiple = false, params = {} } = node.config;
|
||||
const { collection, params = {} } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const options = processor.getParsedValue(params, node);
|
||||
@ -17,7 +17,7 @@ export default {
|
||||
});
|
||||
|
||||
return {
|
||||
result: multiple ? result : result[0] || null,
|
||||
result: result.length ?? result,
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
};
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user