feat: database next (#130)
* FIX: database test with sqlite * more types * filter test * split filter parser * filter test * filter test: hasMany * define inverse association for belongsTo & hasMany * chore: console.log * repository count method * chore: Collection * repository filter & appends & fields & expect * repository: sort option * chore: test * add: test * find & findAndCount * chore: test * database-next: update guard * database-next: update guard associationKeysToBeUpdate * chore: comment * update-guard OneToOne Association * has one repository * support through table value * belongs to many repository * has many repository * has many repository find * fix: has many find and count * clean code * add count method * chore: multiple relation * chore: single relation * repository find * relation repository builder * repository count * repository count test * fix test * close db afterEach test * sort with associations * repository update * has many repository: destroy * belongs to many repository: destroy * add transaction decorator * belongs to many with transaction * has many with transaction * clean types * clean types * clean types * repository transaction * fix test * single relation repository with transaction * single relation repository with transaction * fix: test * fix: option parser fields append * fix: typo * fix: string type * fix: import * collection field methods * cleanup * collection sync * fix: import * fix: test * collection update field * collection update options * database hook * database test * database event test * update database event * add async emmit mixin * async model event * database import * fix: model hook type * fix: collection event * recall model.init on collection update * skip redefine collection test * skip collection model update * add model hook class * global model event support * chore * chore * change utils import * add field types * database import * more import test * test case * fix: through model init... * bugfix * fix * update database import * collection sync by foreachModel * fix collection model sync * update * add field types * custom operator * sqlite array field * postgresql array field * array query escape * mysql array operators * date operators * array field sqlite fix * association operator * date operator empty & notEmpty * fix: fields import * fix array field nested association * filter parse prepare * fix test * string field empty * add date operator test * field option types * fix typo * fix: operator name conflict * rename function Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
parent
bac1912b66
commit
c2ff7882bc
@ -0,0 +1,24 @@
|
|||||||
|
import { ImporterReader } from '../collection-importer';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { extend } from '../database';
|
||||||
|
import { mockDatabase } from './index';
|
||||||
|
|
||||||
|
describe('collection importer', () => {
|
||||||
|
test('import reader', async () => {
|
||||||
|
const reader = new ImporterReader(
|
||||||
|
path.resolve(__dirname, './fixtures/collections'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const modules = await reader.read();
|
||||||
|
expect(modules).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extend', async () => {
|
||||||
|
const extendObject = extend({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(extendObject).toHaveProperty('extend');
|
||||||
|
});
|
||||||
|
});
|
234
packages/database-next/src/__tests__/collection.test.ts
Normal file
234
packages/database-next/src/__tests__/collection.test.ts
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { generatePrefixByPath, mockDatabase } from './index';
|
||||||
|
import { Collection } from '../collection';
|
||||||
|
import { Database } from '../database';
|
||||||
|
|
||||||
|
test('new collection', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(collection.name).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection create field', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
collection.addField('age', {
|
||||||
|
type: 'integer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ageField = collection.getField('age');
|
||||||
|
expect(ageField).toBeDefined();
|
||||||
|
expect(collection.hasField('age')).toBeTruthy();
|
||||||
|
expect(collection.hasField('test')).toBeFalsy();
|
||||||
|
|
||||||
|
collection.removeField('age');
|
||||||
|
expect(collection.hasField('age')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection set fields', () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
collection.setFields([{ type: 'string', name: 'firstName' }]);
|
||||||
|
expect(collection.hasField('firstName')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('collection sync', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
db = mockDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sync fields', async () => {
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
collection.setFields([
|
||||||
|
{ type: 'string', name: 'firstName' },
|
||||||
|
{ type: 'string', name: 'lastName' },
|
||||||
|
{ type: 'integer', name: 'age' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await collection.sync();
|
||||||
|
const tableFields = await (<any>(
|
||||||
|
collection.model
|
||||||
|
)).queryInterface.describeTable(`${generatePrefixByPath()}_users`);
|
||||||
|
|
||||||
|
expect(tableFields).toHaveProperty('firstName');
|
||||||
|
expect(tableFields).toHaveProperty('lastName');
|
||||||
|
expect(tableFields).toHaveProperty('age');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sync with association not exists', async () => {
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'users' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
await collection.sync();
|
||||||
|
|
||||||
|
const model = collection.model;
|
||||||
|
|
||||||
|
const tableFields = await (<any>model).queryInterface.describeTable(
|
||||||
|
`${generatePrefixByPath()}_posts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tableFields['user_id']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sync with association', async () => {
|
||||||
|
new Collection(
|
||||||
|
{
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
},
|
||||||
|
{ database: db },
|
||||||
|
);
|
||||||
|
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsToMany', name: 'tags' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
database: db,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const model = collection.model;
|
||||||
|
await collection.sync();
|
||||||
|
const tableFields = await (<any>model).queryInterface.describeTable(
|
||||||
|
`${generatePrefixByPath()}_posts_tags`,
|
||||||
|
);
|
||||||
|
expect(tableFields['postId']).toBeDefined();
|
||||||
|
expect(tableFields['tagId']).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update collection field', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
database: db,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(collection.hasField('title')).toBeTruthy();
|
||||||
|
|
||||||
|
collection.updateField('title', {
|
||||||
|
type: 'string',
|
||||||
|
name: 'content',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(collection.hasField('title')).toBeFalsy();
|
||||||
|
expect(collection.hasField('content')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('update collection options', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const collection = new Collection(
|
||||||
|
{
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
database: db,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(collection.model.getTableName()).toEqual(
|
||||||
|
`${generatePrefixByPath()}_posts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
collection.updateOptions({
|
||||||
|
name: 'articles',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(collection.model.getTableName()).toEqual(
|
||||||
|
`${generatePrefixByPath()}_articles`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection with association', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'integer', name: 'age' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'comments',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'string', name: 'comment_as' },
|
||||||
|
{ type: 'belongsTo', name: 'post' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(User.model.associations['posts']).toBeDefined();
|
||||||
|
expect(Post.model.associations['comments']).toBeDefined();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
User.model.associations['posts'].target.associations['comments'],
|
||||||
|
).toBeDefined();
|
||||||
|
|
||||||
|
await db.close();
|
||||||
|
});
|
23
packages/database-next/src/__tests__/database.import.test.ts
Normal file
23
packages/database-next/src/__tests__/database.import.test.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { mockDatabase } from './index';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
describe('database', () => {
|
||||||
|
test('import', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
await db.import({
|
||||||
|
directory: path.resolve(__dirname, './fixtures/c0'),
|
||||||
|
});
|
||||||
|
await db.import({
|
||||||
|
directory: path.resolve(__dirname, './fixtures/c1'),
|
||||||
|
});
|
||||||
|
await db.import({
|
||||||
|
directory: path.resolve(__dirname, './fixtures/c2'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const test = db.getCollection('tests');
|
||||||
|
|
||||||
|
expect(test.getField('n0')).toBeDefined();
|
||||||
|
expect(test.getField('n1')).toBeDefined();
|
||||||
|
expect(test.getField('n2')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
206
packages/database-next/src/__tests__/database.test.ts
Normal file
206
packages/database-next/src/__tests__/database.test.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { mockDatabase } from './index';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
describe('database', () => {
|
||||||
|
test('import', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
await db.import({
|
||||||
|
directory: path.resolve(__dirname, './fixtures/collections'),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.getCollection('posts')).toBeDefined();
|
||||||
|
expect(db.getCollection('users')).toBeDefined();
|
||||||
|
expect(db.getCollection('tags')).toBeDefined();
|
||||||
|
|
||||||
|
const tagCollection = db.getCollection('tags');
|
||||||
|
|
||||||
|
// extend field
|
||||||
|
expect(tagCollection.fields.has('color')).toBeTruthy();
|
||||||
|
expect(tagCollection.fields.has('color2')).toBeTruthy();
|
||||||
|
expect(tagCollection.fields.has('name')).toBeTruthy();
|
||||||
|
|
||||||
|
// delay extend
|
||||||
|
expect(db.getCollection('images')).toBeUndefined();
|
||||||
|
|
||||||
|
db.collection({
|
||||||
|
name: 'images',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageCollection = db.getCollection('images');
|
||||||
|
|
||||||
|
expect(imageCollection).toBeDefined();
|
||||||
|
expect(imageCollection.fields.has('name')).toBeTruthy();
|
||||||
|
expect(imageCollection.fields.has('url')).toBeTruthy();
|
||||||
|
expect(imageCollection.fields.has('url2')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasMany with inverse belongsTo relation', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const UserCollection = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostCollection = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(UserCollection.model.associations.posts).toBeDefined();
|
||||||
|
expect(PostCollection.model.associations.user).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('belongsTo with inverse hasMany relation', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const UserCollection = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostCollection = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(PostCollection.model.associations.user).toBeDefined();
|
||||||
|
expect(UserCollection.model.associations.posts).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get collection', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
expect(db.getCollection('test')).toBeUndefined();
|
||||||
|
expect(db.hasCollection('test')).toBeFalsy();
|
||||||
|
db.collection({
|
||||||
|
name: 'test',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.getCollection('test')).toBeDefined();
|
||||||
|
expect(db.hasCollection('test')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection beforeBulkCreate event', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const listener = jest.fn();
|
||||||
|
|
||||||
|
db.on('posts.beforeBulkUpdate', listener);
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'old',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Post.model.update(
|
||||||
|
{
|
||||||
|
title: 'new',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
title: 'old',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(listener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('global model event', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const listener = jest.fn();
|
||||||
|
const listener2 = jest.fn();
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
db.on('afterCreate', listener);
|
||||||
|
db.on('posts.afterCreate', listener2);
|
||||||
|
|
||||||
|
await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
expect(listener2).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection multiple model event', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const listener = jest.fn();
|
||||||
|
const listener2 = jest.fn();
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
db.on('posts.afterCreate', listener);
|
||||||
|
db.on('posts.afterCreate', listener2);
|
||||||
|
|
||||||
|
await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalledTimes(1);
|
||||||
|
expect(listener2).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection afterCreate model event', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const postAfterCreateListener = jest.fn();
|
||||||
|
|
||||||
|
db.on('posts.afterCreate', postAfterCreateListener);
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Post.repository.find();
|
||||||
|
|
||||||
|
expect(postAfterCreateListener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collection event', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const listener = jest.fn();
|
||||||
|
db.on('beforeDefineCollection', listener);
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -31,9 +31,7 @@ describe('belongs to field', () => {
|
|||||||
expect(Comment.model.associations.post).toBeUndefined();
|
expect(Comment.model.associations.post).toBeUndefined();
|
||||||
const Post = db.collection({
|
const Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
{ type: 'string', name: 'title' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const association = Comment.model.associations.post;
|
const association = Comment.model.associations.post;
|
||||||
expect(Comment.model.associations.post).toBeDefined();
|
expect(Comment.model.associations.post).toBeDefined();
|
||||||
@ -51,7 +49,7 @@ describe('belongs to field', () => {
|
|||||||
title: 'title222',
|
title: 'title222',
|
||||||
});
|
});
|
||||||
const post = await Post.model.create<any>({
|
const post = await Post.model.create<any>({
|
||||||
title: 'title111'
|
title: 'title111',
|
||||||
});
|
});
|
||||||
await comment.setPost(post);
|
await comment.setPost(post);
|
||||||
const post2 = await comment.getPost();
|
const post2 = await comment.getPost();
|
||||||
@ -63,9 +61,7 @@ describe('belongs to field', () => {
|
|||||||
it('custom targetKey and foreignKey', async () => {
|
it('custom targetKey and foreignKey', async () => {
|
||||||
const Post = db.collection({
|
const Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'key', unique: true }],
|
||||||
{ type: 'string', name: 'key', unique: true },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const Comment = db.collection({
|
const Comment = db.collection({
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
@ -103,9 +99,7 @@ describe('belongs to field', () => {
|
|||||||
expect(Comment.model.associations.article).toBeUndefined();
|
expect(Comment.model.associations.article).toBeUndefined();
|
||||||
const Post = db.collection({
|
const Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'key', unique: true }],
|
||||||
{ type: 'string', name: 'key', unique: true },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
const association = Comment.model.associations.article;
|
const association = Comment.model.associations.article;
|
||||||
expect(Comment.model.associations.article).toBeDefined();
|
expect(Comment.model.associations.article).toBeDefined();
|
||||||
@ -123,7 +117,7 @@ describe('belongs to field', () => {
|
|||||||
key: 'title222',
|
key: 'title222',
|
||||||
});
|
});
|
||||||
const post = await Post.model.create<any>({
|
const post = await Post.model.create<any>({
|
||||||
key: 'title111'
|
key: 'title111',
|
||||||
});
|
});
|
||||||
await comment.setArticle(post);
|
await comment.setArticle(post);
|
||||||
const post2 = await comment.getArticle();
|
const post2 = await comment.getArticle();
|
||||||
@ -148,4 +142,21 @@ describe('belongs to field', () => {
|
|||||||
Post.removeField('comments');
|
Post.removeField('comments');
|
||||||
expect(Comment.model.rawAttributes.postId).toBeUndefined();
|
expect(Comment.model.rawAttributes.postId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has inverse field', async () => {
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'hasMany', name: 'comments' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'post' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const belongsToField = Comment.fields.get('post');
|
||||||
|
expect(belongsToField).toBeDefined();
|
||||||
|
const association = Post.model.associations;
|
||||||
|
expect(association['comments']).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ describe('belongs to many field', () => {
|
|||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('association undefined', async () => {
|
test('association undefined', async () => {
|
||||||
const Post = db.collection({
|
const Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [
|
||||||
@ -20,22 +20,41 @@ describe('belongs to many field', () => {
|
|||||||
{ type: 'belongsToMany', name: 'tags' },
|
{ type: 'belongsToMany', name: 'tags' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Post.model.associations.tags).toBeUndefined();
|
expect(Post.model.associations.tags).toBeUndefined();
|
||||||
expect(db.getCollection('posts_tags')).toBeUndefined();
|
expect(db.getCollection('posts_tags')).toBeUndefined();
|
||||||
|
|
||||||
const Tag = db.collection({
|
const Tag = db.collection({
|
||||||
name: 'tags',
|
name: 'tags',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
{ type: 'string', name: 'name' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
expect(Post.model.associations.tags).toBeDefined();
|
expect(Post.model.associations.tags).toBeDefined();
|
||||||
const Through = db.getCollection('posts_tags');
|
const Through = db.getCollection('posts_tags');
|
||||||
expect(Through).toBeDefined();
|
expect(Through).toBeDefined();
|
||||||
|
|
||||||
expect(Through.model.rawAttributes['postId']).toBeDefined();
|
expect(Through.model.rawAttributes['postId']).toBeDefined();
|
||||||
expect(Through.model.rawAttributes['tagId']).toBeDefined();
|
expect(Through.model.rawAttributes['tagId']).toBeDefined();
|
||||||
const PostTag = db.collection({
|
});
|
||||||
name: 'posts_tags',
|
|
||||||
|
test('redefine collection', () => {
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'belongsToMany', name: 'tags' },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(Post.model.associations.tags).toBeUndefined();
|
||||||
|
expect(db.getCollection('posts_tags')).toBeUndefined();
|
||||||
|
|
||||||
|
const Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTag = db.collection({ name: 'posts_tags' });
|
||||||
|
|
||||||
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
|
expect(PostTag.model.rawAttributes['postId']).toBeDefined();
|
||||||
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
|
expect(PostTag.model.rawAttributes['tagId']).toBeDefined();
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ describe('string field', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
db = mockDatabase();
|
db = mockDatabase();
|
||||||
db.registerFieldTypes({
|
db.registerFieldTypes({
|
||||||
sort: SortField
|
sort: SortField,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -19,9 +19,7 @@ describe('string field', () => {
|
|||||||
it('sort', async () => {
|
it('sort', async () => {
|
||||||
const Test = db.collection({
|
const Test = db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
fields: [
|
fields: [{ type: 'sort', name: 'sort' }],
|
||||||
{ type: 'sort', name: 'sort' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
const test1 = await Test.model.create<any>();
|
const test1 = await Test.model.create<any>();
|
||||||
@ -35,9 +33,7 @@ describe('string field', () => {
|
|||||||
it('skip if sort value not empty', async () => {
|
it('skip if sort value not empty', async () => {
|
||||||
const Test = db.collection({
|
const Test = db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
fields: [
|
fields: [{ type: 'sort', name: 'sort' }],
|
||||||
{ type: 'sort', name: 'sort' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
const test1 = await Test.model.create<any>({ sort: 3 });
|
const test1 = await Test.model.create<any>({ sort: 3 });
|
||||||
@ -66,5 +62,4 @@ describe('string field', () => {
|
|||||||
expect(t3.get('sort')).toBe(1);
|
expect(t3.get('sort')).toBe(1);
|
||||||
expect(t4.get('sort')).toBe(2);
|
expect(t4.get('sort')).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -15,9 +15,7 @@ describe('string field', () => {
|
|||||||
it('define', async () => {
|
it('define', async () => {
|
||||||
const Test = db.collection({
|
const Test = db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
{ type: 'string', name: 'name' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
expect(Test.model.rawAttributes['name']).toBeDefined();
|
expect(Test.model.rawAttributes['name']).toBeDefined();
|
||||||
@ -32,13 +30,13 @@ describe('string field', () => {
|
|||||||
it('set', async () => {
|
it('set', async () => {
|
||||||
const Test = db.collection({
|
const Test = db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'name1' }],
|
||||||
{ type: 'string', name: 'name1' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
Test.addField({ type: 'string', name: 'name2' });
|
Test.addField('name2', { type: 'string', name: 'name2' });
|
||||||
await db.sync();
|
await db.sync({
|
||||||
|
alter: true,
|
||||||
|
});
|
||||||
expect(Test.model.rawAttributes['name1']).toBeDefined();
|
expect(Test.model.rawAttributes['name1']).toBeDefined();
|
||||||
expect(Test.model.rawAttributes['name2']).toBeDefined();
|
expect(Test.model.rawAttributes['name2']).toBeDefined();
|
||||||
const model = await Test.model.create({
|
const model = await Test.model.create({
|
||||||
@ -54,9 +52,7 @@ describe('string field', () => {
|
|||||||
it('model hook', async () => {
|
it('model hook', async () => {
|
||||||
const collection = db.collection({
|
const collection = db.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
{ type: 'string', name: 'name' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
collection.model.beforeCreate((model) => {
|
collection.model.beforeCreate((model) => {
|
||||||
@ -65,8 +61,10 @@ describe('string field', () => {
|
|||||||
model.set(name, `${model.get(name)}111`);
|
model.set(name, `${model.get(name)}111`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
collection.addField({ type: 'string', name: 'name2' });
|
collection.addField('name2', { type: 'string', name: 'name2' });
|
||||||
await db.sync();
|
await db.sync({
|
||||||
|
alter: true,
|
||||||
|
});
|
||||||
const model = await collection.model.create({
|
const model = await collection.model.create({
|
||||||
name: 'n1',
|
name: 'n1',
|
||||||
name2: 'n2',
|
name2: 'n2',
|
||||||
|
114
packages/database-next/src/__tests__/filter-parser.test.ts
Normal file
114
packages/database-next/src/__tests__/filter-parser.test.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { mockDatabase } from './index';
|
||||||
|
import FilterParser from '../filter-parser';
|
||||||
|
import { Op } from 'sequelize';
|
||||||
|
import { Database } from '../database';
|
||||||
|
|
||||||
|
test('filter item by string', async () => {
|
||||||
|
const database = mockDatabase();
|
||||||
|
const UserCollection = database.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await database.sync();
|
||||||
|
|
||||||
|
const filterParser = new FilterParser(
|
||||||
|
UserCollection.model,
|
||||||
|
UserCollection.context.database,
|
||||||
|
{
|
||||||
|
name: 'hello',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterParams = filterParser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(filterParams).toMatchObject({
|
||||||
|
where: {
|
||||||
|
name: 'hello',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await database.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('filter by related', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'comments',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'post',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasMany', async () => {
|
||||||
|
const filter = {
|
||||||
|
'posts.title.$iLike': '%hello%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterParser = new FilterParser(
|
||||||
|
db.getCollection('users').model,
|
||||||
|
db.getCollection('users').context.database,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterParams = filterParser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(filterParams.where['$posts.title$'][Op.iLike]).toEqual('%hello%');
|
||||||
|
expect(filterParams.include[0]['association']).toEqual('posts');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('belongsTo', async () => {
|
||||||
|
const filter = {
|
||||||
|
'posts.comments.content.$iLike': '%hello%',
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterParser = new FilterParser(
|
||||||
|
db.getCollection('users').model,
|
||||||
|
db.getCollection('users').context.database,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterParams = filterParser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(filterParams.where['$posts.comments.content$'][Op.iLike]).toEqual(
|
||||||
|
'%hello%',
|
||||||
|
);
|
||||||
|
expect(filterParams.include[0]['association']).toEqual('posts');
|
||||||
|
expect(filterParams.include[0]['include'][0]['association']).toEqual(
|
||||||
|
'comments',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
6
packages/database-next/src/__tests__/fixtures/c0/a.ts
Normal file
6
packages/database-next/src/__tests__/fixtures/c0/a.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'string', name: 'n0' }],
|
||||||
|
});
|
6
packages/database-next/src/__tests__/fixtures/c1/b.ts
Normal file
6
packages/database-next/src/__tests__/fixtures/c1/b.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'string', name: 'n1' }],
|
||||||
|
});
|
6
packages/database-next/src/__tests__/fixtures/c2/a.ts
Normal file
6
packages/database-next/src/__tests__/fixtures/c2/a.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'string', name: 'n2' }],
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'images',
|
||||||
|
fields: [{ type: 'string', name: 'url' }],
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'images',
|
||||||
|
fields: [{ type: 'string', name: 'url2' }],
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'color' }],
|
||||||
|
});
|
@ -0,0 +1,6 @@
|
|||||||
|
import { extend } from '../../../database';
|
||||||
|
|
||||||
|
export default extend({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'color2' }],
|
||||||
|
});
|
@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'string', name: 'title' }],
|
||||||
|
};
|
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "users",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"name": "name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -9,31 +9,27 @@ export function generatePrefixByPath() {
|
|||||||
.replace('.test.ts', '')
|
.replace('.test.ts', '')
|
||||||
.replace(/[^\w]/g, '_')
|
.replace(/[^\w]/g, '_')
|
||||||
.replace(/_+/g, '_');
|
.replace(/_+/g, '_');
|
||||||
return key
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||||
return merge({
|
return merge(
|
||||||
username: process.env.DB_USER,
|
{
|
||||||
password: process.env.DB_PASSWORD,
|
dialect: 'sqlite',
|
||||||
database: process.env.DB_DATABASE,
|
storage: options?.storage || ':memory:',
|
||||||
host: process.env.DB_HOST,
|
logging: options?.logging || false,
|
||||||
port: process.env.DB_PORT,
|
hooks: {
|
||||||
dialect: process.env.DB_DIALECT,
|
beforeDefine(model, options) {
|
||||||
logging: process.env.DB_LOG_SQL === 'on',
|
options.tableName = `${generatePrefixByPath()}_${
|
||||||
sync: {
|
options.tableName || options.modelName || options.name.plural
|
||||||
force: true,
|
}`;
|
||||||
alter: {
|
},
|
||||||
drop: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hooks: {
|
config || {},
|
||||||
beforeDefine(model, options) {
|
options,
|
||||||
options.tableName = `${generatePrefixByPath()}_${options.tableName || options.modelName || options.name.plural}`;
|
) as any;
|
||||||
},
|
}
|
||||||
},
|
|
||||||
}, config || {}, options) as any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function mockDatabase(options?: DatabaseOptions): Database {
|
export function mockDatabase(options?: DatabaseOptions): Database {
|
||||||
return new Database(getConfig(options));
|
return new Database(getConfig(options));
|
||||||
|
@ -0,0 +1,171 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import Database from '../../database';
|
||||||
|
|
||||||
|
describe('array field operator', function () {
|
||||||
|
let db: Database;
|
||||||
|
let Test;
|
||||||
|
|
||||||
|
let t1;
|
||||||
|
let t2;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
|
||||||
|
Test = db.collection({
|
||||||
|
name: 'test',
|
||||||
|
fields: [
|
||||||
|
{ type: 'array', name: 'selected' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync({ force: true });
|
||||||
|
|
||||||
|
t1 = await Test.repository.create({
|
||||||
|
values: {
|
||||||
|
selected: [1, 2, 'a', 'b'],
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
t2 = await Test.repository.create({
|
||||||
|
values: {
|
||||||
|
selected: [11, 22, 'aa', 'bb', 'cc'],
|
||||||
|
name: 't2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nested array field', async () => {
|
||||||
|
const User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'array', name: 'tags' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u0',
|
||||||
|
posts: [{ title: 'u0p1' }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
posts: [{ title: 'u1p1', tags: ['t1', 't2'] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.tags.$empty': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('name')).toEqual('u0');
|
||||||
|
result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.tags.$anyOf': ['t1'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('name')).toEqual('u1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$match', async () => {
|
||||||
|
const filter1 = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$match': [2, 1, 'a', 'b'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter1.length).toEqual(1);
|
||||||
|
expect(filter1[0].get('name')).toEqual(t1.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$notMatch', async () => {
|
||||||
|
const filter2 = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$notMatch': [1, 2, 'a', 'b'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter2.length).toEqual(1);
|
||||||
|
expect(filter2[0].get('name')).toEqual(t2.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$anyOf', async () => {
|
||||||
|
const filter3 = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$anyOf': ['aa'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter3.length).toEqual(1);
|
||||||
|
expect(filter3[0].get('name')).toEqual(t2.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$noneOf', async () => {
|
||||||
|
const filter = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$noneOf': ['aa'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter.length).toEqual(1);
|
||||||
|
expect(filter[0].get('name')).toEqual(t1.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$empty', async () => {
|
||||||
|
const t3 = await Test.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 't3',
|
||||||
|
selected: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$empty': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(filter.length).toEqual(1);
|
||||||
|
expect(filter[0].get('name')).toEqual(t3.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$notEmpty', async () => {
|
||||||
|
const t3 = await Test.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 't3',
|
||||||
|
selected: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const filter = await Test.repository.find({
|
||||||
|
filter: {
|
||||||
|
'selected.$notEmpty': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(filter.length).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,267 @@
|
|||||||
|
import Database from '../../database';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
|
||||||
|
import { mockDatabase } from '../index';
|
||||||
|
|
||||||
|
describe('association operator', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
let Group: Collection;
|
||||||
|
|
||||||
|
let User: Collection;
|
||||||
|
let Profile: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
let Tag: Collection;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
|
||||||
|
Group = db.collection({
|
||||||
|
name: 'groups',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'users' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsTo', name: 'group' },
|
||||||
|
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'posts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'tags',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'posts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nested association', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'u1p1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u2 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u2',
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'u1p1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'u1p2',
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u3 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.tags.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('id')).toEqual(u2.get('id'));
|
||||||
|
|
||||||
|
result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.tags.id.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('id')).toEqual(u2.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('belongs to', async () => {
|
||||||
|
const g1 = await Group.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'g1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
group: g1['id'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u2 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'group.id.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('name')).toEqual(u1.get('name'));
|
||||||
|
|
||||||
|
result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'group.id.$notExists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('name')).toEqual(u2.get('name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('belongs to many', async () => {
|
||||||
|
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: 'p1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// p2 belongs to many t1 t2
|
||||||
|
const p2 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p2',
|
||||||
|
tags: [t1['id'], t2['id']],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const p3 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = await Post.repository.find({
|
||||||
|
filter: {
|
||||||
|
'tags.id.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('title')).toEqual(p2.get('title'));
|
||||||
|
|
||||||
|
result = await Tag.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.id.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(2);
|
||||||
|
expect(result.map((r) => r.get('id'))).toEqual([
|
||||||
|
t1.get('id'),
|
||||||
|
t2.get('id'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
result = await Tag.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.id.$notExists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('id')).toEqual(t3.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has many', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
{ title: 'p2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u2 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.id.$exists': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('id')).toEqual(u1.get('id'));
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,163 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
import Database from '../../database';
|
||||||
|
|
||||||
|
describe('date operator test', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
let User: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase({
|
||||||
|
logging: console.log,
|
||||||
|
});
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'birthday',
|
||||||
|
type: 'date',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateOn', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-02 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateOn': '1990-01-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(user1.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateNotOn', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-02 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateNotOn': '1990-01-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(u0.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateBefore', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-05-01 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateBefore': '1990-04-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(user1.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateNotBefore', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-05-01 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateNotBefore': '1990-04-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(u0.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateAfter', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-05-01 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateAfter': '1990-04-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(u0.get('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('$dateNotAfter', async () => {
|
||||||
|
const u0 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-05-01 12:03:02',
|
||||||
|
name: 'u0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
birthday: '1990-01-01 12:03:02',
|
||||||
|
name: 'user1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: { 'birthday.$dateNotAfter': '1990-04-01' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user.get('id')).toEqual(user1.get('id'));
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,52 @@
|
|||||||
|
import Database from '../../database';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
|
||||||
|
import { mockDatabase } from '../index';
|
||||||
|
|
||||||
|
describe('empty operator', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
let User: Collection;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase({
|
||||||
|
logging: console.log,
|
||||||
|
});
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync({
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('string field empty', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const u2 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'name.$empty': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].get('id')).toEqual(u2.get('id'));
|
||||||
|
});
|
||||||
|
});
|
166
packages/database-next/src/__tests__/option-parser.test.ts
Normal file
166
packages/database-next/src/__tests__/option-parser.test.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { Collection } from '../collection';
|
||||||
|
import { mockDatabase } from './index';
|
||||||
|
import { OptionsParser } from '../options-parser';
|
||||||
|
import { Database } from '../database';
|
||||||
|
|
||||||
|
describe('option parser', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
let Comment: Collection;
|
||||||
|
let Tag: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
User = db.collection<{ id: number; name: string }, { name: string }>({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'integer', name: 'age' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'comments',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'tags',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'belongsTo', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fields with association', () => {
|
||||||
|
let options: any = {
|
||||||
|
fields: ['id', 'name', 'tags.id', 'tags.name'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const parser = new OptionsParser(
|
||||||
|
Post.model,
|
||||||
|
Post.context.database,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
const params = parser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(params).toEqual({
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
association: 'tags',
|
||||||
|
attributes: ['id', 'name'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('with sort option', () => {
|
||||||
|
let options: any = {
|
||||||
|
sort: ['id'],
|
||||||
|
};
|
||||||
|
|
||||||
|
let parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
let params = parser.toSequelizeParams();
|
||||||
|
expect(params['order']).toEqual([['id', 'ASC']]);
|
||||||
|
|
||||||
|
options = {
|
||||||
|
sort: ['id', '-posts.title', 'posts.comments.createdAt'],
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
expect(params['order']).toEqual([
|
||||||
|
['id', 'ASC'],
|
||||||
|
[Post.model, 'title', 'DESC'],
|
||||||
|
[Post.model, Comment.model, 'createdAt', 'ASC'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('option parser with fields option', async () => {
|
||||||
|
let options: any = {
|
||||||
|
fields: ['id', 'posts'],
|
||||||
|
};
|
||||||
|
// 转换为 attributes: ['id'], include: [{association: 'posts'}]
|
||||||
|
let parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
let params = parser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(params['attributes']).toContain('id');
|
||||||
|
expect(params['include'][0]['association']).toEqual('posts');
|
||||||
|
|
||||||
|
// only appends
|
||||||
|
options = {
|
||||||
|
appends: ['posts'],
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
expect(params['attributes']['include']).toEqual([]);
|
||||||
|
expect(params['include'][0]['association']).toEqual('posts');
|
||||||
|
|
||||||
|
// fields with association field
|
||||||
|
options = {
|
||||||
|
fields: ['id', 'posts.title'],
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
expect(params['attributes']).toContain('id');
|
||||||
|
expect(params['include'][0]['association']).toEqual('posts');
|
||||||
|
expect(params['include'][0]['attributes']).toContain('title');
|
||||||
|
|
||||||
|
// fields with nested field
|
||||||
|
options = {
|
||||||
|
fields: ['id', 'posts', 'posts.comments.content'],
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
expect(params['attributes']).toContain('id');
|
||||||
|
expect(params['include'][0]['association']).toEqual('posts');
|
||||||
|
expect(params['include'][0]['attributes']).toEqual({ include: [] });
|
||||||
|
expect(params['include'][0]['include'][0]['association']).toEqual(
|
||||||
|
'comments',
|
||||||
|
);
|
||||||
|
|
||||||
|
// fields with expect
|
||||||
|
options = {
|
||||||
|
except: ['id'],
|
||||||
|
};
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
expect(params['attributes']['exclude']).toContain('id');
|
||||||
|
|
||||||
|
// expect with association
|
||||||
|
options = {
|
||||||
|
fields: ['posts'],
|
||||||
|
except: ['posts.id'],
|
||||||
|
};
|
||||||
|
|
||||||
|
parser = new OptionsParser(User.model, User.context.database, options);
|
||||||
|
params = parser.toSequelizeParams();
|
||||||
|
|
||||||
|
expect(params['include'][0]['attributes']['exclude']).toContain('id');
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,444 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { BelongsToManyRepository } from '../../relation-repository/belongs-to-many-repository';
|
||||||
|
|
||||||
|
describe('belongs to many', () => {
|
||||||
|
let db;
|
||||||
|
let Post;
|
||||||
|
let Tag;
|
||||||
|
let PostTag;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
PostTag = db.collection({
|
||||||
|
name: 'posts_tags',
|
||||||
|
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create with through values', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
await PostTagRepository.create({
|
||||||
|
values: {
|
||||||
|
name: 't1',
|
||||||
|
posts_tags: {
|
||||||
|
tagged_at: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const t1 = await PostTagRepository.findOne();
|
||||||
|
expect(t1.posts_tags.tagged_at).toEqual('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
const t1 = await PostTagRepository.create({
|
||||||
|
values: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1).toBeDefined();
|
||||||
|
|
||||||
|
const t2 = await Tag.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 't2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await PostTagRepository.add(t2.id);
|
||||||
|
|
||||||
|
const findResult = await PostTagRepository.find();
|
||||||
|
expect(findResult.length).toEqual(2);
|
||||||
|
|
||||||
|
const findFilterResult = await PostTagRepository.find({
|
||||||
|
filter: { name: 't2' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findFilterResult.length).toEqual(1);
|
||||||
|
expect(findFilterResult[0].name).toEqual('t2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find and count', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p1',
|
||||||
|
tags: [{ name: 't1' }, { name: 't2' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
let [findResult, count] = await PostTagRepository.findAndCount();
|
||||||
|
|
||||||
|
expect(count).toEqual(2);
|
||||||
|
|
||||||
|
[findResult, count] = await PostTagRepository.findAndCount({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(count).toEqual(1);
|
||||||
|
expect(findResult[0].name).toEqual('t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find one', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1', tags: [{ name: 't1' }, { name: 't2' }] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
let t1 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1.name).toEqual('t1');
|
||||||
|
|
||||||
|
t1 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'tabcaa',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(t1).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update raw attribute', async () => {
|
||||||
|
const otherTag = await Tag.repository.create({
|
||||||
|
values: { name: 'other_tag' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p1',
|
||||||
|
tags: [{ name: 't1' }, { name: 't2' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
// rename t1 to t3
|
||||||
|
await PostTagRepository.update({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
name: 't3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const t1 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1).toBeNull();
|
||||||
|
|
||||||
|
const t3 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 't3',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t3.name).toEqual('t3');
|
||||||
|
|
||||||
|
await PostTagRepository.update({
|
||||||
|
values: {
|
||||||
|
name: 'updated',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await otherTag.reload();
|
||||||
|
expect(otherTag.name).toEqual('other_tag');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update through table attribute', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p1',
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 't1',
|
||||||
|
posts_tags: {
|
||||||
|
tagged_at: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 't2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
let t1 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1.posts_tags.tagged_at).toEqual('123');
|
||||||
|
|
||||||
|
const p2 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'p2',
|
||||||
|
tags: [t1.id],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Post2TagRepository = new BelongsToManyRepository(Post, 'tags', p2.id);
|
||||||
|
const p2Tag = await Post2TagRepository.findOne();
|
||||||
|
expect(p2Tag.posts_tags.tagged_at).toBeNull();
|
||||||
|
|
||||||
|
// 设置p1与t1关联的tagged_at
|
||||||
|
await PostTagRepository.update({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
posts_tags: {
|
||||||
|
tagged_at: '456',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await t1.reload();
|
||||||
|
|
||||||
|
expect(t1.posts_tags.tagged_at).toEqual('456');
|
||||||
|
|
||||||
|
await p2Tag.reload();
|
||||||
|
// p2-tag1 still not change
|
||||||
|
expect(p2Tag.posts_tags.tagged_at).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add', async () => {
|
||||||
|
let t1 = await Tag.repository.create({
|
||||||
|
values: { name: 't1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
await PostTagRepository.add([[t1.id, { tagged_at: '123' }]]);
|
||||||
|
|
||||||
|
let p1Tag = await PostTagRepository.findOne();
|
||||||
|
expect(p1Tag.posts_tags.tagged_at).toEqual('123');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set', async () => {
|
||||||
|
let 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.id);
|
||||||
|
|
||||||
|
await PostTagRepository.set([t1.id]);
|
||||||
|
|
||||||
|
let p1Tags = await PostTagRepository.find();
|
||||||
|
expect(p1Tags.length).toEqual(1);
|
||||||
|
|
||||||
|
await PostTagRepository.set([[t1.id, { tagged_at: '999' }]]);
|
||||||
|
|
||||||
|
t1 = await PostTagRepository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(t1.posts_tags.tagged_at).toEqual('999');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find by pk', async () => {
|
||||||
|
let 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.id);
|
||||||
|
|
||||||
|
await PostTagRepository.set([t1.id, t2.id]);
|
||||||
|
|
||||||
|
const findByPkResult = await PostTagRepository.findOne({
|
||||||
|
filterByPk: t2.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findByPkResult.name).toEqual('t2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggle', async () => {
|
||||||
|
let t1 = await Tag.repository.create({
|
||||||
|
values: { name: 't1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
await PostTagRepository.toggle(t1.id);
|
||||||
|
expect(await PostTagRepository.findOne()).not.toBeNull();
|
||||||
|
|
||||||
|
await PostTagRepository.toggle(t1.id);
|
||||||
|
expect(await PostTagRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remove', async () => {
|
||||||
|
let t1 = await Tag.repository.create({
|
||||||
|
values: { name: 't1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: { title: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
await PostTagRepository.add(t1.id);
|
||||||
|
expect(await PostTagRepository.findOne()).not.toBeNull();
|
||||||
|
|
||||||
|
await PostTagRepository.remove(t1.id);
|
||||||
|
expect(await PostTagRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy all', async () => {
|
||||||
|
let 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.id);
|
||||||
|
|
||||||
|
await PostTagRepository.set([t1.id, t2.id]);
|
||||||
|
|
||||||
|
await PostTagRepository.destroy();
|
||||||
|
|
||||||
|
const [_, count] = await PostTagRepository.findAndCount();
|
||||||
|
expect(count).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy with id', async () => {
|
||||||
|
let 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.id);
|
||||||
|
|
||||||
|
await PostTagRepository.set([t1.id, t2.id]);
|
||||||
|
|
||||||
|
await PostTagRepository.destroy(t2.id);
|
||||||
|
|
||||||
|
const result = await PostTagRepository.findAndCount();
|
||||||
|
expect(result[1]).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transaction', async () => {
|
||||||
|
let 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 transaction = await Tag.model.sequelize.transaction();
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
|
||||||
|
await PostTagRepository.set({
|
||||||
|
pk: [t1.id, t2.id],
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,245 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { HasManyRepository } from '../../relation-repository/hasmany-repository';
|
||||||
|
import { BelongsToManyRepository } from '../../relation-repository/belongs-to-many-repository';
|
||||||
|
|
||||||
|
describe('has many repository', () => {
|
||||||
|
let db;
|
||||||
|
let User;
|
||||||
|
let Post;
|
||||||
|
let Comment;
|
||||||
|
let Tag;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||||
|
{ type: 'hasMany', name: 'comments' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'belongsTo', name: 'post' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync({ force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
await UserPostRepository.create({
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const t1 = await UserPostRepository.findOne();
|
||||||
|
expect(t1.title).toEqual('t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('create', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
const post = await UserPostRepository.create({
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
comments: [{ content: 'content1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(post.title).toEqual('t1');
|
||||||
|
expect(post.userId).toEqual(u1.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
await UserPostRepository.create({
|
||||||
|
values: {
|
||||||
|
title: 't1',
|
||||||
|
comments: [{ content: 'content1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.update({
|
||||||
|
filter: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
title: 'u1t1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const p1 = await UserPostRepository.findOne();
|
||||||
|
expect(p1.title).toEqual('u1t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find', async () => {
|
||||||
|
const u1 = await User.repository.create({ values: { name: 'u1' } });
|
||||||
|
|
||||||
|
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: 'p1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p2 = await Post.repository.create({
|
||||||
|
values: { title: 'p2', tags: [t1, t2, t3] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const p3 = await Post.repository.create({
|
||||||
|
values: { title: 'p3', tags: [t1, t2, t3] },
|
||||||
|
});
|
||||||
|
const p4 = await Post.repository.create({
|
||||||
|
values: { title: 'p4', tags: [t1, t2, t3] },
|
||||||
|
});
|
||||||
|
const p5 = await Post.repository.create({
|
||||||
|
values: { title: 'p5', tags: [t1, t2, t3] },
|
||||||
|
});
|
||||||
|
const p6 = await Post.repository.create({
|
||||||
|
values: { title: 'p6', tags: [t1, t2, t3] },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
|
||||||
|
const ids = [p1, p2, p3, p4, p5, p6].map((p) => p.id);
|
||||||
|
await UserPostRepository.add(ids);
|
||||||
|
|
||||||
|
const PostTagRepository = new BelongsToManyRepository(Post, 'tags', p1.id);
|
||||||
|
await PostTagRepository.set([t1.id, t2.id, t3.id]);
|
||||||
|
|
||||||
|
const posts = await UserPostRepository.find({
|
||||||
|
filter: {
|
||||||
|
'tags.name': 't1',
|
||||||
|
},
|
||||||
|
appends: ['tags'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const post = posts[0];
|
||||||
|
|
||||||
|
expect(post.tags.length).toEqual(3);
|
||||||
|
|
||||||
|
const findAndCount = await UserPostRepository.findAndCount({
|
||||||
|
filter: {
|
||||||
|
'tags.name.$like': 't%',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findAndCount[1]).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy by pk', 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.destroy(p1.id);
|
||||||
|
|
||||||
|
expect(await UserPostRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy', 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.destroy();
|
||||||
|
|
||||||
|
expect(await UserPostRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy by 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.destroy({
|
||||||
|
filter: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await UserPostRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transaction', 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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.destroy({
|
||||||
|
filter: {
|
||||||
|
title: 't1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await UserPostRepository.findOne()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,84 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { HasOneRepository } from '../../relation-repository/hasone-repository';
|
||||||
|
import Database from '../../database';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
|
||||||
|
describe('has one repository', () => {
|
||||||
|
let db: Database;
|
||||||
|
|
||||||
|
let User: Collection;
|
||||||
|
let Profile: Collection;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'hasOne', name: 'profile' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Profile = db.collection({
|
||||||
|
name: 'profiles',
|
||||||
|
fields: [{ type: 'string', name: 'avatar' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find', async () => {
|
||||||
|
const user = await User.repository.create({
|
||||||
|
values: { name: 'u1' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const UserProfileRepository = new HasOneRepository(
|
||||||
|
User,
|
||||||
|
'profile',
|
||||||
|
user['id'],
|
||||||
|
);
|
||||||
|
|
||||||
|
let userProfile = await UserProfileRepository.create({
|
||||||
|
values: {
|
||||||
|
avatar: 'test_avatar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(userProfile).toBeDefined();
|
||||||
|
|
||||||
|
userProfile = await UserProfileRepository.find();
|
||||||
|
expect(userProfile['avatar']).toEqual('test_avatar');
|
||||||
|
userProfile = await UserProfileRepository.find({
|
||||||
|
fields: ['id'],
|
||||||
|
});
|
||||||
|
expect(userProfile['id']).toBeDefined();
|
||||||
|
expect(userProfile['avatar']).toBeUndefined();
|
||||||
|
|
||||||
|
await UserProfileRepository.remove();
|
||||||
|
expect(await UserProfileRepository.find()).toBeNull();
|
||||||
|
|
||||||
|
const newProfile = await Profile.repository.create({
|
||||||
|
values: { avatar: 'new_avatar' },
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserProfileRepository.set(newProfile['id']);
|
||||||
|
userProfile = await UserProfileRepository.find();
|
||||||
|
expect(userProfile['id']).toEqual(newProfile['id']);
|
||||||
|
|
||||||
|
await UserProfileRepository.update({
|
||||||
|
values: {
|
||||||
|
avatar: 'new_updated_avatar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect((await UserProfileRepository.find())['avatar']).toEqual(
|
||||||
|
'new_updated_avatar',
|
||||||
|
);
|
||||||
|
await UserProfileRepository.destroy();
|
||||||
|
expect(await UserProfileRepository.find()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -29,98 +29,100 @@ describe('repository.find', () => {
|
|||||||
fields: [{ type: 'string', name: 'name' }],
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
await User.repository.createMany([
|
await User.repository.createMany({
|
||||||
{
|
records: [
|
||||||
name: 'user1',
|
{
|
||||||
posts: [
|
name: 'user1',
|
||||||
{
|
posts: [
|
||||||
name: 'post11',
|
{
|
||||||
comments: [
|
name: 'post11',
|
||||||
{ name: 'comment111' },
|
comments: [
|
||||||
{ name: 'comment112' },
|
{ name: 'comment111' },
|
||||||
{ name: 'comment113' },
|
{ name: 'comment112' },
|
||||||
],
|
{ name: 'comment113' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'post12',
|
{
|
||||||
comments: [
|
name: 'post12',
|
||||||
{ name: 'comment121' },
|
comments: [
|
||||||
{ name: 'comment122' },
|
{ name: 'comment121' },
|
||||||
{ name: 'comment123' },
|
{ name: 'comment122' },
|
||||||
],
|
{ name: 'comment123' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'post13',
|
{
|
||||||
comments: [
|
name: 'post13',
|
||||||
{ name: 'comment131' },
|
comments: [
|
||||||
{ name: 'comment132' },
|
{ name: 'comment131' },
|
||||||
{ name: 'comment133' },
|
{ name: 'comment132' },
|
||||||
],
|
{ name: 'comment133' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'post14',
|
{
|
||||||
comments: [
|
name: 'post14',
|
||||||
{ name: 'comment141' },
|
comments: [
|
||||||
{ name: 'comment142' },
|
{ name: 'comment141' },
|
||||||
{ name: 'comment143' },
|
{ name: 'comment142' },
|
||||||
],
|
{ name: 'comment143' },
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'user2',
|
{
|
||||||
posts: [
|
name: 'user2',
|
||||||
{
|
posts: [
|
||||||
name: 'post21',
|
{
|
||||||
comments: [
|
name: 'post21',
|
||||||
{ name: 'comment211' },
|
comments: [
|
||||||
{ name: 'comment212' },
|
{ name: 'comment211' },
|
||||||
{ name: 'comment213' },
|
{ name: 'comment212' },
|
||||||
],
|
{ name: 'comment213' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'post22',
|
{
|
||||||
comments: [
|
name: 'post22',
|
||||||
{ name: 'comment221' },
|
comments: [
|
||||||
{ name: 'comment222' },
|
{ name: 'comment221' },
|
||||||
{ name: 'comment223' },
|
{ name: 'comment222' },
|
||||||
],
|
{ name: 'comment223' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'post23',
|
{
|
||||||
comments: [
|
name: 'post23',
|
||||||
{ name: 'comment231' },
|
comments: [
|
||||||
{ name: 'comment232' },
|
{ name: 'comment231' },
|
||||||
{ name: 'comment233' },
|
{ name: 'comment232' },
|
||||||
],
|
{ name: 'comment233' },
|
||||||
},
|
],
|
||||||
{ name: 'post24' },
|
},
|
||||||
],
|
{ name: 'post24' },
|
||||||
},
|
],
|
||||||
{
|
},
|
||||||
name: 'user3',
|
{
|
||||||
posts: [
|
name: 'user3',
|
||||||
{
|
posts: [
|
||||||
name: 'post31',
|
{
|
||||||
comments: [
|
name: 'post31',
|
||||||
{ name: 'comment311' },
|
comments: [
|
||||||
{ name: 'comment312' },
|
{ name: 'comment311' },
|
||||||
{ name: 'comment313' },
|
{ name: 'comment312' },
|
||||||
],
|
{ name: 'comment313' },
|
||||||
},
|
],
|
||||||
{ name: 'post32' },
|
},
|
||||||
{
|
{ name: 'post32' },
|
||||||
name: 'post33',
|
{
|
||||||
comments: [
|
name: 'post33',
|
||||||
{ name: 'comment331' },
|
comments: [
|
||||||
{ name: 'comment332' },
|
{ name: 'comment331' },
|
||||||
{ name: 'comment333' },
|
{ name: 'comment332' },
|
||||||
],
|
{ name: 'comment333' },
|
||||||
},
|
],
|
||||||
{ name: 'post34' },
|
},
|
||||||
],
|
{ name: 'post34' },
|
||||||
},
|
],
|
||||||
]);
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -136,25 +138,12 @@ describe('repository.find', () => {
|
|||||||
console.log(data);
|
console.log(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('findMany', async () => {
|
it('find item', async () => {
|
||||||
const data = await User.repository.findMany({
|
const data = await User.repository.find({
|
||||||
filter: {
|
filter: {
|
||||||
'posts.comments.id': null,
|
'posts.comments.id': null,
|
||||||
},
|
},
|
||||||
page: 1,
|
|
||||||
pageSize: 1,
|
|
||||||
});
|
});
|
||||||
console.log(
|
|
||||||
data.count,
|
|
||||||
JSON.stringify(
|
|
||||||
data.rows.map((row) => row.toJSON()),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// expect(data.toJSON()).toMatchObject({
|
|
||||||
// name: 'user3',
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -193,17 +182,19 @@ describe('repository.create', () => {
|
|||||||
|
|
||||||
it('create', async () => {
|
it('create', async () => {
|
||||||
const user = await User.repository.create({
|
const user = await User.repository.create({
|
||||||
name: 'user1',
|
values: {
|
||||||
posts: [
|
name: 'user1',
|
||||||
{
|
posts: [
|
||||||
name: 'post11',
|
{
|
||||||
comments: [
|
name: 'post11',
|
||||||
{ name: 'comment111' },
|
comments: [
|
||||||
{ name: 'comment112' },
|
{ name: 'comment111' },
|
||||||
{ name: 'comment113' },
|
{ name: 'comment112' },
|
||||||
],
|
{ name: 'comment113' },
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const post = await Post.model.findOne();
|
const post = await Post.model.findOne();
|
||||||
expect(post).toMatchObject({
|
expect(post).toMatchObject({
|
||||||
@ -256,22 +247,25 @@ describe('repository.update', () => {
|
|||||||
const user = await User.model.create<any>({
|
const user = await User.model.create<any>({
|
||||||
name: 'user1',
|
name: 'user1',
|
||||||
});
|
});
|
||||||
await User.repository.update(
|
await User.repository.update({
|
||||||
{
|
filterByPk: user.id,
|
||||||
|
values: {
|
||||||
name: 'user11',
|
name: 'user11',
|
||||||
posts: [{ name: 'post1' }],
|
posts: [{ name: 'post1' }],
|
||||||
},
|
},
|
||||||
user,
|
});
|
||||||
);
|
|
||||||
const updated = await User.model.findByPk(user.id);
|
const updated = await User.model.findByPk(user.id);
|
||||||
expect(updated).toMatchObject({
|
expect(updated).toMatchObject({
|
||||||
name: 'user11',
|
name: 'user11',
|
||||||
});
|
});
|
||||||
|
|
||||||
const post = await Post.model.findOne({
|
const post = await Post.model.findOne({
|
||||||
where: {
|
where: {
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post).toMatchObject({
|
expect(post).toMatchObject({
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
@ -283,13 +277,13 @@ describe('repository.update', () => {
|
|||||||
name: 'user1',
|
name: 'user1',
|
||||||
posts: [{ name: 'post1' }],
|
posts: [{ name: 'post1' }],
|
||||||
});
|
});
|
||||||
await User.repository.update(
|
await User.repository.update({
|
||||||
{
|
filterByPk: user.id,
|
||||||
|
values: {
|
||||||
name: 'user11',
|
name: 'user11',
|
||||||
posts: [{ name: 'post1' }],
|
posts: [{ name: 'post1' }],
|
||||||
},
|
},
|
||||||
user.id,
|
});
|
||||||
);
|
|
||||||
const updated = await User.model.findByPk(user.id);
|
const updated = await User.model.findByPk(user.id);
|
||||||
expect(updated).toMatchObject({
|
expect(updated).toMatchObject({
|
||||||
name: 'user11',
|
name: 'user11',
|
||||||
@ -393,34 +387,31 @@ describe('repository.relatedQuery', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('create', async () => {
|
it('create', async () => {
|
||||||
const user = await User.repository.create();
|
const user = await User.repository.create({
|
||||||
const post = await User.repository.relatedQuery('posts').for(user).create({
|
values: {
|
||||||
name: 'post1',
|
name: 'u1',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userPostRepository = await User.repository
|
||||||
|
.relation('posts')
|
||||||
|
.of(<number>user.get('id'));
|
||||||
|
|
||||||
|
const post = await userPostRepository.create({
|
||||||
|
values: { name: 'post1' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(post).toMatchObject({
|
expect(post).toMatchObject({
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
userId: user.id,
|
userId: user.get('id'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const post2 = await userPostRepository.create({
|
||||||
|
values: { name: 'post2' },
|
||||||
});
|
});
|
||||||
const post2 = await User.repository
|
|
||||||
.relatedQuery('posts')
|
|
||||||
.for(user.id)
|
|
||||||
.create({
|
|
||||||
name: 'post2',
|
|
||||||
});
|
|
||||||
expect(post2).toMatchObject({
|
expect(post2).toMatchObject({
|
||||||
name: 'post2',
|
name: 'post2',
|
||||||
userId: user.id,
|
userId: user.get('id'),
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('update', async () => {
|
|
||||||
const post = await Post.repository.create({
|
|
||||||
user: {
|
|
||||||
name: 'user11',
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Post.repository.relatedQuery('user').for(post).update({
|
|
||||||
name: 'user12',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
177
packages/database-next/src/__tests__/respsitory/count.test.ts
Normal file
177
packages/database-next/src/__tests__/respsitory/count.test.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { HasManyRepository } from '../../relation-repository/hasmany-repository';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
|
||||||
|
describe('count', () => {
|
||||||
|
let db;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
let Tag;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
{ type: 'belongsToMany', name: 'tags' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'belongsToMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('count with association', async () => {
|
||||||
|
const user1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 UserPostRepository =
|
||||||
|
User.repository.relation<HasManyRepository>('posts');
|
||||||
|
|
||||||
|
await UserPostRepository.of(user1['id']).create({
|
||||||
|
values: {
|
||||||
|
title: 'u1p1',
|
||||||
|
tags: [t1, t2, t3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.of(user1['id']).create({
|
||||||
|
values: {
|
||||||
|
title: 'u1p2',
|
||||||
|
tags: [t1, t2, t3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserPostRepository.of(user1['id']).create({
|
||||||
|
values: {
|
||||||
|
title: 'u1p3',
|
||||||
|
tags: [t1, t2, t3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await Post.repository.count()).toEqual(3);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await Post.repository.count({
|
||||||
|
filter: {
|
||||||
|
'tags.name': 't1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(3);
|
||||||
|
|
||||||
|
let posts = await Post.repository.findAndCount();
|
||||||
|
expect(posts[1]).toEqual(3);
|
||||||
|
|
||||||
|
posts = await Post.repository.findAndCount({
|
||||||
|
filter: {
|
||||||
|
title: 'u1p1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(posts[0][0]['tags']).toBeUndefined();
|
||||||
|
|
||||||
|
posts = await Post.repository.findAndCount({
|
||||||
|
filter: {
|
||||||
|
title: 'u1p1',
|
||||||
|
},
|
||||||
|
appends: ['tags'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(posts[0][0]['tags']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('without filter params', async () => {
|
||||||
|
const repository = User.repository;
|
||||||
|
|
||||||
|
await repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
age: 10,
|
||||||
|
posts: [{ title: 'u1t1', comments: ['u1t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
age: 20,
|
||||||
|
posts: [{ title: 'u2t1', comments: ['u2t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
age: 30,
|
||||||
|
posts: [{ title: 'u3t1', comments: ['u3t1c1'] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await User.repository.count()).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('with filter params', async () => {
|
||||||
|
const repository = User.repository;
|
||||||
|
|
||||||
|
await repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
age: 10,
|
||||||
|
posts: [{ title: 'u1t1', comments: ['u1t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
age: 20,
|
||||||
|
posts: [{ title: 'u2t1', comments: ['u2t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
age: 30,
|
||||||
|
posts: [{ title: 'u3t1', comments: ['u3t1c1'] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await User.repository.count({
|
||||||
|
filter: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,39 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import Database from '../../database';
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User;
|
||||||
|
let Post;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
test('create with association', async () => {
|
||||||
|
const u1 = await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
posts: [{ title: 'u1p1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(u1.name).toEqual('u1');
|
||||||
|
expect(await u1.countPosts()).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,98 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
let db;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy all', async () => {
|
||||||
|
await User.repository.create({
|
||||||
|
values: {
|
||||||
|
name: 'u1',
|
||||||
|
posts: [{ title: 'u1p1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.repository.destroy();
|
||||||
|
expect(await User.repository.count()).toEqual(1);
|
||||||
|
await User.repository.destroy({ truncate: true });
|
||||||
|
expect(await User.repository.count()).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy with filter', async () => {
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.repository.destroy({
|
||||||
|
filter: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBeNull();
|
||||||
|
expect(await User.repository.count()).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy with filterByPK', async () => {
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const u2 = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await User.repository.destroy(u2['id']);
|
||||||
|
expect(await User.repository.count()).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
204
packages/database-next/src/__tests__/respsitory/find.test.ts
Normal file
204
packages/database-next/src/__tests__/respsitory/find.test.ts
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import { mockDatabase } from '../index';
|
||||||
|
import Database from '@nocobase/database';
|
||||||
|
import { Collection } from '../../collection';
|
||||||
|
import { OptionsParser } from '../../options-parser';
|
||||||
|
|
||||||
|
describe('repository find', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
let Comment: Collection;
|
||||||
|
beforeEach(async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
User = db.collection<{ id: number; name: string }, { name: string }>({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'integer', name: 'age' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'comments',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'belongsTo', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
const repository = User.repository;
|
||||||
|
|
||||||
|
await repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
age: 10,
|
||||||
|
posts: [{ title: 'u1t1', comments: ['u1t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
age: 20,
|
||||||
|
posts: [{ title: 'u2t1', comments: ['u2t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
age: 30,
|
||||||
|
posts: [{ title: 'u3t1', comments: ['u3t1c1'] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('findOne', () => {
|
||||||
|
test('find one with attribute', async () => {
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user['name']).toEqual('u2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find one with relation', async () => {
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
'posts.title': 'u2t1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(user['name']).toEqual('u2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find one with fields', async () => {
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'u2',
|
||||||
|
},
|
||||||
|
fields: ['id'],
|
||||||
|
appends: ['posts'],
|
||||||
|
except: ['posts.id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = user.toJSON();
|
||||||
|
expect(Object.keys(data)).toEqual(['id', 'posts']);
|
||||||
|
expect(Object.keys(data['posts'])).not.toContain('id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('find', () => {
|
||||||
|
test('find with logic or', async () => {
|
||||||
|
const users = await User.repository.findAndCount({
|
||||||
|
filter: {
|
||||||
|
$or: [{ 'posts.title': 'u1t1' }, { 'posts.title': 'u2t1' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(users[1]).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find with fields', async () => {
|
||||||
|
let user = await User.repository.findOne({
|
||||||
|
fields: ['name'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user['name']).toBeDefined();
|
||||||
|
expect(user['age']).toBeUndefined();
|
||||||
|
expect(user['posts']).toBeUndefined();
|
||||||
|
|
||||||
|
user = await User.repository.findOne({
|
||||||
|
fields: ['name', 'posts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user['posts']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('find with appends', () => {
|
||||||
|
test('filter attribute', async () => {
|
||||||
|
const user = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
appends: ['posts'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user['posts']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('filter association attribute', async () => {
|
||||||
|
const user2 = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
'posts.title': 'u1t1',
|
||||||
|
},
|
||||||
|
appends: ['posts'],
|
||||||
|
});
|
||||||
|
expect(user2['posts']).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('without appends', async () => {
|
||||||
|
const user3 = await User.repository.findOne({
|
||||||
|
filter: {
|
||||||
|
'posts.title': 'u1t1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(user3['posts']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('find all', () => {
|
||||||
|
test('without params', async () => {
|
||||||
|
expect((await User.repository.find()).length).toEqual(3);
|
||||||
|
});
|
||||||
|
test('with limit', async () => {
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await User.repository.find({
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
).length,
|
||||||
|
).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('find and count', () => {
|
||||||
|
test('without params', async () => {
|
||||||
|
expect((await User.repository.findAndCount())[1]).toEqual(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find with filter', async () => {
|
||||||
|
const results = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
name: 'u1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('find with association', async () => {
|
||||||
|
const results = await User.repository.find({
|
||||||
|
filter: {
|
||||||
|
'posts.title': 'u1t1',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(results.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -4,20 +4,25 @@ import { updateAssociation, updateAssociations } from '../update-associations';
|
|||||||
import { mockDatabase } from './';
|
import { mockDatabase } from './';
|
||||||
|
|
||||||
describe('update associations', () => {
|
describe('update associations', () => {
|
||||||
|
|
||||||
describe('belongsTo', () => {
|
describe('belongsTo', () => {
|
||||||
let db: Database;
|
let db: Database;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
db = mockDatabase();
|
db = mockDatabase();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('post.user', async () => {
|
it('post.user', async () => {
|
||||||
const User = db.collection({
|
const User = db.collection<
|
||||||
|
{ id: string; name: string },
|
||||||
|
{ name: string }
|
||||||
|
>({
|
||||||
name: 'users',
|
name: 'users',
|
||||||
fields: [{ type: 'string', name: 'name' }],
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const Post = db.collection({
|
const Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [
|
||||||
@ -25,12 +30,16 @@ describe('update associations', () => {
|
|||||||
{ type: 'belongsTo', name: 'user' },
|
{ type: 'belongsTo', name: 'user' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.sync();
|
await db.sync();
|
||||||
const user = await User.model.create<any>({ name: 'user1' });
|
|
||||||
|
const user = await User.model.create({ name: 'user1' });
|
||||||
const post1 = await Post.model.create({ name: 'post1' });
|
const post1 = await Post.model.create({ name: 'post1' });
|
||||||
|
|
||||||
await updateAssociations(post1, {
|
await updateAssociations(post1, {
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post1.toJSON()).toMatchObject({
|
expect(post1.toJSON()).toMatchObject({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
@ -40,21 +49,25 @@ describe('update associations', () => {
|
|||||||
name: 'user1',
|
name: 'user1',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const post2 = await Post.model.create({ name: 'post2' });
|
const post2 = await Post.model.create({ name: 'post2' });
|
||||||
await updateAssociations(post2, {
|
await updateAssociations(post2, {
|
||||||
user: user.id,
|
user: user.getDataValue('id'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post2.toJSON()).toMatchObject({
|
expect(post2.toJSON()).toMatchObject({
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'post2',
|
name: 'post2',
|
||||||
userId: 1,
|
userId: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const post3 = await Post.model.create({ name: 'post3' });
|
const post3 = await Post.model.create({ name: 'post3' });
|
||||||
await updateAssociations(post3, {
|
await updateAssociations(post3, {
|
||||||
user: {
|
user: {
|
||||||
name: 'user3',
|
name: 'user3',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post3.toJSON()).toMatchObject({
|
expect(post3.toJSON()).toMatchObject({
|
||||||
id: 3,
|
id: 3,
|
||||||
name: 'post3',
|
name: 'post3',
|
||||||
@ -64,13 +77,15 @@ describe('update associations', () => {
|
|||||||
name: 'user3',
|
name: 'user3',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const post4 = await Post.model.create({ name: 'post4' });
|
const post4 = await Post.model.create({ name: 'post4' });
|
||||||
await updateAssociations(post4, {
|
await updateAssociations(post4, {
|
||||||
user: {
|
user: {
|
||||||
id: user.id,
|
id: user.getDataValue('id'),
|
||||||
name: 'user4',
|
name: 'user4',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post4.toJSON()).toMatchObject({
|
expect(post4.toJSON()).toMatchObject({
|
||||||
id: 4,
|
id: 4,
|
||||||
name: 'post4',
|
name: 'post4',
|
||||||
@ -98,9 +113,7 @@ describe('update associations', () => {
|
|||||||
});
|
});
|
||||||
Post = db.collection({
|
Post = db.collection({
|
||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
{ type: 'string', name: 'name' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
await db.sync();
|
await db.sync();
|
||||||
});
|
});
|
||||||
@ -120,16 +133,18 @@ describe('update associations', () => {
|
|||||||
{
|
{
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
userId: user1.id,
|
userId: user1.id,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('user.posts', async () => {
|
it('user.posts', async () => {
|
||||||
const user1 = await User.model.create<any>({ name: 'user1' });
|
const user1 = await User.model.create<any>({ name: 'user1' });
|
||||||
await updateAssociations(user1, {
|
await updateAssociations(user1, {
|
||||||
posts: [{
|
posts: [
|
||||||
name: 'post1',
|
{
|
||||||
}],
|
name: 'post1',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
expect(user1.toJSON()).toMatchObject({
|
expect(user1.toJSON()).toMatchObject({
|
||||||
name: 'user1',
|
name: 'user1',
|
||||||
@ -137,7 +152,7 @@ describe('update associations', () => {
|
|||||||
{
|
{
|
||||||
name: 'post1',
|
name: 'post1',
|
||||||
userId: user1.id,
|
userId: user1.id,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -202,7 +217,7 @@ describe('update associations', () => {
|
|||||||
},
|
},
|
||||||
post2.id,
|
post2.id,
|
||||||
post3,
|
post3,
|
||||||
]
|
],
|
||||||
});
|
});
|
||||||
console.log(JSON.stringify(user1, null, 2));
|
console.log(JSON.stringify(user1, null, 2));
|
||||||
expect(user1.toJSON()).toMatchObject({
|
expect(user1.toJSON()).toMatchObject({
|
||||||
@ -263,8 +278,41 @@ describe('update associations', () => {
|
|||||||
await db.close();
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('create many with nested associations', async () => {
|
||||||
|
await User.repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
name: 'u1p1',
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
name: 'u1p1c1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
name: 'u2p1',
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
name: 'u2p1c1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('nested', async () => {
|
it('nested', async () => {
|
||||||
const user = await User.model.create<any>({ name: 'user1' });
|
const user = await User.model.create({ name: 'user1' });
|
||||||
await updateAssociations(user, {
|
await updateAssociations(user, {
|
||||||
posts: [
|
posts: [
|
||||||
{
|
{
|
||||||
@ -273,22 +321,92 @@ describe('update associations', () => {
|
|||||||
{
|
{
|
||||||
name: 'comment1',
|
name: 'comment1',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'comment12',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'post2',
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
name: 'comment21',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'comment22',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const post1 = await Post.model.findOne({
|
const post1 = await Post.model.findOne({
|
||||||
where: { name: 'post1' }
|
where: { name: 'post1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const comment1 = await Comment.model.findOne({
|
const comment1 = await Comment.model.findOne({
|
||||||
where: { name: 'comment1' }
|
where: { name: 'comment1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post1).toMatchObject({
|
expect(post1).toMatchObject({
|
||||||
userId: user.get('id'),
|
userId: user.get('id'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(comment1).toMatchObject({
|
expect(comment1).toMatchObject({
|
||||||
postId: post1.get('id'),
|
postId: post1.get('id'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('belongsToMany', () => {
|
||||||
|
let db;
|
||||||
|
let Post;
|
||||||
|
let Tag;
|
||||||
|
let PostTag;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
PostTag = db.collection({
|
||||||
|
name: 'posts_tags',
|
||||||
|
fields: [{ type: 'string', name: 'tagged_at' }],
|
||||||
|
});
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Tag = db.collection({
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{ type: 'belongsToMany', name: 'posts', through: 'posts_tags' },
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set through value', async () => {
|
||||||
|
const p1 = await Post.repository.create({
|
||||||
|
values: {
|
||||||
|
title: 'hello',
|
||||||
|
tags: [
|
||||||
|
{
|
||||||
|
name: 't1',
|
||||||
|
posts_tags: {
|
||||||
|
tagged_at: '123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ name: 't2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const t1 = (await p1.getTags())[0];
|
||||||
|
expect(t1.posts_tags.tagged_at).toEqual('123');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
372
packages/database-next/src/__tests__/update-guard.test.ts
Normal file
372
packages/database-next/src/__tests__/update-guard.test.ts
Normal file
@ -0,0 +1,372 @@
|
|||||||
|
import { Collection } from '../collection';
|
||||||
|
import { mockDatabase } from './index';
|
||||||
|
import { UpdateGuard } from '../update-guard';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { Database } from '../database';
|
||||||
|
|
||||||
|
describe('update-guard', () => {
|
||||||
|
let db: Database;
|
||||||
|
let User: Collection;
|
||||||
|
let Post: Collection;
|
||||||
|
let Comment: Collection;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
db = mockDatabase();
|
||||||
|
|
||||||
|
User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'name' },
|
||||||
|
{ type: 'integer', name: 'age' },
|
||||||
|
{ type: 'hasMany', name: 'posts' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'hasMany',
|
||||||
|
name: 'comments',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Comment = db.collection({
|
||||||
|
name: 'comments',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'content' },
|
||||||
|
{ type: 'string', name: 'comment_as' },
|
||||||
|
{ type: 'belongsTo', name: 'post' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
const repository = User.repository;
|
||||||
|
|
||||||
|
await repository.createMany({
|
||||||
|
records: [
|
||||||
|
{
|
||||||
|
name: 'u1',
|
||||||
|
age: 10,
|
||||||
|
posts: [{ title: 'u1t1', comments: [{ content: 'u1t1c1' }] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u2',
|
||||||
|
age: 20,
|
||||||
|
posts: [{ title: 'u2t1', comments: ['u2t1c1'] }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'u3',
|
||||||
|
age: 30,
|
||||||
|
posts: [{ title: 'u3t1', comments: ['u3t1c1'] }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('white list', () => {
|
||||||
|
const values = {
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
};
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setWhiteList(['name']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
name: '123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('black list', () => {
|
||||||
|
const values = {
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setBlackList(['name']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
age: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('association black list', () => {
|
||||||
|
const values = {
|
||||||
|
name: 'username123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'post-title123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setBlackList(['name', 'posts']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
age: 30,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('association fields black list', () => {
|
||||||
|
const values = {
|
||||||
|
name: 'username123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'post-title123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setBlackList(['name', 'posts.content']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
age: 30,
|
||||||
|
posts: values.posts.map((p) => {
|
||||||
|
return {
|
||||||
|
title: p.title,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('association subfield white list', () => {
|
||||||
|
const values = {
|
||||||
|
name: 'username123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'post-title123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setWhiteList(['name', 'posts.title']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
name: values.name,
|
||||||
|
posts: values.posts.map((post) => {
|
||||||
|
return {
|
||||||
|
title: post.title,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('association nested fields white list', () => {
|
||||||
|
const values = {
|
||||||
|
name: 'username123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: 'post-title123',
|
||||||
|
content: '345',
|
||||||
|
comments: [1, 2, 3],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setWhiteList(['name', 'posts.comments']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
name: values.name,
|
||||||
|
posts: values.posts.map((post) => {
|
||||||
|
return {
|
||||||
|
comments: post.comments,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('association white list', () => {
|
||||||
|
const values = {
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
guard.setWhiteList(['posts']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
posts: values.posts,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('associationKeysToBeUpdate', () => {
|
||||||
|
const values = {
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '456',
|
||||||
|
content: '789',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
guard.setAssociationKeysToBeUpdate(['posts']);
|
||||||
|
expect(guard.sanitize(values)).toEqual(values);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('associationKeysToBeUpdate nested association', () => {
|
||||||
|
const values = {
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '456',
|
||||||
|
content: '789',
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: '123',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(User.model);
|
||||||
|
|
||||||
|
expect(guard.sanitize(lodash.clone(values))).toEqual({
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
guard.setAssociationKeysToBeUpdate(['posts']);
|
||||||
|
|
||||||
|
expect(guard.sanitize(lodash.clone(values))).toEqual({
|
||||||
|
name: '123',
|
||||||
|
age: 30,
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
title: '123',
|
||||||
|
content: '345',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: '456',
|
||||||
|
content: '789',
|
||||||
|
comments: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('One2One Association', () => {
|
||||||
|
test('associationKeysToBeUpdate hasOne & BelongsTo', () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [{ type: 'belongsTo', name: 'user', targetKey: 'uid' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [{ type: 'string', name: 'uid', unique: true }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
title: '123',
|
||||||
|
content: '456',
|
||||||
|
user: {
|
||||||
|
uid: 1,
|
||||||
|
name: '123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(Post.model);
|
||||||
|
|
||||||
|
expect(guard.sanitize(values)).toEqual({
|
||||||
|
title: '123',
|
||||||
|
content: '456',
|
||||||
|
user: {
|
||||||
|
uid: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
guard.setAssociationKeysToBeUpdate(['user']);
|
||||||
|
expect(guard.sanitize(values)).toEqual(values);
|
||||||
|
});
|
||||||
|
});
|
50
packages/database-next/src/collection-importer.ts
Normal file
50
packages/database-next/src/collection-importer.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
export type ImportFileExtension = 'js' | 'ts' | 'json';
|
||||||
|
|
||||||
|
async function requireModule(module: any) {
|
||||||
|
if (typeof module === 'string') {
|
||||||
|
module = require(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof module !== 'object') {
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
return module.__esModule ? module.default : module;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImporterReader {
|
||||||
|
directory: string;
|
||||||
|
extensions: Set<string>;
|
||||||
|
|
||||||
|
constructor(directory: string, extensions?: ImportFileExtension[]) {
|
||||||
|
this.directory = directory;
|
||||||
|
|
||||||
|
if (!extensions) {
|
||||||
|
extensions = ['js', 'ts', 'json'];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extensions = new Set(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async read() {
|
||||||
|
const modules = (
|
||||||
|
await fs.promises.readdir(this.directory, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.filter((fileName) =>
|
||||||
|
this.extensions.has(path.parse(fileName).ext.replace('.', '')),
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
async (fileName) =>
|
||||||
|
await requireModule(path.join(this.directory, fileName)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (await Promise.all(modules)).filter((module) =>
|
||||||
|
lodash.isPlainObject(module),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,39 @@
|
|||||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
import { Sequelize, ModelCtor, Model, ModelOptions } from 'sequelize';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { Field } from './fields';
|
import { Field, FieldOptions } from './fields';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { Repository } from './repository';
|
import { Repository } from './repository';
|
||||||
|
import { SyncOptions } from 'sequelize/types/lib/sequelize';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import merge from 'deepmerge';
|
||||||
|
const { hooks } = require('sequelize/lib/hooks');
|
||||||
|
|
||||||
export interface CollectionOptions {
|
export type RepositoryType = typeof Repository;
|
||||||
|
|
||||||
|
export interface CollectionOptions extends Omit<ModelOptions, 'name'> {
|
||||||
name: string;
|
name: string;
|
||||||
tableName?: string;
|
tableName?: string;
|
||||||
fields?: any;
|
fields?: FieldOptions[];
|
||||||
[key: string]: any;
|
model?: string | ModelCtor<Model>;
|
||||||
|
repository?: string | RepositoryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionContext {
|
export interface CollectionContext {
|
||||||
database: Database;
|
database: Database;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Collection extends EventEmitter {
|
export class Collection<
|
||||||
|
TModelAttributes extends {} = any,
|
||||||
|
TCreationAttributes extends {} = TModelAttributes,
|
||||||
|
> extends EventEmitter {
|
||||||
options: CollectionOptions;
|
options: CollectionOptions;
|
||||||
context: CollectionContext;
|
context: CollectionContext;
|
||||||
fields: Map<string, any>;
|
isThrough?: boolean;
|
||||||
model: ModelCtor<Model>;
|
fields: Map<string, any> = new Map<string, any>();
|
||||||
repository: Repository;
|
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>;
|
||||||
|
repository: Repository<TModelAttributes, TCreationAttributes>;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this.options.name;
|
return this.options.name;
|
||||||
@ -29,23 +41,62 @@ export class Collection extends EventEmitter {
|
|||||||
|
|
||||||
constructor(options: CollectionOptions, context?: CollectionContext) {
|
constructor(options: CollectionOptions, context?: CollectionContext) {
|
||||||
super();
|
super();
|
||||||
this.options = options;
|
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.fields = new Map<string, any>();
|
this.options = options;
|
||||||
this.model = class extends Model<any, any> {};
|
this.bindFieldEventListener();
|
||||||
const attributes = {};
|
this.modelInit();
|
||||||
const { name, tableName } = options;
|
|
||||||
// TODO: 不能重复 model.init,如果有涉及 InitOptions 参数修改,需要另外处理。
|
|
||||||
this.model.init(attributes, {
|
|
||||||
..._.omit(options, ['name', 'fields']),
|
|
||||||
sequelize: context.database.sequelize,
|
|
||||||
modelName: name,
|
|
||||||
tableName: tableName || name,
|
|
||||||
});
|
|
||||||
this.on('field.afterAdd', (field) => field.bind());
|
|
||||||
this.on('field.afterRemove', (field) => field.unbind());
|
|
||||||
this.setFields(options.fields);
|
this.setFields(options.fields);
|
||||||
this.repository = new Repository(this);
|
this.setRepository(options.repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sequelizeModelOptions() {
|
||||||
|
const { name, tableName } = this.options;
|
||||||
|
return {
|
||||||
|
..._.omit(this.options, ['name', 'fields']),
|
||||||
|
modelName: name,
|
||||||
|
sequelize: this.context.database.sequelize,
|
||||||
|
tableName: tableName || name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*/
|
||||||
|
modelInit() {
|
||||||
|
if (this.model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { name, model } = this.options;
|
||||||
|
let M = Model;
|
||||||
|
if (this.context.database.sequelize.isDefined(name)) {
|
||||||
|
const m = this.context.database.sequelize.model(name);
|
||||||
|
if ((m as any).isThrough) {
|
||||||
|
this.model = m;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof model === 'string') {
|
||||||
|
M = this.context.database.models.get(model) || Model;
|
||||||
|
} else if (model) {
|
||||||
|
M = model;
|
||||||
|
}
|
||||||
|
this.model = class extends M {};
|
||||||
|
this.model.init(null, this.sequelizeModelOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
setRepository(repository?: RepositoryType | string) {
|
||||||
|
let repo = Repository;
|
||||||
|
if (typeof repository === 'string') {
|
||||||
|
repo = this.context.database.repositories.get(repository) || Repository;
|
||||||
|
}
|
||||||
|
this.repository = new repo(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindFieldEventListener() {
|
||||||
|
this.on('field.afterAdd', (field: Field) => {
|
||||||
|
field.bind();
|
||||||
|
});
|
||||||
|
this.on('field.afterRemove', (field) => field.unbind());
|
||||||
}
|
}
|
||||||
|
|
||||||
forEachField(callback: (field: Field) => void) {
|
forEachField(callback: (field: Field) => void) {
|
||||||
@ -64,36 +115,44 @@ export class Collection extends EventEmitter {
|
|||||||
return this.fields.get(name);
|
return this.fields.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
addField(options) {
|
addField(name: string, options: Omit<FieldOptions, 'name'>): Field {
|
||||||
const { name, ...others } = options;
|
return this.setField(name, options);
|
||||||
if (!name) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
const { database } = this.context;
|
|
||||||
const field = database.buildField({ name, ...others }, {
|
|
||||||
...this.context,
|
|
||||||
collection: this,
|
|
||||||
model: this.model,
|
|
||||||
});
|
|
||||||
this.fields.set(name, field);
|
|
||||||
this.emit('field.afterAdd', field);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFields(fields: any, reset = true) {
|
setField(name: string, options: Omit<FieldOptions, 'name'>): Field {
|
||||||
if (!fields) {
|
const { database } = this.context;
|
||||||
return this;
|
|
||||||
|
const field = database.buildField(
|
||||||
|
{ name, ...options },
|
||||||
|
{
|
||||||
|
...this.context,
|
||||||
|
collection: this,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.fields.set(name, field);
|
||||||
|
this.emit('field.afterAdd', field);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
setFields(fields: FieldOptions[], resetFields = true) {
|
||||||
|
if (!Array.isArray(fields)) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (reset) {
|
|
||||||
this.fields.clear();
|
if (resetFields) {
|
||||||
|
this.resetFields();
|
||||||
}
|
}
|
||||||
if (Array.isArray(fields)) {
|
|
||||||
for (const field of fields) {
|
for (const { name, ...options } of fields) {
|
||||||
this.addField(field);
|
this.addField(name, options);
|
||||||
}
|
}
|
||||||
} else if (typeof fields === 'object') {
|
}
|
||||||
for (const [name, options] of Object.entries<any>(fields)) {
|
|
||||||
this.addField({...options, name});
|
resetFields() {
|
||||||
}
|
const fieldNames = this.fields.keys();
|
||||||
|
for (const fieldName of fieldNames) {
|
||||||
|
this.removeField(fieldName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,13 +165,73 @@ export class Collection extends EventEmitter {
|
|||||||
return bool;
|
return bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
/**
|
||||||
extend(options) {
|
* TODO
|
||||||
const { fields } = options;
|
*
|
||||||
this.setFields(fields);
|
* @param name
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||||
|
let newOptions = lodash.cloneDeep(options);
|
||||||
|
newOptions = merge(this.options, newOptions, mergeOptions);
|
||||||
|
|
||||||
|
this.context.database.emit('beforeUpdateCollection', this, newOptions);
|
||||||
|
|
||||||
|
this.setFields(options.fields, false);
|
||||||
|
this.setRepository(options.repository);
|
||||||
|
|
||||||
|
if (newOptions.hooks) {
|
||||||
|
this.setUpHooks(newOptions.hooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.database.emit('afterUpdateCollection', this);
|
||||||
}
|
}
|
||||||
|
|
||||||
sync() {
|
setUpHooks(bindHooks) {
|
||||||
|
(<any>this.model)._setupHooks(bindHooks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
updateField(name: string, options: FieldOptions) {
|
||||||
|
if (!this.hasField(name)) {
|
||||||
|
throw new Error(`field ${name} not exists`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.name && options.name !== name) {
|
||||||
|
this.removeField(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setField(options.name || name, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(syncOptions?: SyncOptions) {
|
||||||
|
const modelNames = [this.model.name];
|
||||||
|
|
||||||
|
const associations = this.model.associations;
|
||||||
|
|
||||||
|
for (const associationKey in associations) {
|
||||||
|
const association = associations[associationKey];
|
||||||
|
modelNames.push(association.target.name);
|
||||||
|
if ((<any>association).through) {
|
||||||
|
modelNames.push((<any>association).through.model.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const models: ModelCtor<Model>[] = [];
|
||||||
|
// @ts-ignore
|
||||||
|
this.context.database.sequelize.modelManager.forEachModel((model) => {
|
||||||
|
if (modelNames.includes(model.name)) {
|
||||||
|
models.push(model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const model of models) {
|
||||||
|
await model.sync(syncOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,26 +7,62 @@ import {
|
|||||||
Op,
|
Op,
|
||||||
Utils,
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
|
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { Collection, CollectionOptions } from './collection';
|
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||||
import * as FieldTypes from './fields';
|
import * as FieldTypes from './fields';
|
||||||
import { RelationField } from './fields';
|
import {
|
||||||
|
BaseFieldOptions,
|
||||||
|
Field,
|
||||||
|
FieldContext,
|
||||||
|
FieldOptions,
|
||||||
|
RelationField,
|
||||||
|
} from './fields';
|
||||||
|
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||||
|
|
||||||
|
import merge from 'deepmerge';
|
||||||
|
import { ModelHook } from './model-hook';
|
||||||
|
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||||
|
|
||||||
|
import extendOperators from './operators';
|
||||||
|
|
||||||
|
export interface MergeOptions extends merge.Options {}
|
||||||
|
|
||||||
export interface PendingOptions {
|
export interface PendingOptions {
|
||||||
field: RelationField;
|
field: RelationField;
|
||||||
model: ModelCtor<Model>;
|
model: ModelCtor<Model>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MapOf<T> {
|
||||||
|
[key: string]: T;
|
||||||
|
}
|
||||||
|
|
||||||
export type DatabaseOptions = Options | Sequelize;
|
export type DatabaseOptions = Options | Sequelize;
|
||||||
|
|
||||||
export class Database extends EventEmitter {
|
interface RegisterOperatorsContext {
|
||||||
|
db?: Database;
|
||||||
|
path?: string;
|
||||||
|
field?: Field;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
|
||||||
|
|
||||||
|
export class Database extends EventEmitter implements AsyncEmitter {
|
||||||
sequelize: Sequelize;
|
sequelize: Sequelize;
|
||||||
fieldTypes = new Map();
|
fieldTypes = new Map();
|
||||||
models = new Map();
|
models = new Map<string, ModelCtor<any>>();
|
||||||
repositories = new Map();
|
repositories = new Map<string, RepositoryType>();
|
||||||
operators = new Map();
|
operators = new Map();
|
||||||
collections: Map<string, Collection>;
|
collections = new Map<string, Collection>();
|
||||||
pendingFields = new Map<string, RelationField[]>();
|
pendingFields = new Map<string, RelationField[]>();
|
||||||
|
modelCollection = new Map<ModelCtor<any>, Collection>();
|
||||||
|
|
||||||
|
modelHook: ModelHook;
|
||||||
|
|
||||||
|
delayCollectionExtend = new Map<
|
||||||
|
string,
|
||||||
|
{ collectionOptions: CollectionOptions; mergeOptions?: any }[]
|
||||||
|
>();
|
||||||
|
|
||||||
constructor(options: DatabaseOptions) {
|
constructor(options: DatabaseOptions) {
|
||||||
super();
|
super();
|
||||||
@ -38,14 +74,22 @@ export class Database extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.collections = new Map();
|
this.collections = new Map();
|
||||||
|
this.modelHook = new ModelHook(this);
|
||||||
|
|
||||||
this.on('collection.afterDefine', (collection) => {
|
this.on('afterDefineCollection', (collection: Collection) => {
|
||||||
const items = this.pendingFields.get(collection.name);
|
// after collection defined, call bind method on pending fields
|
||||||
for (const field of items || []) {
|
this.pendingFields.get(collection.name)?.forEach((field) => field.bind());
|
||||||
field.bind();
|
this.delayCollectionExtend
|
||||||
}
|
.get(collection.name)
|
||||||
|
?.forEach((collectionExtend) => {
|
||||||
|
collection.updateOptions(
|
||||||
|
collectionExtend.collectionOptions,
|
||||||
|
collectionExtend.mergeOptions,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// register database field types
|
||||||
for (const [name, field] of Object.entries(FieldTypes)) {
|
for (const [name, field] of Object.entries(FieldTypes)) {
|
||||||
if (['Field', 'RelationField'].includes(name)) {
|
if (['Field', 'RelationField'].includes(name)) {
|
||||||
continue;
|
continue;
|
||||||
@ -57,35 +101,53 @@ export class Database extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const operators = new Map();
|
this.initOperators();
|
||||||
|
|
||||||
// Sequelize 内置
|
|
||||||
for (const key in Op) {
|
|
||||||
operators.set('$' + key, Op[key]);
|
|
||||||
const val = Utils.underscoredIf(key, true);
|
|
||||||
operators.set('$' + val, Op[key]);
|
|
||||||
operators.set('$' + val.replace(/_/g, ''), Op[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.operators = operators;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collection(options: CollectionOptions) {
|
/**
|
||||||
let collection = this.collections.get(options.name);
|
* Add collection to database
|
||||||
if (collection) {
|
* @param options
|
||||||
collection.extend(options);
|
*/
|
||||||
} else {
|
collection<Attributes = any, CreateAttributes = Attributes>(
|
||||||
collection = new Collection(options, { database: this });
|
options: CollectionOptions,
|
||||||
}
|
): Collection<Attributes, CreateAttributes> {
|
||||||
|
this.emit('beforeDefineCollection', options);
|
||||||
|
|
||||||
|
const collection = new Collection(options, {
|
||||||
|
database: this,
|
||||||
|
});
|
||||||
|
|
||||||
this.collections.set(collection.name, collection);
|
this.collections.set(collection.name, collection);
|
||||||
this.emit('collection.afterDefine', collection);
|
this.modelCollection.set(collection.model, collection);
|
||||||
|
|
||||||
|
this.emit('afterDefineCollection', collection);
|
||||||
|
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCollection(name: string) {
|
/**
|
||||||
|
* get exists collection by its name
|
||||||
|
* @param name
|
||||||
|
*/
|
||||||
|
getCollection(name: string): Collection {
|
||||||
return this.collections.get(name);
|
return this.collections.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasCollection(name: string): boolean {
|
||||||
|
return this.collections.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCollection(name: string) {
|
||||||
|
const collection = this.collections.get(name);
|
||||||
|
this.emit('beforeRemoveCollection', collection);
|
||||||
|
|
||||||
|
const result = this.collections.delete(name);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
this.emit('afterRemoveCollection', collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addPendingField(field: RelationField) {
|
addPendingField(field: RelationField) {
|
||||||
const associating = this.pendingFields;
|
const associating = this.pendingFields;
|
||||||
const items = this.pendingFields.get(field.target) || [];
|
const items = this.pendingFields.get(field.target) || [];
|
||||||
@ -102,33 +164,54 @@ export class Database extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFieldTypes(fieldTypes: any) {
|
registerFieldTypes(fieldTypes: MapOf<typeof Field>) {
|
||||||
for (const [type, fieldType] of Object.entries(fieldTypes)) {
|
for (const [type, fieldType] of Object.entries(fieldTypes)) {
|
||||||
this.fieldTypes.set(type, fieldType);
|
this.fieldTypes.set(type, fieldType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerModels(models: any) {
|
registerModels(models: MapOf<ModelCtor<any>>) {
|
||||||
for (const [type, schemaType] of Object.entries(models)) {
|
for (const [type, schemaType] of Object.entries(models)) {
|
||||||
this.models.set(type, schemaType);
|
this.models.set(type, schemaType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerRepositories(repositories: any) {
|
registerRepositories(repositories: MapOf<RepositoryType>) {
|
||||||
for (const [type, schemaType] of Object.entries(repositories)) {
|
for (const [type, schemaType] of Object.entries(repositories)) {
|
||||||
this.repositories.set(type, schemaType);
|
this.repositories.set(type, schemaType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerOperators(operators) {
|
initOperators() {
|
||||||
|
const operators = new Map();
|
||||||
|
|
||||||
|
// Sequelize 内置
|
||||||
|
for (const key in Op) {
|
||||||
|
operators.set('$' + key, Op[key]);
|
||||||
|
const val = Utils.underscoredIf(key, true);
|
||||||
|
operators.set('$' + val, Op[key]);
|
||||||
|
operators.set('$' + val.replace(/_/g, ''), Op[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.operators = operators;
|
||||||
|
|
||||||
|
this.registerOperators({
|
||||||
|
...extendOperators,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
registerOperators(operators: MapOf<OperatorFunc>) {
|
||||||
for (const [key, operator] of Object.entries(operators)) {
|
for (const [key, operator] of Object.entries(operators)) {
|
||||||
this.operators.set(key, operator);
|
this.operators.set(key, operator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildField(options, context) {
|
buildField(options, context: FieldContext) {
|
||||||
const { type } = options;
|
const { type } = options;
|
||||||
const Field = this.fieldTypes.get(type);
|
const Field = this.fieldTypes.get(type);
|
||||||
|
if (!Field) {
|
||||||
|
throw Error(`unsupported field type ${type}`);
|
||||||
|
}
|
||||||
return new Field(options, context);
|
return new Field(options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,4 +230,71 @@ export class Database extends EventEmitter {
|
|||||||
async close() {
|
async close() {
|
||||||
return this.sequelize.close();
|
return this.sequelize.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
||||||
|
const modelEventName = this.modelHook.isModelHook(event);
|
||||||
|
|
||||||
|
if (modelEventName && !this.modelHook.hasBindEvent(modelEventName)) {
|
||||||
|
this.sequelize.addHook(
|
||||||
|
modelEventName,
|
||||||
|
this.modelHook.sequelizeHookBuilder(modelEventName),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.modelHook.bindEvent(modelEventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
async import(options: {
|
||||||
|
directory: string;
|
||||||
|
extensions?: ImportFileExtension[];
|
||||||
|
}): Promise<Map<string, Collection>> {
|
||||||
|
const reader = new ImporterReader(options.directory, options.extensions);
|
||||||
|
const modules = await reader.read();
|
||||||
|
const result = new Map<string, Collection>();
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
if (module.extend) {
|
||||||
|
const collectionName = module.collectionOptions.name;
|
||||||
|
const existCollection = this.getCollection(collectionName);
|
||||||
|
if (existCollection) {
|
||||||
|
existCollection.updateOptions(
|
||||||
|
module.collectionOptions,
|
||||||
|
module.mergeOptions,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const existDelayExtends =
|
||||||
|
this.delayCollectionExtend.get(collectionName) || [];
|
||||||
|
|
||||||
|
this.delayCollectionExtend.set(collectionName, [
|
||||||
|
...existDelayExtends,
|
||||||
|
module,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const collection = this.collection(module);
|
||||||
|
result.set(collection.name, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extend(
|
||||||
|
collectionOptions: CollectionOptions,
|
||||||
|
mergeOptions?: MergeOptions,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
collectionOptions,
|
||||||
|
mergeOptions,
|
||||||
|
extend: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMixins(Database, [AsyncEmitter]);
|
||||||
|
|
||||||
|
export default Database;
|
||||||
|
43
packages/database-next/src/fields/array-field.ts
Normal file
43
packages/database-next/src/fields/array-field.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
|
export class ArrayField extends Field {
|
||||||
|
get dataType() {
|
||||||
|
if (this.database.sequelize.getDialect() === 'postgres') {
|
||||||
|
return DataTypes.JSONB;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DataTypes.JSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortValue(model) {
|
||||||
|
const oldValue = model.get(this.options.name);
|
||||||
|
if (oldValue) {
|
||||||
|
const newValue = oldValue.sort();
|
||||||
|
model.set(this.options.name, newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind() {
|
||||||
|
super.bind();
|
||||||
|
|
||||||
|
if (this.isSqlite()) {
|
||||||
|
this.collection.model.addHook(
|
||||||
|
'beforeCreate',
|
||||||
|
'array-field-sort',
|
||||||
|
this.sortValue.bind(this),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind() {
|
||||||
|
super.unbind();
|
||||||
|
if (this.isSqlite()) {
|
||||||
|
this.collection.model.removeHook('beforeCreate', 'array-field-sort');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArrayFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'array';
|
||||||
|
}
|
@ -1,9 +1,12 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||||
import { RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { HasInverseField } from './has-inverse-field';
|
||||||
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
import { HasManyField } from './has-many-field';
|
||||||
|
import { BelongsToOptions as SequelizeBelongsToOptions } from 'sequelize/types/lib/associations/belongs-to';
|
||||||
|
|
||||||
export class BelongsToField extends RelationField {
|
export class BelongsToField extends RelationField {
|
||||||
|
|
||||||
static type = 'belongsTo';
|
static type = 'belongsTo';
|
||||||
|
|
||||||
get target() {
|
get target() {
|
||||||
@ -14,23 +17,39 @@ export class BelongsToField extends RelationField {
|
|||||||
bind() {
|
bind() {
|
||||||
const { database, collection } = this.context;
|
const { database, collection } = this.context;
|
||||||
const Target = this.TargetModel;
|
const Target = this.TargetModel;
|
||||||
|
|
||||||
|
// if target model not exists, add it to pending field,
|
||||||
|
// it will bind later
|
||||||
if (!Target) {
|
if (!Target) {
|
||||||
database.addPendingField(this);
|
database.addPendingField(this);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collection.model.associations[this.name]) {
|
||||||
|
delete collection.model.associations[this.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// define relation on sequelize model
|
||||||
const association = collection.model.belongsTo(Target, {
|
const association = collection.model.belongsTo(Target, {
|
||||||
as: this.name,
|
as: this.name,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inverse relation
|
||||||
|
this.TargetModel.hasMany(collection.model);
|
||||||
|
|
||||||
// 建立关系之后从 pending 列表中删除
|
// 建立关系之后从 pending 列表中删除
|
||||||
database.removePendingField(this);
|
database.removePendingField(this);
|
||||||
|
|
||||||
if (!this.options.foreignKey) {
|
if (!this.options.foreignKey) {
|
||||||
this.options.foreignKey = association.foreignKey;
|
this.options.foreignKey = association.foreignKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.options.sourceKey) {
|
if (!this.options.sourceKey) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.options.sourceKey = association.sourceKey;
|
this.options.sourceKey = association.sourceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,3 +73,9 @@ export class BelongsToField extends RelationField {
|
|||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BelongsToFieldOptions
|
||||||
|
extends BaseRelationFieldOptions,
|
||||||
|
SequelizeBelongsToOptions {
|
||||||
|
type: 'belongsTo';
|
||||||
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
||||||
import { RelationField } from './relation-field';
|
import { Collection } from '../collection';
|
||||||
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { BaseColumnFieldOptions } from './field';
|
||||||
|
import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize/types/lib/associations/belongs-to-many';
|
||||||
|
|
||||||
export class BelongsToManyField extends RelationField {
|
export class BelongsToManyField extends RelationField {
|
||||||
|
|
||||||
get through() {
|
get through() {
|
||||||
return (
|
return (
|
||||||
this.options.through ||
|
this.options.through ||
|
||||||
@ -22,18 +24,27 @@ export class BelongsToManyField extends RelationField {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const through = this.through;
|
const through = this.through;
|
||||||
let Through =
|
|
||||||
database.getCollection(through) ||
|
let Through: Collection;
|
||||||
database.collection({
|
|
||||||
|
if (database.hasCollection(through)) {
|
||||||
|
Through = database.getCollection(through);
|
||||||
|
} else {
|
||||||
|
Through = database.collection({
|
||||||
name: through,
|
name: through,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||||
|
}
|
||||||
|
|
||||||
const association = collection.model.belongsToMany(Target, {
|
const association = collection.model.belongsToMany(Target, {
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target']),
|
||||||
as: this.name,
|
as: this.name,
|
||||||
through: Through.model,
|
through: Through.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 建立关系之后从 pending 列表中删除
|
// 建立关系之后从 pending 列表中删除
|
||||||
database.removePendingField(this);
|
database.removePendingField(this);
|
||||||
|
|
||||||
if (!this.options.foreignKey) {
|
if (!this.options.foreignKey) {
|
||||||
this.options.foreignKey = association.foreignKey;
|
this.options.foreignKey = association.foreignKey;
|
||||||
}
|
}
|
||||||
@ -51,3 +62,10 @@ export class BelongsToManyField extends RelationField {
|
|||||||
delete collection.model.associations[this.name];
|
delete collection.model.associations[this.name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BelongsToManyFieldOptions
|
||||||
|
extends BaseRelationFieldOptions,
|
||||||
|
Omit<SequelizeBelongsToManyOptions, 'through'> {
|
||||||
|
type: 'belongsToMany';
|
||||||
|
through?: string;
|
||||||
|
}
|
||||||
|
12
packages/database-next/src/fields/boolean-field.ts
Normal file
12
packages/database-next/src/fields/boolean-field.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
|
export class BooleanField extends Field {
|
||||||
|
get dataType() {
|
||||||
|
return DataTypes.BOOLEAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BooleanFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'boolean';
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class DateField extends Field {
|
export class DateField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DATE;
|
return DataTypes.DATE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DateFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'date';
|
||||||
|
}
|
||||||
|
@ -1,14 +1,28 @@
|
|||||||
import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
|
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { Database } from '../database';
|
import { Database } from '../database';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import {
|
||||||
|
DataType,
|
||||||
|
ModelAttributeColumnOptions,
|
||||||
|
ModelIndexesOptions,
|
||||||
|
} from 'sequelize';
|
||||||
|
|
||||||
export interface FieldContext {
|
export interface FieldContext {
|
||||||
database: Database;
|
database: Database;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BaseFieldOptions {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseColumnFieldOptions
|
||||||
|
extends BaseFieldOptions,
|
||||||
|
Omit<ModelAttributeColumnOptions, 'type'> {
|
||||||
|
dataType?: DataType;
|
||||||
|
index?: boolean | ModelIndexesOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class Field {
|
export abstract class Field {
|
||||||
options: any;
|
options: any;
|
||||||
context: FieldContext;
|
context: FieldContext;
|
||||||
@ -67,4 +81,8 @@ export abstract class Field {
|
|||||||
}
|
}
|
||||||
return opts;
|
return opts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSqlite() {
|
||||||
|
return this.database.sequelize.getDialect() === 'sqlite';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
|
||||||
import { Field } from './field';
|
|
||||||
|
|
||||||
export class FloatField extends Field {
|
|
||||||
get dataType() {
|
|
||||||
return DataTypes.FLOAT;
|
|
||||||
}
|
|
||||||
}
|
|
5
packages/database-next/src/fields/has-inverse-field.ts
Normal file
5
packages/database-next/src/fields/has-inverse-field.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Field } from './field';
|
||||||
|
|
||||||
|
export interface HasInverseField {
|
||||||
|
inverseField: () => Field;
|
||||||
|
}
|
@ -9,7 +9,9 @@ import {
|
|||||||
HasManyOptions,
|
HasManyOptions,
|
||||||
Utils,
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { BaseColumnFieldOptions } from './field';
|
||||||
|
import { HasManyOptions as SequelizeHasManyOptions } from 'sequelize/types/lib/associations/has-many';
|
||||||
|
|
||||||
export interface HasManyFieldOptions extends HasManyOptions {
|
export interface HasManyFieldOptions extends HasManyOptions {
|
||||||
/**
|
/**
|
||||||
@ -73,7 +75,6 @@ export interface HasManyFieldOptions extends HasManyOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class HasManyField extends RelationField {
|
export class HasManyField extends RelationField {
|
||||||
|
|
||||||
get foreignKey() {
|
get foreignKey() {
|
||||||
if (this.options.foreignKey) {
|
if (this.options.foreignKey) {
|
||||||
return this.options.foreignKey;
|
return this.options.foreignKey;
|
||||||
@ -82,8 +83,8 @@ export class HasManyField extends RelationField {
|
|||||||
return Utils.camelize(
|
return Utils.camelize(
|
||||||
[
|
[
|
||||||
model.options.name.singular,
|
model.options.name.singular,
|
||||||
this.sourceKey || model.primaryKeyAttribute
|
this.sourceKey || model.primaryKeyAttribute,
|
||||||
].join('_')
|
].join('_'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,13 +95,23 @@ export class HasManyField extends RelationField {
|
|||||||
database.addPendingField(this);
|
database.addPendingField(this);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (collection.model.associations[this.name]) {
|
||||||
|
delete collection.model.associations[this.name];
|
||||||
|
}
|
||||||
|
|
||||||
const association = collection.model.hasMany(Target, {
|
const association = collection.model.hasMany(Target, {
|
||||||
as: this.name,
|
as: this.name,
|
||||||
foreignKey: this.foreignKey,
|
foreignKey: this.foreignKey,
|
||||||
...omit(this.options, ['name', 'type', 'target']),
|
...omit(this.options, ['name', 'type', 'target']),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// inverse relation
|
||||||
|
this.TargetModel.belongsTo(collection.model);
|
||||||
|
|
||||||
// 建立关系之后从 pending 列表中删除
|
// 建立关系之后从 pending 列表中删除
|
||||||
database.removePendingField(this);
|
database.removePendingField(this);
|
||||||
|
|
||||||
if (!this.options.foreignKey) {
|
if (!this.options.foreignKey) {
|
||||||
this.options.foreignKey = association.foreignKey;
|
this.options.foreignKey = association.foreignKey;
|
||||||
}
|
}
|
||||||
@ -133,3 +144,9 @@ export class HasManyField extends RelationField {
|
|||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HasManyFieldOptions
|
||||||
|
extends BaseRelationFieldOptions,
|
||||||
|
SequelizeHasManyOptions {
|
||||||
|
type: 'hasMany';
|
||||||
|
}
|
||||||
|
@ -9,7 +9,9 @@ import {
|
|||||||
HasOneOptions,
|
HasOneOptions,
|
||||||
Utils,
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
import { BaseColumnFieldOptions } from './field';
|
||||||
|
import { HasOneOptions as SequelizeHasOneOptions } from 'sequelize/types/lib/associations/has-one';
|
||||||
|
|
||||||
export interface HasOneFieldOptions extends HasOneOptions {
|
export interface HasOneFieldOptions extends HasOneOptions {
|
||||||
/**
|
/**
|
||||||
@ -73,7 +75,6 @@ export interface HasOneFieldOptions extends HasOneOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class HasOneField extends RelationField {
|
export class HasOneField extends RelationField {
|
||||||
|
|
||||||
get target() {
|
get target() {
|
||||||
const { target, name } = this.options;
|
const { target, name } = this.options;
|
||||||
return target || Utils.pluralize(name);
|
return target || Utils.pluralize(name);
|
||||||
@ -85,10 +86,7 @@ export class HasOneField extends RelationField {
|
|||||||
}
|
}
|
||||||
const { model } = this.context.collection;
|
const { model } = this.context.collection;
|
||||||
return Utils.camelize(
|
return Utils.camelize(
|
||||||
[
|
[model.options.name.singular, model.primaryKeyAttribute].join('_'),
|
||||||
model.options.name.singular,
|
|
||||||
model.primaryKeyAttribute
|
|
||||||
].join('_')
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,3 +136,9 @@ export class HasOneField extends RelationField {
|
|||||||
collection.model.refreshAttributes();
|
collection.model.refreshAttributes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HasOneFieldOptions
|
||||||
|
extends BaseRelationFieldOptions,
|
||||||
|
SequelizeHasOneOptions {
|
||||||
|
type: 'hasOne';
|
||||||
|
}
|
||||||
|
@ -1,9 +1,60 @@
|
|||||||
export * from './field';
|
import { StringFieldOptions } from './string-field';
|
||||||
export * from './string-field';
|
|
||||||
export * from './relation-field'
|
import { BooleanFieldOptions } from './boolean-field';
|
||||||
export * from './belongs-to-field'
|
import { BelongsToFieldOptions } from './belongs-to-field';
|
||||||
|
import { HasOneFieldOptions } from './has-one-field';
|
||||||
|
import { HasManyFieldOptions } from './has-many-field';
|
||||||
|
import { BelongsToManyFieldOptions } from './belongs-to-many-field';
|
||||||
|
import {
|
||||||
|
DecimalFieldOptions,
|
||||||
|
DoubleFieldOptions,
|
||||||
|
FloatFieldOptions,
|
||||||
|
IntegerFieldOptions,
|
||||||
|
RealFieldOptions,
|
||||||
|
} from './number-field';
|
||||||
|
import { JsonbFieldOptions, JsonFieldOptions } from './json-field';
|
||||||
|
import { SortFieldOptions } from './sort-field';
|
||||||
|
import { TextFieldOptions } from './text-field';
|
||||||
|
import { VirtualFieldOptions } from './virtual-field';
|
||||||
|
import { TimeFieldOptions } from './time-field';
|
||||||
|
import { DateFieldOptions } from './date-field';
|
||||||
|
import { ArrayFieldOptions } from './array-field';
|
||||||
|
|
||||||
|
export * from './array-field';
|
||||||
|
export * from './belongs-to-field';
|
||||||
export * from './belongs-to-many-field';
|
export * from './belongs-to-many-field';
|
||||||
export * from './has-one-field';
|
export * from './boolean-field';
|
||||||
|
export * from './date-field';
|
||||||
export * from './has-many-field';
|
export * from './has-many-field';
|
||||||
|
export * from './has-one-field';
|
||||||
export * from './json-field';
|
export * from './json-field';
|
||||||
|
export * from './number-field';
|
||||||
|
export * from './relation-field';
|
||||||
export * from './sort-field';
|
export * from './sort-field';
|
||||||
|
export * from './string-field';
|
||||||
|
export * from './text-field';
|
||||||
|
export * from './time-field';
|
||||||
|
export * from './uid-field';
|
||||||
|
export * from './virtual-field';
|
||||||
|
export * from './field';
|
||||||
|
|
||||||
|
export type FieldOptions =
|
||||||
|
| StringFieldOptions
|
||||||
|
| IntegerFieldOptions
|
||||||
|
| FloatFieldOptions
|
||||||
|
| DecimalFieldOptions
|
||||||
|
| DoubleFieldOptions
|
||||||
|
| RealFieldOptions
|
||||||
|
| JsonFieldOptions
|
||||||
|
| JsonbFieldOptions
|
||||||
|
| BooleanFieldOptions
|
||||||
|
| SortFieldOptions
|
||||||
|
| TextFieldOptions
|
||||||
|
| VirtualFieldOptions
|
||||||
|
| ArrayFieldOptions
|
||||||
|
| TimeFieldOptions
|
||||||
|
| DateFieldOptions
|
||||||
|
| BelongsToFieldOptions
|
||||||
|
| HasOneFieldOptions
|
||||||
|
| HasManyFieldOptions
|
||||||
|
| BelongsToManyFieldOptions;
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
|
||||||
import { Field } from './field';
|
|
||||||
|
|
||||||
export class IntegerField extends Field {
|
|
||||||
get dataType() {
|
|
||||||
return DataTypes.INTEGER;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class JsonField extends Field {
|
export class JsonField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
@ -7,6 +7,10 @@ export class JsonField extends Field {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JsonFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'json';
|
||||||
|
}
|
||||||
|
|
||||||
export class JsonbField extends Field {
|
export class JsonbField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
const dialect = this.context.database.sequelize.getDialect();
|
const dialect = this.context.database.sequelize.getDialect();
|
||||||
@ -16,3 +20,6 @@ export class JsonbField extends Field {
|
|||||||
return DataTypes.JSON;
|
return DataTypes.JSON;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export interface JsonbFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'jsonb';
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class IntegerField extends Field {
|
export class IntegerField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
@ -7,26 +7,46 @@ export class IntegerField extends Field {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IntegerFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'integer';
|
||||||
|
}
|
||||||
|
|
||||||
export class FloatField extends Field {
|
export class FloatField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.FLOAT;
|
return DataTypes.FLOAT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FloatFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'float';
|
||||||
|
}
|
||||||
|
|
||||||
export class DoubleField extends Field {
|
export class DoubleField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DOUBLE;
|
return DataTypes.DOUBLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DoubleFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'double';
|
||||||
|
}
|
||||||
|
|
||||||
export class RealField extends Field {
|
export class RealField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.REAL;
|
return DataTypes.REAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RealFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'real';
|
||||||
|
}
|
||||||
|
|
||||||
export class DecimalField extends Field {
|
export class DecimalField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.DECIMAL;
|
return DataTypes.DECIMAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DecimalFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'decimal';
|
||||||
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { Field } from './field';
|
import { BaseFieldOptions, Field } from './field';
|
||||||
|
|
||||||
|
export interface BaseRelationFieldOptions extends BaseFieldOptions {}
|
||||||
|
|
||||||
export abstract class RelationField extends Field {
|
export abstract class RelationField extends Field {
|
||||||
|
/**
|
||||||
|
* target relation name
|
||||||
|
*/
|
||||||
get target() {
|
get target() {
|
||||||
const { target, name } = this.options;
|
const { target, name } = this.options;
|
||||||
return target || name;
|
return target || name;
|
||||||
@ -14,6 +19,10 @@ export abstract class RelationField extends Field {
|
|||||||
return this.options.sourceKey;
|
return this.options.sourceKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get target model from database by it's name
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
get TargetModel() {
|
get TargetModel() {
|
||||||
return this.context.database.sequelize.models[this.target];
|
return this.context.database.sequelize.models[this.target];
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class SortField extends Field {
|
export class SortField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
@ -23,3 +23,8 @@ export class SortField extends Field {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SortFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'sort';
|
||||||
|
scopeKey?: string;
|
||||||
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class StringField extends Field {
|
export class StringField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.STRING;
|
return DataTypes.STRING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'string';
|
||||||
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class TextField extends Field {
|
export class TextField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.TEXT;
|
return DataTypes.TEXT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TextFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'text';
|
||||||
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class TimeField extends Field {
|
export class TimeField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.TIME;
|
return DataTypes.TIME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TimeFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'time';
|
||||||
|
}
|
||||||
|
23
packages/database-next/src/fields/uid-field.ts
Normal file
23
packages/database-next/src/fields/uid-field.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
import { uid } from '../utils';
|
||||||
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
|
export class UidField extends Field {
|
||||||
|
get dataType() {
|
||||||
|
return DataTypes.STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const { name, prefix = '' } = this.options;
|
||||||
|
const { model } = this.context.collection;
|
||||||
|
model.beforeCreate(async (instance) => {
|
||||||
|
if (!instance.get(name)) {
|
||||||
|
instance.set(name, `${prefix}${uid()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UidFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'uid';
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
|
||||||
export class VirtualField extends Field {
|
export class VirtualField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
return DataTypes.VIRTUAL;
|
return DataTypes.VIRTUAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VirtualFieldOptions extends BaseColumnFieldOptions {
|
||||||
|
type: 'virtual';
|
||||||
|
}
|
||||||
|
220
packages/database-next/src/filter-parser.ts
Normal file
220
packages/database-next/src/filter-parser.ts
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
import { Model, ModelCtor } from 'sequelize';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { flatten, unflatten } from 'flat';
|
||||||
|
import { Database } from './database';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
const debug = require('debug')('noco-database');
|
||||||
|
|
||||||
|
type FilterType = any;
|
||||||
|
|
||||||
|
export default class FilterParser {
|
||||||
|
database: Database;
|
||||||
|
model: ModelCtor<Model>;
|
||||||
|
filter: FilterType;
|
||||||
|
|
||||||
|
constructor(model: ModelCtor<any>, database: Database, filter: FilterType) {
|
||||||
|
this.model = model;
|
||||||
|
this.filter = this.prepareFilter(filter);
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareFilter(filter: FilterType) {
|
||||||
|
if (lodash.isPlainObject(filter)) {
|
||||||
|
const renamedKey = {};
|
||||||
|
|
||||||
|
for (const key of Object.keys(filter)) {
|
||||||
|
if (key.endsWith('.$exists') || key.endsWith('.$notExists')) {
|
||||||
|
const keyArr = key.split('.');
|
||||||
|
if (keyArr[keyArr.length - 2] == 'id') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyArr.splice(keyArr.length - 1, 0, 'id');
|
||||||
|
renamedKey[key] = keyArr.join('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [oldKey, newKey] of Object.entries(renamedKey)) {
|
||||||
|
// @ts-ignore
|
||||||
|
filter[newKey] = filter[oldKey];
|
||||||
|
delete filter[oldKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
toSequelizeParams() {
|
||||||
|
debug('filter %o', this.filter);
|
||||||
|
|
||||||
|
if (!this.filter) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = this.filter;
|
||||||
|
|
||||||
|
const model = this.model;
|
||||||
|
|
||||||
|
// supported operators
|
||||||
|
const operators = this.database.operators;
|
||||||
|
|
||||||
|
const originalFiler = lodash.cloneDeep(filter || {});
|
||||||
|
|
||||||
|
const flattenedFilter = flatten(filter || {});
|
||||||
|
|
||||||
|
debug('flattened filter %o', flattenedFilter);
|
||||||
|
|
||||||
|
const include = {};
|
||||||
|
const where = {};
|
||||||
|
const filter2 = lodash.cloneDeep(flattenedFilter);
|
||||||
|
|
||||||
|
let skipPrefix = null;
|
||||||
|
const associations = model.associations;
|
||||||
|
debug('associations %O', associations);
|
||||||
|
|
||||||
|
for (let [key, value] of Object.entries(flattenedFilter)) {
|
||||||
|
// 处理 filter 条件
|
||||||
|
if (skipPrefix && key.startsWith(skipPrefix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('handle filter key "%s: "%s"', key, value);
|
||||||
|
let keys = key.split('.');
|
||||||
|
|
||||||
|
// paths ?
|
||||||
|
const paths = [];
|
||||||
|
|
||||||
|
// origins ?
|
||||||
|
const origins = [];
|
||||||
|
|
||||||
|
while (keys.length) {
|
||||||
|
debug('keys: %o, paths: %o, origins: %o', keys, paths, origins);
|
||||||
|
|
||||||
|
// move key from keys to origins
|
||||||
|
const firstKey = keys.shift();
|
||||||
|
origins.push(firstKey);
|
||||||
|
|
||||||
|
debug('origins: %o', origins);
|
||||||
|
|
||||||
|
if (firstKey.startsWith('$')) {
|
||||||
|
if (operators.has(firstKey)) {
|
||||||
|
debug('%s is operator', firstKey);
|
||||||
|
// if firstKey is operator
|
||||||
|
const opKey = operators.get(firstKey);
|
||||||
|
debug('operator key %s, operator: %o', firstKey, opKey);
|
||||||
|
|
||||||
|
// 默认操作符
|
||||||
|
if (typeof opKey === 'symbol') {
|
||||||
|
paths.push(opKey);
|
||||||
|
continue;
|
||||||
|
} else if (typeof opKey === 'function') {
|
||||||
|
skipPrefix = origins.join('.');
|
||||||
|
|
||||||
|
value = opKey(originalFiler[skipPrefix], {
|
||||||
|
db: this.database,
|
||||||
|
path: skipPrefix,
|
||||||
|
fieldName: skipPrefix.replace(`.${firstKey}`, ''),
|
||||||
|
model: this.model,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paths.push(firstKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstKey is number
|
||||||
|
if (/\d+/.test(firstKey)) {
|
||||||
|
paths.push(firstKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// firstKey is not association
|
||||||
|
if (!associations[firstKey]) {
|
||||||
|
paths.push(firstKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const associationKeys = [];
|
||||||
|
|
||||||
|
associationKeys.push(firstKey);
|
||||||
|
|
||||||
|
debug('associationKeys %o', associationKeys);
|
||||||
|
|
||||||
|
// set sequelize include option
|
||||||
|
_.set(include, firstKey, {
|
||||||
|
association: firstKey,
|
||||||
|
attributes: [], // out put empty fields by default
|
||||||
|
});
|
||||||
|
|
||||||
|
// association target model
|
||||||
|
let target = associations[firstKey].target;
|
||||||
|
debug('association target %o', target);
|
||||||
|
|
||||||
|
while (target) {
|
||||||
|
const attr = keys.shift();
|
||||||
|
origins.push(attr);
|
||||||
|
// if it is target model attribute
|
||||||
|
if (target.rawAttributes[attr]) {
|
||||||
|
associationKeys.push(attr);
|
||||||
|
target = null;
|
||||||
|
} else if (target.associations[attr]) {
|
||||||
|
// if it is target model association (nested association filter)
|
||||||
|
associationKeys.push(attr);
|
||||||
|
const assoc = [];
|
||||||
|
associationKeys.forEach((associationKey, index) => {
|
||||||
|
if (index > 0) {
|
||||||
|
assoc.push('include');
|
||||||
|
}
|
||||||
|
assoc.push(associationKey);
|
||||||
|
});
|
||||||
|
_.set(include, assoc, {
|
||||||
|
association: attr,
|
||||||
|
attributes: [],
|
||||||
|
});
|
||||||
|
target = target.associations[attr].target;
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`${attr} neither ${firstKey}'s association nor ${firstKey}'s attribute`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('associationKeys %o', associationKeys);
|
||||||
|
|
||||||
|
if (associationKeys.length > 1) {
|
||||||
|
paths.push(`$${associationKeys.join('.')}$`);
|
||||||
|
} else {
|
||||||
|
paths.push(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('where %o, paths %o, value, %o', where, paths, value);
|
||||||
|
|
||||||
|
const values = _.get(where, paths);
|
||||||
|
|
||||||
|
if (
|
||||||
|
values &&
|
||||||
|
typeof values === 'object' &&
|
||||||
|
value &&
|
||||||
|
typeof value === 'object'
|
||||||
|
) {
|
||||||
|
value = { ...value, ...values };
|
||||||
|
}
|
||||||
|
_.set(where, paths, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toInclude = (items) => {
|
||||||
|
return Object.values(items).map((item: any) => {
|
||||||
|
if (item.include) {
|
||||||
|
item.include = toInclude(item.include);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
debug('where %o, include %o', where, include);
|
||||||
|
return { where, include: toInclude(include) };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
export * from './database';
|
||||||
|
export * from './collection';
|
||||||
|
export * from './utils';
|
||||||
|
export { Database as default } from './database';
|
62
packages/database-next/src/model-hook.ts
Normal file
62
packages/database-next/src/model-hook.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Database from './database';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { Model } from 'sequelize';
|
||||||
|
import { SequelizeHooks } from 'sequelize/types/lib/hooks';
|
||||||
|
|
||||||
|
const { hooks } = require('sequelize/lib/hooks');
|
||||||
|
|
||||||
|
export class ModelHook {
|
||||||
|
database: Database;
|
||||||
|
boundEvent = new Set<string>();
|
||||||
|
|
||||||
|
constructor(database: Database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
isModelHook(eventName: string | symbol): keyof SequelizeHooks | false {
|
||||||
|
if (lodash.isString(eventName)) {
|
||||||
|
const hookType = eventName.split('.').pop();
|
||||||
|
|
||||||
|
if (hooks[hookType]) {
|
||||||
|
return <keyof SequelizeHooks>hookType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
findModelName(hookArgs) {
|
||||||
|
for (const arg of hookArgs) {
|
||||||
|
if (arg instanceof Model) {
|
||||||
|
return (<Model>arg).constructor.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lodash.isPlainObject(arg) && arg['model']) {
|
||||||
|
return arg['model'].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvent(eventName) {
|
||||||
|
this.boundEvent.add(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasBindEvent(eventName) {
|
||||||
|
return this.boundEvent.has(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
sequelizeHookBuilder(eventName) {
|
||||||
|
return async (...args: any[]) => {
|
||||||
|
const modelName = this.findModelName(args);
|
||||||
|
if (modelName) {
|
||||||
|
// emit model event
|
||||||
|
await this.database.emitAsync(`${modelName}.${eventName}`, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// emit sequelize global event
|
||||||
|
await this.database.emitAsync(eventName, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
158
packages/database-next/src/operators/array.ts
Normal file
158
packages/database-next/src/operators/array.ts
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import { Op, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
const getFieldName = (ctx) => {
|
||||||
|
const fieldName = ctx.fieldName;
|
||||||
|
return fieldName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const escape = (value, ctx) => {
|
||||||
|
const sequelize: Sequelize = ctx.db.sequelize;
|
||||||
|
return sequelize.escape(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sqliteExistQuery = (value, ctx) => {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
|
||||||
|
const sqlArray = `(${value
|
||||||
|
.map((v) => JSON.stringify(v.toString()))
|
||||||
|
.join(', ')})`;
|
||||||
|
|
||||||
|
const subQuery = `exists (select * from json_each(${fieldName}) where json_each.value in ${sqlArray})`;
|
||||||
|
|
||||||
|
return subQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyQuery = (ctx, operator: '=' | '>') => {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
|
||||||
|
let funcName = 'json_array_length';
|
||||||
|
let ifNull = 'IFNULL';
|
||||||
|
|
||||||
|
if (isPg(ctx)) {
|
||||||
|
funcName = 'jsonb_array_length';
|
||||||
|
ifNull = 'coalesce';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMySQL(ctx)) {
|
||||||
|
funcName = 'json_length';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `(select ${ifNull}(${funcName}(${fieldName}), 0) ${operator} 0)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDialect = (ctx) => {
|
||||||
|
return ctx.db.sequelize.getDialect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPg = (ctx) => {
|
||||||
|
return getDialect(ctx) === 'postgres';
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMySQL = (ctx) => {
|
||||||
|
return getDialect(ctx) === 'mysql';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
$match(value, ctx) {
|
||||||
|
value = escape(JSON.stringify(value.sort()), ctx);
|
||||||
|
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
if (isPg(ctx)) {
|
||||||
|
return {
|
||||||
|
[Op.contained]: value,
|
||||||
|
[Op.contains]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMySQL(ctx)) {
|
||||||
|
return Sequelize.literal(
|
||||||
|
`JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.eq]: Sequelize.literal(`json(${value})`),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$notMatch(value, ctx) {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
value = escape(JSON.stringify(value), ctx);
|
||||||
|
|
||||||
|
if (isPg(ctx)) {
|
||||||
|
return Sequelize.literal(
|
||||||
|
`not (${fieldName} <@ ${escape(
|
||||||
|
value,
|
||||||
|
ctx,
|
||||||
|
)}::JSONB and ${fieldName} @> ${value}::JSONB)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMySQL(ctx)) {
|
||||||
|
return Sequelize.literal(
|
||||||
|
`not (JSON_CONTAINS(${fieldName}, ${value}) AND JSON_CONTAINS(${value}, ${fieldName}))`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
[Op.ne]: Sequelize.literal(`json(${value})`),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO sql injection
|
||||||
|
$anyOf(value, ctx) {
|
||||||
|
if (isPg(ctx)) {
|
||||||
|
return {
|
||||||
|
[Op.contains]: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMySQL(ctx)) {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
value = escape(JSON.stringify(value), ctx);
|
||||||
|
return Sequelize.literal(`JSON_OVERLAPS(${fieldName}, ${value})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subQuery = sqliteExistQuery(value, ctx);
|
||||||
|
|
||||||
|
return Sequelize.literal(subQuery);
|
||||||
|
},
|
||||||
|
|
||||||
|
$noneOf(value, ctx) {
|
||||||
|
if (isPg(ctx)) {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
// pg single quote
|
||||||
|
const queryValue = JSON.stringify(value).replace("'", "''");
|
||||||
|
return Sequelize.literal(
|
||||||
|
`not (${fieldName} @> ${escape(queryValue, ctx)}::JSONB)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMySQL(ctx)) {
|
||||||
|
const fieldName = getFieldName(ctx);
|
||||||
|
value = escape(JSON.stringify(value), ctx);
|
||||||
|
return Sequelize.literal(`NOT JSON_OVERLAPS(${fieldName}, ${value})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subQuery = sqliteExistQuery(value, ctx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.and]: [Sequelize.literal(`not ${subQuery}`)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$arrayEmpty(value, ctx) {
|
||||||
|
const subQuery = emptyQuery(ctx, '=');
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.and]: [Sequelize.literal(`${subQuery}`)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$arrayNotEmpty(value, ctx) {
|
||||||
|
const subQuery = emptyQuery(ctx, '>');
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.and]: [Sequelize.literal(`${subQuery}`)],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
14
packages/database-next/src/operators/association.ts
Normal file
14
packages/database-next/src/operators/association.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Op, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
$exists(value, ctx) {
|
||||||
|
return {
|
||||||
|
[Op.not]: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
$notExists(value, ctx) {
|
||||||
|
return {
|
||||||
|
[Op.is]: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
47
packages/database-next/src/operators/date.ts
Normal file
47
packages/database-next/src/operators/date.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Op } from 'sequelize';
|
||||||
|
import moment, { MomentInput } from 'moment';
|
||||||
|
function stringToDate(value: string): Date {
|
||||||
|
return moment(value).toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextDay(value: MomentInput): Date {
|
||||||
|
return moment(value).add(1, 'd').toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
$dateOn(value) {
|
||||||
|
return {
|
||||||
|
[Op.and]: [
|
||||||
|
{ [Op.gte]: stringToDate(value) },
|
||||||
|
{ [Op.lt]: getNextDay(value) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$dateNotOn(value) {
|
||||||
|
return {
|
||||||
|
[Op.or]: [
|
||||||
|
{ [Op.lt]: stringToDate(value) },
|
||||||
|
{ [Op.gte]: getNextDay(value) },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$dateBefore(value) {
|
||||||
|
return { [Op.lt]: stringToDate(value) };
|
||||||
|
},
|
||||||
|
|
||||||
|
$dateNotBefore(value) {
|
||||||
|
return {
|
||||||
|
[Op.gte]: stringToDate(value),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$dateAfter(value) {
|
||||||
|
return { [Op.gte]: getNextDay(value) };
|
||||||
|
},
|
||||||
|
|
||||||
|
$dateNotAfter(value) {
|
||||||
|
return { [Op.lt]: getNextDay(value) };
|
||||||
|
},
|
||||||
|
};
|
69
packages/database-next/src/operators/empty.ts
Normal file
69
packages/database-next/src/operators/empty.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { DataTypes, Op } from 'sequelize';
|
||||||
|
import { ArrayField, StringField } from '../fields';
|
||||||
|
import arrayOperators from './array';
|
||||||
|
|
||||||
|
const findFilterFieldType = (ctx) => {
|
||||||
|
const db = ctx.db;
|
||||||
|
|
||||||
|
let path = ctx.path.split('.');
|
||||||
|
|
||||||
|
// remove operators
|
||||||
|
path.pop();
|
||||||
|
|
||||||
|
const fieldName = path.pop();
|
||||||
|
|
||||||
|
let model = ctx.model;
|
||||||
|
|
||||||
|
const associationPath = path;
|
||||||
|
|
||||||
|
for (const association of associationPath) {
|
||||||
|
model = model.associations[association].target;
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = db.modelCollection.get(model);
|
||||||
|
|
||||||
|
return collection.getField(fieldName);
|
||||||
|
};
|
||||||
|
export default {
|
||||||
|
$empty(_, ctx) {
|
||||||
|
const field = findFilterFieldType(ctx);
|
||||||
|
|
||||||
|
if (field instanceof StringField) {
|
||||||
|
return {
|
||||||
|
[Op.or]: {
|
||||||
|
[Op.is]: null,
|
||||||
|
[Op.eq]: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field instanceof ArrayField) {
|
||||||
|
return arrayOperators.$arrayEmpty(_, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.is]: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
$notEmpty(_, ctx) {
|
||||||
|
const field = findFilterFieldType(ctx);
|
||||||
|
|
||||||
|
if (field instanceof StringField) {
|
||||||
|
return {
|
||||||
|
[Op.and]: {
|
||||||
|
[Op.not]: null,
|
||||||
|
[Op.ne]: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field instanceof ArrayField) {
|
||||||
|
return arrayOperators.$arrayNotEmpty(_, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[Op.not]: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
6
packages/database-next/src/operators/index.ts
Normal file
6
packages/database-next/src/operators/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
...require('./association').default,
|
||||||
|
...require('./date').default,
|
||||||
|
...require('./array').default,
|
||||||
|
...require('./empty').default,
|
||||||
|
};
|
273
packages/database-next/src/options-parser.ts
Normal file
273
packages/database-next/src/options-parser.ts
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
import { Appends, Except, FindOptions } from './repository';
|
||||||
|
import FilterParser from './filter-parser';
|
||||||
|
import { FindAttributeOptions, ModelCtor } from 'sequelize';
|
||||||
|
import { Database } from './database';
|
||||||
|
|
||||||
|
const debug = require('debug')('noco-database');
|
||||||
|
|
||||||
|
export class OptionsParser {
|
||||||
|
options: FindOptions;
|
||||||
|
database: Database;
|
||||||
|
model: ModelCtor<any>;
|
||||||
|
filterParser: FilterParser;
|
||||||
|
|
||||||
|
constructor(model: ModelCtor<any>, database: Database, options: FindOptions) {
|
||||||
|
this.model = model;
|
||||||
|
this.options = options;
|
||||||
|
this.database = database;
|
||||||
|
this.filterParser = new FilterParser(model, this.database, options?.filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
isAssociation(key: string) {
|
||||||
|
return this.model.associations[key] !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAssociationPath(path: string) {
|
||||||
|
return this.isAssociation(path.split('.')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFilterByPk() {
|
||||||
|
if (this.options?.filterByPk) {
|
||||||
|
return {
|
||||||
|
where: {
|
||||||
|
[this.model.primaryKeyAttribute]: this.options.filterByPk,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toSequelizeParams() {
|
||||||
|
const filterParams = this.options?.filterByPk
|
||||||
|
? this.parseFilterByPk()
|
||||||
|
: this.filterParser.toSequelizeParams();
|
||||||
|
return this.parseSort(this.parseFields(filterParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parser sort options
|
||||||
|
* @param filterParams
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected parseSort(filterParams) {
|
||||||
|
const sort = this.options?.sort || [];
|
||||||
|
|
||||||
|
const orderParams = sort.map((sortKey: string) => {
|
||||||
|
const direction = sortKey.startsWith('-') ? 'DESC' : 'ASC';
|
||||||
|
const sortField: Array<any> = sortKey.replace('-', '').split('.');
|
||||||
|
|
||||||
|
// handle sort by association
|
||||||
|
if (sortField.length > 1) {
|
||||||
|
let associationModel = this.model;
|
||||||
|
|
||||||
|
for (let i = 0; i < sortField.length - 1; i++) {
|
||||||
|
const associationKey = sortField[i];
|
||||||
|
sortField[i] = associationModel.associations[associationKey].target;
|
||||||
|
associationModel = sortField[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortField.push(direction);
|
||||||
|
return sortField;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (orderParams.length > 0) {
|
||||||
|
return {
|
||||||
|
order: orderParams,
|
||||||
|
...filterParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseFields(filterParams: any) {
|
||||||
|
const appends = this.options?.appends || [];
|
||||||
|
const except = [];
|
||||||
|
|
||||||
|
let attributes: FindAttributeOptions = {
|
||||||
|
include: [],
|
||||||
|
exclude: [],
|
||||||
|
}; // out put all fields by default
|
||||||
|
|
||||||
|
if (this.options?.fields) {
|
||||||
|
// 将fields拆分为 attributes 和 appends
|
||||||
|
for (const field of this.options.fields) {
|
||||||
|
if (this.isAssociationPath(field)) {
|
||||||
|
// field is association field
|
||||||
|
appends.push(field);
|
||||||
|
} else {
|
||||||
|
// field is model attribute, change attributes to array type
|
||||||
|
if (!Array.isArray(attributes)) attributes = [];
|
||||||
|
|
||||||
|
attributes.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options?.except) {
|
||||||
|
for (const exceptKey of this.options.except) {
|
||||||
|
if (this.isAssociationPath(exceptKey)) {
|
||||||
|
// except association field
|
||||||
|
except.push(exceptKey);
|
||||||
|
} else {
|
||||||
|
// if attributes is array form, ignore except
|
||||||
|
if (Array.isArray(attributes)) continue;
|
||||||
|
attributes.exclude.push(exceptKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
attributes,
|
||||||
|
...this.parseExcept(except, this.parseAppends(appends, filterParams)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseExcept(except: Except, filterParams: any) {
|
||||||
|
if (!except) return filterParams;
|
||||||
|
const setExcept = (queryParams: any, except: string) => {
|
||||||
|
// split exceptKey to path form
|
||||||
|
// posts.comments.content => ['posts', 'comments', 'content']
|
||||||
|
// then set except on include attributes
|
||||||
|
const exceptPath = except.split('.');
|
||||||
|
const association = exceptPath[0];
|
||||||
|
const lastLevel = exceptPath.length <= 2;
|
||||||
|
|
||||||
|
let existIncludeIndex = queryParams['include'].findIndex(
|
||||||
|
(include) => include['association'] == association,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existIncludeIndex == -1) {
|
||||||
|
// if include not exists, ignore this except
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastLevel) {
|
||||||
|
// if it not have exclude form
|
||||||
|
if (
|
||||||
|
Array.isArray(queryParams['include'][existIncludeIndex]['attributes'])
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!queryParams['include'][existIncludeIndex]['attributes']['exclude']
|
||||||
|
) {
|
||||||
|
queryParams['include'][existIncludeIndex]['attributes']['exclude'] =
|
||||||
|
[];
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams['include'][existIncludeIndex]['attributes'][
|
||||||
|
'exclude'
|
||||||
|
].push(exceptPath[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setExcept(
|
||||||
|
queryParams['include'][existIncludeIndex],
|
||||||
|
exceptPath.filter((_, index) => index !== 0).join('.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const exceptKey of except) {
|
||||||
|
setExcept(filterParams, exceptKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseAppends(appends: Appends, filterParams: any) {
|
||||||
|
if (!appends) return filterParams;
|
||||||
|
const associations = this.model.associations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set include params
|
||||||
|
* @param includeRoot
|
||||||
|
* @param appends
|
||||||
|
*/
|
||||||
|
const setInclude = (queryParams: any, append: string) => {
|
||||||
|
const appendFields = append.split('.');
|
||||||
|
const appendAssociation = appendFields[0];
|
||||||
|
|
||||||
|
// if append length less or equal 2
|
||||||
|
// example:
|
||||||
|
// appends: ['posts']
|
||||||
|
// appends: ['posts.title']
|
||||||
|
// All of these can be seen as last level
|
||||||
|
const lastLevel = appendFields.length <= 2;
|
||||||
|
|
||||||
|
// find association index
|
||||||
|
if (queryParams['include'] == undefined) {
|
||||||
|
queryParams['include'] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let existIncludeIndex = queryParams['include'].findIndex(
|
||||||
|
(include) => include['association'] == appendAssociation,
|
||||||
|
);
|
||||||
|
|
||||||
|
// if association not exist, create it
|
||||||
|
if (existIncludeIndex == -1) {
|
||||||
|
// association not exists
|
||||||
|
queryParams['include'].push({
|
||||||
|
association: appendAssociation,
|
||||||
|
});
|
||||||
|
|
||||||
|
existIncludeIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end appends
|
||||||
|
// without nests association
|
||||||
|
if (lastLevel) {
|
||||||
|
// get exist association attributes
|
||||||
|
let attributes = queryParams['include'][existIncludeIndex][
|
||||||
|
'attributes'
|
||||||
|
] || {
|
||||||
|
include: [], // all fields are output by default
|
||||||
|
};
|
||||||
|
|
||||||
|
// if need set attribute
|
||||||
|
if (appendFields.length == 2) {
|
||||||
|
if (!Array.isArray(attributes)) {
|
||||||
|
attributes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// push field to it
|
||||||
|
attributes.push(appendFields[1]);
|
||||||
|
} else {
|
||||||
|
// if attributes is empty array, change it to object
|
||||||
|
if (Array.isArray(attributes) && attributes.length == 0) {
|
||||||
|
attributes = {
|
||||||
|
include: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set new attributes
|
||||||
|
queryParams['include'][existIncludeIndex] = {
|
||||||
|
...queryParams['include'][existIncludeIndex],
|
||||||
|
attributes,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setInclude(
|
||||||
|
queryParams['include'][existIncludeIndex],
|
||||||
|
appendFields.filter((_, index) => index !== 0).join('.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle every appends
|
||||||
|
for (const append of appends) {
|
||||||
|
const appendFields = append.split('.');
|
||||||
|
|
||||||
|
if (!associations[appendFields[0]]) {
|
||||||
|
throw new Error(`${append} is not a valid association`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInclude(filterParams, append);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('filter params: %o', filterParams);
|
||||||
|
return filterParams;
|
||||||
|
}
|
||||||
|
}
|
52
packages/database-next/src/playground.ts
Normal file
52
packages/database-next/src/playground.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Database } from './database';
|
||||||
|
import FilterParser from './filter-parser';
|
||||||
|
|
||||||
|
const db = new Database({
|
||||||
|
dialect: 'sqlite',
|
||||||
|
dialectModule: require('sqlite3'),
|
||||||
|
storage: ':memory:',
|
||||||
|
});
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const User = db.collection({
|
||||||
|
name: 'users',
|
||||||
|
fields: [{ type: 'string', name: 'name' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const Post = db.collection({
|
||||||
|
name: 'posts',
|
||||||
|
fields: [
|
||||||
|
{ type: 'string', name: 'title' },
|
||||||
|
{
|
||||||
|
type: 'belongsTo',
|
||||||
|
name: 'user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const repository = User.repository;
|
||||||
|
|
||||||
|
await repository.createMany({
|
||||||
|
records: [
|
||||||
|
{ name: 'u1', posts: [{ title: 'u1t1' }] },
|
||||||
|
{ name: 'u2', posts: [{ title: 'u2t1' }] },
|
||||||
|
{ name: 'u3', posts: [{ title: 'u3t1' }] },
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const Model = User.model;
|
||||||
|
const user = await Model.findOne({
|
||||||
|
subQuery: false,
|
||||||
|
where: {
|
||||||
|
'$posts.title$': 'u1t1',
|
||||||
|
},
|
||||||
|
include: { association: 'posts', attributes: [] },
|
||||||
|
attributes: {
|
||||||
|
include: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(user.toJSON());
|
||||||
|
})();
|
@ -0,0 +1,270 @@
|
|||||||
|
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 { AssociatedOptions, PrimaryKeyWithThroughValues } from './types';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { transaction } from './relation-repository';
|
||||||
|
|
||||||
|
type CreateBelongsToManyOptions = CreateOptions;
|
||||||
|
|
||||||
|
interface IBelongsToManyRepository<M extends Model> {
|
||||||
|
find(options?: FindOptions): Promise<M[]>;
|
||||||
|
findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>;
|
||||||
|
findOne(options?: FindOneOptions): Promise<M>;
|
||||||
|
// 新增并关联,存在中间表数据
|
||||||
|
create(options?: CreateBelongsToManyOptions): Promise<M>;
|
||||||
|
// 更新,存在中间表数据
|
||||||
|
update(options?: UpdateOptions): Promise<M>;
|
||||||
|
// 删除
|
||||||
|
destroy(
|
||||||
|
options?: number | string | number[] | string[] | DestroyOptions,
|
||||||
|
): Promise<Boolean>;
|
||||||
|
// 建立关联
|
||||||
|
set(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
// 附加关联,存在中间表数据
|
||||||
|
add(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
// 移除关联
|
||||||
|
remove(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
toggle(
|
||||||
|
options: PrimaryKey | { pk?: PrimaryKey; transaction?: Transaction },
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BelongsToManyRepository
|
||||||
|
extends MultipleRelationRepository
|
||||||
|
implements IBelongsToManyRepository<any>
|
||||||
|
{
|
||||||
|
@transaction()
|
||||||
|
async create(options?: CreateBelongsToManyOptions): Promise<any> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const createAccessor = this.accessors().create;
|
||||||
|
const values = options.values;
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
const createOptions = {
|
||||||
|
through: values[this.throughName()],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
return sourceModel[createAccessor](values, createOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
filterByPk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async destroy(
|
||||||
|
options?: PrimaryKey | PrimaryKey[] | 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),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Through Table
|
||||||
|
const throughTableWhere: Array<any> = [
|
||||||
|
{
|
||||||
|
[association.foreignKey]: this.sourceId,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let ids;
|
||||||
|
|
||||||
|
if (options && options['filter']) {
|
||||||
|
const instances = await this.find({
|
||||||
|
filter: options['filter'],
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
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']) {
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
const instances = await sourceModel[this.accessors().get]({
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
ids = instancesToIds(instances);
|
||||||
|
}
|
||||||
|
|
||||||
|
throughTableWhere.push({
|
||||||
|
[association.otherKey]: {
|
||||||
|
[Op.in]: ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// delete through table data
|
||||||
|
await this.throughModel().destroy({
|
||||||
|
where: throughTableWhere,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.target.destroy({
|
||||||
|
where: {
|
||||||
|
[this.target.primaryKeyAttribute]: {
|
||||||
|
[Op.in]: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async setTargets(
|
||||||
|
call: 'add' | 'set',
|
||||||
|
options:
|
||||||
|
| PrimaryKey
|
||||||
|
| PrimaryKey[]
|
||||||
|
| PrimaryKeyWithThroughValues
|
||||||
|
| PrimaryKeyWithThroughValues[]
|
||||||
|
| AssociatedOptions,
|
||||||
|
) {
|
||||||
|
let handleKeys: PrimaryKey[] | PrimaryKeyWithThroughValues[];
|
||||||
|
|
||||||
|
const transaction = await this.getTransaction(options, false);
|
||||||
|
|
||||||
|
if (lodash.isPlainObject(options)) {
|
||||||
|
options = (<AssociatedOptions>options).pk || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lodash.isString(options) || lodash.isNumber(options)) {
|
||||||
|
handleKeys = [<PrimaryKey>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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
const setObj = (<any>handleKeys).reduce((carry, item) => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
carry[item[0]] = item[1];
|
||||||
|
} else {
|
||||||
|
carry[item] = true;
|
||||||
|
}
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
await sourceModel[this.accessors()[call]](Object.keys(setObj), {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [id, throughValues] of Object.entries(setObj)) {
|
||||||
|
if (typeof throughValues === 'object') {
|
||||||
|
const instance = await this.target.findByPk(id, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
await updateThroughTableValue(
|
||||||
|
instance,
|
||||||
|
this.throughName(),
|
||||||
|
throughValues,
|
||||||
|
sourceModel,
|
||||||
|
transaction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async add(
|
||||||
|
options:
|
||||||
|
| PrimaryKey
|
||||||
|
| PrimaryKey[]
|
||||||
|
| PrimaryKeyWithThroughValues
|
||||||
|
| PrimaryKeyWithThroughValues[]
|
||||||
|
| AssociatedOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.setTargets('add', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async set(
|
||||||
|
options:
|
||||||
|
| PrimaryKey
|
||||||
|
| PrimaryKey[]
|
||||||
|
| PrimaryKeyWithThroughValues
|
||||||
|
| PrimaryKeyWithThroughValues[]
|
||||||
|
| AssociatedOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.setTargets('set', options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async toggle(
|
||||||
|
options: PrimaryKey | { pk?: PrimaryKey; 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'], {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (has) {
|
||||||
|
await this.remove({
|
||||||
|
...(<any>options),
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.add({
|
||||||
|
...(<any>options),
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throughName() {
|
||||||
|
return this.throughModel().name;
|
||||||
|
}
|
||||||
|
|
||||||
|
throughModel() {
|
||||||
|
return (<any>this.association).through.model;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Model } from 'sequelize';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SingleRelationFindOption,
|
||||||
|
SingleRelationRepository,
|
||||||
|
} from './single-relation-repository';
|
||||||
|
import { CreateOptions, UpdateOptions } from '../repository';
|
||||||
|
|
||||||
|
interface BelongsToFindOptions extends SingleRelationFindOption {}
|
||||||
|
|
||||||
|
interface IBelongsToRepository<M extends Model> {
|
||||||
|
// 不需要 findOne,find 就是 findOne
|
||||||
|
find(options?: BelongsToFindOptions): Promise<M>;
|
||||||
|
// 新增并关联,如果存在关联,解除之后,与新数据建立关联
|
||||||
|
create(options?: CreateOptions): Promise<M>;
|
||||||
|
// 更新
|
||||||
|
update(options?: UpdateOptions): Promise<M>;
|
||||||
|
// 删除
|
||||||
|
destroy(): Promise<Boolean>;
|
||||||
|
// 建立关联
|
||||||
|
set(primaryKey: any): Promise<void>;
|
||||||
|
// 移除关联
|
||||||
|
remove(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BelongsToRepository
|
||||||
|
extends SingleRelationRepository
|
||||||
|
implements IBelongsToRepository<any> {}
|
@ -0,0 +1,143 @@
|
|||||||
|
import { BelongsToMany, HasMany, Model, Op, Sequelize } from 'sequelize';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AssociatedOptions,
|
||||||
|
FindAndCountOptions,
|
||||||
|
FindOneOptions,
|
||||||
|
MultipleRelationRepository,
|
||||||
|
} from './multiple-relation-repository';
|
||||||
|
import {
|
||||||
|
CreateOptions,
|
||||||
|
DestroyOptions,
|
||||||
|
FindOptions,
|
||||||
|
PK,
|
||||||
|
PrimaryKey,
|
||||||
|
UpdateOptions,
|
||||||
|
} from '../repository';
|
||||||
|
import { transaction } from './relation-repository';
|
||||||
|
|
||||||
|
interface IHasManyRepository<M extends Model> {
|
||||||
|
find(options?: FindOptions): Promise<M>;
|
||||||
|
findAndCount(options?: FindAndCountOptions): Promise<[M[], number]>;
|
||||||
|
findOne(options?: FindOneOptions): Promise<M>;
|
||||||
|
// 新增并关联
|
||||||
|
create(options?: CreateOptions): Promise<M>;
|
||||||
|
// 更新
|
||||||
|
update(options?: UpdateOptions): Promise<M>;
|
||||||
|
// 删除
|
||||||
|
destroy(options?: PK | DestroyOptions): Promise<Boolean>;
|
||||||
|
// 建立关联
|
||||||
|
set(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
// 附加关联
|
||||||
|
add(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
// 移除关联
|
||||||
|
remove(options: PrimaryKey | PrimaryKey[] | AssociatedOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasManyRepository
|
||||||
|
extends MultipleRelationRepository
|
||||||
|
implements IHasManyRepository<any>
|
||||||
|
{
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
filterByPk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async destroy(options?: PK | 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,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (options && options['filter']) {
|
||||||
|
const filterResult = this.parseFilter(options['filter']);
|
||||||
|
|
||||||
|
if (filterResult.include && filterResult.include.length > 0) {
|
||||||
|
return await this.destroyByFilter(options['filter'], transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
where.push(filterResult.where);
|
||||||
|
} else if (options && options['filterByPk']) {
|
||||||
|
if (typeof options === 'object' && options['filterByPk']) {
|
||||||
|
options = options['filterByPk'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetInstances = (<any>this.association).toInstanceArray(options);
|
||||||
|
|
||||||
|
where.push({
|
||||||
|
[this.target.primaryKeyAttribute]: targetInstances.map(
|
||||||
|
(targetInstance) =>
|
||||||
|
targetInstance.get(this.target.primaryKeyAttribute),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.target.destroy({
|
||||||
|
where: {
|
||||||
|
[Op.and]: where,
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyOfAdd(options) {
|
||||||
|
let handleKeys;
|
||||||
|
|
||||||
|
if (typeof options !== 'object' && !Array.isArray(options)) {
|
||||||
|
handleKeys = [options];
|
||||||
|
} else {
|
||||||
|
handleKeys = options['pk'];
|
||||||
|
}
|
||||||
|
return handleKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async set(
|
||||||
|
options: PrimaryKey | PrimaryKey[] | AssociatedOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
await sourceModel[this.accessors().set](this.handleKeyOfAdd(options), {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async add(
|
||||||
|
options: PrimaryKey | PrimaryKey[] | AssociatedOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
await sourceModel[this.accessors().add](this.handleKeyOfAdd(options), {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
accessors() {
|
||||||
|
return (<HasMany>this.association).accessors;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Model } from 'sequelize';
|
||||||
|
import {
|
||||||
|
SingleRelationFindOption,
|
||||||
|
SingleRelationRepository,
|
||||||
|
} from './single-relation-repository';
|
||||||
|
import { CreateOptions } from '../repository';
|
||||||
|
|
||||||
|
interface HasOneFindOptions extends SingleRelationFindOption {}
|
||||||
|
|
||||||
|
interface IHasOneRepository<M extends Model> {
|
||||||
|
// 不需要 findOne,find 就是 findOne
|
||||||
|
find(options?: HasOneFindOptions): Promise<Model<any>>;
|
||||||
|
// 新增并关联,如果存在关联,解除之后,与新数据建立关联
|
||||||
|
create(options?: CreateOptions): Promise<M>;
|
||||||
|
// 更新
|
||||||
|
update(options?): Promise<M>;
|
||||||
|
// 删除
|
||||||
|
destroy(): Promise<Boolean>;
|
||||||
|
// 建立关联
|
||||||
|
set(primaryKey: any): Promise<void>;
|
||||||
|
// 移除关联
|
||||||
|
remove(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HasOneRepository<M extends Model>
|
||||||
|
extends SingleRelationRepository
|
||||||
|
implements IHasOneRepository<M> {}
|
@ -0,0 +1,193 @@
|
|||||||
|
import { RelationRepository, transaction } from './relation-repository';
|
||||||
|
import { omit } from 'lodash';
|
||||||
|
import {
|
||||||
|
MultiAssociationAccessors,
|
||||||
|
Op,
|
||||||
|
Sequelize,
|
||||||
|
Transaction,
|
||||||
|
} from 'sequelize';
|
||||||
|
import { UpdateGuard } from '../update-guard';
|
||||||
|
import { updateModelByValues } from '../update-associations';
|
||||||
|
import {
|
||||||
|
CommonFindOptions,
|
||||||
|
CountOptions,
|
||||||
|
DestroyOptions,
|
||||||
|
Filter,
|
||||||
|
FilterByPK,
|
||||||
|
FindOptions,
|
||||||
|
PK,
|
||||||
|
PrimaryKey,
|
||||||
|
TransactionAble,
|
||||||
|
UpdateOptions,
|
||||||
|
} from '../repository';
|
||||||
|
|
||||||
|
export interface FindAndCountOptions extends CommonFindOptions {}
|
||||||
|
|
||||||
|
export interface FindOneOptions extends CommonFindOptions, FilterByPK {}
|
||||||
|
|
||||||
|
export interface AssociatedOptions extends TransactionAble {
|
||||||
|
pk?: PK;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class MultipleRelationRepository extends RelationRepository {
|
||||||
|
async find(options?: FindOptions): Promise<any> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const findOptions = this.buildQueryOptions({
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccessor = this.accessors().get;
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
if (findOptions.include && findOptions.include.length > 0) {
|
||||||
|
const ids = (
|
||||||
|
await sourceModel[getAccessor]({
|
||||||
|
...findOptions,
|
||||||
|
includeIgnoreAttributes: false,
|
||||||
|
attributes: [this.target.primaryKeyAttribute],
|
||||||
|
group: `${this.target.name}.${this.target.primaryKeyAttribute}`,
|
||||||
|
transaction,
|
||||||
|
})
|
||||||
|
).map((row) => row.get(this.target.primaryKeyAttribute));
|
||||||
|
|
||||||
|
return await sourceModel[getAccessor]({
|
||||||
|
...omit(findOptions, ['limit', 'offset']),
|
||||||
|
where: {
|
||||||
|
[this.target.primaryKeyAttribute]: {
|
||||||
|
[Op.in]: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sourceModel[getAccessor]({
|
||||||
|
...findOptions,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAndCount(options?: FindAndCountOptions): Promise<[any[], number]> {
|
||||||
|
const transaction = await this.getTransaction(options, false);
|
||||||
|
return [
|
||||||
|
await this.find({
|
||||||
|
...options,
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
await this.count({
|
||||||
|
...options,
|
||||||
|
transaction,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async count(options: CountOptions) {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
const queryOptions = this.buildQueryOptions(options);
|
||||||
|
|
||||||
|
const count = await sourceModel[this.accessors().get]({
|
||||||
|
where: queryOptions.where,
|
||||||
|
include: queryOptions.include,
|
||||||
|
includeIgnoreAttributes: false,
|
||||||
|
attributes: [
|
||||||
|
[
|
||||||
|
Sequelize.fn(
|
||||||
|
'COUNT',
|
||||||
|
Sequelize.fn(
|
||||||
|
'DISTINCT',
|
||||||
|
Sequelize.col(
|
||||||
|
`${this.target.name}.${this.target.primaryKeyAttribute}`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'count',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
raw: true,
|
||||||
|
plain: true,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return count.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(options?: FindOneOptions): Promise<any> {
|
||||||
|
const transaction = await this.getTransaction(options, false);
|
||||||
|
const rows = await this.find({ ...options, limit: 1, transaction });
|
||||||
|
return rows.length == 1 ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async remove(
|
||||||
|
options: PrimaryKey | PrimaryKey[] | AssociatedOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
let handleKeys = options['pk'];
|
||||||
|
|
||||||
|
if (!Array.isArray(handleKeys)) {
|
||||||
|
handleKeys = [handleKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
await sourceModel[this.accessors().removeMultiple](handleKeys, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async update(options?: UpdateOptions): Promise<any> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const guard = UpdateGuard.fromOptions(this.target, options);
|
||||||
|
|
||||||
|
const values = guard.sanitize(options.values);
|
||||||
|
|
||||||
|
const queryOptions = this.buildQueryOptions(options);
|
||||||
|
|
||||||
|
const instances = await this.find(queryOptions);
|
||||||
|
|
||||||
|
for (const instance of instances) {
|
||||||
|
await updateModelByValues(instance, values, {
|
||||||
|
sanitized: true,
|
||||||
|
sourceModel: this.sourceModel,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(options?: PK | DestroyOptions): Promise<Boolean> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async destroyByFilter(filter: Filter, transaction?: Transaction) {
|
||||||
|
const instances = await this.find({
|
||||||
|
filter: filter,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
return await this.destroy({
|
||||||
|
filterByPk: instances.map(
|
||||||
|
(instance) => instance[this.target.primaryKeyAttribute],
|
||||||
|
),
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected filterHasInclude(filter: Filter) {
|
||||||
|
const filterResult = this.parseFilter(filter);
|
||||||
|
return filterResult.include && filterResult.include.length > 0;
|
||||||
|
}
|
||||||
|
protected accessors() {
|
||||||
|
return <MultiAssociationAccessors>super.accessors();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
Association,
|
||||||
|
BelongsTo,
|
||||||
|
BelongsToMany,
|
||||||
|
HasMany,
|
||||||
|
HasOne,
|
||||||
|
Model,
|
||||||
|
ModelCtor,
|
||||||
|
Transaction,
|
||||||
|
} from 'sequelize';
|
||||||
|
import { OptionsParser } from '../options-parser';
|
||||||
|
import { Collection } from '../collection';
|
||||||
|
import { CreateOptions, Filter, FindOptions } from '../repository';
|
||||||
|
import FilterParser from '../filter-parser';
|
||||||
|
import { UpdateGuard } from '../update-guard';
|
||||||
|
import { updateAssociations } from '../update-associations';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { transactionWrapperBuilder } from '../transaction-decorator';
|
||||||
|
|
||||||
|
export const transaction = transactionWrapperBuilder(function () {
|
||||||
|
return this.source.model.sequelize.transaction();
|
||||||
|
});
|
||||||
|
|
||||||
|
export abstract class RelationRepository {
|
||||||
|
source: Collection;
|
||||||
|
association: Association;
|
||||||
|
target: ModelCtor<any>;
|
||||||
|
sourceId: string | number;
|
||||||
|
sourceModel: Model;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
source: Collection,
|
||||||
|
association: string,
|
||||||
|
sourceId: string | number,
|
||||||
|
) {
|
||||||
|
this.source = source;
|
||||||
|
this.sourceId = sourceId;
|
||||||
|
this.association = this.source.model.associations[association];
|
||||||
|
|
||||||
|
this.target = this.association.target;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected accessors() {
|
||||||
|
return (<BelongsTo | HasOne | HasMany | BelongsToMany>this.association)
|
||||||
|
.accessors;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(options?: CreateOptions): Promise<any> {
|
||||||
|
const createAccessor = this.accessors().create;
|
||||||
|
|
||||||
|
const guard = UpdateGuard.fromOptions(this.target, options);
|
||||||
|
const values = options.values;
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel();
|
||||||
|
|
||||||
|
const instance = await sourceModel[createAccessor](
|
||||||
|
guard.sanitize(options.values),
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateAssociations(instance, values, options);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSourceModel(transaction?: any) {
|
||||||
|
if (!this.sourceModel) {
|
||||||
|
this.sourceModel = await this.source.model.findByPk(this.sourceId, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sourceModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildQueryOptions(options: FindOptions) {
|
||||||
|
const parser = new OptionsParser(
|
||||||
|
this.target,
|
||||||
|
this.source.context.database,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
const params = parser.toSequelizeParams();
|
||||||
|
return { ...options, ...params };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseFilter(filter: Filter) {
|
||||||
|
const parser = new FilterParser(
|
||||||
|
this.target,
|
||||||
|
this.source.context.database,
|
||||||
|
filter,
|
||||||
|
);
|
||||||
|
return parser.toSequelizeParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getTransaction(
|
||||||
|
options: any,
|
||||||
|
autoGen = false,
|
||||||
|
): Promise<Transaction | null> {
|
||||||
|
if (lodash.isPlainObject(options) && options.transaction) {
|
||||||
|
return options.transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoGen) {
|
||||||
|
return await this.source.model.sequelize.transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export interface SingleRelationFindOption extends TransactionAble {
|
||||||
|
fields?: Fields;
|
||||||
|
except?: Except;
|
||||||
|
appends?: Appends;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SetOption extends TransactionAble {
|
||||||
|
pk?: PrimaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class SingleRelationRepository extends RelationRepository {
|
||||||
|
@transaction()
|
||||||
|
async remove(options?: TransactionAble): Promise<void> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
return await sourceModel[this.accessors().set](null, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
pk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async set(options: PrimaryKey | SetOption): Promise<void> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
let handleKey = lodash.isPlainObject(options)
|
||||||
|
? (<SetOption>options).pk
|
||||||
|
: options;
|
||||||
|
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
return await sourceModel[this.accessors().set](handleKey, {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(options?: SingleRelationFindOption): Promise<Model<any>> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
const findOptions = this.buildQueryOptions({
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAccessor = this.accessors().get;
|
||||||
|
const sourceModel = await this.getSourceModel(transaction);
|
||||||
|
|
||||||
|
return await sourceModel[getAccessor]({
|
||||||
|
...findOptions,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async destroy(options?: TransactionAble): Promise<Boolean> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const target = await this.find({
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await target.destroy({
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction()
|
||||||
|
async update(options: UpdateOptions): Promise<any> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const target = await this.find({
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateModelByValues(target, options?.values, {
|
||||||
|
...lodash.omit(options, 'values'),
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
accessors() {
|
||||||
|
return <SingleAssociationAccessors>super.accessors();
|
||||||
|
}
|
||||||
|
}
|
19
packages/database-next/src/relation-repository/types.ts
Normal file
19
packages/database-next/src/relation-repository/types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { PrimaryKey, Values } from '../repository';
|
||||||
|
import { Transactionable } from 'sequelize';
|
||||||
|
|
||||||
|
export type PrimaryKeyWithThroughValues = [PrimaryKey, Values];
|
||||||
|
|
||||||
|
export interface AssociatedOptions extends Transactionable {
|
||||||
|
pk?:
|
||||||
|
| PrimaryKey
|
||||||
|
| PrimaryKey[]
|
||||||
|
| PrimaryKeyWithThroughValues
|
||||||
|
| PrimaryKeyWithThroughValues[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type setAssociationOptions =
|
||||||
|
| PrimaryKey
|
||||||
|
| PrimaryKey[]
|
||||||
|
| PrimaryKeyWithThroughValues
|
||||||
|
| PrimaryKeyWithThroughValues[]
|
||||||
|
| AssociatedOptions;
|
@ -1,57 +1,123 @@
|
|||||||
import {
|
import {
|
||||||
Op,
|
Association,
|
||||||
|
BulkCreateOptions,
|
||||||
|
CreateOptions as SequelizeCreateOptions,
|
||||||
|
FindAndCountOptions as SequelizeAndCountOptions,
|
||||||
|
FindOptions as SequelizeFindOptions,
|
||||||
Model,
|
Model,
|
||||||
ModelCtor,
|
ModelCtor,
|
||||||
Association,
|
Op,
|
||||||
FindOptions,
|
Transaction,
|
||||||
BulkCreateOptions,
|
|
||||||
DestroyOptions as SequelizeDestroyOptions,
|
|
||||||
CreateOptions as SequelizeCreateOptions,
|
|
||||||
UpdateOptions as SequelizeUpdateOptions,
|
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { flatten } from 'flat';
|
|
||||||
import { Collection } from './collection';
|
import { Collection } from './collection';
|
||||||
import _ from 'lodash';
|
import lodash, { omit } from 'lodash';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
import { updateAssociations } from './update-associations';
|
import { updateAssociations, updateModelByValues } from './update-associations';
|
||||||
import { RelationField } from './fields';
|
import { RelationField } from './fields';
|
||||||
|
import FilterParser from './filter-parser';
|
||||||
|
import { OptionsParser } from './options-parser';
|
||||||
|
import { RelationRepository } from './relation-repository/relation-repository';
|
||||||
|
import { HasOneRepository } from './relation-repository/hasone-repository';
|
||||||
|
import { BelongsToRepository } from './relation-repository/belongs-to-repository';
|
||||||
|
import { BelongsToManyRepository } from './relation-repository/belongs-to-many-repository';
|
||||||
|
import { HasManyRepository } from './relation-repository/hasmany-repository';
|
||||||
|
import { UpdateGuard } from './update-guard';
|
||||||
|
import { transactionWrapperBuilder } from './transaction-decorator';
|
||||||
|
|
||||||
|
const debug = require('debug')('noco-database');
|
||||||
|
|
||||||
export interface IRepository {}
|
export interface IRepository {}
|
||||||
|
|
||||||
interface CreateManyOptions extends BulkCreateOptions {}
|
interface CreateManyOptions extends BulkCreateOptions {
|
||||||
|
records: Values[];
|
||||||
interface FindManyOptions extends FindOptions {
|
|
||||||
filter?: any;
|
|
||||||
fields?: any;
|
|
||||||
appends?: any;
|
|
||||||
expect?: any;
|
|
||||||
page?: any;
|
|
||||||
pageSize?: any;
|
|
||||||
sort?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FindOneOptions extends FindOptions {
|
export interface TransactionAble {
|
||||||
filter?: any;
|
transaction?: Transaction;
|
||||||
fields?: any;
|
|
||||||
appends?: any;
|
|
||||||
expect?: any;
|
|
||||||
sort?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateOptions extends SequelizeCreateOptions {
|
export interface FilterAble {
|
||||||
values?: any;
|
filter: Filter;
|
||||||
whitelist?: any;
|
|
||||||
blacklist?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateOptions extends SequelizeUpdateOptions {
|
export type PrimaryKey = string | number;
|
||||||
values?: any;
|
export type PK = PrimaryKey | PrimaryKey[];
|
||||||
whitelist?: any;
|
|
||||||
blacklist?: any;
|
export type Filter = any;
|
||||||
|
export type Appends = string[];
|
||||||
|
export type Except = string[];
|
||||||
|
export type Fields = string[];
|
||||||
|
export type Sort = string[];
|
||||||
|
|
||||||
|
export type WhiteList = string[];
|
||||||
|
export type BlackList = string[];
|
||||||
|
export type AssociationKeysToBeUpdate = string[];
|
||||||
|
|
||||||
|
export type Values = {
|
||||||
|
[key: string]: string | number | Values | Array<number | string | Values>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CountOptions
|
||||||
|
extends Omit<SequelizeCreateOptions, 'distinct' | 'where' | 'include'>,
|
||||||
|
TransactionAble {
|
||||||
|
fields?: Fields;
|
||||||
|
filter?: Filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DestroyOptions extends SequelizeDestroyOptions {
|
export interface FilterByPK {
|
||||||
filter?: any;
|
filterByPk?: PrimaryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindOptions
|
||||||
|
extends SequelizeFindOptions,
|
||||||
|
CommonFindOptions,
|
||||||
|
FilterByPK {}
|
||||||
|
|
||||||
|
export interface CommonFindOptions {
|
||||||
|
filter?: Filter;
|
||||||
|
fields?: Fields;
|
||||||
|
appends?: Appends;
|
||||||
|
except?: Except;
|
||||||
|
sort?: Sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FindOneOptions extends FindOptions, CommonFindOptions {}
|
||||||
|
|
||||||
|
export interface DestroyOptions extends TransactionAble {
|
||||||
|
filter?: Filter;
|
||||||
|
filterByPk?: PrimaryKey | PrimaryKey[];
|
||||||
|
truncate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FindAndCountOptions
|
||||||
|
extends Omit<SequelizeAndCountOptions, 'where' | 'include' | 'order'> {
|
||||||
|
// 数据过滤
|
||||||
|
filter?: Filter;
|
||||||
|
// 输出结果显示哪些字段
|
||||||
|
fields?: Fields;
|
||||||
|
// 输出结果不显示哪些字段
|
||||||
|
except?: Except;
|
||||||
|
// 附加字段,用于控制关系字段的输出
|
||||||
|
appends?: Appends;
|
||||||
|
// 排序,字段前面加上 “-” 表示降序
|
||||||
|
sort?: Sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateOptions extends TransactionAble {
|
||||||
|
values?: Values;
|
||||||
|
whitelist?: WhiteList;
|
||||||
|
blacklist?: BlackList;
|
||||||
|
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateOptions extends TransactionAble {
|
||||||
|
values: Values;
|
||||||
|
filter?: Filter;
|
||||||
|
filterByPk?: PrimaryKey;
|
||||||
|
whitelist?: WhiteList;
|
||||||
|
blacklist?: BlackList;
|
||||||
|
updateAssociationValues?: AssociationKeysToBeUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RelatedQueryOptions {
|
interface RelatedQueryOptions {
|
||||||
@ -69,101 +135,43 @@ interface RelatedQueryOptions {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Identity = string | number;
|
const transaction = transactionWrapperBuilder(function () {
|
||||||
|
return (<Repository>this).collection.model.sequelize.transaction();
|
||||||
|
});
|
||||||
|
|
||||||
class RelatedQuery {
|
class RelationRepositoryBuilder<R extends RelationRepository> {
|
||||||
options: RelatedQueryOptions;
|
collection: Collection;
|
||||||
sourceInstance: Model;
|
associationName: string;
|
||||||
|
association: Association;
|
||||||
|
|
||||||
constructor(options: RelatedQueryOptions) {
|
builderMap = {
|
||||||
this.options = options;
|
HasOne: HasOneRepository,
|
||||||
|
BelongsTo: BelongsToRepository,
|
||||||
|
BelongsToMany: BelongsToManyRepository,
|
||||||
|
HasMany: HasManyRepository,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(collection: Collection, associationName: string) {
|
||||||
|
this.collection = collection;
|
||||||
|
this.associationName = associationName;
|
||||||
|
this.association = this.collection.model.associations[this.associationName];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSourceInstance() {
|
protected builder() {
|
||||||
if (this.sourceInstance) {
|
return this.builderMap;
|
||||||
return this.sourceInstance;
|
|
||||||
}
|
|
||||||
const { idOrInstance, collection } = this.options.source;
|
|
||||||
if (idOrInstance instanceof Model) {
|
|
||||||
return (this.sourceInstance = idOrInstance);
|
|
||||||
}
|
|
||||||
this.sourceInstance = await collection.model.findByPk(idOrInstance);
|
|
||||||
return this.sourceInstance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(options?: any) {
|
of(id: string | number): R {
|
||||||
const { collection } = this.options.target;
|
const klass = this.builder()[this.association.associationType];
|
||||||
return await collection.repository.findMany(options);
|
return new klass(this.collection, this.associationName, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(options?: any) {
|
|
||||||
const { collection } = this.options.target;
|
|
||||||
return await collection.repository.findOne(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(values?: any, options?: any) {
|
|
||||||
const { association } = this.options.target;
|
|
||||||
const createAccessor = association.accessors.create;
|
|
||||||
const source = await this.getSourceInstance();
|
|
||||||
const instance = await source[createAccessor](values, options);
|
|
||||||
if (!instance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await updateAssociations(instance, values);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(values: any, options?: Identity | Model | UpdateOptions) {
|
|
||||||
const { association, collection } = this.options.target;
|
|
||||||
if (options instanceof Model) {
|
|
||||||
return await collection.repository.update(values, options);
|
|
||||||
}
|
|
||||||
const { field } = this.options;
|
|
||||||
if (field.type === 'hasOne' || field.type === 'belongsTo') {
|
|
||||||
const getAccessor = association.accessors.get;
|
|
||||||
const source = await this.getSourceInstance();
|
|
||||||
const instance = await source[getAccessor]();
|
|
||||||
return await collection.repository.update(values, instance);
|
|
||||||
}
|
|
||||||
// TODO
|
|
||||||
return await collection.repository.update(values, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy(options?: any) {
|
|
||||||
const { association, collection } = this.options.target;
|
|
||||||
const { field } = this.options;
|
|
||||||
if (field.type === 'hasOne' || field.type === 'belongsTo') {
|
|
||||||
const getAccessor = association.accessors.get;
|
|
||||||
const source = await this.getSourceInstance();
|
|
||||||
const instance = await source[getAccessor]();
|
|
||||||
if (!instance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return await collection.repository.destroy(instance.id);
|
|
||||||
}
|
|
||||||
return await collection.repository.destroy(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async set(options?: any) {}
|
|
||||||
|
|
||||||
async add(options?: any) {}
|
|
||||||
|
|
||||||
async remove(options?: any) {}
|
|
||||||
|
|
||||||
async toggle(options?: any) {}
|
|
||||||
|
|
||||||
async sync(options?: any) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HasOneQuery extends RelatedQuery {}
|
export class Repository<
|
||||||
|
TModelAttributes extends {} = any,
|
||||||
class HasManyQuery extends RelatedQuery {}
|
TCreationAttributes extends {} = TModelAttributes,
|
||||||
|
> implements IRepository
|
||||||
class BelongsToQuery extends RelatedQuery {}
|
{
|
||||||
|
|
||||||
class BelongsToManyQuery extends RelatedQuery {}
|
|
||||||
|
|
||||||
export class Repository implements IRepository {
|
|
||||||
database: Database;
|
database: Database;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
model: ModelCtor<Model>;
|
model: ModelCtor<Model>;
|
||||||
@ -174,280 +182,274 @@ export class Repository implements IRepository {
|
|||||||
this.model = collection.model;
|
this.model = collection.model;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findMany(options?: FindManyOptions) {
|
/**
|
||||||
|
* return count by filter
|
||||||
|
*/
|
||||||
|
async count(countOptions?: CountOptions): Promise<number> {
|
||||||
|
let options = countOptions ? lodash.clone(countOptions) : {};
|
||||||
|
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
if (countOptions?.filter) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
...this.parseFilter(countOptions.filter),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = await this.collection.model.count({
|
||||||
|
...options,
|
||||||
|
distinct: true,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
return count as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* find
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async find(options?: FindOptions) {
|
||||||
const model = this.collection.model;
|
const model = this.collection.model;
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
const opts = {
|
const opts = {
|
||||||
subQuery: false,
|
subQuery: false,
|
||||||
...this.buildQueryOptions(options),
|
...this.buildQueryOptions(options),
|
||||||
};
|
};
|
||||||
let rows = [];
|
|
||||||
if (opts.include) {
|
if (opts.include && opts.include.length > 0) {
|
||||||
const ids = (
|
const ids = (
|
||||||
await model.findAll({
|
await model.findAll({
|
||||||
...opts,
|
...opts,
|
||||||
includeIgnoreAttributes: false,
|
includeIgnoreAttributes: false,
|
||||||
attributes: [model.primaryKeyAttribute],
|
attributes: [model.primaryKeyAttribute],
|
||||||
group: `${model.name}.${model.primaryKeyAttribute}`,
|
group: `${model.name}.${model.primaryKeyAttribute}`,
|
||||||
|
transaction,
|
||||||
})
|
})
|
||||||
).map((item) => item[model.primaryKeyAttribute]);
|
).map((row) => row.get(model.primaryKeyAttribute));
|
||||||
if (ids.length > 0) {
|
|
||||||
rows = await model.findAll({
|
const where = {
|
||||||
...opts,
|
[model.primaryKeyAttribute]: {
|
||||||
where: {
|
[Op.in]: ids,
|
||||||
[model.primaryKeyAttribute]: {
|
},
|
||||||
[Op.in]: ids,
|
};
|
||||||
},
|
|
||||||
},
|
return await model.findAll({
|
||||||
});
|
...omit(opts, ['limit', 'offset']),
|
||||||
}
|
where,
|
||||||
} else {
|
transaction,
|
||||||
rows = await model.findAll({
|
|
||||||
...opts,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const count = await model.count({
|
|
||||||
|
return await model.findAll({
|
||||||
...opts,
|
...opts,
|
||||||
distinct: opts.include ? true : undefined,
|
transaction,
|
||||||
});
|
});
|
||||||
return { count, rows };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(options?: FindOneOptions) {
|
/**
|
||||||
const model = this.collection.model;
|
* find and count
|
||||||
const opts = {
|
* @param options
|
||||||
subQuery: false,
|
*/
|
||||||
...this.buildQueryOptions(options),
|
async findAndCount(
|
||||||
|
options?: FindAndCountOptions,
|
||||||
|
): Promise<[Model[], number]> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
transaction,
|
||||||
};
|
};
|
||||||
let data: Model;
|
|
||||||
if (opts.include) {
|
return [await this.find(options), await this.count(options)];
|
||||||
const item = await model.findOne({
|
|
||||||
...opts,
|
|
||||||
includeIgnoreAttributes: false,
|
|
||||||
attributes: [model.primaryKeyAttribute],
|
|
||||||
group: `${model.name}.${model.primaryKeyAttribute}`,
|
|
||||||
});
|
|
||||||
if (!item) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
data = await model.findOne({
|
|
||||||
...opts,
|
|
||||||
where: item.toJSON(),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
data = await model.findOne({
|
|
||||||
...opts,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(values?: any, options?: CreateOptions) {
|
/**
|
||||||
const instance = await this.model.create<any>(values, options);
|
* Find By Id
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
findById(id: PrimaryKey) {
|
||||||
|
return this.collection.model.findByPk(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find one record from database
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async findOne(options?: FindOneOptions) {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const rows = await this.find({ ...options, limit: 1, transaction });
|
||||||
|
return rows.length == 1 ? rows[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save instance to database
|
||||||
|
*
|
||||||
|
* @param values
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
@transaction()
|
||||||
|
async create(options: CreateOptions): Promise<Model> {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
const guard = UpdateGuard.fromOptions(this.model, options);
|
||||||
|
const values = guard.sanitize(options.values || {});
|
||||||
|
|
||||||
|
const instance = await this.model.create<any>(values, { transaction });
|
||||||
|
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await updateAssociations(instance, values, options);
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createMany(records: any[], options?: CreateManyOptions) {
|
await updateAssociations(instance, values, {
|
||||||
const instances = await this.collection.model.bulkCreate(records, options);
|
transaction,
|
||||||
const promises = instances.map((instance, index) => {
|
|
||||||
return updateAssociations(instance, records[index]);
|
|
||||||
});
|
});
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(values: any, options: Identity | Model | UpdateOptions) {
|
|
||||||
if (options instanceof Model) {
|
|
||||||
await options.update(values);
|
|
||||||
await updateAssociations(options, values);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
let instance: Model;
|
|
||||||
if (typeof options === 'string' || typeof options === 'number') {
|
|
||||||
instance = await this.model.findByPk(options);
|
|
||||||
} else {
|
|
||||||
// TODO
|
|
||||||
instance = await this.findOne(options);
|
|
||||||
}
|
|
||||||
await instance.update(values);
|
|
||||||
await updateAssociations(instance, values);
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(options: Identity | Identity[] | DestroyOptions) {
|
/**
|
||||||
if (typeof options === 'number' || typeof options === 'string') {
|
* Save Many instances to database
|
||||||
return await this.model.destroy({
|
*
|
||||||
where: {
|
* @param records
|
||||||
[this.model.primaryKeyAttribute]: options,
|
* @param options
|
||||||
},
|
*/
|
||||||
|
@transaction()
|
||||||
|
async createMany(options: CreateManyOptions) {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
const { records } = options;
|
||||||
|
const instances = await this.collection.model.bulkCreate(records, {
|
||||||
|
...options,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < instances.length; i++) {
|
||||||
|
await updateAssociations(instances[i], records[i], { transaction });
|
||||||
|
}
|
||||||
|
|
||||||
|
return instances;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update model value
|
||||||
|
*
|
||||||
|
* @param values
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
@transaction()
|
||||||
|
async update(options: UpdateOptions) {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
const guard = UpdateGuard.fromOptions(this.model, options);
|
||||||
|
|
||||||
|
const values = guard.sanitize(options.values);
|
||||||
|
|
||||||
|
const queryOptions = this.buildQueryOptions(options);
|
||||||
|
|
||||||
|
const instances = await this.find({
|
||||||
|
...queryOptions,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const instance of instances) {
|
||||||
|
await updateModelByValues(instance, values, {
|
||||||
|
sanitized: true,
|
||||||
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (Array.isArray(options)) {
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@transaction((args, transaction) => {
|
||||||
|
return {
|
||||||
|
filterByPk: args[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
async destroy(options?: PrimaryKey | PrimaryKey[] | DestroyOptions) {
|
||||||
|
const transaction = await this.getTransaction(options);
|
||||||
|
|
||||||
|
options = <DestroyOptions>options;
|
||||||
|
|
||||||
|
let filterByPk = options.filterByPk;
|
||||||
|
|
||||||
|
if (filterByPk) {
|
||||||
|
if (!Array.isArray(filterByPk)) {
|
||||||
|
filterByPk = [filterByPk];
|
||||||
|
}
|
||||||
|
|
||||||
return await this.model.destroy({
|
return await this.model.destroy({
|
||||||
where: {
|
where: {
|
||||||
[this.model.primaryKeyAttribute]: {
|
[this.model.primaryKeyAttribute]: {
|
||||||
[Op.in]: options,
|
[Op.in]: filterByPk,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const opts = this.buildQueryOptions(options);
|
|
||||||
return await this.model.destroy(opts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
if (options.filter) {
|
||||||
async sort() {}
|
const instances = await this.find({
|
||||||
|
filter: options.filter,
|
||||||
relatedQuery(name: string) {
|
transaction,
|
||||||
return {
|
|
||||||
for: (sourceIdOrInstance: any) => {
|
|
||||||
const field = this.collection.getField(name) as RelationField;
|
|
||||||
const database = this.collection.context.database;
|
|
||||||
const collection = database.getCollection(field.target);
|
|
||||||
const options: RelatedQueryOptions = {
|
|
||||||
field,
|
|
||||||
database: database,
|
|
||||||
source: {
|
|
||||||
collection: this.collection,
|
|
||||||
idOrInstance: sourceIdOrInstance,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
collection,
|
|
||||||
association: this.collection.model.associations[name] as any,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
switch (field.type) {
|
|
||||||
case 'hasOne':
|
|
||||||
return new HasOneQuery(options);
|
|
||||||
case 'hasMany':
|
|
||||||
return new HasManyQuery(options);
|
|
||||||
case 'belongsTo':
|
|
||||||
return new BelongsToQuery(options);
|
|
||||||
case 'belongsToMany':
|
|
||||||
return new BelongsToManyQuery(options);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
buildQueryOptions(options: any) {
|
|
||||||
const opts = this.parseFilter(options.filter);
|
|
||||||
return { ...options, ...opts };
|
|
||||||
}
|
|
||||||
|
|
||||||
parseFilter(filter?: any) {
|
|
||||||
if (!filter) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const model = this.collection.model;
|
|
||||||
if (typeof filter === 'number' || typeof filter === 'string') {
|
|
||||||
return {
|
|
||||||
where: {
|
|
||||||
[model.primaryKeyAttribute]: filter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const operators = this.database.operators;
|
|
||||||
const obj = flatten(filter || {});
|
|
||||||
const include = {};
|
|
||||||
const where = {};
|
|
||||||
let skipPrefix = null;
|
|
||||||
const filter2 = {};
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
_.set(filter2, key, value);
|
|
||||||
}
|
|
||||||
for (let [key, value] of Object.entries(obj)) {
|
|
||||||
if (skipPrefix && key.startsWith(skipPrefix)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let keys = key.split('.');
|
|
||||||
const associations = model.associations;
|
|
||||||
const paths = [];
|
|
||||||
const origins = [];
|
|
||||||
while (keys.length) {
|
|
||||||
const k = keys.shift();
|
|
||||||
origins.push(k);
|
|
||||||
if (k.startsWith('$')) {
|
|
||||||
if (operators.has(k)) {
|
|
||||||
const opKey = operators.get(k);
|
|
||||||
if (typeof opKey === 'symbol') {
|
|
||||||
paths.push(opKey);
|
|
||||||
continue;
|
|
||||||
} else if (typeof opKey === 'function') {
|
|
||||||
skipPrefix = origins.join('.');
|
|
||||||
// console.log({ skipPrefix }, filter2, _.get(filter2, origins));
|
|
||||||
value = opKey(_.get(filter2, origins));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
paths.push(k);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (/\d+/.test(k)) {
|
|
||||||
paths.push(k);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!associations[k]) {
|
|
||||||
paths.push(k);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const associationKeys = [];
|
|
||||||
associationKeys.push(k);
|
|
||||||
_.set(include, k, {
|
|
||||||
association: k,
|
|
||||||
attributes: [],
|
|
||||||
});
|
|
||||||
let target = associations[k].target;
|
|
||||||
while (target) {
|
|
||||||
const attr = keys.shift();
|
|
||||||
if (target.rawAttributes[attr]) {
|
|
||||||
associationKeys.push(attr);
|
|
||||||
target = null;
|
|
||||||
} else if (target.associations[attr]) {
|
|
||||||
associationKeys.push(attr);
|
|
||||||
const assoc = [];
|
|
||||||
associationKeys.forEach((associationKey, index) => {
|
|
||||||
if (index > 0) {
|
|
||||||
assoc.push('include');
|
|
||||||
}
|
|
||||||
assoc.push(associationKey);
|
|
||||||
});
|
|
||||||
_.set(include, assoc, {
|
|
||||||
association: attr,
|
|
||||||
attributes: [],
|
|
||||||
});
|
|
||||||
target = target.associations[attr].target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (associationKeys.length > 1) {
|
|
||||||
paths.push(`$${associationKeys.join('.')}$`);
|
|
||||||
} else {
|
|
||||||
paths.push(k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log(paths, value);
|
|
||||||
const values = _.get(where, paths);
|
|
||||||
if (
|
|
||||||
values &&
|
|
||||||
typeof values === 'object' &&
|
|
||||||
value &&
|
|
||||||
typeof value === 'object'
|
|
||||||
) {
|
|
||||||
value = { ...value, ...values };
|
|
||||||
}
|
|
||||||
_.set(where, paths, value);
|
|
||||||
}
|
|
||||||
const toInclude = (items) => {
|
|
||||||
return Object.values(items).map((item: any) => {
|
|
||||||
if (item.include) {
|
|
||||||
item.include = toInclude(item.include);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
return { where, include: toInclude(include) };
|
return await this.destroy({
|
||||||
|
filterByPk: instances.map(
|
||||||
|
(instance) => instance[this.model.primaryKeyAttribute],
|
||||||
|
),
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.truncate) {
|
||||||
|
return await this.model.destroy({
|
||||||
|
truncate: true,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param association target association
|
||||||
|
*/
|
||||||
|
relation<R extends RelationRepository>(
|
||||||
|
association: string,
|
||||||
|
): RelationRepositoryBuilder<R> {
|
||||||
|
return new RelationRepositoryBuilder<R>(this.collection, association);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected buildQueryOptions(options: any) {
|
||||||
|
const parser = new OptionsParser(
|
||||||
|
this.collection.model,
|
||||||
|
this.collection.context.database,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
return parser.toSequelizeParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getTransaction(options: any, autoGen = false) {
|
||||||
|
if (lodash.isPlainObject(options) && options.transaction) {
|
||||||
|
return options.transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoGen) {
|
||||||
|
return await this.model.sequelize.transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
62
packages/database-next/src/transaction-decorator.ts
Normal file
62
packages/database-next/src/transaction-decorator.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
export function transactionWrapperBuilder(transactionGenerator) {
|
||||||
|
return function transaction(transactionInjector?) {
|
||||||
|
return (target, name, descriptor) => {
|
||||||
|
const oldValue = descriptor.value;
|
||||||
|
|
||||||
|
descriptor.value = async function () {
|
||||||
|
let transaction;
|
||||||
|
let newTransaction = false;
|
||||||
|
|
||||||
|
if (arguments.length > 0 && typeof arguments[0] === 'object') {
|
||||||
|
transaction = arguments[0]['transaction'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!transaction) {
|
||||||
|
transaction = await transactionGenerator.apply(this);
|
||||||
|
newTransaction = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 需要将 newTransaction 注入到被装饰函数参数内
|
||||||
|
if (newTransaction) {
|
||||||
|
try {
|
||||||
|
let callArguments;
|
||||||
|
if (lodash.isPlainObject(arguments[0])) {
|
||||||
|
callArguments = {
|
||||||
|
...arguments[0],
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
} else if (transactionInjector) {
|
||||||
|
callArguments = transactionInjector(arguments, transaction);
|
||||||
|
} else if (
|
||||||
|
lodash.isNull(arguments[0]) ||
|
||||||
|
lodash.isUndefined(arguments[0])
|
||||||
|
) {
|
||||||
|
callArguments = {
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`please provide transactionInjector for ${name} call`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await oldValue.apply(this, [callArguments]);
|
||||||
|
|
||||||
|
await transaction.commit();
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
await transaction.rollback();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return oldValue.apply(this, arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return descriptor;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
@ -5,7 +5,13 @@ import {
|
|||||||
DataTypes,
|
DataTypes,
|
||||||
Utils,
|
Utils,
|
||||||
Association,
|
Association,
|
||||||
|
HasOne,
|
||||||
|
BelongsTo,
|
||||||
|
BelongsToMany,
|
||||||
|
HasMany,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
|
import { UpdateGuard } from './update-guard';
|
||||||
|
import { TransactionAble } from './repository';
|
||||||
|
|
||||||
function isUndefinedOrNull(value: any) {
|
function isUndefinedOrNull(value: any) {
|
||||||
return typeof value === 'undefined' || value === null;
|
return typeof value === 'undefined' || value === null;
|
||||||
@ -15,39 +21,171 @@ function isStringOrNumber(value: any) {
|
|||||||
return typeof value === 'string' || typeof value === 'number';
|
return typeof value === 'string' || typeof value === 'number';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function modelAssociations(instance: Model) {
|
||||||
|
return (<typeof Model>instance.constructor).associations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function belongsToManyAssociations(
|
||||||
|
instance: Model,
|
||||||
|
): Array<BelongsToMany> {
|
||||||
|
const associations = modelAssociations(instance);
|
||||||
|
return Object.entries(associations)
|
||||||
|
.filter((entry) => {
|
||||||
|
const [key, association] = entry;
|
||||||
|
return association.associationType == 'BelongsToMany';
|
||||||
|
})
|
||||||
|
.map((association) => {
|
||||||
|
return <BelongsToMany>association[1];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function modelAssociationByKey(
|
||||||
|
instance: Model,
|
||||||
|
key: string,
|
||||||
|
): Association {
|
||||||
|
return modelAssociations(instance)[key] as Association;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateValue = { [key: string]: any };
|
||||||
|
|
||||||
|
interface UpdateOptions extends TransactionAble {
|
||||||
|
filter?: any;
|
||||||
|
filterByPk?: number | string;
|
||||||
|
// 字段白名单
|
||||||
|
whitelist?: string[];
|
||||||
|
// 字段黑名单
|
||||||
|
blacklist?: string[];
|
||||||
|
// 关系数据默认会新建并建立关联处理,如果是已存在的数据只关联,但不更新关系数据
|
||||||
|
// 如果需要更新关联数据,可以通过 updateAssociationValues 指定
|
||||||
|
updateAssociationValues?: string[];
|
||||||
|
sanitized?: boolean;
|
||||||
|
sourceModel?: Model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateModelByValues(
|
||||||
|
instance: Model,
|
||||||
|
values: UpdateValue,
|
||||||
|
options?: UpdateOptions,
|
||||||
|
) {
|
||||||
|
if (!options?.sanitized) {
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
//@ts-ignore
|
||||||
|
guard.setModel(instance.constructor);
|
||||||
|
guard.setBlackList(options.blacklist);
|
||||||
|
guard.setWhiteList(options.whitelist);
|
||||||
|
guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
|
||||||
|
values = guard.sanitize(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
await instance.update(values);
|
||||||
|
await updateAssociations(instance, values, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateThroughTableValue(
|
||||||
|
instance: Model,
|
||||||
|
throughName: string,
|
||||||
|
throughValues: any,
|
||||||
|
source: Model,
|
||||||
|
transaction = null,
|
||||||
|
) {
|
||||||
|
// update through table values
|
||||||
|
for (const belongsToMany of belongsToManyAssociations(instance)) {
|
||||||
|
// @ts-ignore
|
||||||
|
const throughModel = belongsToMany.through.model;
|
||||||
|
const throughModelName = throughModel.name;
|
||||||
|
|
||||||
|
if (throughModelName === throughModelName) {
|
||||||
|
const where = {
|
||||||
|
[belongsToMany.foreignKey]: instance.get(belongsToMany.sourceKey),
|
||||||
|
[belongsToMany.otherKey]: source.get(belongsToMany.targetKey),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await throughModel.update(throughValues, {
|
||||||
|
where,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update association of instance by values
|
||||||
|
* @param instance
|
||||||
|
* @param values
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
export async function updateAssociations(
|
export async function updateAssociations(
|
||||||
instance: Model,
|
instance: Model,
|
||||||
values: any,
|
values: any,
|
||||||
options: any = {},
|
options: any = {},
|
||||||
) {
|
) {
|
||||||
const { transaction = await instance.sequelize.transaction() } = options;
|
// if no values set, return
|
||||||
// @ts-ignore
|
if (!values) {
|
||||||
for (const key of Object.keys(instance.constructor.associations)) {
|
return;
|
||||||
// 如果 key 不存在才跳过
|
|
||||||
if (!Object.keys(values||{}).includes(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await updateAssociation(instance, key, values[key], {
|
|
||||||
...options,
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!options.transaction) {
|
|
||||||
|
let newTransaction = false;
|
||||||
|
let transaction = options.transaction;
|
||||||
|
|
||||||
|
if (!transaction) {
|
||||||
|
newTransaction = true;
|
||||||
|
transaction = await instance.sequelize.transaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(modelAssociations(instance))) {
|
||||||
|
if (values[key]) {
|
||||||
|
await updateAssociation(instance, key, values[key], {
|
||||||
|
...options,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update through table values
|
||||||
|
for (const belongsToMany of belongsToManyAssociations(instance)) {
|
||||||
|
// @ts-ignore
|
||||||
|
const throughModel = belongsToMany.through.model;
|
||||||
|
const throughModelName = throughModel.name;
|
||||||
|
|
||||||
|
if (values[throughModelName] && options.sourceModel) {
|
||||||
|
const where = {
|
||||||
|
[belongsToMany.foreignKey]: instance.get(belongsToMany.sourceKey),
|
||||||
|
[belongsToMany.otherKey]: options.sourceModel.get(
|
||||||
|
belongsToMany.targetKey,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
await throughModel.update(values[throughModel.name], {
|
||||||
|
where,
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTransaction) {
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update model association by key
|
||||||
|
* @param instance
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
export async function updateAssociation(
|
export async function updateAssociation(
|
||||||
instance: Model,
|
instance: Model,
|
||||||
key: string,
|
key: string,
|
||||||
value: any,
|
value: any,
|
||||||
options: any = {},
|
options: any = {},
|
||||||
) {
|
) {
|
||||||
// @ts-ignore
|
const association = modelAssociationByKey(instance, key);
|
||||||
const association = instance.constructor.associations[key] as Association;
|
|
||||||
if (!association) {
|
if (!association) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (association.associationType) {
|
switch (association.associationType) {
|
||||||
case 'HasOne':
|
case 'HasOne':
|
||||||
case 'BelongsTo':
|
case 'BelongsTo':
|
||||||
@ -58,32 +196,48 @@ export async function updateAssociation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update belongsTo and HasOne
|
||||||
|
* @param model
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
export async function updateSingleAssociation(
|
export async function updateSingleAssociation(
|
||||||
model: Model,
|
model: Model,
|
||||||
key: string,
|
key: string,
|
||||||
value: any,
|
value: any,
|
||||||
options: any = {},
|
options: any = {},
|
||||||
) {
|
) {
|
||||||
// @ts-ignore
|
const association = <HasOne | BelongsTo>modelAssociationByKey(model, key);
|
||||||
const association = model.constructor.associations[key] as Association;
|
|
||||||
if (!association) {
|
if (!association) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { transaction = await model.sequelize.transaction() } = options;
|
const { transaction = await model.sequelize.transaction() } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// set method of association
|
||||||
const setAccessor = association.accessors.set;
|
const setAccessor = association.accessors.set;
|
||||||
if (isUndefinedOrNull(value)) {
|
|
||||||
|
const removeAssociation = async () => {
|
||||||
await model[setAccessor](null, { transaction });
|
await model[setAccessor](null, { transaction });
|
||||||
model.setDataValue(key, null);
|
model.setDataValue(key, null);
|
||||||
if (!options.transaction) {
|
if (!options.transaction) {
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(value)) {
|
||||||
|
return await removeAssociation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStringOrNumber(value)) {
|
if (isStringOrNumber(value)) {
|
||||||
await model[setAccessor](value, { transaction });
|
await model[setAccessor](value, { transaction });
|
||||||
if (!options.transaction) {
|
if (!options.transaction) {
|
||||||
@ -99,7 +253,7 @@ export async function updateSingleAssociation(
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
|
||||||
const createAccessor = association.accessors.create;
|
const createAccessor = association.accessors.create;
|
||||||
let dataKey: string;
|
let dataKey: string;
|
||||||
let M: ModelCtor<Model>;
|
let M: ModelCtor<Model>;
|
||||||
@ -111,6 +265,7 @@ export async function updateSingleAssociation(
|
|||||||
M = association.source;
|
M = association.source;
|
||||||
dataKey = M.primaryKeyAttribute;
|
dataKey = M.primaryKeyAttribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStringOrNumber(value[dataKey])) {
|
if (isStringOrNumber(value[dataKey])) {
|
||||||
let instance: any = await M.findOne({
|
let instance: any = await M.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -128,6 +283,11 @@ export async function updateSingleAssociation(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (value[dataKey] === null) {
|
||||||
|
return await removeAssociation();
|
||||||
|
}
|
||||||
|
|
||||||
const instance = await model[createAccessor](value, { transaction });
|
const instance = await model[createAccessor](value, { transaction });
|
||||||
await updateAssociations(instance, value, { transaction, ...options });
|
await updateAssociations(instance, value, { transaction, ...options });
|
||||||
model.setDataValue(key, instance);
|
model.setDataValue(key, instance);
|
||||||
@ -146,38 +306,52 @@ export async function updateSingleAssociation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update multiple association of model by value
|
||||||
|
* @param model
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
export async function updateMultipleAssociation(
|
export async function updateMultipleAssociation(
|
||||||
model: Model,
|
model: Model,
|
||||||
key: string,
|
key: string,
|
||||||
value: any,
|
value: any,
|
||||||
options: any = {},
|
options: any = {},
|
||||||
) {
|
) {
|
||||||
// @ts-ignore
|
const association = <BelongsToMany | HasMany>(
|
||||||
const association = model.constructor.associations[key] as Association;
|
modelAssociationByKey(model, key)
|
||||||
|
);
|
||||||
|
|
||||||
if (!association) {
|
if (!association) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { transaction = await model.sequelize.transaction() } = options;
|
const { transaction = await model.sequelize.transaction() } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
|
||||||
const setAccessor = association.accessors.set;
|
const setAccessor = association.accessors.set;
|
||||||
// @ts-ignore
|
|
||||||
const createAccessor = association.accessors.create;
|
const createAccessor = association.accessors.create;
|
||||||
if (isUndefinedOrNull(value)) {
|
if (isUndefinedOrNull(value)) {
|
||||||
await model[setAccessor](null, { transaction });
|
await model[setAccessor](null, { transaction });
|
||||||
model.setDataValue(key, null);
|
model.setDataValue(key, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isStringOrNumber(value)) {
|
if (isStringOrNumber(value)) {
|
||||||
await model[setAccessor](value, { transaction });
|
await model[setAccessor](value, { transaction });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(value)) {
|
if (!Array.isArray(value)) {
|
||||||
value = [value];
|
value = [value];
|
||||||
}
|
}
|
||||||
|
|
||||||
const list1 = []; // to be setted
|
const list1 = []; // to be setted
|
||||||
const list2 = []; // to be added
|
const list2 = []; // to be added
|
||||||
for (const item of value) {
|
for (const item of value) {
|
||||||
@ -194,23 +368,46 @@ export async function updateMultipleAssociation(
|
|||||||
list2.push(item);
|
list2.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// associate targets in lists1
|
||||||
await model[setAccessor](list1, { transaction });
|
await model[setAccessor](list1, { transaction });
|
||||||
|
|
||||||
const list3 = [];
|
const list3 = [];
|
||||||
for (const item of list2) {
|
for (const item of list2) {
|
||||||
const pk = association.target.primaryKeyAttribute;
|
const pk = association.target.primaryKeyAttribute;
|
||||||
|
|
||||||
|
const through = (<any>association).through
|
||||||
|
? (<any>association).through.model.name
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const accessorOptions = {
|
||||||
|
transaction,
|
||||||
|
};
|
||||||
|
|
||||||
|
const throughValue = item[through];
|
||||||
|
|
||||||
|
if (throughValue) {
|
||||||
|
accessorOptions['through'] = throughValue;
|
||||||
|
}
|
||||||
|
|
||||||
if (isUndefinedOrNull(item[pk])) {
|
if (isUndefinedOrNull(item[pk])) {
|
||||||
const instance = await model[createAccessor](item, { transaction });
|
// create new record
|
||||||
|
const instance = await model[createAccessor](item, accessorOptions);
|
||||||
await updateAssociations(instance, item, { transaction, ...options });
|
await updateAssociations(instance, item, { transaction, ...options });
|
||||||
list3.push(instance);
|
list3.push(instance);
|
||||||
} else {
|
} else {
|
||||||
const instance = await association.target.findByPk(item[pk], { transaction });
|
// set & update record
|
||||||
// @ts-ignore
|
const instance = await association.target.findByPk(item[pk], {
|
||||||
|
transaction,
|
||||||
|
});
|
||||||
const addAccessor = association.accessors.add;
|
const addAccessor = association.accessors.add;
|
||||||
await model[addAccessor](item[pk], { transaction });
|
|
||||||
|
await model[addAccessor](item[pk], accessorOptions);
|
||||||
await updateAssociations(instance, item, { transaction, ...options });
|
await updateAssociations(instance, item, { transaction, ...options });
|
||||||
list3.push(instance);
|
list3.push(instance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
model.setDataValue(key, list1.concat(list3));
|
model.setDataValue(key, list1.concat(list3));
|
||||||
if (!options.transaction) {
|
if (!options.transaction) {
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
|
172
packages/database-next/src/update-guard.ts
Normal file
172
packages/database-next/src/update-guard.ts
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { flatten } from 'flat';
|
||||||
|
import lodash, { keys } from 'lodash';
|
||||||
|
|
||||||
|
import { Collection } from './collection';
|
||||||
|
import { BelongsTo, HasOne, Model, ModelCtor } from 'sequelize';
|
||||||
|
import { AssociationKeysToBeUpdate, BlackList, WhiteList } from './repository';
|
||||||
|
|
||||||
|
type UpdateValueItem = string | number | UpdateValues;
|
||||||
|
|
||||||
|
type UpdateValues = {
|
||||||
|
[key: string]: UpdateValueItem | Array<UpdateValueItem>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class UpdateGuard {
|
||||||
|
model: ModelCtor<any>;
|
||||||
|
private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
|
||||||
|
private blackList: BlackList;
|
||||||
|
private whiteList: WhiteList;
|
||||||
|
|
||||||
|
setModel(model: ModelCtor<any>) {
|
||||||
|
this.model = model;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssociationKeysToBeUpdate(
|
||||||
|
associationKeysToBeUpdate: AssociationKeysToBeUpdate,
|
||||||
|
) {
|
||||||
|
this.associationKeysToBeUpdate = associationKeysToBeUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWhiteList(whiteList: WhiteList) {
|
||||||
|
this.whiteList = whiteList;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBlackList(blackList: BlackList) {
|
||||||
|
this.blackList = blackList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize values by whitelist blacklist
|
||||||
|
* @param values
|
||||||
|
*/
|
||||||
|
sanitize(values: UpdateValues) {
|
||||||
|
values = lodash.clone(values);
|
||||||
|
|
||||||
|
if (!this.model) {
|
||||||
|
throw new Error('please set model first');
|
||||||
|
}
|
||||||
|
|
||||||
|
const associations = this.model.associations;
|
||||||
|
const associationsValues = lodash.pick(values, Object.keys(associations));
|
||||||
|
|
||||||
|
// build params of association update guard
|
||||||
|
const listOfAssociation = (list, association) => {
|
||||||
|
if (list) {
|
||||||
|
list = list
|
||||||
|
.filter((whiteListKey) => whiteListKey.startsWith(`${association}.`))
|
||||||
|
.map((whiteListKey) => whiteListKey.replace(`${association}.`, ''));
|
||||||
|
|
||||||
|
if (list.length == 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sanitize association values
|
||||||
|
Object.keys(associationsValues).forEach((association) => {
|
||||||
|
let associationValues = associationsValues[association];
|
||||||
|
|
||||||
|
const filterAssociationToBeUpdate = (value) => {
|
||||||
|
const associationKeysToBeUpdate = this.associationKeysToBeUpdate || [];
|
||||||
|
|
||||||
|
if (associationKeysToBeUpdate.includes(association)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const associationObj = associations[association];
|
||||||
|
|
||||||
|
const associationKeyName =
|
||||||
|
associationObj.associationType == 'BelongsTo' ||
|
||||||
|
associationObj.associationType == 'HasOne'
|
||||||
|
? (<any>associationObj).targetKey
|
||||||
|
: associationObj.target.primaryKeyAttribute;
|
||||||
|
|
||||||
|
if (value[associationKeyName]) {
|
||||||
|
return lodash.pick(value, [
|
||||||
|
associationKeyName,
|
||||||
|
...Object.keys(associationObj.target.associations),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sanitizeValue = (value) => {
|
||||||
|
const associationUpdateGuard = new UpdateGuard();
|
||||||
|
associationUpdateGuard.setModel(associations[association].target);
|
||||||
|
|
||||||
|
['whiteList', 'blackList', 'associationKeysToBeUpdate'].forEach(
|
||||||
|
(optionKey) => {
|
||||||
|
associationUpdateGuard[`set${lodash.upperFirst(optionKey)}`](
|
||||||
|
listOfAssociation(this[optionKey], association),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return associationUpdateGuard.sanitize(
|
||||||
|
filterAssociationToBeUpdate(value),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Array.isArray(associationValues)) {
|
||||||
|
associationValues = associationValues.map((value) => {
|
||||||
|
if (typeof value == 'string' || typeof value == 'number') {
|
||||||
|
return value;
|
||||||
|
} else {
|
||||||
|
return sanitizeValue(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (
|
||||||
|
typeof associationValues === 'object' &&
|
||||||
|
associationValues !== null
|
||||||
|
) {
|
||||||
|
associationValues = sanitizeValue(associationValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// set association values to sanitized value
|
||||||
|
values[association] = associationValues;
|
||||||
|
});
|
||||||
|
|
||||||
|
let valuesKeys = Object.keys(values);
|
||||||
|
|
||||||
|
// handle whitelist
|
||||||
|
if (this.whiteList) {
|
||||||
|
valuesKeys = valuesKeys.filter((valueKey) => {
|
||||||
|
return (
|
||||||
|
this.whiteList.findIndex((whiteKey) => {
|
||||||
|
const keyPaths = whiteKey.split('.');
|
||||||
|
return keyPaths[0] === valueKey;
|
||||||
|
}) !== -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle blacklist
|
||||||
|
if (this.blackList) {
|
||||||
|
valuesKeys = valuesKeys.filter(
|
||||||
|
(valueKey) => !this.blackList.includes(valueKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = valuesKeys.reduce((obj, key) => {
|
||||||
|
lodash.set(obj, key, values[key]);
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromOptions(model, options) {
|
||||||
|
const guard = new UpdateGuard();
|
||||||
|
guard.setModel(model);
|
||||||
|
guard.setWhiteList(options.whitelist);
|
||||||
|
guard.setBlackList(options.blacklist);
|
||||||
|
guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
|
||||||
|
|
||||||
|
return guard;
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,10 @@
|
|||||||
export default {
|
let IDX = 36,
|
||||||
fiter: {
|
HEX = '';
|
||||||
and: [
|
while (IDX--) HEX += IDX.toString(36);
|
||||||
{ a: 'a' },
|
|
||||||
{ b: 'b' },
|
export function uid(len?: number) {
|
||||||
{ c: 'c' },
|
let str = '',
|
||||||
{ 'assoc.a': 'abc1' },
|
num = len || 11;
|
||||||
{ 'assoc.b': 'abc2' },
|
while (num--) str += HEX[(Math.random() * 36) | 0];
|
||||||
{ 'assoc.c': 'abc3' },
|
return str;
|
||||||
{
|
}
|
||||||
and: [
|
|
||||||
{ 'assoc.a': 'abc1' },
|
|
||||||
{ 'assoc.b': 'abc2' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
registerMiddlewares,
|
registerMiddlewares,
|
||||||
} from './helper';
|
} from './helper';
|
||||||
import { i18n, InitOptions } from 'i18next';
|
import { i18n, InitOptions } from 'i18next';
|
||||||
|
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||||
|
|
||||||
export interface ResourcerOptions {
|
export interface ResourcerOptions {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
@ -51,10 +52,10 @@ interface ActionsOptions {
|
|||||||
resourceNames?: string[];
|
resourceNames?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Application<
|
export class Application<StateT = DefaultState, ContextT = DefaultContext>
|
||||||
StateT = DefaultState,
|
extends Koa
|
||||||
ContextT = DefaultContext
|
implements AsyncEmitter
|
||||||
> extends Koa {
|
{
|
||||||
public readonly db: Database;
|
public readonly db: Database;
|
||||||
|
|
||||||
public readonly resourcer: Resourcer;
|
public readonly resourcer: Resourcer;
|
||||||
@ -160,57 +161,6 @@ export class Application<
|
|||||||
await this.emitAsync('plugins.afterLoad');
|
await this.emitAsync('plugins.afterLoad');
|
||||||
}
|
}
|
||||||
|
|
||||||
async emitAsync(event: string | symbol, ...args: any[]): Promise<boolean> {
|
|
||||||
// @ts-ignore
|
|
||||||
const events = this._events;
|
|
||||||
let callbacks = events[event];
|
|
||||||
if (!callbacks) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// helper function to reuse as much code as possible
|
|
||||||
const run = (cb) => {
|
|
||||||
switch (args.length) {
|
|
||||||
// fast cases
|
|
||||||
case 0:
|
|
||||||
cb = cb.call(this);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
cb = cb.call(this, args[0]);
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
cb = cb.call(this, args[0], args[1]);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
cb = cb.call(this, args[0], args[1], args[2]);
|
|
||||||
break;
|
|
||||||
// slower
|
|
||||||
default:
|
|
||||||
cb = cb.apply(this, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cb && (cb instanceof Promise || typeof cb.then === 'function')) {
|
|
||||||
return cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof callbacks === 'function') {
|
|
||||||
await run(callbacks);
|
|
||||||
} else if (typeof callbacks === 'object') {
|
|
||||||
callbacks = callbacks.slice().filter(Boolean);
|
|
||||||
await callbacks.reduce((prev, next) => {
|
|
||||||
return prev.then((res) => {
|
|
||||||
return run(next).then((result) =>
|
|
||||||
Promise.resolve(res.concat(result)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, Promise.resolve([]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async parse(argv = process.argv) {
|
async parse(argv = process.argv) {
|
||||||
await this.load();
|
await this.load();
|
||||||
return this.cli.parseAsync(argv);
|
return this.cli.parseAsync(argv);
|
||||||
@ -219,6 +169,9 @@ export class Application<
|
|||||||
async destroy() {
|
async destroy() {
|
||||||
await this.db.close();
|
await this.db.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
applyMixins(Application, [AsyncEmitter]);
|
||||||
|
|
||||||
export default Application;
|
export default Application;
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
|
export * from './mixin';
|
||||||
|
export * from './mixin/AsyncEmitter';
|
||||||
export * from './merge';
|
export * from './merge';
|
||||||
export * from './umiConfig';
|
export * from './umiConfig';
|
||||||
|
52
packages/utils/src/mixin/AsyncEmitter.ts
Normal file
52
packages/utils/src/mixin/AsyncEmitter.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
export class AsyncEmitter {
|
||||||
|
async emitAsync(event: string | symbol, ...args: any[]): Promise<boolean> {
|
||||||
|
// @ts-ignore
|
||||||
|
const events = this._events;
|
||||||
|
let callbacks = events[event];
|
||||||
|
if (!callbacks) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// helper function to reuse as much code as possible
|
||||||
|
const run = (cb) => {
|
||||||
|
switch (args.length) {
|
||||||
|
// fast cases
|
||||||
|
case 0:
|
||||||
|
cb = cb.call(this);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
cb = cb.call(this, args[0]);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
cb = cb.call(this, args[0], args[1]);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
cb = cb.call(this, args[0], args[1], args[2]);
|
||||||
|
break;
|
||||||
|
// slower
|
||||||
|
default:
|
||||||
|
cb = cb.apply(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb && (cb instanceof Promise || typeof cb.then === 'function')) {
|
||||||
|
return cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof callbacks === 'function') {
|
||||||
|
await run(callbacks);
|
||||||
|
} else if (typeof callbacks === 'object') {
|
||||||
|
callbacks = callbacks.slice().filter(Boolean);
|
||||||
|
await callbacks.reduce((prev, next) => {
|
||||||
|
return prev.then((res) => {
|
||||||
|
return run(next).then((result) =>
|
||||||
|
Promise.resolve(res.concat(result)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, Promise.resolve([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
12
packages/utils/src/mixin/index.ts
Normal file
12
packages/utils/src/mixin/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export function applyMixins(derivedCtor: any, constructors: any[]) {
|
||||||
|
constructors.forEach((baseCtor) => {
|
||||||
|
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
|
||||||
|
Object.defineProperty(
|
||||||
|
derivedCtor.prototype,
|
||||||
|
name,
|
||||||
|
Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
|
||||||
|
Object.create(null),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -5,13 +5,8 @@
|
|||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@nocobase/*": [
|
"@nocobase/*": ["./packages/*/src"]
|
||||||
"./packages/*/src"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["./packages/*/esm", "./packages/*/lib"]
|
||||||
"./packages/*/esm",
|
|
||||||
"./packages/*/lib"
|
|
||||||
]
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user