feat(database): add firstOrCreate and updateOrCreate in repository (#1943)
* chore: tmp commit * feat: firstOrCreate * chore: firstOrCreate * feat: updateOrCreate * chore: test * feat: values to filter * feat: firstOrCreate http api * fix: build error
This commit is contained in:
parent
57d47371da
commit
d7e6b7b320
44
packages/core/actions/src/__tests__/first-or-create.test.ts
Normal file
44
packages/core/actions/src/__tests__/first-or-create.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
import { Collection } from '@nocobase/database';
|
||||
|
||||
describe('first or create', () => {
|
||||
let app;
|
||||
|
||||
let Post: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
await app.db.clean({ drop: true });
|
||||
registerActions(app);
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [{ type: 'string', name: 'title' }],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('first or create resource', async () => {
|
||||
expect(await Post.repository.findOne()).toBeNull();
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.firstOrCreate({
|
||||
values: {
|
||||
title: 't1',
|
||||
},
|
||||
filterKeys: ['title'],
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
const post = await Post.repository.findOne();
|
||||
expect(post).not.toBeNull();
|
||||
expect(post['title']).toEqual('t1');
|
||||
});
|
||||
});
|
@ -81,6 +81,7 @@ describe('get action', () => {
|
||||
});
|
||||
|
||||
const body = response.body;
|
||||
|
||||
expect(body['id']).toEqual(p1.get('id'));
|
||||
});
|
||||
|
||||
|
44
packages/core/actions/src/__tests__/update-or-create.test.ts
Normal file
44
packages/core/actions/src/__tests__/update-or-create.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { mockServer } from './index';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
import { Collection } from '@nocobase/database';
|
||||
|
||||
describe('update or create', () => {
|
||||
let app;
|
||||
|
||||
let Post: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
await app.db.clean({ drop: true });
|
||||
registerActions(app);
|
||||
|
||||
Post = app.collection({
|
||||
name: 'posts',
|
||||
fields: [{ type: 'string', name: 'title' }],
|
||||
});
|
||||
|
||||
await app.db.sync();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('update or create resource', async () => {
|
||||
expect(await Post.repository.findOne()).toBeNull();
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('posts')
|
||||
.updateOrCreate({
|
||||
values: {
|
||||
title: 't1',
|
||||
},
|
||||
filterKeys: ['title'],
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
const post = await Post.repository.findOne();
|
||||
expect(post).not.toBeNull();
|
||||
expect(post['title']).toEqual('t1');
|
||||
});
|
||||
});
|
3
packages/core/actions/src/actions/first-or-create.ts
Normal file
3
packages/core/actions/src/actions/first-or-create.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { proxyToRepository } from './proxy-to-repository';
|
||||
|
||||
export const firstOrCreate = proxyToRepository(['values', 'filterKeys'], 'firstOrCreate');
|
@ -1,22 +1,3 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
import { proxyToRepository } from './proxy-to-repository';
|
||||
|
||||
export async function get(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const { filterByTk, fields, appends, except, filter } = ctx.action.params;
|
||||
|
||||
const instance = await repository.findOne({
|
||||
filterByTk,
|
||||
fields,
|
||||
appends,
|
||||
except,
|
||||
filter,
|
||||
context: ctx,
|
||||
});
|
||||
|
||||
ctx.body = instance || null;
|
||||
ctx.status = 200;
|
||||
|
||||
await next();
|
||||
}
|
||||
export const get = proxyToRepository(['filterByTk', 'fields', 'appends', 'except', 'filter'], 'findOne');
|
||||
|
@ -8,3 +8,5 @@ export * from './set';
|
||||
export * from './remove';
|
||||
export * from './toggle';
|
||||
export * from './move';
|
||||
export * from './first-or-create';
|
||||
export * from './update-or-create';
|
||||
|
17
packages/core/actions/src/actions/proxy-to-repository.ts
Normal file
17
packages/core/actions/src/actions/proxy-to-repository.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
import { Context } from '../index';
|
||||
import lodash from 'lodash';
|
||||
|
||||
export function proxyToRepository(paramKeys: string[], repositoryMethod: string) {
|
||||
return async function (ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
|
||||
const callObj = lodash.pick(ctx.action.params, paramKeys);
|
||||
callObj.context = ctx;
|
||||
|
||||
ctx.body = await repository[repositoryMethod](callObj);
|
||||
|
||||
ctx.status = 200;
|
||||
await next();
|
||||
};
|
||||
}
|
3
packages/core/actions/src/actions/update-or-create.ts
Normal file
3
packages/core/actions/src/actions/update-or-create.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { proxyToRepository } from './proxy-to-repository';
|
||||
|
||||
export const updateOrCreate = proxyToRepository(['values', 'filterKeys'], 'updateOrCreate');
|
@ -1,20 +1,6 @@
|
||||
import { Context } from '..';
|
||||
import { getRepositoryFromParams } from '../utils';
|
||||
import { proxyToRepository } from './proxy-to-repository';
|
||||
|
||||
export async function update(ctx: Context, next) {
|
||||
const repository = getRepositoryFromParams(ctx);
|
||||
const { forceUpdate, filterByTk, values, whitelist, blacklist, filter, updateAssociationValues } = ctx.action.params;
|
||||
|
||||
ctx.body = await repository.update({
|
||||
filterByTk,
|
||||
values,
|
||||
whitelist,
|
||||
blacklist,
|
||||
filter,
|
||||
updateAssociationValues,
|
||||
context: ctx,
|
||||
forceUpdate,
|
||||
});
|
||||
|
||||
await next();
|
||||
}
|
||||
export const update = proxyToRepository(
|
||||
['filterByTk', 'values', 'whitelist', 'blacklist', 'filter', 'updateAssociationValues', 'forceUpdate'],
|
||||
'update',
|
||||
);
|
||||
|
@ -21,7 +21,20 @@ export interface Context extends Koa.Context {
|
||||
|
||||
export function registerActions(api: any) {
|
||||
api.actions(
|
||||
lodash.pick(actions, ['add', 'create', 'destroy', 'get', 'list', 'remove', 'set', 'toggle', 'update', 'move']),
|
||||
lodash.pick(actions, [
|
||||
'add',
|
||||
'create',
|
||||
'destroy',
|
||||
'get',
|
||||
'list',
|
||||
'remove',
|
||||
'set',
|
||||
'toggle',
|
||||
'update',
|
||||
'move',
|
||||
'firstOrCreate',
|
||||
'updateOrCreate',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,29 @@
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import { mockDatabase } from './';
|
||||
import { Repository } from '@nocobase/database';
|
||||
|
||||
describe('repository', () => {
|
||||
test('value to filter', async () => {
|
||||
const value = {
|
||||
tags: [
|
||||
{
|
||||
categories: [{ name: 'c1' }, { name: 'c2' }],
|
||||
},
|
||||
{
|
||||
categories: [{ name: 'c3' }, { name: 'c4' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const filter = Repository.valuesToFilter(value, ['tags.categories.name']);
|
||||
expect(filter.$and).toEqual([
|
||||
{
|
||||
'tags.categories.name': ['c1', 'c2', 'c3', 'c4'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('find by targetKey', function () {
|
||||
let db: Database;
|
||||
@ -563,6 +586,7 @@ describe('repository.relatedQuery', () => {
|
||||
const post2 = await userPostRepository.create({
|
||||
values: { name: 'post2' },
|
||||
});
|
||||
|
||||
expect(post2).toMatchObject({
|
||||
name: 'post2',
|
||||
userId: user.get('id'),
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { mockDatabase } from '../index';
|
||||
import Database from '../../database';
|
||||
import { Collection } from '../../collection';
|
||||
|
||||
describe('create with hasMany', () => {
|
||||
let db: Database;
|
||||
let Post;
|
||||
let User;
|
||||
let Post: Collection;
|
||||
let User: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
@ -61,8 +62,8 @@ describe('create with hasMany', () => {
|
||||
|
||||
describe('create with belongsToMany', () => {
|
||||
let db: Database;
|
||||
let Post;
|
||||
let Tag;
|
||||
let Post: Collection;
|
||||
let Tag: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
@ -122,16 +123,36 @@ describe('create with belongsToMany', () => {
|
||||
|
||||
describe('create', () => {
|
||||
let db: Database;
|
||||
let User;
|
||||
let Post;
|
||||
let User: Collection;
|
||||
let Post: Collection;
|
||||
let Group: Collection;
|
||||
let Role: Collection;
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
Group = db.collection({
|
||||
name: 'groups',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
|
||||
User = db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'integer', name: 'age' },
|
||||
{ type: 'hasMany', name: 'posts' },
|
||||
{ type: 'belongsTo', name: 'group' },
|
||||
{ type: 'belongsToMany', name: 'roles' },
|
||||
],
|
||||
});
|
||||
|
||||
Role = db.collection({
|
||||
name: 'roles',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{ type: 'belongsToMany', name: 'users' },
|
||||
],
|
||||
});
|
||||
|
||||
@ -149,6 +170,108 @@ describe('create', () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
test('firstOrCreate by filterKeys', async () => {
|
||||
const u1 = await User.repository.firstOrCreate({
|
||||
filterKeys: ['name'],
|
||||
values: {
|
||||
name: 'u1',
|
||||
age: 10,
|
||||
group: {
|
||||
name: 'g1',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(u1.name).toEqual('u1');
|
||||
const group = await u1.get('group');
|
||||
expect(group.name).toEqual('g1');
|
||||
|
||||
const u2 = await User.repository.firstOrCreate({
|
||||
filterKeys: ['group'],
|
||||
values: {
|
||||
group: {
|
||||
name: 'g1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(u2.name).toEqual('u1');
|
||||
});
|
||||
|
||||
test('firstOrCreate by many to many associations', async () => {
|
||||
const roles = await Role.repository.create({
|
||||
values: [{ name: 'r1' }, { name: 'r2' }],
|
||||
});
|
||||
|
||||
const u1 = await User.repository.firstOrCreate({
|
||||
values: {
|
||||
name: 'u1',
|
||||
roles: [{ name: 'r1' }, { name: 'r2' }],
|
||||
},
|
||||
filterKeys: ['roles.name'],
|
||||
});
|
||||
|
||||
expect(u1.get('roles')).toHaveLength(2);
|
||||
|
||||
const u1a = await User.repository.firstOrCreate({
|
||||
values: {
|
||||
name: 'u1',
|
||||
roles: [{ name: 'r1' }, { name: 'r2' }],
|
||||
},
|
||||
filterKeys: ['roles.name'],
|
||||
});
|
||||
|
||||
expect(u1a.get('id')).toEqual(u1.get('id'));
|
||||
});
|
||||
|
||||
test('firstOrCreate', async () => {
|
||||
const u1 = await User.repository.firstOrCreate({
|
||||
filterKeys: ['name'],
|
||||
values: {
|
||||
name: 'u1',
|
||||
age: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(u1.name).toEqual('u1');
|
||||
expect(u1.age).toEqual(10);
|
||||
|
||||
expect(
|
||||
(
|
||||
await User.repository.firstOrCreate({
|
||||
filterKeys: ['name'],
|
||||
values: {
|
||||
name: 'u1',
|
||||
age: 10,
|
||||
},
|
||||
})
|
||||
).get('id'),
|
||||
).toEqual(u1.get('id'));
|
||||
});
|
||||
|
||||
test('updateOrCreate', async () => {
|
||||
const u1 = await User.repository.updateOrCreate({
|
||||
filterKeys: ['name'],
|
||||
values: {
|
||||
name: 'u1',
|
||||
age: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(u1.name).toEqual('u1');
|
||||
expect(u1.age).toEqual(10);
|
||||
|
||||
await User.repository.updateOrCreate({
|
||||
filterKeys: ['name'],
|
||||
values: {
|
||||
name: 'u1',
|
||||
age: 20,
|
||||
},
|
||||
});
|
||||
|
||||
await u1.reload();
|
||||
expect(u1.age).toEqual(20);
|
||||
});
|
||||
|
||||
test('create with association', async () => {
|
||||
const u1 = await User.repository.create({
|
||||
values: {
|
||||
|
@ -32,6 +32,7 @@ import { RelationRepository } from './relation-repository/relation-repository';
|
||||
import { updateAssociations, updateModelByValues } from './update-associations';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { EagerLoadingTree } from './eager-loading/eager-loading-tree';
|
||||
import { flatten } from 'flat';
|
||||
|
||||
const debug = require('debug')('noco-database');
|
||||
|
||||
@ -210,6 +211,11 @@ export interface AggregateOptions {
|
||||
distinct?: boolean;
|
||||
}
|
||||
|
||||
interface FirstOrCreateOptions extends Transactionable {
|
||||
filterKeys: string[];
|
||||
values?: Values;
|
||||
}
|
||||
|
||||
export class Repository<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
|
||||
implements IRepository
|
||||
{
|
||||
@ -223,6 +229,50 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
this.model = collection.model;
|
||||
}
|
||||
|
||||
public static valuesToFilter(values: Values, filterKeys: Array<string>) {
|
||||
const filterAnd = [];
|
||||
const flattedValues = flatten(values);
|
||||
|
||||
const keyWithOutArrayIndex = (key) => {
|
||||
const chunks = key.split('.');
|
||||
return chunks
|
||||
.filter((chunk) => {
|
||||
return !Boolean(chunk.match(/\d+/));
|
||||
})
|
||||
.join('.');
|
||||
};
|
||||
|
||||
for (const filterKey of filterKeys) {
|
||||
let filterValue;
|
||||
|
||||
for (const flattedKey of Object.keys(flattedValues)) {
|
||||
const flattedKeyWithoutIndex = keyWithOutArrayIndex(flattedKey);
|
||||
|
||||
if (flattedKeyWithoutIndex === filterKey) {
|
||||
if (filterValue) {
|
||||
if (Array.isArray(filterValue)) {
|
||||
filterValue.push(flattedValues[flattedKey]);
|
||||
} else {
|
||||
filterValue = [filterValue, flattedValues[flattedKey]];
|
||||
}
|
||||
} else {
|
||||
filterValue = flattedValues[flattedKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValue) {
|
||||
filterAnd.push({
|
||||
[filterKey]: filterValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$and: filterAnd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* return count by filter
|
||||
*/
|
||||
@ -428,6 +478,39 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
return rows.length == 1 ? rows[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first record matching the attributes or create it.
|
||||
*/
|
||||
async firstOrCreate(options: FirstOrCreateOptions) {
|
||||
const { filterKeys, values, transaction } = options;
|
||||
const filter = Repository.valuesToFilter(values, filterKeys);
|
||||
|
||||
const instance = await this.findOne({ filter, transaction });
|
||||
|
||||
if (instance) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
return this.create({ values, transaction });
|
||||
}
|
||||
|
||||
async updateOrCreate(options: FirstOrCreateOptions) {
|
||||
const { filterKeys, values, transaction } = options;
|
||||
const filter = Repository.valuesToFilter(values, filterKeys);
|
||||
|
||||
const instance = await this.findOne({ filter, transaction });
|
||||
|
||||
if (instance) {
|
||||
return await this.update({
|
||||
filterByTk: instance.get(this.collection.model.primaryKeyAttribute),
|
||||
values,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
return this.create({ values, transaction });
|
||||
}
|
||||
|
||||
/**
|
||||
* Save instance to database
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user