refactor: actions (#137)
* db test * associated list action * associated list action * fix belongs to many repository test * create action * update action * add update & destroy has one * get action * add action * set action * remove action * toggle action * chore: code import * add sort field mutex * change field mutex position * feat: handle sort field scope change * feat: sort actions * fix: add action * rename sort action to move action * more actions params * feat: repository destroy with filter and filterByPK * feat: hasmany repository destroy with filter and filterByPK * feat: belongsToMany repository destroy with filter and filterByPK * fix: actions tests lock error * feat: code cleanup Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
8f0a71a1cf
commit
79ba391aee
112
packages/actions/src/__tests__/add-action.test.ts
Normal file
112
packages/actions/src/__tests__/add-action.test.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('add action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('add belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t3 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't3',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(0);
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.add({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [t1.get('id'), t2.get('id')],
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(2);
|
||||
|
||||
// add with through values
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.add({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [
|
||||
[
|
||||
t3.get('id'),
|
||||
{
|
||||
tagged_at: '123',
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const tags = await p1.getTags();
|
||||
expect(tags.length).toEqual(3);
|
||||
const tag = tags.find((t) => t.id == t3.get('id'));
|
||||
expect(tag.posts_tags.tagged_at).toEqual('123');
|
||||
});
|
||||
});
|
112
packages/actions/src/__tests__/create-action.test.ts
Normal file
112
packages/actions/src/__tests__/create-action.test.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('create action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'belongsToMany', name: 'tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('create resource', async () => {
|
||||
expect(await Post.repository.findOne()).toBeNull();
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
const post = await Post.repository.findOne();
|
||||
expect(post).not.toBeNull();
|
||||
expect(post['title']).toEqual('t1');
|
||||
});
|
||||
|
||||
test('create has many nested resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await Comment.repository.findOne()).toBeNull();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.comments')
|
||||
.create({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: {
|
||||
content: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
const comment = await Comment.repository.findOne();
|
||||
expect(comment).not.toBeNull();
|
||||
expect(comment.get('postId')).toEqual(p1.get('id'));
|
||||
expect(comment.get('content')).toEqual('hello');
|
||||
});
|
||||
|
||||
test('create belongs to many nested resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await Tag.repository.findOne()).toBeNull();
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.create({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: {
|
||||
name: 'hello',
|
||||
},
|
||||
});
|
||||
|
||||
const tag = await Tag.repository.findOne();
|
||||
|
||||
expect(tag).not.toBeNull();
|
||||
expect(await tag.hasPost(p1)).toBeTruthy();
|
||||
expect(tag.get('name')).toEqual('hello');
|
||||
});
|
||||
});
|
156
packages/actions/src/__tests__/destroy-action.test.ts
Normal file
156
packages/actions/src/__tests__/destroy-action.test.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('destroy action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
let Profile;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Profile = app.collection({
|
||||
name: 'profiles',
|
||||
fields: [
|
||||
{ type: 'string', name: 'post_profile' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('destroy resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.destroy({
|
||||
resourceIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
expect(await Post.repository.count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('destroy has many resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
comments: [
|
||||
{
|
||||
content: 'c1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const c1 = await Comment.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.comments')
|
||||
.destroy({
|
||||
resourceIndex: c1.get('id'),
|
||||
associatedIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
expect(await Comment.repository.count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('destroy belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
tags: [
|
||||
{
|
||||
name: 't1',
|
||||
posts_tags: {
|
||||
tagged_at: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(await Tag.repository.count()).toEqual(1);
|
||||
|
||||
const p1t1 = (await p1.getTags())[0];
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.destroy({
|
||||
resourceIndex: p1.get('id'),
|
||||
associatedIndex: p1t1.get('id'),
|
||||
});
|
||||
|
||||
expect(await Tag.repository.count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('destroy has one', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
profile: {
|
||||
post_profile: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postProfile = await Profile.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.profile')
|
||||
.destroy({
|
||||
associatedIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
expect(await Profile.repository.count()).toEqual(0);
|
||||
});
|
||||
});
|
133
packages/actions/src/__tests__/get-action.test.ts
Normal file
133
packages/actions/src/__tests__/get-action.test.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('get action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
let Profile;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Profile = app.collection({
|
||||
name: 'profiles',
|
||||
fields: [
|
||||
{ type: 'string', name: 'post_profile' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('get resource', async () => {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: 'p0',
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.get({
|
||||
resourceIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
const body = response.body;
|
||||
expect(body['id']).toEqual(p1.get('id'));
|
||||
});
|
||||
|
||||
test('get has many resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
comments: [
|
||||
{
|
||||
content: 'c1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const c1 = await Comment.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.comments')
|
||||
.get({
|
||||
resourceIndex: c1.get('id'),
|
||||
associatedIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
expect(response.body['id']).toEqual(c1.get('id'));
|
||||
});
|
||||
|
||||
test('get has one resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
profile: {
|
||||
post_profile: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postProfile = await Profile.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.profile')
|
||||
.get({
|
||||
associatedIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
expect(response.body['id']).toEqual(postProfile.get('id'));
|
||||
});
|
||||
});
|
@ -1,19 +0,0 @@
|
||||
import { mockServer } from '.';
|
||||
|
||||
async function test(ctx, next) {
|
||||
ctx.body = ctx.action.params;
|
||||
await next();
|
||||
}
|
||||
|
||||
describe('hello', () => {
|
||||
it('hello', async () => {
|
||||
const app = mockServer();
|
||||
app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
app.actions({ test });
|
||||
const response = await app.agent().resource('tests').test({ key1: 'val1' });
|
||||
expect(response.body.key1).toBe('val1');
|
||||
await app.destroy();
|
||||
});
|
||||
});
|
@ -27,12 +27,10 @@ export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
storage: process.env.DB_STORAGE,
|
||||
logging: process.env.DB_LOG_SQL === 'on',
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
beforeDefine(model, options) {
|
||||
@ -99,7 +97,11 @@ export class MockServer extends Koa {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.db = mockDatabase();
|
||||
this.db = mockDatabase({
|
||||
sync: {
|
||||
force: true,
|
||||
},
|
||||
});
|
||||
this.resourcer = new Resourcer({
|
||||
prefix: '/api',
|
||||
});
|
||||
@ -118,7 +120,7 @@ export class MockServer extends Koa {
|
||||
}
|
||||
|
||||
collection(options: CollectionOptions) {
|
||||
this.db.collection(options);
|
||||
return this.db.collection(options);
|
||||
}
|
||||
|
||||
resource(options: ResourceOptions) {
|
||||
@ -158,8 +160,6 @@ export class MockServer extends Koa {
|
||||
url += `/${resourceIndex}`;
|
||||
}
|
||||
|
||||
console.log(url);
|
||||
|
||||
switch (method) {
|
||||
case 'upload':
|
||||
return agent
|
||||
|
113
packages/actions/src/__tests__/list-action.test.ts
Normal file
113
packages/actions/src/__tests__/list-action.test.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('list action', () => {
|
||||
let app;
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
const Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags',
|
||||
},
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
const Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts' },
|
||||
],
|
||||
});
|
||||
|
||||
app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
|
||||
const t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t3 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't3',
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'pt1',
|
||||
tags: [t1.get('id'), t2.get('id')],
|
||||
},
|
||||
});
|
||||
|
||||
await Post.repository.createMany({
|
||||
records: [
|
||||
{
|
||||
title: 'pt2',
|
||||
tags: [t2.get('id')],
|
||||
},
|
||||
{
|
||||
title: 'pt3',
|
||||
tags: [t3.get('id')],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('list with pagination', async () => {
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.list({
|
||||
fields: ['id'],
|
||||
perPage: 1,
|
||||
page: 2,
|
||||
sort: ['id'],
|
||||
});
|
||||
|
||||
const body = response.body;
|
||||
expect(body.rows.length).toEqual(1);
|
||||
expect(body.rows[0]['id']).toEqual(2);
|
||||
expect(body.count).toEqual(3);
|
||||
expect(body.totalPage).toEqual(3);
|
||||
});
|
||||
|
||||
test('list by association', async () => {
|
||||
// tags with posts id eq 1
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.list({ associatedIndex: 1, fields: ['id', 'posts_tags.createdAt'], sort: ['id'] });
|
||||
|
||||
const body = response.body;
|
||||
expect(body.count).toEqual(2);
|
||||
expect(body.rows).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
});
|
579
packages/actions/src/__tests__/move-action.test.ts
Normal file
579
packages/actions/src/__tests__/move-action.test.ts
Normal file
@ -0,0 +1,579 @@
|
||||
import { mockServer, MockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('sort action', () => {
|
||||
describe('same scope', () => {
|
||||
let api: MockServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = mockServer();
|
||||
|
||||
registerActions(api);
|
||||
api.db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'sort', name: 'sort' },
|
||||
{ type: 'sort', name: 'sort2' },
|
||||
],
|
||||
});
|
||||
await api.db.sync();
|
||||
const Test = api.db.getCollection('tests');
|
||||
|
||||
for (let index = 1; index < 5; index++) {
|
||||
await Test.repository.create({ values: { title: `t${index}` } });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
it('targetId', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 1,
|
||||
targetId: 3,
|
||||
});
|
||||
|
||||
const response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't2',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't3',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't4',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetId', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 3,
|
||||
targetId: 1,
|
||||
});
|
||||
|
||||
const response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't3',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't2',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't4',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('sortField', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sortField: 'sort2',
|
||||
sourceId: 1,
|
||||
targetId: 3,
|
||||
});
|
||||
|
||||
const response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort2'],
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't2',
|
||||
sort2: 1,
|
||||
},
|
||||
{
|
||||
title: 't3',
|
||||
sort2: 2,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
sort2: 3,
|
||||
},
|
||||
{
|
||||
title: 't4',
|
||||
sort2: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('sticky', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 3,
|
||||
sticky: true,
|
||||
});
|
||||
|
||||
const response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't3',
|
||||
sort: 0,
|
||||
},
|
||||
{
|
||||
title: 't1',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't2',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't4',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('different scope', () => {
|
||||
let api: MockServer;
|
||||
|
||||
beforeEach(async () => {
|
||||
api = mockServer();
|
||||
registerActions(api);
|
||||
api.db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'integer', name: 'state' },
|
||||
{ type: 'sort', name: 'sort', scopeKey: 'state' },
|
||||
],
|
||||
});
|
||||
await api.db.sync();
|
||||
const Test = api.db.getCollection('tests');
|
||||
|
||||
for (let index = 1; index < 5; index++) {
|
||||
await Test.repository.create({ values: { title: `t1${index}`, state: 1 } });
|
||||
}
|
||||
for (let index = 1; index < 5; index++) {
|
||||
await Test.repository.create({ values: { title: `t2${index}`, state: 2 } });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return api.destroy();
|
||||
});
|
||||
|
||||
it('targetId/1->6', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 1,
|
||||
targetId: 6,
|
||||
});
|
||||
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
fields: ['title', 'sort'],
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't12',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't21',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't11',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
sort: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetId/1->6 - method=insertAfter', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 1,
|
||||
targetId: 6,
|
||||
method: 'insertAfter',
|
||||
});
|
||||
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
});
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't12',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't21',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't11',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
sort: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetId/6->2', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 6,
|
||||
targetId: 2,
|
||||
});
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't11',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't12',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
sort: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't21',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetId/6->2 - method=insertAfter', async () => {
|
||||
await api.agent().resource('tests').move({
|
||||
sourceId: 6,
|
||||
targetId: 2,
|
||||
method: 'insertAfter',
|
||||
});
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't11',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't12',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
sort: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't21',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetScope', async () => {
|
||||
await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.move({
|
||||
sourceId: 1,
|
||||
targetScope: {
|
||||
state: 2,
|
||||
},
|
||||
});
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't12',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
sort: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't21',
|
||||
sort: 1,
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
sort: 2,
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
sort: 3,
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
sort: 4,
|
||||
},
|
||||
{
|
||||
title: 't11',
|
||||
sort: 5,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('targetScope - method=prepend', async () => {
|
||||
await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.move({
|
||||
sourceId: 1,
|
||||
targetScope: {
|
||||
state: 2,
|
||||
},
|
||||
method: 'prepend',
|
||||
});
|
||||
let response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 1 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't12',
|
||||
},
|
||||
{
|
||||
title: 't13',
|
||||
},
|
||||
{
|
||||
title: 't14',
|
||||
},
|
||||
],
|
||||
});
|
||||
response = await api
|
||||
.agent()
|
||||
.resource('tests')
|
||||
.list({
|
||||
sort: ['sort'],
|
||||
filter: { state: 2 },
|
||||
});
|
||||
expect(response.body).toMatchObject({
|
||||
rows: [
|
||||
{
|
||||
title: 't11',
|
||||
},
|
||||
{
|
||||
title: 't21',
|
||||
},
|
||||
{
|
||||
title: 't22',
|
||||
},
|
||||
{
|
||||
title: 't23',
|
||||
},
|
||||
{
|
||||
title: 't24',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
126
packages/actions/src/__tests__/remove-action.test.ts
Normal file
126
packages/actions/src/__tests__/remove-action.test.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('remove action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
let Profile;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Profile = app.collection({
|
||||
name: 'profiles',
|
||||
fields: [
|
||||
{ type: 'string', name: 'post_profile' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('remove belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t3 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't3',
|
||||
},
|
||||
});
|
||||
|
||||
await p1.setTags([t1, t2]);
|
||||
|
||||
expect(await p1.countTags()).toEqual(2);
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.remove({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [t1.get('id')],
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(1);
|
||||
});
|
||||
|
||||
test('remove has one', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
profile: {
|
||||
post_profile: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postProfile = await Profile.repository.findOne();
|
||||
expect(await postProfile.getPost()).not.toBeNull();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.profile')
|
||||
.remove({
|
||||
associatedIndex: p1.get('id'),
|
||||
});
|
||||
|
||||
await postProfile.reload();
|
||||
expect(await postProfile.getPost()).toBeNull();
|
||||
});
|
||||
});
|
112
packages/actions/src/__tests__/set-action.test.ts
Normal file
112
packages/actions/src/__tests__/set-action.test.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('set action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('set belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t3 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't3',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(0);
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.set({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [t1.get('id'), t2.get('id')],
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(2);
|
||||
|
||||
// add with through values
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.set({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [
|
||||
[
|
||||
t3.get('id'),
|
||||
{
|
||||
tagged_at: '123',
|
||||
},
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
const tags = await p1.getTags();
|
||||
expect(tags.length).toEqual(1);
|
||||
const tag = tags.find((t) => t.id == t3.get('id'));
|
||||
expect(tag.posts_tags.tagged_at).toEqual('123');
|
||||
});
|
||||
});
|
264
packages/actions/src/__tests__/sort-collection.test.ts
Normal file
264
packages/actions/src/__tests__/sort-collection.test.ts
Normal file
@ -0,0 +1,264 @@
|
||||
import { mockServer } from './index';
|
||||
import { SortAbleCollection } from '../actions';
|
||||
import lodash from 'lodash';
|
||||
|
||||
describe('sort collections', () => {
|
||||
let app;
|
||||
let Post;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('sort collection', () => {
|
||||
beforeEach(async () => {
|
||||
Post = app.db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: `t${i + 1}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('forward insert', async () => {
|
||||
const t2 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t4 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 't4',
|
||||
},
|
||||
});
|
||||
const sortCollection = new SortAbleCollection(Post);
|
||||
|
||||
await sortCollection.move(t2.get('id'), t4.get('id'));
|
||||
|
||||
const results = (
|
||||
await Post.repository.find({
|
||||
sort: ['sort'],
|
||||
})
|
||||
).map((r) => {
|
||||
return lodash.pick(r.toJSON(), ['title', 'sort']);
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 't1', sort: 1 },
|
||||
{ title: 't3', sort: 2 },
|
||||
{ title: 't4', sort: 3 },
|
||||
{ title: 't2', sort: 4 },
|
||||
{ title: 't5', sort: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('backward insert', async () => {
|
||||
const t2 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t4 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 't4',
|
||||
},
|
||||
});
|
||||
const sortCollection = new SortAbleCollection(Post);
|
||||
|
||||
await sortCollection.move(t4.get('id'), t2.get('id'));
|
||||
|
||||
const results = (
|
||||
await Post.repository.find({
|
||||
sort: ['sort'],
|
||||
})
|
||||
).map((r) => {
|
||||
return lodash.pick(r.toJSON(), ['title', 'sort']);
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 't1', sort: 1 },
|
||||
{ title: 't4', sort: 2 },
|
||||
{ title: 't2', sort: 3 },
|
||||
{ title: 't3', sort: 4 },
|
||||
{ title: 't5', sort: 5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('two scope move', () => {
|
||||
beforeEach(async () => {
|
||||
Post = app.db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scopeKey: 'status',
|
||||
},
|
||||
|
||||
{
|
||||
type: 'string',
|
||||
name: 'status',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
test('move in scope', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: `s1:t${i + 1}`,
|
||||
status: 'status1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: `s2:t${i + 1}`,
|
||||
status: 'status2',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const s1t2 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 's1:t2',
|
||||
},
|
||||
});
|
||||
|
||||
const s1t4 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 's1:t4',
|
||||
},
|
||||
});
|
||||
|
||||
const sortCollection = new SortAbleCollection(Post);
|
||||
await sortCollection.move(s1t2.get('id'), s1t4.get('id'));
|
||||
const results = (
|
||||
await Post.repository.find({
|
||||
sort: ['sort'],
|
||||
filter: {
|
||||
status: 'status1',
|
||||
},
|
||||
})
|
||||
).map((r) => {
|
||||
return lodash.pick(r.toJSON(), ['title', 'sort']);
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 's1:t1', sort: 1 },
|
||||
{ title: 's1:t3', sort: 2 },
|
||||
{ title: 's1:t4', sort: 3 },
|
||||
{ title: 's1:t2', sort: 4 },
|
||||
{ title: 's1:t5', sort: 5 },
|
||||
]);
|
||||
|
||||
const s2results = (
|
||||
await Post.repository.find({
|
||||
sort: ['sort'],
|
||||
filter: {
|
||||
status: 'status2',
|
||||
},
|
||||
})
|
||||
).map((r) => {
|
||||
return lodash.pick(r.toJSON(), ['title', 'sort']);
|
||||
});
|
||||
|
||||
expect(s2results).toEqual([
|
||||
{ title: 's2:t1', sort: 1 },
|
||||
{ title: 's2:t2', sort: 2 },
|
||||
{ title: 's2:t3', sort: 3 },
|
||||
{ title: 's2:t4', sort: 4 },
|
||||
{ title: 's2:t5', sort: 5 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('move between scope', async () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: `s1:t${i + 1}`,
|
||||
status: 'status1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await Post.repository.create({
|
||||
values: {
|
||||
title: `s2:t${i + 1}`,
|
||||
status: 'status2',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const s1t1 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 's1:t1',
|
||||
},
|
||||
});
|
||||
|
||||
const s2t3 = await Post.repository.findOne({
|
||||
filter: {
|
||||
title: 's2:t3',
|
||||
},
|
||||
});
|
||||
|
||||
const sortCollection = new SortAbleCollection(Post);
|
||||
|
||||
await sortCollection.move(s1t1.get('id'), s2t3.get('id'));
|
||||
|
||||
const results = (
|
||||
await Post.repository.find({
|
||||
sort: ['sort'],
|
||||
filter: {
|
||||
status: 'status2',
|
||||
},
|
||||
})
|
||||
).map((r) => {
|
||||
return lodash.pick(r.toJSON(), ['title', 'sort']);
|
||||
});
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 's2:t1', sort: 1 },
|
||||
{ title: 's2:t2', sort: 2 },
|
||||
{ title: 's1:t1', sort: 3 },
|
||||
{ title: 's2:t3', sort: 4 },
|
||||
{ title: 's2:t4', sort: 5 },
|
||||
{ title: 's2:t5', sort: 6 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
101
packages/actions/src/__tests__/toggle-action.test.ts
Normal file
101
packages/actions/src/__tests__/toggle-action.test.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('toggle action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('toggle belongs to many', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
},
|
||||
});
|
||||
|
||||
const t3 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't3',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(0);
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.toggle({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [t1.get('id'), t2.get('id')],
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(2);
|
||||
|
||||
await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.toggle({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: [t2.get('id')],
|
||||
});
|
||||
|
||||
expect(await p1.countTags()).toEqual(1);
|
||||
});
|
||||
});
|
172
packages/actions/src/__tests__/update-action.test.ts
Normal file
172
packages/actions/src/__tests__/update-action.test.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
|
||||
describe('update action', () => {
|
||||
let app;
|
||||
let Post;
|
||||
let Comment;
|
||||
let Tag;
|
||||
let PostTag;
|
||||
let Profile;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
registerActions(app);
|
||||
|
||||
PostTag = app.collection({
|
||||
name: 'posts_tags',
|
||||
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||
});
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'hasOne', name: 'profile' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'status', defaultValue: 'draft' },
|
||||
],
|
||||
});
|
||||
|
||||
Profile = app.collection({
|
||||
name: 'profiles',
|
||||
fields: [
|
||||
{ type: 'string', name: 'post_profile' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Comment = app.collection({
|
||||
name: 'comments',
|
||||
fields: [
|
||||
{ type: 'string', name: 'content' },
|
||||
{ type: 'belongsTo', name: 'post' },
|
||||
],
|
||||
});
|
||||
|
||||
Tag = app.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('update resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.update({
|
||||
resourceIndex: p1.get('id'),
|
||||
values: {
|
||||
title: 'p0',
|
||||
},
|
||||
});
|
||||
|
||||
await p1.reload();
|
||||
expect(p1.get('title')).toEqual('p0');
|
||||
});
|
||||
|
||||
test('update has many resource', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
comments: [
|
||||
{
|
||||
content: 'c1',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const c1 = await Comment.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.comments')
|
||||
.update({
|
||||
resourceIndex: c1.get('id'),
|
||||
associatedIndex: p1.get('id'),
|
||||
values: {
|
||||
content: 'c0',
|
||||
},
|
||||
});
|
||||
|
||||
await c1.reload();
|
||||
expect(c1.get('content')).toEqual('c0');
|
||||
});
|
||||
|
||||
test('update belongs to many through value', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
tags: [
|
||||
{
|
||||
name: 't1',
|
||||
posts_tags: {
|
||||
tagged_at: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const p1t1 = (await p1.getTags())[0];
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.tags')
|
||||
.update({
|
||||
resourceIndex: p1.get('id'),
|
||||
associatedIndex: p1t1.get('id'),
|
||||
values: {
|
||||
posts_tags: {
|
||||
tagged_at: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await p1t1.reload();
|
||||
expect(p1t1.posts_tags.tagged_at).toEqual('test');
|
||||
});
|
||||
|
||||
test('update has one', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
profile: {
|
||||
post_profile: 'test',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const postProfile = await Profile.repository.findOne();
|
||||
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts.profile')
|
||||
.update({
|
||||
associatedIndex: p1.get('id'),
|
||||
values: {
|
||||
post_profile: 'test0',
|
||||
},
|
||||
});
|
||||
|
||||
await postProfile.reload();
|
||||
expect(postProfile.get('post_profile')).toEqual('test0');
|
||||
});
|
||||
});
|
14
packages/actions/src/actions/add.ts
Normal file
14
packages/actions/src/actions/add.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
import { BelongsToManyRepository, MultipleRelationRepository, HasManyRepository } from '@nocobase/database';
|
||||
|
||||
export async function add(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
if (!(repository instanceof MultipleRelationRepository || repository instanceof HasManyRepository)) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
await (<HasManyRepository | BelongsToManyRepository>repository).add(ctx.action.params.values);
|
||||
await next();
|
||||
}
|
19
packages/actions/src/actions/create.ts
Normal file
19
packages/actions/src/actions/create.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
|
||||
export async function create(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
const { whitelist, blacklist, updateAssociationValues, values } = ctx.action.params;
|
||||
|
||||
const instance = await repository.create({
|
||||
values,
|
||||
whitelist,
|
||||
blacklist,
|
||||
updateAssociationValues,
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
ctx.body = instance;
|
||||
|
||||
await next();
|
||||
}
|
17
packages/actions/src/actions/destroy.ts
Normal file
17
packages/actions/src/actions/destroy.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
|
||||
export async function destroy(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const { resourceIndex, filter } = ctx.action.params;
|
||||
|
||||
const instance = await repository.destroy({
|
||||
filter,
|
||||
filterByPk: resourceIndex,
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
ctx.body = instance;
|
||||
await next();
|
||||
}
|
20
packages/actions/src/actions/get.ts
Normal file
20
packages/actions/src/actions/get.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
import { SingleRelationRepository } from '@nocobase/database';
|
||||
|
||||
export async function get(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const { resourceIndex, fields, appends, except, filter } = ctx.action.params;
|
||||
|
||||
const instance = await repository.findOne({
|
||||
filterByPk: resourceIndex,
|
||||
fields,
|
||||
appends,
|
||||
except,
|
||||
filter,
|
||||
});
|
||||
|
||||
ctx.body = instance;
|
||||
await next();
|
||||
}
|
10
packages/actions/src/actions/index.ts
Normal file
10
packages/actions/src/actions/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export * from './list';
|
||||
export * from './create';
|
||||
export * from './update';
|
||||
export * from './destroy';
|
||||
export * from './get';
|
||||
export * from './add';
|
||||
export * from './set';
|
||||
export * from './remove';
|
||||
export * from './toggle';
|
||||
export * from './move';
|
47
packages/actions/src/actions/list.ts
Normal file
47
packages/actions/src/actions/list.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
|
||||
export const DEFAULT_PAGE = 1;
|
||||
export const DEFAULT_PER_PAGE = 20;
|
||||
|
||||
function pageArgsToLimitArgs(
|
||||
page: number,
|
||||
perPage: number,
|
||||
): {
|
||||
offset: number;
|
||||
limit: number;
|
||||
} {
|
||||
return {
|
||||
offset: (page - 1) * perPage,
|
||||
limit: perPage,
|
||||
};
|
||||
}
|
||||
|
||||
function totalPage(total, perPage): number {
|
||||
return Math.ceil(total / perPage);
|
||||
}
|
||||
|
||||
export async function list(ctx: Context, next) {
|
||||
const { page = DEFAULT_PAGE, perPage = DEFAULT_PER_PAGE, fields, filter, appends, except, sort } = ctx.action.params;
|
||||
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const [rows, count] = await repository.findAndCount({
|
||||
filter,
|
||||
fields,
|
||||
appends,
|
||||
except,
|
||||
sort,
|
||||
...pageArgsToLimitArgs(page, perPage),
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
count,
|
||||
rows,
|
||||
page,
|
||||
perPage,
|
||||
totalPage: totalPage(count, perPage),
|
||||
};
|
||||
|
||||
await next();
|
||||
}
|
138
packages/actions/src/actions/move.ts
Normal file
138
packages/actions/src/actions/move.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import { Op, Model } from 'sequelize';
|
||||
|
||||
import { Context } from '..';
|
||||
import { Collection, PrimaryKey, Repository, SortField } from '@nocobase/database';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
|
||||
export async function move(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const { sourceId, targetId, sortField, targetScope, sticky, method } = ctx.action.params;
|
||||
|
||||
if (repository instanceof Repository) {
|
||||
const sortAbleCollection = new SortAbleCollection(repository.collection, sortField);
|
||||
|
||||
if (sourceId && targetId) {
|
||||
await sortAbleCollection.move(sourceId, targetId, {
|
||||
insertAfter: method === 'insertAfter',
|
||||
});
|
||||
}
|
||||
|
||||
// change scope
|
||||
if (sourceId && targetScope) {
|
||||
await sortAbleCollection.changeScope(sourceId, targetScope, method);
|
||||
}
|
||||
|
||||
if (sourceId && sticky) {
|
||||
await sortAbleCollection.sticky(sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
await next();
|
||||
}
|
||||
|
||||
interface SortPosition {
|
||||
scope?: string;
|
||||
id: PrimaryKey;
|
||||
}
|
||||
|
||||
interface MoveOptions {
|
||||
insertAfter?: boolean;
|
||||
}
|
||||
|
||||
export class SortAbleCollection {
|
||||
collection: Collection;
|
||||
field: SortField;
|
||||
scopeKey: string;
|
||||
|
||||
constructor(collection: Collection, fieldName: string = 'sort') {
|
||||
this.collection = collection;
|
||||
this.field = collection.getField(fieldName);
|
||||
|
||||
if (!(this.field instanceof SortField)) {
|
||||
throw new Error(`${fieldName} is not a sort field`);
|
||||
}
|
||||
|
||||
this.scopeKey = this.field.get('scopeKey');
|
||||
}
|
||||
|
||||
// insert source position to target position
|
||||
async move(sourceInstanceId: PrimaryKey, targetInstanceId: PrimaryKey, options: MoveOptions = {}) {
|
||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
||||
const targetInstance = await this.collection.repository.findById(targetInstanceId);
|
||||
|
||||
if (this.scopeKey && sourceInstance.get(this.scopeKey) !== targetInstance.get(this.scopeKey)) {
|
||||
await sourceInstance.set(this.scopeKey, targetInstance.get(this.scopeKey));
|
||||
await sourceInstance.save();
|
||||
}
|
||||
|
||||
await this.sameScopeMove(sourceInstance, targetInstance, options);
|
||||
}
|
||||
|
||||
async changeScope(sourceInstanceId: PrimaryKey, targetScope: any, method?: string) {
|
||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
||||
const targetScopeValue = targetScope[this.scopeKey];
|
||||
|
||||
if (targetScopeValue && sourceInstance.get(this.scopeKey) !== targetScopeValue) {
|
||||
await sourceInstance.set(this.scopeKey, targetScopeValue);
|
||||
await sourceInstance.save();
|
||||
|
||||
if (method === 'prepend') {
|
||||
await this.sticky(sourceInstanceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async sticky(sourceInstanceId: PrimaryKey) {
|
||||
const sourceInstance = await this.collection.repository.findById(sourceInstanceId);
|
||||
sourceInstance.set(this.field.get('name'), 0);
|
||||
await sourceInstance.save();
|
||||
}
|
||||
|
||||
async sameScopeMove(sourceInstance: Model, targetInstance: Model, options: MoveOptions) {
|
||||
const fieldName = this.field.get('name');
|
||||
|
||||
const sourceSort = sourceInstance.get(fieldName);
|
||||
let targetSort = targetInstance.get(fieldName);
|
||||
|
||||
if (options.insertAfter) {
|
||||
targetSort = targetSort + 1;
|
||||
}
|
||||
|
||||
let scopeValue = this.scopeKey ? sourceInstance.get(this.scopeKey) : null;
|
||||
let updateCondition;
|
||||
let change;
|
||||
|
||||
if (targetSort > sourceSort) {
|
||||
updateCondition = {
|
||||
[Op.gt]: sourceSort,
|
||||
[Op.lte]: targetSort,
|
||||
};
|
||||
change = -1;
|
||||
} else {
|
||||
updateCondition = {
|
||||
[Op.lt]: sourceSort,
|
||||
[Op.gte]: targetSort,
|
||||
};
|
||||
change = 1;
|
||||
}
|
||||
|
||||
const where = {
|
||||
[fieldName]: updateCondition,
|
||||
};
|
||||
|
||||
if (scopeValue) {
|
||||
where[this.scopeKey] = {
|
||||
[Op.eq]: scopeValue,
|
||||
};
|
||||
}
|
||||
await this.collection.model.increment(fieldName, {
|
||||
where,
|
||||
by: change,
|
||||
});
|
||||
|
||||
await sourceInstance.update({
|
||||
[fieldName]: targetSort,
|
||||
});
|
||||
}
|
||||
}
|
2
packages/actions/src/actions/remove.ts
Normal file
2
packages/actions/src/actions/remove.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { RelationRepositoryActionBuilder } from './utils';
|
||||
export const remove = RelationRepositoryActionBuilder('remove');
|
2
packages/actions/src/actions/set.ts
Normal file
2
packages/actions/src/actions/set.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { RelationRepositoryActionBuilder } from './utils';
|
||||
export const set = RelationRepositoryActionBuilder('set');
|
14
packages/actions/src/actions/toggle.ts
Normal file
14
packages/actions/src/actions/toggle.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
import { BelongsToManyRepository } from '@nocobase/database';
|
||||
|
||||
export async function toggle(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
if (!(repository instanceof BelongsToManyRepository)) {
|
||||
return await next();
|
||||
}
|
||||
|
||||
await (<BelongsToManyRepository>repository).toggle(ctx.action.params.values);
|
||||
await next();
|
||||
}
|
20
packages/actions/src/actions/update.ts
Normal file
20
packages/actions/src/actions/update.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from './utils';
|
||||
|
||||
export async function update(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
const { resourceIndex, values, whitelist, blacklist, filter, updateAssociationValues } = ctx.action.params;
|
||||
|
||||
const instance = await repository.update({
|
||||
filterByPk: resourceIndex,
|
||||
values,
|
||||
whitelist,
|
||||
blacklist,
|
||||
filter,
|
||||
updateAssociationValues,
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
ctx.body = instance;
|
||||
await next();
|
||||
}
|
27
packages/actions/src/actions/utils.ts
Normal file
27
packages/actions/src/actions/utils.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { MultipleRelationRepository, Repository } from '@nocobase/database';
|
||||
import { Context } from '..';
|
||||
|
||||
export function getRepositoryFromParams(ctx: Context) {
|
||||
const { resourceName, associatedName, associatedIndex } = ctx.action.params;
|
||||
|
||||
let repository: MultipleRelationRepository | Repository;
|
||||
|
||||
if (associatedName) {
|
||||
repository = <MultipleRelationRepository>(
|
||||
ctx.db.getCollection(associatedName).repository.relation(resourceName).of(associatedIndex)
|
||||
);
|
||||
} else {
|
||||
repository = <Repository>ctx.db.getCollection(resourceName).repository;
|
||||
}
|
||||
|
||||
return repository;
|
||||
}
|
||||
|
||||
export function RelationRepositoryActionBuilder(method: 'remove' | 'set') {
|
||||
return async function (ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
await repository[method](ctx.action.params.values);
|
||||
await next();
|
||||
};
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import Koa from 'koa';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Action } from '@nocobase/resourcer';
|
||||
import lodash from 'lodash';
|
||||
import * as actions from './actions';
|
||||
|
||||
export type Next = () => Promise<any>;
|
||||
|
||||
@ -11,6 +13,10 @@ export interface Context extends Koa.Context {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function registerActions(api: any) {}
|
||||
export function registerActions(api: any) {
|
||||
api.actions(
|
||||
lodash.pick(actions, ['add', 'create', 'destroy', 'get', 'list', 'remove', 'set', 'toggle', 'update', 'move']),
|
||||
);
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
@ -13,6 +13,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/utils": "^0.6.0-alpha.0",
|
||||
"async-mutex": "^0.3.2",
|
||||
"bcrypt": "^5.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"flat": "^5.0.2",
|
||||
|
@ -22,6 +22,7 @@ describe('string field', () => {
|
||||
fields: [{ type: 'sort', name: 'sort' }],
|
||||
});
|
||||
await db.sync();
|
||||
|
||||
const test1 = await Test.model.create<any>();
|
||||
expect(test1.sort).toBe(1);
|
||||
const test2 = await Test.model.create<any>();
|
||||
@ -30,6 +31,24 @@ describe('string field', () => {
|
||||
expect(test3.sort).toBe(3);
|
||||
});
|
||||
|
||||
test('simultaneously create ', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [{ type: 'sort', name: 'sort' }],
|
||||
});
|
||||
await db.sync();
|
||||
|
||||
const promise = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
promise.push(Test.model.create());
|
||||
}
|
||||
|
||||
await Promise.all(promise);
|
||||
const tests = await Test.model.findAll();
|
||||
const sortValues = tests.map((t) => t.get('sort')).sort();
|
||||
expect(sortValues).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('skip if sort value not empty', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
@ -53,13 +72,21 @@ describe('string field', () => {
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
|
||||
const t1 = await Test.model.create({ status: 'publish' });
|
||||
const t2 = await Test.model.create({ status: 'publish' });
|
||||
const t3 = await Test.model.create({ status: 'draft' });
|
||||
const t4 = await Test.model.create({ status: 'draft' });
|
||||
|
||||
expect(t1.get('sort')).toBe(1);
|
||||
expect(t2.get('sort')).toBe(2);
|
||||
expect(t3.get('sort')).toBe(1);
|
||||
expect(t4.get('sort')).toBe(2);
|
||||
|
||||
t1.set('status', 'draft');
|
||||
await t1.save();
|
||||
|
||||
await t1.reload();
|
||||
expect(t1.get('sort')).toBe(3);
|
||||
});
|
||||
});
|
||||
|
@ -47,6 +47,7 @@ describe('belongs to many', () => {
|
||||
fields: [
|
||||
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'string', name: 'status' },
|
||||
],
|
||||
});
|
||||
|
||||
@ -124,7 +125,9 @@ describe('belongs to many', () => {
|
||||
});
|
||||
|
||||
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||
let [findResult, count] = await PostTagRepository.findAndCount();
|
||||
let [findResult, count] = await PostTagRepository.findAndCount({
|
||||
fields: ['id'],
|
||||
});
|
||||
|
||||
expect(count).toEqual(2);
|
||||
|
||||
@ -450,6 +453,43 @@ describe('belongs to many', () => {
|
||||
expect(count).toEqual(0);
|
||||
});
|
||||
|
||||
test('destroy by id and filter', async () => {
|
||||
let t1 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't1',
|
||||
status: 'published',
|
||||
},
|
||||
});
|
||||
|
||||
const t2 = await Tag.repository.create({
|
||||
values: {
|
||||
name: 't2',
|
||||
status: 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await Post.repository.create({
|
||||
values: { title: 'p1' },
|
||||
});
|
||||
|
||||
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||
|
||||
await PostTagRepository.set([t1.id, t2.id]);
|
||||
|
||||
let [_, count] = await PostTagRepository.findAndCount();
|
||||
expect(count).toEqual(2);
|
||||
|
||||
await PostTagRepository.destroy({
|
||||
filterByPk: t1.get('id') as number,
|
||||
filter: {
|
||||
status: 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
[_, count] = await PostTagRepository.findAndCount();
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
||||
test('destroy with id', async () => {
|
||||
let t1 = await Tag.repository.create({
|
||||
values: {
|
||||
|
@ -29,6 +29,7 @@ describe('has many repository', () => {
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||
{ type: 'hasMany', name: 'comments' },
|
||||
{ type: 'string', name: 'status' },
|
||||
],
|
||||
});
|
||||
|
||||
@ -167,6 +168,56 @@ describe('has many repository', () => {
|
||||
expect(findAndCount[1]).toEqual(6);
|
||||
});
|
||||
|
||||
test('destroy by pk and filter', async () => {
|
||||
const u1 = await User.repository.create({
|
||||
values: { name: 'u1' },
|
||||
});
|
||||
|
||||
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||
|
||||
const p1 = await UserPostRepository.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
status: 'published',
|
||||
},
|
||||
});
|
||||
|
||||
const p2 = await UserPostRepository.create({
|
||||
values: {
|
||||
title: 't2',
|
||||
status: 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
await UserPostRepository.destroy({
|
||||
filterByPk: p1.id,
|
||||
filter: {
|
||||
status: 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await UserPostRepository.count()).toEqual(2);
|
||||
|
||||
await UserPostRepository.destroy({
|
||||
filterByPk: p1.id,
|
||||
filter: {
|
||||
status: 'published',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
await UserPostRepository.findOne({
|
||||
filterByPk: p1.id,
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
expect(
|
||||
await UserPostRepository.findOne({
|
||||
filterByPk: p2.id,
|
||||
}),
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
test('destroy by pk', async () => {
|
||||
const u1 = await User.repository.create({
|
||||
values: { name: 'u1' },
|
||||
|
@ -20,12 +20,31 @@ describe('destroy', () => {
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
{ type: 'string', name: 'status' },
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
test('destroy with filter and filterByPk', async () => {
|
||||
const p1 = await Post.repository.create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
status: 'published',
|
||||
},
|
||||
});
|
||||
|
||||
await Post.repository.destroy({
|
||||
filterByPk: p1.get('id') as number,
|
||||
filter: {
|
||||
status: 'draft',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await Post.repository.count()).toEqual(1);
|
||||
});
|
||||
|
||||
test('destroy all', async () => {
|
||||
await User.repository.create({
|
||||
values: {
|
||||
|
@ -1,27 +1,54 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { isNumber } from 'lodash';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
const sortFieldMutex = new Mutex();
|
||||
|
||||
export class SortField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.INTEGER;
|
||||
}
|
||||
|
||||
init() {
|
||||
async setSortValue(instance, options) {
|
||||
const { name, scopeKey } = this.options;
|
||||
const { model } = this.context.collection;
|
||||
model.beforeCreate(async (instance, options) => {
|
||||
if (isNumber(instance.get(name))) {
|
||||
|
||||
if (isNumber(instance.get(name)) && instance._previousDataValues[scopeKey] == instance[scopeKey]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const where = {};
|
||||
|
||||
if (scopeKey) {
|
||||
where[scopeKey] = instance.get(scopeKey);
|
||||
}
|
||||
|
||||
await sortFieldMutex.runExclusive(async () => {
|
||||
const max = await model.max<number, any>(name, { ...options, where });
|
||||
instance.set(name, (max || 0) + 1);
|
||||
const newValue = (max || 0) + 1;
|
||||
instance.set(name, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
async onScopeChange(instance, options) {
|
||||
const { scopeKey } = this.options;
|
||||
if (scopeKey && !instance.isNewRecord && instance._previousDataValues[scopeKey] != instance[scopeKey]) {
|
||||
await this.setSortValue(instance, options);
|
||||
}
|
||||
}
|
||||
|
||||
bind() {
|
||||
super.bind();
|
||||
this.on('beforeUpdate', this.onScopeChange.bind(this));
|
||||
this.on('beforeCreate', this.setSortValue.bind(this));
|
||||
}
|
||||
|
||||
unbind() {
|
||||
super.unbind();
|
||||
this.off('beforeUpdate', this.onScopeChange.bind(this));
|
||||
this.off('beforeCreate', this.setSortValue.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
export interface SortFieldOptions extends BaseColumnFieldOptions {
|
||||
|
@ -1,9 +1,14 @@
|
||||
export * from './database';
|
||||
export * from './collection';
|
||||
export * from './repository';
|
||||
export * from './utils';
|
||||
export { Database as default } from './database';
|
||||
export * from './relation-repository/belongs-to-many-repository';
|
||||
export * from './relation-repository/belongs-to-repository';
|
||||
export * from './relation-repository/hasmany-repository';
|
||||
export * from './relation-repository/single-relation-repository';
|
||||
export * from './relation-repository/multiple-relation-repository';
|
||||
|
||||
export { Model, ModelCtor } from 'sequelize';
|
||||
export * from './fields';
|
||||
export * from './update-associations';
|
@ -76,12 +76,14 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
|
||||
});
|
||||
|
||||
ids = instancesToIds(instances);
|
||||
} else if (options && options['filterByPk']) {
|
||||
options = options['filterByPk'];
|
||||
}
|
||||
|
||||
const instances = (<any>this.association).toInstanceArray(options);
|
||||
ids = instancesToIds(instances);
|
||||
} else if (options && !options['filterByPk']) {
|
||||
if (options && options['filterByPk']) {
|
||||
const instances = (<any>this.association).toInstanceArray(options['filterByPk']);
|
||||
ids = ids ? lodash.intersection(ids, instancesToIds(instances)) : instancesToIds(instances);
|
||||
}
|
||||
|
||||
if (options && !options['filterByPk'] && !options['filter']) {
|
||||
const sourceModel = await this.getSourceModel(transaction);
|
||||
|
||||
const instances = await sourceModel[this.accessors().get]({
|
||||
@ -228,6 +230,18 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
|
||||
return;
|
||||
}
|
||||
|
||||
extendFindOptions(findOptions) {
|
||||
let joinTableAttributes;
|
||||
if (lodash.get(findOptions, 'fields')) {
|
||||
joinTableAttributes = [];
|
||||
}
|
||||
|
||||
return {
|
||||
...findOptions,
|
||||
joinTableAttributes,
|
||||
};
|
||||
}
|
||||
|
||||
throughName() {
|
||||
return this.throughModel().name;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ interface BelongsToFindOptions extends SingleRelationFindOption {}
|
||||
interface IBelongsToRepository<M extends Model> {
|
||||
// 不需要 findOne,find 就是 findOne
|
||||
find(options?: BelongsToFindOptions): Promise<M>;
|
||||
findOne(options?: BelongsToFindOptions): Promise<M>;
|
||||
// 新增并关联,如果存在关联,解除之后,与新数据建立关联
|
||||
create(options?: CreateOptions): Promise<M>;
|
||||
// 更新
|
||||
|
@ -53,7 +53,8 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
|
||||
}
|
||||
|
||||
where.push(filterResult.where);
|
||||
} else if (options && options['filterByPk']) {
|
||||
}
|
||||
if (options && options['filterByPk']) {
|
||||
if (typeof options === 'object' && options['filterByPk']) {
|
||||
options = options['filterByPk'];
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ interface HasOneFindOptions extends SingleRelationFindOption {}
|
||||
|
||||
interface IHasOneRepository<M extends Model> {
|
||||
// 不需要 findOne,find 就是 findOne
|
||||
find(options?: HasOneFindOptions): Promise<Model<any>>;
|
||||
find(options?: HasOneFindOptions): Promise<M>;
|
||||
findOne(options?: HasOneFindOptions): Promise<M>;
|
||||
// 新增并关联,如果存在关联,解除之后,与新数据建立关联
|
||||
create(options?: CreateOptions): Promise<M>;
|
||||
// 更新
|
||||
@ -19,4 +20,4 @@ interface IHasOneRepository<M extends Model> {
|
||||
remove(): Promise<void>;
|
||||
}
|
||||
|
||||
export class HasOneRepository<M extends Model> extends SingleRelationRepository implements IHasOneRepository<M> {}
|
||||
export class HasOneRepository extends SingleRelationRepository implements IHasOneRepository<any> {}
|
||||
|
@ -25,12 +25,18 @@ export interface AssociatedOptions extends TransactionAble {
|
||||
}
|
||||
|
||||
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
extendFindOptions(findOptions) {
|
||||
return findOptions;
|
||||
}
|
||||
|
||||
async find(options?: FindOptions): Promise<any> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
|
||||
const findOptions = this.buildQueryOptions({
|
||||
const findOptions = this.extendFindOptions(
|
||||
this.buildQueryOptions({
|
||||
...options,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const getAccessor = this.accessors().get;
|
||||
const sourceModel = await this.getSourceModel(transaction);
|
||||
@ -77,7 +83,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
];
|
||||
}
|
||||
|
||||
async count(options: CountOptions) {
|
||||
async count(options?: CountOptions) {
|
||||
const transaction = await this.getTransaction(options);
|
||||
|
||||
const sourceModel = await this.getSourceModel(transaction);
|
||||
@ -101,7 +107,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return count.count;
|
||||
return parseInt(count.count);
|
||||
}
|
||||
|
||||
async findOne(options?: FindOneOptions): Promise<any> {
|
||||
|
@ -2,12 +2,13 @@ import { RelationRepository, transaction } from './relation-repository';
|
||||
import { Model, SingleAssociationAccessors } from 'sequelize';
|
||||
import { updateModelByValues } from '../update-associations';
|
||||
import lodash from 'lodash';
|
||||
import { Appends, Except, Fields, PrimaryKey, TransactionAble, UpdateOptions } from '../repository';
|
||||
import { Appends, Except, Fields, Filter, PrimaryKey, TransactionAble, UpdateOptions } from '../repository';
|
||||
|
||||
export interface SingleRelationFindOption extends TransactionAble {
|
||||
fields?: Fields;
|
||||
except?: Except;
|
||||
appends?: Appends;
|
||||
filter?: Filter;
|
||||
}
|
||||
|
||||
interface SetOption extends TransactionAble {
|
||||
@ -56,6 +57,10 @@ export abstract class SingleRelationRepository extends RelationRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(options?: SingleRelationFindOption): Promise<Model<any>> {
|
||||
return this.find(options);
|
||||
}
|
||||
|
||||
@transaction()
|
||||
async destroy(options?: TransactionAble): Promise<Boolean> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
|
@ -83,6 +83,7 @@ export interface DestroyOptions extends SequelizeDestroyOptions {
|
||||
filter?: Filter;
|
||||
filterByPk?: PrimaryKey | PrimaryKey[];
|
||||
truncate?: boolean;
|
||||
context?: any;
|
||||
}
|
||||
|
||||
interface FindAndCountOptions extends Omit<SequelizeAndCountOptions, 'where' | 'include' | 'order'> {
|
||||
@ -197,7 +198,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
transaction,
|
||||
});
|
||||
|
||||
return count as any;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -370,14 +371,14 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
|
||||
options = <DestroyOptions>options;
|
||||
|
||||
let filterByPk = options.filterByPk;
|
||||
|
||||
if (filterByPk) {
|
||||
if (!Array.isArray(filterByPk)) {
|
||||
filterByPk = [filterByPk];
|
||||
}
|
||||
const filterByPk: PrimaryKey[] | undefined =
|
||||
options.filterByPk && !lodash.isArray(options.filterByPk)
|
||||
? [options.filterByPk]
|
||||
: (options.filterByPk as PrimaryKey[] | undefined);
|
||||
|
||||
if (filterByPk && !options.filter) {
|
||||
return await this.model.destroy({
|
||||
...options,
|
||||
where: {
|
||||
[this.model.primaryKeyAttribute]: {
|
||||
[Op.in]: filterByPk,
|
||||
@ -388,19 +389,27 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
}
|
||||
|
||||
if (options.filter) {
|
||||
const instances = await this.find({
|
||||
let pks = (
|
||||
await this.find({
|
||||
filter: options.filter,
|
||||
transaction,
|
||||
});
|
||||
})
|
||||
).map((instance) => instance[this.model.primaryKeyAttribute]);
|
||||
|
||||
if (filterByPk) {
|
||||
pks = lodash.intersection(pks, filterByPk);
|
||||
}
|
||||
|
||||
return await this.destroy({
|
||||
filterByPk: instances.map((instance) => instance[this.model.primaryKeyAttribute]),
|
||||
context: options.context,
|
||||
filterByPk: pks,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
if (options.truncate) {
|
||||
return await this.model.destroy({
|
||||
...options,
|
||||
truncate: true,
|
||||
transaction,
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
import qs from 'qs';
|
||||
import glob from 'glob';
|
||||
import compose from 'koa-compose';
|
||||
import Action, { ActionName } from './action';
|
||||
@ -280,6 +279,7 @@ export class Resourcer {
|
||||
accessors: this.options.accessors || accessors,
|
||||
},
|
||||
);
|
||||
|
||||
if (!params) {
|
||||
return next();
|
||||
}
|
||||
|
@ -3368,6 +3368,13 @@ astral-regex@^2.0.0:
|
||||
resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async-mutex@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.nlark.com/async-mutex/download/async-mutex-0.3.2.tgz#1485eda5bda1b0ec7c8df1ac2e815757ad1831df"
|
||||
integrity sha1-FIXtpb2hsOx8jfGsLoFXV60YMd8=
|
||||
dependencies:
|
||||
tslib "^2.3.1"
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
@ -13249,7 +13256,7 @@ tslib@^1.8.1, tslib@^1.9.0:
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.1:
|
||||
tslib@^2.0.1, tslib@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
Loading…
Reference in New Issue
Block a user