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();
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'title' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'title' }],
|
||||
});
|
||||
const association = Comment.model.associations.post;
|
||||
expect(Comment.model.associations.post).toBeDefined();
|
||||
@ -51,7 +49,7 @@ describe('belongs to field', () => {
|
||||
title: 'title222',
|
||||
});
|
||||
const post = await Post.model.create<any>({
|
||||
title: 'title111'
|
||||
title: 'title111',
|
||||
});
|
||||
await comment.setPost(post);
|
||||
const post2 = await comment.getPost();
|
||||
@ -63,9 +61,7 @@ describe('belongs to field', () => {
|
||||
it('custom targetKey and foreignKey', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'key', unique: true }],
|
||||
});
|
||||
const Comment = db.collection({
|
||||
name: 'comments',
|
||||
@ -103,9 +99,7 @@ describe('belongs to field', () => {
|
||||
expect(Comment.model.associations.article).toBeUndefined();
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'key', unique: true },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'key', unique: true }],
|
||||
});
|
||||
const association = Comment.model.associations.article;
|
||||
expect(Comment.model.associations.article).toBeDefined();
|
||||
@ -123,7 +117,7 @@ describe('belongs to field', () => {
|
||||
key: 'title222',
|
||||
});
|
||||
const post = await Post.model.create<any>({
|
||||
key: 'title111'
|
||||
key: 'title111',
|
||||
});
|
||||
await comment.setArticle(post);
|
||||
const post2 = await comment.getArticle();
|
||||
@ -148,4 +142,21 @@ describe('belongs to field', () => {
|
||||
Post.removeField('comments');
|
||||
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();
|
||||
});
|
||||
|
||||
it('association undefined', async () => {
|
||||
test('association undefined', async () => {
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
@ -20,22 +20,41 @@ describe('belongs to many field', () => {
|
||||
{ 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' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
expect(Post.model.associations.tags).toBeDefined();
|
||||
const Through = db.getCollection('posts_tags');
|
||||
expect(Through).toBeDefined();
|
||||
|
||||
expect(Through.model.rawAttributes['postId']).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['tagId']).toBeDefined();
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ describe('string field', () => {
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
db.registerFieldTypes({
|
||||
sort: SortField
|
||||
sort: SortField,
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,9 +19,7 @@ describe('string field', () => {
|
||||
it('sort', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'sort', name: 'sort' },
|
||||
],
|
||||
fields: [{ type: 'sort', name: 'sort' }],
|
||||
});
|
||||
await db.sync();
|
||||
const test1 = await Test.model.create<any>();
|
||||
@ -35,9 +33,7 @@ describe('string field', () => {
|
||||
it('skip if sort value not empty', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'sort', name: 'sort' },
|
||||
],
|
||||
fields: [{ type: 'sort', name: 'sort' }],
|
||||
});
|
||||
await db.sync();
|
||||
const test1 = await Test.model.create<any>({ sort: 3 });
|
||||
@ -66,5 +62,4 @@ describe('string field', () => {
|
||||
expect(t3.get('sort')).toBe(1);
|
||||
expect(t4.get('sort')).toBe(2);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -15,9 +15,7 @@ describe('string field', () => {
|
||||
it('define', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
await db.sync();
|
||||
expect(Test.model.rawAttributes['name']).toBeDefined();
|
||||
@ -32,13 +30,13 @@ describe('string field', () => {
|
||||
it('set', async () => {
|
||||
const Test = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name1' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'name1' }],
|
||||
});
|
||||
await db.sync();
|
||||
Test.addField({ type: 'string', name: 'name2' });
|
||||
await db.sync();
|
||||
Test.addField('name2', { type: 'string', name: 'name2' });
|
||||
await db.sync({
|
||||
alter: true,
|
||||
});
|
||||
expect(Test.model.rawAttributes['name1']).toBeDefined();
|
||||
expect(Test.model.rawAttributes['name2']).toBeDefined();
|
||||
const model = await Test.model.create({
|
||||
@ -54,9 +52,7 @@ describe('string field', () => {
|
||||
it('model hook', async () => {
|
||||
const collection = db.collection({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
await db.sync();
|
||||
collection.model.beforeCreate((model) => {
|
||||
@ -65,8 +61,10 @@ describe('string field', () => {
|
||||
model.set(name, `${model.get(name)}111`);
|
||||
}
|
||||
});
|
||||
collection.addField({ type: 'string', name: 'name2' });
|
||||
await db.sync();
|
||||
collection.addField('name2', { type: 'string', name: 'name2' });
|
||||
await db.sync({
|
||||
alter: true,
|
||||
});
|
||||
const model = await collection.model.create({
|
||||
name: 'n1',
|
||||
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(/[^\w]/g, '_')
|
||||
.replace(/_+/g, '_');
|
||||
return key
|
||||
return key;
|
||||
}
|
||||
|
||||
export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||
return merge({
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
logging: process.env.DB_LOG_SQL === 'on',
|
||||
sync: {
|
||||
force: true,
|
||||
alter: {
|
||||
drop: true,
|
||||
},
|
||||
},
|
||||
return merge(
|
||||
{
|
||||
dialect: 'sqlite',
|
||||
storage: options?.storage || ':memory:',
|
||||
logging: options?.logging || false,
|
||||
hooks: {
|
||||
beforeDefine(model, options) {
|
||||
options.tableName = `${generatePrefixByPath()}_${options.tableName || options.modelName || options.name.plural}`;
|
||||
options.tableName = `${generatePrefixByPath()}_${
|
||||
options.tableName || options.modelName || options.name.plural
|
||||
}`;
|
||||
},
|
||||
},
|
||||
}, config || {}, options) as any;
|
||||
};
|
||||
},
|
||||
config || {},
|
||||
options,
|
||||
) as any;
|
||||
}
|
||||
|
||||
export function mockDatabase(options?: DatabaseOptions): Database {
|
||||
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,7 +29,8 @@ describe('repository.find', () => {
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
await db.sync();
|
||||
await User.repository.createMany([
|
||||
await User.repository.createMany({
|
||||
records: [
|
||||
{
|
||||
name: 'user1',
|
||||
posts: [
|
||||
@ -120,7 +121,8 @@ describe('repository.find', () => {
|
||||
{ name: 'post34' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@ -136,25 +138,12 @@ describe('repository.find', () => {
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
it('findMany', async () => {
|
||||
const data = await User.repository.findMany({
|
||||
it('find item', async () => {
|
||||
const data = await User.repository.find({
|
||||
filter: {
|
||||
'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,6 +182,7 @@ describe('repository.create', () => {
|
||||
|
||||
it('create', async () => {
|
||||
const user = await User.repository.create({
|
||||
values: {
|
||||
name: 'user1',
|
||||
posts: [
|
||||
{
|
||||
@ -204,6 +194,7 @@ describe('repository.create', () => {
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const post = await Post.model.findOne();
|
||||
expect(post).toMatchObject({
|
||||
@ -256,22 +247,25 @@ describe('repository.update', () => {
|
||||
const user = await User.model.create<any>({
|
||||
name: 'user1',
|
||||
});
|
||||
await User.repository.update(
|
||||
{
|
||||
await User.repository.update({
|
||||
filterByPk: user.id,
|
||||
values: {
|
||||
name: 'user11',
|
||||
posts: [{ name: 'post1' }],
|
||||
},
|
||||
user,
|
||||
);
|
||||
});
|
||||
|
||||
const updated = await User.model.findByPk(user.id);
|
||||
expect(updated).toMatchObject({
|
||||
name: 'user11',
|
||||
});
|
||||
|
||||
const post = await Post.model.findOne({
|
||||
where: {
|
||||
name: 'post1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(post).toMatchObject({
|
||||
name: 'post1',
|
||||
userId: user.id,
|
||||
@ -283,13 +277,13 @@ describe('repository.update', () => {
|
||||
name: 'user1',
|
||||
posts: [{ name: 'post1' }],
|
||||
});
|
||||
await User.repository.update(
|
||||
{
|
||||
await User.repository.update({
|
||||
filterByPk: user.id,
|
||||
values: {
|
||||
name: 'user11',
|
||||
posts: [{ name: 'post1' }],
|
||||
},
|
||||
user.id,
|
||||
);
|
||||
});
|
||||
const updated = await User.model.findByPk(user.id);
|
||||
expect(updated).toMatchObject({
|
||||
name: 'user11',
|
||||
@ -393,34 +387,31 @@ describe('repository.relatedQuery', () => {
|
||||
});
|
||||
|
||||
it('create', async () => {
|
||||
const user = await User.repository.create();
|
||||
const post = await User.repository.relatedQuery('posts').for(user).create({
|
||||
name: 'post1',
|
||||
const user = await User.repository.create({
|
||||
values: {
|
||||
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({
|
||||
name: 'post1',
|
||||
userId: user.id,
|
||||
userId: user.get('id'),
|
||||
});
|
||||
const post2 = await User.repository
|
||||
.relatedQuery('posts')
|
||||
.for(user.id)
|
||||
.create({
|
||||
name: 'post2',
|
||||
|
||||
const post2 = await userPostRepository.create({
|
||||
values: { name: 'post2' },
|
||||
});
|
||||
expect(post2).toMatchObject({
|
||||
name: 'post2',
|
||||
userId: user.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('update', async () => {
|
||||
const post = await Post.repository.create({
|
||||
user: {
|
||||
name: 'user11',
|
||||
}
|
||||
});
|
||||
await Post.repository.relatedQuery('user').for(post).update({
|
||||
name: 'user12',
|
||||
userId: user.get('id'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
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 './';
|
||||
|
||||
describe('update associations', () => {
|
||||
|
||||
describe('belongsTo', () => {
|
||||
let db: Database;
|
||||
beforeEach(() => {
|
||||
db = mockDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('post.user', async () => {
|
||||
const User = db.collection({
|
||||
const User = db.collection<
|
||||
{ id: string; name: string },
|
||||
{ name: string }
|
||||
>({
|
||||
name: 'users',
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
|
||||
const Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
@ -25,12 +30,16 @@ describe('update associations', () => {
|
||||
{ type: 'belongsTo', name: 'user' },
|
||||
],
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
await updateAssociations(post1, {
|
||||
user,
|
||||
});
|
||||
|
||||
expect(post1.toJSON()).toMatchObject({
|
||||
id: 1,
|
||||
name: 'post1',
|
||||
@ -40,21 +49,25 @@ describe('update associations', () => {
|
||||
name: 'user1',
|
||||
},
|
||||
});
|
||||
|
||||
const post2 = await Post.model.create({ name: 'post2' });
|
||||
await updateAssociations(post2, {
|
||||
user: user.id,
|
||||
user: user.getDataValue('id'),
|
||||
});
|
||||
|
||||
expect(post2.toJSON()).toMatchObject({
|
||||
id: 2,
|
||||
name: 'post2',
|
||||
userId: 1,
|
||||
});
|
||||
|
||||
const post3 = await Post.model.create({ name: 'post3' });
|
||||
await updateAssociations(post3, {
|
||||
user: {
|
||||
name: 'user3',
|
||||
},
|
||||
});
|
||||
|
||||
expect(post3.toJSON()).toMatchObject({
|
||||
id: 3,
|
||||
name: 'post3',
|
||||
@ -64,13 +77,15 @@ describe('update associations', () => {
|
||||
name: 'user3',
|
||||
},
|
||||
});
|
||||
|
||||
const post4 = await Post.model.create({ name: 'post4' });
|
||||
await updateAssociations(post4, {
|
||||
user: {
|
||||
id: user.id,
|
||||
id: user.getDataValue('id'),
|
||||
name: 'user4',
|
||||
},
|
||||
});
|
||||
|
||||
expect(post4.toJSON()).toMatchObject({
|
||||
id: 4,
|
||||
name: 'post4',
|
||||
@ -98,9 +113,7 @@ describe('update associations', () => {
|
||||
});
|
||||
Post = db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
],
|
||||
fields: [{ type: 'string', name: 'name' }],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
@ -120,16 +133,18 @@ describe('update associations', () => {
|
||||
{
|
||||
name: 'post1',
|
||||
userId: user1.id,
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it('user.posts', async () => {
|
||||
const user1 = await User.model.create<any>({ name: 'user1' });
|
||||
await updateAssociations(user1, {
|
||||
posts: [{
|
||||
posts: [
|
||||
{
|
||||
name: 'post1',
|
||||
}],
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(user1.toJSON()).toMatchObject({
|
||||
name: 'user1',
|
||||
@ -137,7 +152,7 @@ describe('update associations', () => {
|
||||
{
|
||||
name: 'post1',
|
||||
userId: user1.id,
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
@ -202,7 +217,7 @@ describe('update associations', () => {
|
||||
},
|
||||
post2.id,
|
||||
post3,
|
||||
]
|
||||
],
|
||||
});
|
||||
console.log(JSON.stringify(user1, null, 2));
|
||||
expect(user1.toJSON()).toMatchObject({
|
||||
@ -263,8 +278,41 @@ describe('update associations', () => {
|
||||
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 () => {
|
||||
const user = await User.model.create<any>({ name: 'user1' });
|
||||
const user = await User.model.create({ name: 'user1' });
|
||||
await updateAssociations(user, {
|
||||
posts: [
|
||||
{
|
||||
@ -273,22 +321,92 @@ describe('update associations', () => {
|
||||
{
|
||||
name: 'comment1',
|
||||
},
|
||||
{
|
||||
name: 'comment12',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'post2',
|
||||
comments: [
|
||||
{
|
||||
name: 'comment21',
|
||||
},
|
||||
{
|
||||
name: 'comment22',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const post1 = await Post.model.findOne({
|
||||
where: { name: 'post1' }
|
||||
where: { name: 'post1' },
|
||||
});
|
||||
|
||||
const comment1 = await Comment.model.findOne({
|
||||
where: { name: 'comment1' }
|
||||
where: { name: 'comment1' },
|
||||
});
|
||||
|
||||
expect(post1).toMatchObject({
|
||||
userId: user.get('id'),
|
||||
});
|
||||
|
||||
expect(comment1).toMatchObject({
|
||||
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 { Database } from './database';
|
||||
import { Field } from './fields';
|
||||
import { Field, FieldOptions } from './fields';
|
||||
|
||||
import _ from 'lodash';
|
||||
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;
|
||||
tableName?: string;
|
||||
fields?: any;
|
||||
[key: string]: any;
|
||||
fields?: FieldOptions[];
|
||||
model?: string | ModelCtor<Model>;
|
||||
repository?: string | RepositoryType;
|
||||
}
|
||||
|
||||
export interface CollectionContext {
|
||||
database: Database;
|
||||
}
|
||||
|
||||
export class Collection extends EventEmitter {
|
||||
export class Collection<
|
||||
TModelAttributes extends {} = any,
|
||||
TCreationAttributes extends {} = TModelAttributes,
|
||||
> extends EventEmitter {
|
||||
options: CollectionOptions;
|
||||
context: CollectionContext;
|
||||
fields: Map<string, any>;
|
||||
model: ModelCtor<Model>;
|
||||
repository: Repository;
|
||||
isThrough?: boolean;
|
||||
fields: Map<string, any> = new Map<string, any>();
|
||||
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>;
|
||||
repository: Repository<TModelAttributes, TCreationAttributes>;
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
@ -29,23 +41,62 @@ export class Collection extends EventEmitter {
|
||||
|
||||
constructor(options: CollectionOptions, context?: CollectionContext) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.context = context;
|
||||
this.fields = new Map<string, any>();
|
||||
this.model = class extends Model<any, any> {};
|
||||
const attributes = {};
|
||||
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.options = options;
|
||||
this.bindFieldEventListener();
|
||||
this.modelInit();
|
||||
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) {
|
||||
@ -64,37 +115,45 @@ export class Collection extends EventEmitter {
|
||||
return this.fields.get(name);
|
||||
}
|
||||
|
||||
addField(options) {
|
||||
const { name, ...others } = 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);
|
||||
addField(name: string, options: Omit<FieldOptions, 'name'>): Field {
|
||||
return this.setField(name, options);
|
||||
}
|
||||
|
||||
setFields(fields: any, reset = true) {
|
||||
if (!fields) {
|
||||
return this;
|
||||
setField(name: string, options: Omit<FieldOptions, 'name'>): Field {
|
||||
const { database } = this.context;
|
||||
|
||||
const field = database.buildField(
|
||||
{ name, ...options },
|
||||
{
|
||||
...this.context,
|
||||
collection: this,
|
||||
},
|
||||
);
|
||||
|
||||
this.fields.set(name, field);
|
||||
this.emit('field.afterAdd', field);
|
||||
return field;
|
||||
}
|
||||
if (reset) {
|
||||
this.fields.clear();
|
||||
|
||||
setFields(fields: FieldOptions[], resetFields = true) {
|
||||
if (!Array.isArray(fields)) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(fields)) {
|
||||
for (const field of fields) {
|
||||
this.addField(field);
|
||||
|
||||
if (resetFields) {
|
||||
this.resetFields();
|
||||
}
|
||||
} else if (typeof fields === 'object') {
|
||||
for (const [name, options] of Object.entries<any>(fields)) {
|
||||
this.addField({...options, name});
|
||||
|
||||
for (const { name, ...options } of fields) {
|
||||
this.addField(name, options);
|
||||
}
|
||||
}
|
||||
|
||||
resetFields() {
|
||||
const fieldNames = this.fields.keys();
|
||||
for (const fieldName of fieldNames) {
|
||||
this.removeField(fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
removeField(name) {
|
||||
@ -106,13 +165,73 @@ export class Collection extends EventEmitter {
|
||||
return bool;
|
||||
}
|
||||
|
||||
// TODO
|
||||
extend(options) {
|
||||
const { fields } = options;
|
||||
this.setFields(fields);
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
sync() {
|
||||
this.context.database.emit('afterUpdateCollection', this);
|
||||
}
|
||||
|
||||
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,
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Collection, CollectionOptions } from './collection';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
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 {
|
||||
field: RelationField;
|
||||
model: ModelCtor<Model>;
|
||||
}
|
||||
|
||||
interface MapOf<T> {
|
||||
[key: string]: T;
|
||||
}
|
||||
|
||||
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;
|
||||
fieldTypes = new Map();
|
||||
models = new Map();
|
||||
repositories = new Map();
|
||||
models = new Map<string, ModelCtor<any>>();
|
||||
repositories = new Map<string, RepositoryType>();
|
||||
operators = new Map();
|
||||
collections: Map<string, Collection>;
|
||||
collections = new Map<string, Collection>();
|
||||
pendingFields = new Map<string, RelationField[]>();
|
||||
modelCollection = new Map<ModelCtor<any>, Collection>();
|
||||
|
||||
modelHook: ModelHook;
|
||||
|
||||
delayCollectionExtend = new Map<
|
||||
string,
|
||||
{ collectionOptions: CollectionOptions; mergeOptions?: any }[]
|
||||
>();
|
||||
|
||||
constructor(options: DatabaseOptions) {
|
||||
super();
|
||||
@ -38,14 +74,22 @@ export class Database extends EventEmitter {
|
||||
}
|
||||
|
||||
this.collections = new Map();
|
||||
this.modelHook = new ModelHook(this);
|
||||
|
||||
this.on('collection.afterDefine', (collection) => {
|
||||
const items = this.pendingFields.get(collection.name);
|
||||
for (const field of items || []) {
|
||||
field.bind();
|
||||
}
|
||||
this.on('afterDefineCollection', (collection: Collection) => {
|
||||
// after collection defined, call bind method on pending fields
|
||||
this.pendingFields.get(collection.name)?.forEach((field) => 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)) {
|
||||
if (['Field', 'RelationField'].includes(name)) {
|
||||
continue;
|
||||
@ -57,35 +101,53 @@ export class Database extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
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.initOperators();
|
||||
}
|
||||
|
||||
this.operators = operators;
|
||||
}
|
||||
/**
|
||||
* Add collection to database
|
||||
* @param options
|
||||
*/
|
||||
collection<Attributes = any, CreateAttributes = Attributes>(
|
||||
options: CollectionOptions,
|
||||
): Collection<Attributes, CreateAttributes> {
|
||||
this.emit('beforeDefineCollection', options);
|
||||
|
||||
const collection = new Collection(options, {
|
||||
database: this,
|
||||
});
|
||||
|
||||
collection(options: CollectionOptions) {
|
||||
let collection = this.collections.get(options.name);
|
||||
if (collection) {
|
||||
collection.extend(options);
|
||||
} else {
|
||||
collection = new Collection(options, { database: this });
|
||||
}
|
||||
this.collections.set(collection.name, collection);
|
||||
this.emit('collection.afterDefine', collection);
|
||||
this.modelCollection.set(collection.model, collection);
|
||||
|
||||
this.emit('afterDefineCollection', collection);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
getCollection(name: string) {
|
||||
/**
|
||||
* get exists collection by its name
|
||||
* @param name
|
||||
*/
|
||||
getCollection(name: string): Collection {
|
||||
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) {
|
||||
const associating = this.pendingFields;
|
||||
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)) {
|
||||
this.fieldTypes.set(type, fieldType);
|
||||
}
|
||||
}
|
||||
|
||||
registerModels(models: any) {
|
||||
registerModels(models: MapOf<ModelCtor<any>>) {
|
||||
for (const [type, schemaType] of Object.entries(models)) {
|
||||
this.models.set(type, schemaType);
|
||||
}
|
||||
}
|
||||
|
||||
registerRepositories(repositories: any) {
|
||||
registerRepositories(repositories: MapOf<RepositoryType>) {
|
||||
for (const [type, schemaType] of Object.entries(repositories)) {
|
||||
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)) {
|
||||
this.operators.set(key, operator);
|
||||
}
|
||||
}
|
||||
|
||||
buildField(options, context) {
|
||||
buildField(options, context: FieldContext) {
|
||||
const { type } = options;
|
||||
const Field = this.fieldTypes.get(type);
|
||||
if (!Field) {
|
||||
throw Error(`unsupported field type ${type}`);
|
||||
}
|
||||
return new Field(options, context);
|
||||
}
|
||||
|
||||
@ -147,4 +230,71 @@ export class Database extends EventEmitter {
|
||||
async 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 { 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 {
|
||||
|
||||
static type = 'belongsTo';
|
||||
|
||||
get target() {
|
||||
@ -14,23 +17,39 @@ export class BelongsToField extends RelationField {
|
||||
bind() {
|
||||
const { database, collection } = this.context;
|
||||
const Target = this.TargetModel;
|
||||
|
||||
// if target model not exists, add it to pending field,
|
||||
// it will bind later
|
||||
if (!Target) {
|
||||
database.addPendingField(this);
|
||||
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, {
|
||||
as: this.name,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
});
|
||||
|
||||
// inverse relation
|
||||
this.TargetModel.hasMany(collection.model);
|
||||
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
|
||||
if (!this.options.sourceKey) {
|
||||
// @ts-ignore
|
||||
this.options.sourceKey = association.sourceKey;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -54,3 +73,9 @@ export class BelongsToField extends RelationField {
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
export interface BelongsToFieldOptions
|
||||
extends BaseRelationFieldOptions,
|
||||
SequelizeBelongsToOptions {
|
||||
type: 'belongsTo';
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { omit } from 'lodash';
|
||||
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 {
|
||||
|
||||
get through() {
|
||||
return (
|
||||
this.options.through ||
|
||||
@ -22,18 +24,27 @@ export class BelongsToManyField extends RelationField {
|
||||
return false;
|
||||
}
|
||||
const through = this.through;
|
||||
let Through =
|
||||
database.getCollection(through) ||
|
||||
database.collection({
|
||||
|
||||
let Through: Collection;
|
||||
|
||||
if (database.hasCollection(through)) {
|
||||
Through = database.getCollection(through);
|
||||
} else {
|
||||
Through = database.collection({
|
||||
name: through,
|
||||
});
|
||||
Object.defineProperty(Through.model, 'isThrough', { value: true });
|
||||
}
|
||||
|
||||
const association = collection.model.belongsToMany(Target, {
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
as: this.name,
|
||||
through: Through.model,
|
||||
});
|
||||
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
@ -51,3 +62,10 @@ export class BelongsToManyField extends RelationField {
|
||||
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 { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class DateField extends Field {
|
||||
get dataType() {
|
||||
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 { Database } from '../database';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
DataType,
|
||||
ModelAttributeColumnOptions,
|
||||
ModelIndexesOptions,
|
||||
} from 'sequelize';
|
||||
|
||||
export interface FieldContext {
|
||||
database: Database;
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
export interface BaseFieldOptions {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BaseColumnFieldOptions
|
||||
extends BaseFieldOptions,
|
||||
Omit<ModelAttributeColumnOptions, 'type'> {
|
||||
dataType?: DataType;
|
||||
index?: boolean | ModelIndexesOptions;
|
||||
}
|
||||
|
||||
export abstract class Field {
|
||||
options: any;
|
||||
context: FieldContext;
|
||||
@ -67,4 +81,8 @@ export abstract class Field {
|
||||
}
|
||||
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,
|
||||
Utils,
|
||||
} 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 {
|
||||
/**
|
||||
@ -73,7 +75,6 @@ export interface HasManyFieldOptions extends HasManyOptions {
|
||||
}
|
||||
|
||||
export class HasManyField extends RelationField {
|
||||
|
||||
get foreignKey() {
|
||||
if (this.options.foreignKey) {
|
||||
return this.options.foreignKey;
|
||||
@ -82,8 +83,8 @@ export class HasManyField extends RelationField {
|
||||
return Utils.camelize(
|
||||
[
|
||||
model.options.name.singular,
|
||||
this.sourceKey || model.primaryKeyAttribute
|
||||
].join('_')
|
||||
this.sourceKey || model.primaryKeyAttribute,
|
||||
].join('_'),
|
||||
);
|
||||
}
|
||||
|
||||
@ -94,13 +95,23 @@ export class HasManyField extends RelationField {
|
||||
database.addPendingField(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (collection.model.associations[this.name]) {
|
||||
delete collection.model.associations[this.name];
|
||||
}
|
||||
|
||||
const association = collection.model.hasMany(Target, {
|
||||
as: this.name,
|
||||
foreignKey: this.foreignKey,
|
||||
...omit(this.options, ['name', 'type', 'target']),
|
||||
});
|
||||
|
||||
// inverse relation
|
||||
this.TargetModel.belongsTo(collection.model);
|
||||
|
||||
// 建立关系之后从 pending 列表中删除
|
||||
database.removePendingField(this);
|
||||
|
||||
if (!this.options.foreignKey) {
|
||||
this.options.foreignKey = association.foreignKey;
|
||||
}
|
||||
@ -133,3 +144,9 @@ export class HasManyField extends RelationField {
|
||||
collection.model.refreshAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasManyFieldOptions
|
||||
extends BaseRelationFieldOptions,
|
||||
SequelizeHasManyOptions {
|
||||
type: 'hasMany';
|
||||
}
|
||||
|
@ -9,7 +9,9 @@ import {
|
||||
HasOneOptions,
|
||||
Utils,
|
||||
} 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 {
|
||||
/**
|
||||
@ -73,7 +75,6 @@ export interface HasOneFieldOptions extends HasOneOptions {
|
||||
}
|
||||
|
||||
export class HasOneField extends RelationField {
|
||||
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || Utils.pluralize(name);
|
||||
@ -85,10 +86,7 @@ export class HasOneField extends RelationField {
|
||||
}
|
||||
const { model } = this.context.collection;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasOneFieldOptions
|
||||
extends BaseRelationFieldOptions,
|
||||
SequelizeHasOneOptions {
|
||||
type: 'hasOne';
|
||||
}
|
||||
|
@ -1,9 +1,60 @@
|
||||
export * from './field';
|
||||
export * from './string-field';
|
||||
export * from './relation-field'
|
||||
export * from './belongs-to-field'
|
||||
import { StringFieldOptions } from './string-field';
|
||||
|
||||
import { BooleanFieldOptions } from './boolean-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 './has-one-field';
|
||||
export * from './boolean-field';
|
||||
export * from './date-field';
|
||||
export * from './has-many-field';
|
||||
export * from './has-one-field';
|
||||
export * from './json-field';
|
||||
export * from './number-field';
|
||||
export * from './relation-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 { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class JsonField extends Field {
|
||||
get dataType() {
|
||||
@ -7,6 +7,10 @@ export class JsonField extends Field {
|
||||
}
|
||||
}
|
||||
|
||||
export interface JsonFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'json';
|
||||
}
|
||||
|
||||
export class JsonbField extends Field {
|
||||
get dataType() {
|
||||
const dialect = this.context.database.sequelize.getDialect();
|
||||
@ -16,3 +20,6 @@ export class JsonbField extends Field {
|
||||
return DataTypes.JSON;
|
||||
}
|
||||
}
|
||||
export interface JsonbFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'jsonb';
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class IntegerField extends Field {
|
||||
get dataType() {
|
||||
@ -7,26 +7,46 @@ export class IntegerField extends Field {
|
||||
}
|
||||
}
|
||||
|
||||
export interface IntegerFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'integer';
|
||||
}
|
||||
|
||||
export class FloatField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.FLOAT;
|
||||
}
|
||||
}
|
||||
|
||||
export interface FloatFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'float';
|
||||
}
|
||||
|
||||
export class DoubleField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.DOUBLE;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DoubleFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'double';
|
||||
}
|
||||
|
||||
export class RealField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.REAL;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RealFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'real';
|
||||
}
|
||||
|
||||
export class DecimalField extends Field {
|
||||
get dataType() {
|
||||
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 {
|
||||
/**
|
||||
* target relation name
|
||||
*/
|
||||
get target() {
|
||||
const { target, name } = this.options;
|
||||
return target || name;
|
||||
@ -14,6 +19,10 @@ export abstract class RelationField extends Field {
|
||||
return this.options.sourceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* get target model from database by it's name
|
||||
* @constructor
|
||||
*/
|
||||
get TargetModel() {
|
||||
return this.context.database.sequelize.models[this.target];
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class SortField extends Field {
|
||||
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 { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class StringField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.STRING;
|
||||
}
|
||||
}
|
||||
|
||||
export interface StringFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'string';
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class TextField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.TEXT;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TextFieldOptions extends BaseColumnFieldOptions {
|
||||
type: 'text';
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class TimeField extends Field {
|
||||
get dataType() {
|
||||
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 { Field } from './field';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
|
||||
export class VirtualField extends Field {
|
||||
get dataType() {
|
||||
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 {
|
||||
Op,
|
||||
Association,
|
||||
BulkCreateOptions,
|
||||
CreateOptions as SequelizeCreateOptions,
|
||||
FindAndCountOptions as SequelizeAndCountOptions,
|
||||
FindOptions as SequelizeFindOptions,
|
||||
Model,
|
||||
ModelCtor,
|
||||
Association,
|
||||
FindOptions,
|
||||
BulkCreateOptions,
|
||||
DestroyOptions as SequelizeDestroyOptions,
|
||||
CreateOptions as SequelizeCreateOptions,
|
||||
UpdateOptions as SequelizeUpdateOptions,
|
||||
Op,
|
||||
Transaction,
|
||||
} from 'sequelize';
|
||||
import { flatten } from 'flat';
|
||||
|
||||
import { Collection } from './collection';
|
||||
import _ from 'lodash';
|
||||
import lodash, { omit } from 'lodash';
|
||||
import { Database } from './database';
|
||||
import { updateAssociations } from './update-associations';
|
||||
import { updateAssociations, updateModelByValues } from './update-associations';
|
||||
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 {}
|
||||
|
||||
interface CreateManyOptions extends BulkCreateOptions {}
|
||||
|
||||
interface FindManyOptions extends FindOptions {
|
||||
filter?: any;
|
||||
fields?: any;
|
||||
appends?: any;
|
||||
expect?: any;
|
||||
page?: any;
|
||||
pageSize?: any;
|
||||
sort?: any;
|
||||
interface CreateManyOptions extends BulkCreateOptions {
|
||||
records: Values[];
|
||||
}
|
||||
|
||||
interface FindOneOptions extends FindOptions {
|
||||
filter?: any;
|
||||
fields?: any;
|
||||
appends?: any;
|
||||
expect?: any;
|
||||
sort?: any;
|
||||
export interface TransactionAble {
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
interface CreateOptions extends SequelizeCreateOptions {
|
||||
values?: any;
|
||||
whitelist?: any;
|
||||
blacklist?: any;
|
||||
export interface FilterAble {
|
||||
filter: Filter;
|
||||
}
|
||||
|
||||
interface UpdateOptions extends SequelizeUpdateOptions {
|
||||
values?: any;
|
||||
whitelist?: any;
|
||||
blacklist?: any;
|
||||
export type PrimaryKey = string | number;
|
||||
export type PK = PrimaryKey | PrimaryKey[];
|
||||
|
||||
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 {
|
||||
filter?: any;
|
||||
export interface FilterByPK {
|
||||
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 {
|
||||
@ -69,101 +135,43 @@ interface RelatedQueryOptions {
|
||||
};
|
||||
}
|
||||
|
||||
type Identity = string | number;
|
||||
const transaction = transactionWrapperBuilder(function () {
|
||||
return (<Repository>this).collection.model.sequelize.transaction();
|
||||
});
|
||||
|
||||
class RelatedQuery {
|
||||
options: RelatedQueryOptions;
|
||||
sourceInstance: Model;
|
||||
class RelationRepositoryBuilder<R extends RelationRepository> {
|
||||
collection: Collection;
|
||||
associationName: string;
|
||||
association: Association;
|
||||
|
||||
constructor(options: RelatedQueryOptions) {
|
||||
this.options = options;
|
||||
builderMap = {
|
||||
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() {
|
||||
if (this.sourceInstance) {
|
||||
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;
|
||||
protected builder() {
|
||||
return this.builderMap;
|
||||
}
|
||||
|
||||
async findMany(options?: any) {
|
||||
const { collection } = this.options.target;
|
||||
return await collection.repository.findMany(options);
|
||||
of(id: string | number): R {
|
||||
const klass = this.builder()[this.association.associationType];
|
||||
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 {}
|
||||
|
||||
class HasManyQuery extends RelatedQuery {}
|
||||
|
||||
class BelongsToQuery extends RelatedQuery {}
|
||||
|
||||
class BelongsToManyQuery extends RelatedQuery {}
|
||||
|
||||
export class Repository implements IRepository {
|
||||
export class Repository<
|
||||
TModelAttributes extends {} = any,
|
||||
TCreationAttributes extends {} = TModelAttributes,
|
||||
> implements IRepository
|
||||
{
|
||||
database: Database;
|
||||
collection: Collection;
|
||||
model: ModelCtor<Model>;
|
||||
@ -174,280 +182,274 @@ export class Repository implements IRepository {
|
||||
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 transaction = await this.getTransaction(options);
|
||||
const opts = {
|
||||
subQuery: false,
|
||||
...this.buildQueryOptions(options),
|
||||
};
|
||||
let rows = [];
|
||||
if (opts.include) {
|
||||
|
||||
if (opts.include && opts.include.length > 0) {
|
||||
const ids = (
|
||||
await model.findAll({
|
||||
...opts,
|
||||
includeIgnoreAttributes: false,
|
||||
attributes: [model.primaryKeyAttribute],
|
||||
group: `${model.name}.${model.primaryKeyAttribute}`,
|
||||
transaction,
|
||||
})
|
||||
).map((item) => item[model.primaryKeyAttribute]);
|
||||
if (ids.length > 0) {
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
where: {
|
||||
).map((row) => row.get(model.primaryKeyAttribute));
|
||||
|
||||
const where = {
|
||||
[model.primaryKeyAttribute]: {
|
||||
[Op.in]: ids,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
rows = await model.findAll({
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
const count = await model.count({
|
||||
...opts,
|
||||
distinct: opts.include ? true : undefined,
|
||||
});
|
||||
return { count, rows };
|
||||
}
|
||||
|
||||
async findOne(options?: FindOneOptions) {
|
||||
const model = this.collection.model;
|
||||
const opts = {
|
||||
subQuery: false,
|
||||
...this.buildQueryOptions(options),
|
||||
};
|
||||
let data: Model;
|
||||
if (opts.include) {
|
||||
const item = await model.findOne({
|
||||
...opts,
|
||||
includeIgnoreAttributes: false,
|
||||
attributes: [model.primaryKeyAttribute],
|
||||
group: `${model.name}.${model.primaryKeyAttribute}`,
|
||||
|
||||
return await model.findAll({
|
||||
...omit(opts, ['limit', 'offset']),
|
||||
where,
|
||||
transaction,
|
||||
});
|
||||
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);
|
||||
return await model.findAll({
|
||||
...opts,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* find and count
|
||||
* @param options
|
||||
*/
|
||||
async findAndCount(
|
||||
options?: FindAndCountOptions,
|
||||
): Promise<[Model[], number]> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
options = {
|
||||
...options,
|
||||
transaction,
|
||||
};
|
||||
|
||||
return [await this.find(options), await this.count(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) {
|
||||
return;
|
||||
}
|
||||
await updateAssociations(instance, values, options);
|
||||
|
||||
await updateAssociations(instance, values, {
|
||||
transaction,
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async createMany(records: any[], options?: CreateManyOptions) {
|
||||
const instances = await this.collection.model.bulkCreate(records, options);
|
||||
const promises = instances.map((instance, index) => {
|
||||
return updateAssociations(instance, records[index]);
|
||||
/**
|
||||
* Save Many instances to database
|
||||
*
|
||||
* @param records
|
||||
* @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,
|
||||
});
|
||||
return Promise.all(promises);
|
||||
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
await updateAssociations(instances[i], records[i], { transaction });
|
||||
}
|
||||
|
||||
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 instances;
|
||||
}
|
||||
|
||||
async destroy(options: Identity | Identity[] | DestroyOptions) {
|
||||
if (typeof options === 'number' || typeof options === 'string') {
|
||||
return await this.model.destroy({
|
||||
where: {
|
||||
[this.model.primaryKeyAttribute]: options,
|
||||
},
|
||||
/**
|
||||
* 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({
|
||||
where: {
|
||||
[this.model.primaryKeyAttribute]: {
|
||||
[Op.in]: options,
|
||||
[Op.in]: filterByPk,
|
||||
},
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
const opts = this.buildQueryOptions(options);
|
||||
return await this.model.destroy(opts);
|
||||
}
|
||||
|
||||
// TODO
|
||||
async sort() {}
|
||||
if (options.filter) {
|
||||
const instances = await this.find({
|
||||
filter: options.filter,
|
||||
transaction,
|
||||
});
|
||||
|
||||
relatedQuery(name: string) {
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
return await this.destroy({
|
||||
filterByPk: instances.map(
|
||||
(instance) => instance[this.model.primaryKeyAttribute],
|
||||
),
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
buildQueryOptions(options: any) {
|
||||
const opts = this.parseFilter(options.filter);
|
||||
return { ...options, ...opts };
|
||||
if (options.truncate) {
|
||||
return await this.model.destroy({
|
||||
truncate: true,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
parseFilter(filter?: any) {
|
||||
if (!filter) {
|
||||
return {};
|
||||
/**
|
||||
* @param association target association
|
||||
*/
|
||||
relation<R extends RelationRepository>(
|
||||
association: string,
|
||||
): RelationRepositoryBuilder<R> {
|
||||
return new RelationRepositoryBuilder<R>(this.collection, association);
|
||||
}
|
||||
const model = this.collection.model;
|
||||
if (typeof filter === 'number' || typeof filter === 'string') {
|
||||
return {
|
||||
where: {
|
||||
[model.primaryKeyAttribute]: filter,
|
||||
},
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
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);
|
||||
|
||||
protected parseFilter(filter: Filter) {
|
||||
const parser = new FilterParser(
|
||||
this.collection.model,
|
||||
this.collection.context.database,
|
||||
filter,
|
||||
);
|
||||
return parser.toSequelizeParams();
|
||||
}
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if (skipPrefix && key.startsWith(skipPrefix)) {
|
||||
continue;
|
||||
|
||||
protected async getTransaction(options: any, autoGen = false) {
|
||||
if (lodash.isPlainObject(options) && options.transaction) {
|
||||
return options.transaction;
|
||||
}
|
||||
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;
|
||||
|
||||
if (autoGen) {
|
||||
return await this.model.sequelize.transaction();
|
||||
}
|
||||
} 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 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,
|
||||
Utils,
|
||||
Association,
|
||||
HasOne,
|
||||
BelongsTo,
|
||||
BelongsToMany,
|
||||
HasMany,
|
||||
} from 'sequelize';
|
||||
import { UpdateGuard } from './update-guard';
|
||||
import { TransactionAble } from './repository';
|
||||
|
||||
function isUndefinedOrNull(value: any) {
|
||||
return typeof value === 'undefined' || value === null;
|
||||
@ -15,39 +21,171 @@ function isStringOrNumber(value: any) {
|
||||
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(
|
||||
instance: Model,
|
||||
values: any,
|
||||
options: any = {},
|
||||
) {
|
||||
const { transaction = await instance.sequelize.transaction() } = options;
|
||||
// @ts-ignore
|
||||
for (const key of Object.keys(instance.constructor.associations)) {
|
||||
// 如果 key 不存在才跳过
|
||||
if (!Object.keys(values||{}).includes(key)) {
|
||||
continue;
|
||||
// if no values set, return
|
||||
if (!values) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
if (!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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* update model association by key
|
||||
* @param instance
|
||||
* @param key
|
||||
* @param value
|
||||
* @param options
|
||||
*/
|
||||
export async function updateAssociation(
|
||||
instance: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = instance.constructor.associations[key] as Association;
|
||||
const association = modelAssociationByKey(instance, key);
|
||||
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (association.associationType) {
|
||||
case 'HasOne':
|
||||
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(
|
||||
model: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = model.constructor.associations[key] as Association;
|
||||
const association = <HasOne | BelongsTo>modelAssociationByKey(model, key);
|
||||
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
// set method of association
|
||||
const setAccessor = association.accessors.set;
|
||||
if (isUndefinedOrNull(value)) {
|
||||
|
||||
const removeAssociation = async () => {
|
||||
await model[setAccessor](null, { transaction });
|
||||
model.setDataValue(key, null);
|
||||
if (!options.transaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (isUndefinedOrNull(value)) {
|
||||
return await removeAssociation();
|
||||
}
|
||||
|
||||
if (isStringOrNumber(value)) {
|
||||
await model[setAccessor](value, { transaction });
|
||||
if (!options.transaction) {
|
||||
@ -99,7 +253,7 @@ export async function updateSingleAssociation(
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// @ts-ignore
|
||||
|
||||
const createAccessor = association.accessors.create;
|
||||
let dataKey: string;
|
||||
let M: ModelCtor<Model>;
|
||||
@ -111,6 +265,7 @@ export async function updateSingleAssociation(
|
||||
M = association.source;
|
||||
dataKey = M.primaryKeyAttribute;
|
||||
}
|
||||
|
||||
if (isStringOrNumber(value[dataKey])) {
|
||||
let instance: any = await M.findOne({
|
||||
where: {
|
||||
@ -128,6 +283,11 @@ export async function updateSingleAssociation(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (value[dataKey] === null) {
|
||||
return await removeAssociation();
|
||||
}
|
||||
|
||||
const instance = await model[createAccessor](value, { transaction });
|
||||
await updateAssociations(instance, value, { transaction, ...options });
|
||||
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(
|
||||
model: Model,
|
||||
key: string,
|
||||
value: any,
|
||||
options: any = {},
|
||||
) {
|
||||
// @ts-ignore
|
||||
const association = model.constructor.associations[key] as Association;
|
||||
const association = <BelongsToMany | HasMany>(
|
||||
modelAssociationByKey(model, key)
|
||||
);
|
||||
|
||||
if (!association) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { transaction = await model.sequelize.transaction() } = options;
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
const setAccessor = association.accessors.set;
|
||||
// @ts-ignore
|
||||
|
||||
const createAccessor = association.accessors.create;
|
||||
if (isUndefinedOrNull(value)) {
|
||||
await model[setAccessor](null, { transaction });
|
||||
model.setDataValue(key, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isStringOrNumber(value)) {
|
||||
await model[setAccessor](value, { transaction });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
|
||||
const list1 = []; // to be setted
|
||||
const list2 = []; // to be added
|
||||
for (const item of value) {
|
||||
@ -194,23 +368,46 @@ export async function updateMultipleAssociation(
|
||||
list2.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// associate targets in lists1
|
||||
await model[setAccessor](list1, { transaction });
|
||||
|
||||
const list3 = [];
|
||||
for (const item of list2) {
|
||||
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])) {
|
||||
const instance = await model[createAccessor](item, { transaction });
|
||||
// create new record
|
||||
const instance = await model[createAccessor](item, accessorOptions);
|
||||
await updateAssociations(instance, item, { transaction, ...options });
|
||||
list3.push(instance);
|
||||
} else {
|
||||
const instance = await association.target.findByPk(item[pk], { transaction });
|
||||
// @ts-ignore
|
||||
// set & update record
|
||||
const instance = await association.target.findByPk(item[pk], {
|
||||
transaction,
|
||||
});
|
||||
const addAccessor = association.accessors.add;
|
||||
await model[addAccessor](item[pk], { transaction });
|
||||
|
||||
await model[addAccessor](item[pk], accessorOptions);
|
||||
await updateAssociations(instance, item, { transaction, ...options });
|
||||
list3.push(instance);
|
||||
}
|
||||
}
|
||||
|
||||
model.setDataValue(key, list1.concat(list3));
|
||||
if (!options.transaction) {
|
||||
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 {
|
||||
fiter: {
|
||||
and: [
|
||||
{ a: 'a' },
|
||||
{ b: 'b' },
|
||||
{ c: 'c' },
|
||||
{ 'assoc.a': 'abc1' },
|
||||
{ 'assoc.b': 'abc2' },
|
||||
{ 'assoc.c': 'abc3' },
|
||||
{
|
||||
and: [
|
||||
{ 'assoc.a': 'abc1' },
|
||||
{ 'assoc.b': 'abc2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let IDX = 36,
|
||||
HEX = '';
|
||||
while (IDX--) HEX += IDX.toString(36);
|
||||
|
||||
export function uid(len?: number) {
|
||||
let str = '',
|
||||
num = len || 11;
|
||||
while (num--) str += HEX[(Math.random() * 36) | 0];
|
||||
return str;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
registerMiddlewares,
|
||||
} from './helper';
|
||||
import { i18n, InitOptions } from 'i18next';
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
|
||||
export interface ResourcerOptions {
|
||||
prefix?: string;
|
||||
@ -51,10 +52,10 @@ interface ActionsOptions {
|
||||
resourceNames?: string[];
|
||||
}
|
||||
|
||||
export class Application<
|
||||
StateT = DefaultState,
|
||||
ContextT = DefaultContext
|
||||
> extends Koa {
|
||||
export class Application<StateT = DefaultState, ContextT = DefaultContext>
|
||||
extends Koa
|
||||
implements AsyncEmitter
|
||||
{
|
||||
public readonly db: Database;
|
||||
|
||||
public readonly resourcer: Resourcer;
|
||||
@ -160,57 +161,6 @@ export class Application<
|
||||
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) {
|
||||
await this.load();
|
||||
return this.cli.parseAsync(argv);
|
||||
@ -219,6 +169,9 @@ export class Application<
|
||||
async destroy() {
|
||||
await this.db.close();
|
||||
}
|
||||
|
||||
emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||
}
|
||||
applyMixins(Application, [AsyncEmitter]);
|
||||
|
||||
export default Application;
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from './mixin';
|
||||
export * from './mixin/AsyncEmitter';
|
||||
export * from './merge';
|
||||
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,
|
||||
"target": "ES6",
|
||||
"paths": {
|
||||
"@nocobase/*": [
|
||||
"./packages/*/src"
|
||||
]
|
||||
"@nocobase/*": ["./packages/*/src"]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"./packages/*/esm",
|
||||
"./packages/*/lib"
|
||||
]
|
||||
"exclude": ["./packages/*/esm", "./packages/*/lib"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user