feat: filter by target key (#146)

* feat: filter by target key

* fix: repository test

* change type name

* chore: test

* change PrimaryKey type to TargetKey

* rename filterTargetKey

* rename variables

* change option parser constructor

* add option parser targetKey

* change filter parser constructor

* fix: custom model

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
ChengLei Shao 2022-01-07 20:08:01 +08:00 committed by GitHub
parent 79ba391aee
commit 2bf09bf9bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 599 additions and 195 deletions

View File

@ -1,5 +1,6 @@
import { mockDatabase } from './index';
import path from 'path';
import { Model } from '..';
describe('database', () => {
test('import', async () => {
@ -165,4 +166,30 @@ describe('database', () => {
expect(listener).toHaveBeenCalled();
});
test('custom model', async () => {
class CustomModel extends Model {
customMethod() {
this.setDataValue('abc', 'abc');
}
}
const db = mockDatabase();
db.registerModels({
CustomModel,
});
const Test = db.collection({
name: 'tests',
model: 'CustomModel',
});
await Test.sync();
const test = await Test.model.create<any>();
test.customMethod();
expect(test.get('abc')).toBe('abc');
});
});

View File

@ -87,7 +87,7 @@ describe('context field', () => {
});
expect(t1.get('clientIp')).toBe('11.22.33.44');
const [t2] = await Test.repository.update({
filterByPk: t1.get('id') as any,
filterByTk: t1.get('id') as any,
values: {},
context: {
request: {
@ -124,7 +124,7 @@ describe('context field', () => {
});
expect(t1.get('clientIp')).toBe('11.22.33.44');
const [t2] = await Test.repository.update({
filterByPk: t1.get('id') as any,
filterByTk: t1.get('id') as any,
values: {},
context: {
request: {

View File

@ -12,9 +12,14 @@ test('filter item by string', async () => {
await database.sync();
const filterParser = new FilterParser(UserCollection.model, UserCollection.context.database, {
name: 'hello',
});
const filterParser = new FilterParser(
{
name: 'hello',
},
{
collection: UserCollection,
},
);
const filterParams = filterParser.toSequelizeParams();
@ -74,11 +79,9 @@ describe('filter by related', () => {
'posts.title.$iLike': '%hello%',
};
const filterParser = new FilterParser(
db.getCollection('users').model,
db.getCollection('users').context.database,
filter,
);
const filterParser = new FilterParser(filter, {
collection: db.getCollection('users'),
});
const filterParams = filterParser.toSequelizeParams();
@ -91,11 +94,9 @@ describe('filter by related', () => {
'posts.comments.content.$iLike': '%hello%',
};
const filterParser = new FilterParser(
db.getCollection('users').model,
db.getCollection('users').context.database,
filter,
);
const filterParser = new FilterParser(filter, {
collection: db.getCollection('users'),
});
const filterParams = filterParser.toSequelizeParams();

View File

@ -13,9 +13,7 @@ describe('array field operator', function () {
});
beforeEach(async () => {
db = mockDatabase({
logging: console.log,
});
db = mockDatabase({});
Test = db.collection({
name: 'test',
@ -66,7 +64,7 @@ describe('array field operator', function () {
expect(result.get('id')).toEqual(p1.get('id'));
await Post.repository.update({
filterByPk: <any>p1.get('id'),
filterByTk: <any>p1.get('id'),
values: {
tags: ['t3', 't2'],
},

View File

@ -60,7 +60,10 @@ describe('option parser', () => {
fields: ['id', 'name', 'tags.id', 'tags.name'],
};
const parser = new OptionsParser(Post.model, Post.context.database, options);
const parser = new OptionsParser(options, {
collection: Post,
});
const params = parser.toSequelizeParams();
expect(params).toEqual({
@ -78,7 +81,9 @@ describe('option parser', () => {
sort: ['id'],
};
let parser = new OptionsParser(User.model, User.context.database, options);
let parser = new OptionsParser(options, {
collection: User,
});
let params = parser.toSequelizeParams();
expect(params['order']).toEqual([['id', 'ASC']]);
@ -86,7 +91,9 @@ describe('option parser', () => {
sort: ['id', '-posts.title', 'posts.comments.createdAt'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['order']).toEqual([
['id', 'ASC'],
@ -100,7 +107,9 @@ describe('option parser', () => {
fields: ['id', 'posts'],
};
// 转换为 attributes: ['id'], include: [{association: 'posts'}]
let parser = new OptionsParser(User.model, User.context.database, options);
let parser = new OptionsParser(options, {
collection: User,
});
let params = parser.toSequelizeParams();
expect(params['attributes']).toContain('id');
@ -111,7 +120,9 @@ describe('option parser', () => {
appends: ['posts'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['attributes']['include']).toEqual([]);
expect(params['include'][0]['association']).toEqual('posts');
@ -121,7 +132,9 @@ describe('option parser', () => {
fields: ['id', 'posts.title'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['attributes']).toContain('id');
expect(params['include'][0]['association']).toEqual('posts');
@ -132,7 +145,9 @@ describe('option parser', () => {
fields: ['id', 'posts', 'posts.comments.content'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['attributes']).toContain('id');
expect(params['include'][0]['association']).toEqual('posts');
@ -143,7 +158,9 @@ describe('option parser', () => {
options = {
except: ['id'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['attributes']['exclude']).toContain('id');
@ -153,7 +170,9 @@ describe('option parser', () => {
except: ['posts.id'],
};
parser = new OptionsParser(User.model, User.context.database, options);
parser = new OptionsParser(options, {
collection: User,
});
params = parser.toSequelizeParams();
expect(params['include'][0]['attributes']['exclude']).toContain('id');

View File

@ -1,6 +1,113 @@
import { mockDatabase } from '../index';
import { BelongsToManyRepository } from '../../relation-repository/belongs-to-many-repository';
import Database from '../../database';
import { Collection, HasManyRepository } from '@nocobase/database';
describe('belongs to many with target key', function () {
let db: Database;
let Tag: Collection;
let Post: Collection;
beforeEach(async () => {
db = mockDatabase();
Post = db.collection({
name: 'posts',
filterTargetKey: 'title',
autoGenId: false,
fields: [
{ type: 'string', name: 'title', primaryKey: true },
{
type: 'belongsToMany',
name: 'tags',
sourceKey: 'title',
foreignKey: 'postTitle',
targetKey: 'name',
otherKey: 'tagName',
},
],
});
Tag = db.collection({
name: 'tags',
filterTargetKey: 'name',
autoGenId: false,
fields: [
{ type: 'string', name: 'name', primaryKey: true },
{ type: 'string', name: 'status' },
],
});
await db.sync({ force: true });
});
afterEach(async () => {
await db.close();
});
test('destroy by target key', async () => {
const t1 = await Tag.repository.create({
values: {
name: 't1',
},
});
const t2 = await Tag.repository.create({
values: {
name: 't2',
},
});
const p1 = await Post.repository.create({
values: { title: 'p1' },
});
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.get('title') as string);
await PostTagRepository.set([t1.get('name') as string, t2.get('name')]);
await PostTagRepository.destroy();
const [_, count] = await PostTagRepository.findAndCount();
expect(count).toEqual(0);
});
test('destroy with target key 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.get('title') as string);
await PostTagRepository.set([t1.get('name') as string, t2.get('name') as string]);
let [_, count] = await PostTagRepository.findAndCount();
expect(count).toEqual(2);
await PostTagRepository.destroy({
filterByTk: t1.get('name') as string,
filter: {
status: 'draft',
},
});
[_, count] = await PostTagRepository.findAndCount();
expect(count).toEqual(2);
});
});
describe('belongs to many', () => {
let db: Database;
@ -51,6 +158,7 @@ describe('belongs to many', () => {
],
});
await db.sequelize.getQueryInterface().dropAllTables();
await db.sync({ force: true });
});
@ -384,7 +492,7 @@ describe('belongs to many', () => {
await PostTagRepository.set([t1.id, t2.id]);
const findByPkResult = await PostTagRepository.findOne({
filterByPk: t2.id,
filterByTk: t2.id,
});
expect(findByPkResult.name).toEqual('t2');
@ -480,7 +588,7 @@ describe('belongs to many', () => {
expect(count).toEqual(2);
await PostTagRepository.destroy({
filterByPk: t1.get('id') as number,
filterByTk: t1.get('id') as number,
filter: {
status: 'draft',
},
@ -511,10 +619,11 @@ describe('belongs to many', () => {
await PostTagRepository.set([t1.id, t2.id]);
expect(await PostTagRepository.count()).toEqual(2);
await PostTagRepository.destroy(t2.id);
const result = await PostTagRepository.findAndCount();
expect(result[1]).toEqual(1);
expect(await PostTagRepository.count()).toEqual(1);
});
test('transaction', async () => {

View File

@ -2,6 +2,116 @@ import { mockDatabase } from '../index';
import { HasManyRepository } from '../../relation-repository/hasmany-repository';
import { BelongsToManyRepository } from '../../relation-repository/belongs-to-many-repository';
describe('has many with target key', function () {
test('target key with filterTargetKey', async () => {
const db = mockDatabase();
const User = db.collection<{ id: number; name: string }, { name: string }>({
name: 'users',
filterTargetKey: 'name',
autoGenId: false,
fields: [
{ type: 'string', name: 'name', unique: true },
{ type: 'integer', name: 'age' },
{ type: 'hasMany', name: 'posts', sourceKey: 'name', foreignKey: 'userName', targetKey: 'title' },
],
});
const Post = db.collection({
name: 'posts',
filterTargetKey: 'title',
autoGenId: false,
fields: [
{ type: 'string', name: 'title', unique: true },
{ type: 'string', name: 'status' },
{
type: 'belongsTo',
name: 'user',
targetKey: 'name',
foreignKey: 'userName',
},
],
});
});
test('destroy by target key and filter', async () => {
const db = mockDatabase();
const User = db.collection<{ id: number; name: string }, { name: string }>({
name: 'users',
filterTargetKey: 'name',
autoGenId: false,
fields: [
{ type: 'string', name: 'name', unique: true },
{ type: 'integer', name: 'age' },
{ type: 'hasMany', name: 'posts', sourceKey: 'name', foreignKey: 'userName', targetKey: 'title' },
],
});
const Post = db.collection({
name: 'posts',
filterTargetKey: 'title',
autoGenId: false,
fields: [
{ type: 'string', name: 'title', unique: true },
{ type: 'string', name: 'status' },
{
type: 'belongsTo',
name: 'user',
targetKey: 'name',
foreignKey: 'userName',
},
],
});
await db.sync({ force: true });
const u1 = await User.repository.create({
values: { name: 'u1' },
});
const UserPostRepository = new HasManyRepository(User, 'posts', u1.get('name') as string);
const p1 = await UserPostRepository.create({
values: {
title: 't1',
status: 'published',
},
});
const p2 = await UserPostRepository.create({
values: {
title: 't2',
status: 'draft',
},
});
await UserPostRepository.destroy({
filterByTk: p1.title,
filter: { status: 'draft' },
});
expect(await UserPostRepository.count()).toEqual(2);
await UserPostRepository.destroy({
filterByTk: p1.title,
filter: {
status: 'published',
},
});
expect(
await UserPostRepository.findOne({
filterByTk: p1.title,
}),
).toBeNull();
expect(
await UserPostRepository.findOne({
filterByTk: p2.id,
}),
).not.toBeNull();
});
});
describe('has many repository', () => {
let db;
let User;
@ -190,7 +300,7 @@ describe('has many repository', () => {
});
await UserPostRepository.destroy({
filterByPk: p1.id,
filterByTk: p1.id,
filter: {
status: 'draft',
},
@ -199,7 +309,7 @@ describe('has many repository', () => {
expect(await UserPostRepository.count()).toEqual(2);
await UserPostRepository.destroy({
filterByPk: p1.id,
filterByTk: p1.id,
filter: {
status: 'published',
},
@ -207,13 +317,13 @@ describe('has many repository', () => {
expect(
await UserPostRepository.findOne({
filterByPk: p1.id,
filterByTk: p1.id,
}),
).toBeNull();
expect(
await UserPostRepository.findOne({
filterByPk: p2.id,
filterByTk: p2.id,
}),
).not.toBeNull();
});

View File

@ -2,6 +2,45 @@ import { Collection } from '../collection';
import { Database } from '../database';
import { mockDatabase } from './';
describe('find by targetKey', function () {
it('can filter by target key', async () => {
const db = mockDatabase({});
const User = db.collection({
name: 'users',
filterTargetKey: 'name',
autoGenId: false,
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
],
});
await db.sync();
await User.repository.create({
values: {
name: 'user1',
},
});
await User.repository.create({
values: {
name: 'user2',
},
});
const user2 = await User.repository.findOne({
filterByTk: 'user2',
});
expect(user2.get('name')).toEqual('user2');
});
});
describe('repository.find', () => {
let db: Database;
let User: Collection;
@ -116,7 +155,7 @@ describe('repository.find', () => {
});
const result = await Test.repository.findOne({
filterByPk: <number>t1.get('id'),
filterByTk: <number>t1.get('id'),
filter: {
status: 'published',
},
@ -236,7 +275,7 @@ describe('repository.update', () => {
name: 'user1',
});
await User.repository.update({
filterByPk: user.id,
filterByTk: user.id,
values: {
name: 'user11',
posts: [{ name: 'post1' }],
@ -263,28 +302,25 @@ describe('repository.update', () => {
it('update2', async () => {
const user = await User.model.create<any>({
name: 'user1',
posts: [{ name: 'post1' }],
});
const user2 = await User.model.create<any>({
name: 'user2',
});
await User.repository.update({
filterByPk: user.id,
filterByTk: user.id,
values: {
name: 'user11',
posts: [{ name: 'post1' }],
},
});
const updated = await User.model.findByPk(user.id);
expect(updated).toMatchObject({
name: 'user11',
});
const post = await Post.model.findOne({
where: {
name: 'post1',
},
});
expect(post).toMatchObject({
name: 'post1',
userId: user.id,
});
expect(updated.get('name')).toEqual('user11');
const u2 = await User.model.findByPk(user2.id);
expect(u2.get('name')).toEqual('user2');
});
});

View File

@ -1,5 +1,80 @@
import { mockDatabase } from '../index';
import { Collection } from '../../collection';
import { Database } from '@nocobase/database';
describe('destroy with targetKey', function () {
let db: Database;
let User: Collection;
let u1;
let u2;
afterEach(async () => {
await db.close();
});
beforeEach(async () => {
db = mockDatabase();
User = db.collection({
name: 'users',
autoGenId: false,
fields: [
{ type: 'string', name: 'name', primaryKey: true },
{ type: 'string', name: 'status' },
],
});
await db.sync({
force: true,
});
u1 = await User.repository.create({
values: {
name: 'u1',
status: 'published',
},
});
u2 = await User.repository.create({
values: {
name: 'u2',
status: 'draft',
},
});
});
it('should destroy all', async () => {
expect(await User.repository.count()).toEqual(2);
await User.repository.destroy({ truncate: true });
expect(await User.repository.count()).toEqual(0);
});
it('should destroy by target key', async () => {
await User.repository.destroy({
filterByTk: 'u2',
});
expect(await User.repository.count()).toEqual(1);
});
it('should destroy by target key and filter', async () => {
await User.repository.destroy({
filterByTk: 'u1',
filter: {
status: 'draft',
},
});
expect(await User.repository.count()).toEqual(2);
await User.repository.destroy({
filterByTk: 'u2',
filter: {
status: 'draft',
},
});
expect(await User.repository.count()).toEqual(1);
});
});
describe('destroy', () => {
let db;
@ -36,7 +111,7 @@ describe('destroy', () => {
});
await Post.repository.destroy({
filterByPk: p1.get('id') as number,
filterByTk: p1.get('id') as number,
filter: {
status: 'draft',
},

View File

@ -15,6 +15,7 @@ export type RepositoryType = typeof Repository;
export interface CollectionOptions extends Omit<ModelOptions, 'name'> {
name: string;
tableName?: string;
filterTargetKey?: string;
fields?: FieldOptions[];
model?: string | ModelCtor<Model>;
repository?: string | RepositoryType;
@ -37,6 +38,10 @@ export class Collection<
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>;
repository: Repository<TModelAttributes, TCreationAttributes>;
get filterTargetKey() {
return lodash.get(this.options, 'filterTargetKey', this.model.primaryKeyAttribute);
}
get name() {
return this.options.name;
}
@ -54,7 +59,7 @@ export class Collection<
private sequelizeModelOptions() {
const { name, tableName } = this.options;
return {
..._.omit(this.options, ['name', 'fields']),
..._.omit(this.options, ['name', 'fields', 'model']),
modelName: name,
sequelize: this.context.database.sequelize,
tableName: tableName || name,

View File

@ -19,6 +19,10 @@ export abstract class RelationField extends Field {
return this.options.sourceKey;
}
get targetKey() {
return this.options.targetKey || this.TargetModel.primaryKeyAttribute;
}
/**
* get target model from database by it's name
* @constructor

View File

@ -3,20 +3,30 @@ import _ from 'lodash';
import { flatten, unflatten } from 'flat';
import { Database } from './database';
import lodash from 'lodash';
import { Collection } from './collection';
const debug = require('debug')('noco-database');
type FilterType = any;
interface FilterParserContext {
collection: Collection;
}
export default class FilterParser {
collection: Collection;
database: Database;
model: ModelCtor<Model>;
filter: FilterType;
context: FilterParserContext;
constructor(model: ModelCtor<any>, database: Database, filter: FilterType) {
this.model = model;
constructor(filter: FilterType, context: FilterParserContext) {
const { collection } = context;
this.collection = collection;
this.context = context;
this.model = collection.model;
this.filter = this.prepareFilter(filter);
this.database = database;
this.database = collection.context.database;
}
prepareFilter(filter: FilterType) {

View File

@ -2,20 +2,32 @@ import { Appends, Except, FindOptions } from './repository';
import FilterParser from './filter-parser';
import { FindAttributeOptions, ModelCtor, Op } from 'sequelize';
import { Database } from './database';
import { Collection } from './collection';
const debug = require('debug')('noco-database');
interface OptionsParserContext {
collection: Collection;
targetKey?: string;
}
export class OptionsParser {
options: FindOptions;
database: Database;
collection: Collection;
model: ModelCtor<any>;
filterParser: FilterParser;
context: OptionsParserContext;
constructor(model: ModelCtor<any>, database: Database, options: FindOptions) {
this.model = model;
constructor(options: FindOptions, context: OptionsParserContext) {
const { collection } = context;
this.collection = collection;
this.model = collection.model;
this.options = options;
this.database = database;
this.filterParser = new FilterParser(model, this.database, options?.filter);
this.database = collection.context.database;
this.filterParser = new FilterParser(options?.filter, { collection });
this.context = context;
}
isAssociation(key: string) {
@ -26,27 +38,15 @@ export class OptionsParser {
return this.isAssociation(path.split('.')[0]);
}
parseFilterByPk() {
if (this.options?.filterByPk) {
return {
where: {
[this.model.primaryKeyAttribute]: this.options.filterByPk,
},
};
}
return null;
}
toSequelizeParams() {
const queryParams = this.filterParser.toSequelizeParams();
if (this.options?.filterByPk) {
if (this.options?.filterByTk) {
queryParams.where = {
[Op.and]: [
queryParams.where,
{
[this.model.primaryKeyAttribute]: this.options.filterByPk,
[this.context.targetKey || this.collection.filterTargetKey]: this.options.filterByTk,
},
],
};

View File

@ -1,7 +1,7 @@
import { BelongsToMany, Model, Op, Transaction } from 'sequelize';
import { updateThroughTableValue } from '../update-associations';
import { FindAndCountOptions, FindOneOptions, MultipleRelationRepository } from './multiple-relation-repository';
import { CreateOptions, DestroyOptions, FindOptions, PrimaryKey, UpdateOptions } from '../repository';
import { CreateOptions, DestroyOptions, FindOptions, TargetKey, UpdateOptions } from '../repository';
import { AssociatedOptions, PrimaryKeyWithThroughValues } from './types';
import lodash from 'lodash';
import { transaction } from './relation-repository';
@ -19,12 +19,12 @@ interface IBelongsToManyRepository<M extends Model> {
// 删除
destroy(options?: number | string | number[] | string[] | DestroyOptions): Promise<Boolean>;
// 建立关联
set(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
// 附加关联,存在中间表数据
add(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
// 移除关联
remove(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
toggle(options: PrimaryKey | { pk?: PrimaryKey; transaction?: Transaction }): Promise<void>;
remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
toggle(options: TargetKey | { pk?: TargetKey; transaction?: Transaction }): Promise<void>;
}
export class BelongsToManyRepository extends MultipleRelationRepository implements IBelongsToManyRepository<any> {
@ -48,22 +48,22 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
@transaction((args, transaction) => {
return {
filterByPk: args[0],
filterByTk: args[0],
transaction,
};
})
async destroy(options?: PrimaryKey | PrimaryKey[] | DestroyOptions): Promise<Boolean> {
async destroy(options?: TargetKey | TargetKey[] | DestroyOptions): Promise<Boolean> {
const transaction = await this.getTransaction(options);
const association = <BelongsToMany>this.association;
const instancesToIds = (instances) => {
return instances.map((instance) => instance.get(this.target.primaryKeyAttribute));
return instances.map((instance) => instance.get(this.targetKey()));
};
// Through Table
const throughTableWhere: Array<any> = [
{
[association.foreignKey]: this.sourceId,
[association.foreignKey]: this.sourceKeyValue,
},
];
@ -78,12 +78,12 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
ids = instancesToIds(instances);
}
if (options && options['filterByPk']) {
const instances = (<any>this.association).toInstanceArray(options['filterByPk']);
if (options && options['filterByTk']) {
const instances = (<any>this.association).toInstanceArray(options['filterByTk']);
ids = ids ? lodash.intersection(ids, instancesToIds(instances)) : instancesToIds(instances);
}
if (options && !options['filterByPk'] && !options['filter']) {
if (options && !options['filterByTk'] && !options['filter']) {
const sourceModel = await this.getSourceModel(transaction);
const instances = await sourceModel[this.accessors().get]({
@ -105,9 +105,9 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
transaction,
});
await this.target.destroy({
await this.targetModel.destroy({
where: {
[this.target.primaryKeyAttribute]: {
[this.targetKey()]: {
[Op.in]: ids,
},
},
@ -119,14 +119,9 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
protected async setTargets(
call: 'add' | 'set',
options:
| PrimaryKey
| PrimaryKey[]
| PrimaryKeyWithThroughValues
| PrimaryKeyWithThroughValues[]
| AssociatedOptions,
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions,
) {
let handleKeys: PrimaryKey[] | PrimaryKeyWithThroughValues[];
let handleKeys: TargetKey[] | PrimaryKeyWithThroughValues[];
const transaction = await this.getTransaction(options, false);
@ -135,12 +130,12 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
}
if (lodash.isString(options) || lodash.isNumber(options)) {
handleKeys = [<PrimaryKey>options];
handleKeys = [<TargetKey>options];
} // if it is type primaryKeyWithThroughValues
else if (lodash.isArray(options) && options.length == 2 && lodash.isPlainObject(options[0][1])) {
handleKeys = [<PrimaryKeyWithThroughValues>options];
} else {
handleKeys = <PrimaryKey[] | PrimaryKeyWithThroughValues[]>options;
handleKeys = <TargetKey[] | PrimaryKeyWithThroughValues[]>options;
}
const sourceModel = await this.getSourceModel(transaction);
@ -160,7 +155,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
for (const [id, throughValues] of Object.entries(setObj)) {
if (typeof throughValues === 'object') {
const instance = await this.target.findByPk(id, {
const instance = await this.targetModel.findByPk(id, {
transaction,
});
await updateThroughTableValue(instance, this.throughName(), throughValues, sourceModel, transaction);
@ -175,12 +170,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
};
})
async add(
options:
| PrimaryKey
| PrimaryKey[]
| PrimaryKeyWithThroughValues
| PrimaryKeyWithThroughValues[]
| AssociatedOptions,
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions,
): Promise<void> {
await this.setTargets('add', options);
}
@ -192,12 +182,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
};
})
async set(
options:
| PrimaryKey
| PrimaryKey[]
| PrimaryKeyWithThroughValues
| PrimaryKeyWithThroughValues[]
| AssociatedOptions,
options: TargetKey | TargetKey[] | PrimaryKeyWithThroughValues | PrimaryKeyWithThroughValues[] | AssociatedOptions,
): Promise<void> {
await this.setTargets('set', options);
}
@ -208,7 +193,7 @@ export class BelongsToManyRepository extends MultipleRelationRepository implemen
transaction,
};
})
async toggle(options: PrimaryKey | { pk?: PrimaryKey; transaction?: Transaction }): Promise<void> {
async toggle(options: TargetKey | { pk?: TargetKey; transaction?: Transaction }): Promise<void> {
const transaction = await this.getTransaction(options);
const sourceModel = await this.getSourceModel(transaction);
const has = await sourceModel[this.accessors().hasSingle](options['pk'], {

View File

@ -6,8 +6,9 @@ import {
FindOneOptions,
MultipleRelationRepository,
} from './multiple-relation-repository';
import { CreateOptions, DestroyOptions, FindOptions, PK, PrimaryKey, UpdateOptions } from '../repository';
import { CreateOptions, DestroyOptions, FindOptions, TK, TargetKey, UpdateOptions } from '../repository';
import { transaction } from './relation-repository';
import lodash from 'lodash';
interface IHasManyRepository<M extends Model> {
find(options?: FindOptions): Promise<M>;
@ -18,30 +19,30 @@ interface IHasManyRepository<M extends Model> {
// 更新
update(options?: UpdateOptions): Promise<M>;
// 删除
destroy(options?: PK | DestroyOptions): Promise<Boolean>;
destroy(options?: TK | DestroyOptions): Promise<Boolean>;
// 建立关联
set(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
// 附加关联
add(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
// 移除关联
remove(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void>;
}
export class HasManyRepository extends MultipleRelationRepository implements IHasManyRepository<any> {
@transaction((args, transaction) => {
return {
filterByPk: args[0],
filterByTk: args[0],
transaction,
};
})
async destroy(options?: PK | DestroyOptions): Promise<Boolean> {
async destroy(options?: TK | DestroyOptions): Promise<Boolean> {
const transaction = await this.getTransaction(options);
const sourceModel = await this.getSourceModel(transaction);
const where = [
{
[this.association.foreignKey]: sourceModel.get(this.source.model.primaryKeyAttribute),
[this.association.foreignKey]: sourceModel.get((this.association as any).sourceKey),
},
];
@ -54,21 +55,18 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
where.push(filterResult.where);
}
if (options && options['filterByPk']) {
if (typeof options === 'object' && options['filterByPk']) {
options = options['filterByPk'];
if (options && options['filterByTk']) {
if (typeof options === 'object' && options['filterByTk']) {
options = options['filterByTk'];
}
const targetInstances = (<any>this.association).toInstanceArray(options);
where.push({
[this.target.primaryKeyAttribute]: targetInstances.map((targetInstance) =>
targetInstance.get(this.target.primaryKeyAttribute),
),
[this.targetKey()]: options,
});
}
await this.target.destroy({
await this.targetModel.destroy({
where: {
[Op.and]: where,
},
@ -95,7 +93,7 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
transaction,
};
})
async set(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void> {
async set(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void> {
const transaction = await this.getTransaction(options);
const sourceModel = await this.getSourceModel(transaction);
@ -111,7 +109,7 @@ export class HasManyRepository extends MultipleRelationRepository implements IHa
transaction,
};
})
async add(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void> {
async add(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void> {
const transaction = await this.getTransaction(options);
const sourceModel = await this.getSourceModel(transaction);

View File

@ -1,5 +1,5 @@
import { RelationRepository, transaction } from './relation-repository';
import { omit } from 'lodash';
import lodash, { omit } from 'lodash';
import { MultiAssociationAccessors, Op, Sequelize, Transaction } from 'sequelize';
import { UpdateGuard } from '../update-guard';
import { updateModelByValues } from '../update-associations';
@ -8,20 +8,20 @@ import {
CountOptions,
DestroyOptions,
Filter,
FilterByPK,
FilterByTk,
FindOptions,
PK,
PrimaryKey,
TK,
TargetKey,
TransactionAble,
UpdateOptions,
} from '../repository';
export interface FindAndCountOptions extends CommonFindOptions {}
export interface FindOneOptions extends CommonFindOptions, FilterByPK {}
export interface FindOneOptions extends CommonFindOptions, FilterByTk {}
export interface AssociatedOptions extends TransactionAble {
pk?: PK;
tk?: TK;
}
export abstract class MultipleRelationRepository extends RelationRepository {
@ -46,16 +46,16 @@ export abstract class MultipleRelationRepository extends RelationRepository {
await sourceModel[getAccessor]({
...findOptions,
includeIgnoreAttributes: false,
attributes: [this.target.primaryKeyAttribute],
group: `${this.target.name}.${this.target.primaryKeyAttribute}`,
attributes: [this.targetKey()],
group: `${this.targetModel.name}.${this.targetKey()}`,
transaction,
})
).map((row) => row.get(this.target.primaryKeyAttribute));
).map((row) => row.get(this.targetKey()));
return await sourceModel[getAccessor]({
...omit(findOptions, ['limit', 'offset']),
where: {
[this.target.primaryKeyAttribute]: {
[this.targetKey()]: {
[Op.in]: ids,
},
},
@ -97,7 +97,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
[
Sequelize.fn(
'COUNT',
Sequelize.fn('DISTINCT', Sequelize.col(`${this.target.name}.${this.target.primaryKeyAttribute}`)),
Sequelize.fn('DISTINCT', Sequelize.col(`${this.targetModel.name}.${this.targetKey()}`)),
),
'count',
],
@ -122,7 +122,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
transaction,
};
})
async remove(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void> {
async remove(options: TargetKey | TargetKey[] | AssociatedOptions): Promise<void> {
const transaction = await this.getTransaction(options);
let handleKeys = options['pk'];
@ -141,7 +141,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
async update(options?: UpdateOptions): Promise<any> {
const transaction = await this.getTransaction(options);
const guard = UpdateGuard.fromOptions(this.target, options);
const guard = UpdateGuard.fromOptions(this.targetModel, options);
const values = guard.sanitize(options.values);
@ -153,7 +153,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
await updateModelByValues(instance, values, {
...options,
sanitized: true,
sourceModel: this.sourceModel,
sourceModel: this.sourceInstance,
transaction,
});
}
@ -161,7 +161,7 @@ export abstract class MultipleRelationRepository extends RelationRepository {
return instances;
}
async destroy(options?: PK | DestroyOptions): Promise<Boolean> {
async destroy(options?: TK | DestroyOptions): Promise<Boolean> {
return false;
}
@ -170,8 +170,9 @@ export abstract class MultipleRelationRepository extends RelationRepository {
filter: filter,
transaction,
});
return await this.destroy({
filterByPk: instances.map((instance) => instance[this.target.primaryKeyAttribute]),
filterByTk: instances.map((instance) => instance.get(this.targetCollection.filterTargetKey)),
transaction,
});
}

View File

@ -7,24 +7,36 @@ import { UpdateGuard } from '../update-guard';
import { updateAssociations } from '../update-associations';
import lodash from 'lodash';
import { transactionWrapperBuilder } from '../transaction-decorator';
import { Field, RelationField } from '@nocobase/database';
export const transaction = transactionWrapperBuilder(function () {
return this.source.model.sequelize.transaction();
return this.sourceCollection.model.sequelize.transaction();
});
export abstract class RelationRepository {
source: Collection;
sourceCollection: Collection;
association: Association;
target: ModelCtor<any>;
sourceId: string | number;
sourceModel: Model;
targetModel: ModelCtor<any>;
targetCollection: Collection;
associationName: string;
associationField: RelationField;
sourceKeyValue: string | number;
sourceInstance: Model;
constructor(source: Collection, association: string, sourceId: string | number) {
this.source = source;
this.sourceId = sourceId;
this.association = this.source.model.associations[association];
constructor(sourceCollection: Collection, association: string, sourceKeyValue: string | number) {
this.sourceCollection = sourceCollection;
this.sourceKeyValue = sourceKeyValue;
this.associationName = association;
this.association = this.sourceCollection.model.associations[association];
this.target = this.association.target;
this.associationField = this.sourceCollection.getField(association);
this.targetModel = this.association.target;
this.targetCollection = this.sourceCollection.context.database.modelCollection.get(this.targetModel);
}
targetKey() {
return this.associationField.targetKey;
}
protected accessors() {
@ -34,7 +46,7 @@ export abstract class RelationRepository {
async create(options?: CreateOptions): Promise<any> {
const createAccessor = this.accessors().create;
const guard = UpdateGuard.fromOptions(this.target, options);
const guard = UpdateGuard.fromOptions(this.targetModel, options);
const values = options.values;
const sourceModel = await this.getSourceModel();
@ -47,23 +59,30 @@ export abstract class RelationRepository {
}
async getSourceModel(transaction?: any) {
if (!this.sourceModel) {
this.sourceModel = await this.source.model.findByPk(this.sourceId, {
transaction,
if (!this.sourceInstance) {
this.sourceInstance = await this.sourceCollection.model.findOne({
where: {
[this.associationField.sourceKey]: this.sourceKeyValue,
},
});
}
return this.sourceModel;
return this.sourceInstance;
}
protected buildQueryOptions(options: FindOptions) {
const parser = new OptionsParser(this.target, this.source.context.database, options);
const parser = new OptionsParser(options, {
collection: this.targetCollection,
targetKey: this.targetKey(),
});
const params = parser.toSequelizeParams();
return { ...options, ...params };
}
protected parseFilter(filter: Filter) {
const parser = new FilterParser(this.target, this.source.context.database, filter);
const parser = new FilterParser(filter, {
collection: this.targetCollection,
});
return parser.toSequelizeParams();
}
@ -73,7 +92,7 @@ export abstract class RelationRepository {
}
if (autoGen) {
return await this.source.model.sequelize.transaction();
return await this.sourceCollection.model.sequelize.transaction();
}
return null;

View File

@ -2,7 +2,7 @@ import { RelationRepository, transaction } from './relation-repository';
import { Model, SingleAssociationAccessors } from 'sequelize';
import { updateModelByValues } from '../update-associations';
import lodash from 'lodash';
import { Appends, Except, Fields, Filter, PrimaryKey, TransactionAble, UpdateOptions } from '../repository';
import { Appends, Except, Fields, Filter, TargetKey, TransactionAble, UpdateOptions } from '../repository';
export interface SingleRelationFindOption extends TransactionAble {
fields?: Fields;
@ -12,7 +12,7 @@ export interface SingleRelationFindOption extends TransactionAble {
}
interface SetOption extends TransactionAble {
pk?: PrimaryKey;
tk?: TargetKey;
}
export abstract class SingleRelationRepository extends RelationRepository {
@ -27,13 +27,13 @@ export abstract class SingleRelationRepository extends RelationRepository {
@transaction((args, transaction) => {
return {
pk: args[0],
tk: args[0],
transaction,
};
})
async set(options: PrimaryKey | SetOption): Promise<void> {
async set(options: TargetKey | SetOption): Promise<void> {
const transaction = await this.getTransaction(options);
let handleKey = lodash.isPlainObject(options) ? (<SetOption>options).pk : options;
let handleKey = lodash.isPlainObject(options) ? (<SetOption>options).tk : options;
const sourceModel = await this.getSourceModel(transaction);

View File

@ -43,8 +43,8 @@ export interface FilterAble {
filter: Filter;
}
export type PrimaryKey = string | number;
export type PK = PrimaryKey | PrimaryKey[];
export type TargetKey = string | number;
export type TK = TargetKey | TargetKey[];
export type Filter = any;
export type Appends = string[];
@ -63,11 +63,11 @@ export interface CountOptions extends Omit<SequelizeCreateOptions, 'distinct' |
filter?: Filter;
}
export interface FilterByPK {
filterByPk?: PrimaryKey;
export interface FilterByTk {
filterByTk?: TargetKey;
}
export interface FindOptions extends SequelizeFindOptions, CommonFindOptions, FilterByPK {}
export interface FindOptions extends SequelizeFindOptions, CommonFindOptions, FilterByTk {}
export interface CommonFindOptions {
filter?: Filter;
@ -81,7 +81,7 @@ interface FindOneOptions extends FindOptions, CommonFindOptions {}
export interface DestroyOptions extends SequelizeDestroyOptions {
filter?: Filter;
filterByPk?: PrimaryKey | PrimaryKey[];
filterByTk?: TargetKey | TargetKey[];
truncate?: boolean;
context?: any;
}
@ -110,7 +110,7 @@ export interface CreateOptions extends SequelizeCreateOptions {
export interface UpdateOptions extends Omit<SequelizeUpdateOptions, 'where'> {
values: Values;
filter?: Filter;
filterByPk?: PrimaryKey;
filterByTk?: TargetKey;
whitelist?: WhiteList;
blacklist?: BlackList;
updateAssociationValues?: AssociationKeysToBeUpdate;
@ -261,7 +261,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
* Find By Id
*
*/
findById(id: PrimaryKey) {
findById(id: string | number) {
return this.collection.model.findByPk(id);
}
@ -362,26 +362,28 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
@transaction((args, transaction) => {
return {
filterByPk: args[0],
filterByTk: args[0],
transaction,
};
})
async destroy(options?: PrimaryKey | PrimaryKey[] | DestroyOptions) {
async destroy(options?: TargetKey | TargetKey[] | DestroyOptions) {
const transaction = await this.getTransaction(options);
const modelFilterKey = this.collection.filterTargetKey;
options = <DestroyOptions>options;
const filterByPk: PrimaryKey[] | undefined =
options.filterByPk && !lodash.isArray(options.filterByPk)
? [options.filterByPk]
: (options.filterByPk as PrimaryKey[] | undefined);
const filterByTk: TargetKey[] | undefined =
options.filterByTk && !lodash.isArray(options.filterByTk)
? [options.filterByTk]
: (options.filterByTk as TargetKey[] | undefined);
if (filterByPk && !options.filter) {
if (filterByTk && !options.filter) {
return await this.model.destroy({
...options,
where: {
[this.model.primaryKeyAttribute]: {
[Op.in]: filterByPk,
[modelFilterKey]: {
[Op.in]: filterByTk,
},
},
transaction,
@ -394,15 +396,15 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
filter: options.filter,
transaction,
})
).map((instance) => instance[this.model.primaryKeyAttribute]);
).map((instance) => instance.get(modelFilterKey) as TargetKey);
if (filterByPk) {
pks = lodash.intersection(pks, filterByPk);
if (filterByTk) {
pks = lodash.intersection(pks, filterByTk);
}
return await this.destroy({
context: options.context,
filterByPk: pks,
filterByTk: pks,
transaction,
});
}
@ -424,14 +426,19 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
}
protected buildQueryOptions(options: any) {
const parser = new OptionsParser(this.collection.model, this.collection.context.database, options);
const parser = new OptionsParser(options, {
collection: this.collection,
});
const params = parser.toSequelizeParams();
debug('sequelize query params %o', params);
return { ...options, ...params };
}
protected parseFilter(filter: Filter) {
const parser = new FilterParser(this.collection.model, this.collection.context.database, filter);
const parser = new FilterParser(filter, {
collection: this.collection,
});
return parser.toSequelizeParams();
}

View File

@ -51,7 +51,7 @@ type UpdateValue = { [key: string]: any };
interface UpdateOptions extends TransactionAble {
filter?: any;
filterByPk?: number | string;
filterByTk?: number | string;
// 字段白名单
whitelist?: string[];
// 字段黑名单