first commit

This commit is contained in:
chen 2020-10-24 15:34:43 +08:00
commit e5d30b30ba
509 changed files with 15121 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
DIALECT=postgres

17
.eslintrc Executable file
View File

@ -0,0 +1,17 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"globals": {
"sleep": true,
"prettyFormat": true
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
// "quotes": ["error", "single"]
}
}

23
.fatherrc.ts Executable file
View File

@ -0,0 +1,23 @@
import { readdirSync } from 'fs';
import { join } from 'path';
// utils must build before core
// runtime must build before renderer-react
const headPkgs = [
'database',
'resourcer',
'actions',
];
const tailPkgs = [];
const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(
(pkg) => {
return pkg !== 'father-build' && pkg.charAt(0) !== '.' && !headPkgs.includes(pkg) && !tailPkgs.includes(pkg)
},
);
export default {
target: 'node',
cjs: { type: 'babel', lazy: true },
// disableTypeCheck: true,
pkgs: [...headPkgs, ...otherPkgs, ...tailPkgs],
};

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
node_modules/
lib/
.DS_Store
package-lock.json
yarn.lock
yarn-error.log
lerna-debug.log
packages/database/package-lock.json
packages/resourcer/package-lock.json
verdaccio

38
docker-compose.yml Normal file
View File

@ -0,0 +1,38 @@
version: "3"
networks:
node-network:
driver: bridge
services:
verdaccio:
image: verdaccio/verdaccio
container_name: "verdaccio"
networks:
- node-network
environment:
- VERDACCIO_PORT=4873
ports:
- "4873:4873"
# volumes:
# - "./verdaccio/storage:/verdaccio/storage"
# - "./verdaccio/config:/verdaccio/conf"
# - "./verdaccio/plugins:/verdaccio/plugins"
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: "test"
MYSQL_USER: "test"
MYSQL_PASSWORD: "test"
MYSQL_ROOT_PASSWORD: "test"
restart: always
ports:
- "43306:3306"
postgres:
image: postgres:10
restart: always
ports:
- "45432:5432"
command: postgres -c wal_level=logical
environment:
POSTGRES_USER: test
POSTGRES_DB: test
POSTGRES_PASSWORD: test

1
dotenv.js Normal file
View File

@ -0,0 +1 @@
require('dotenv').config();

11
jest.config.js Executable file
View File

@ -0,0 +1,11 @@
const path = require('path');
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFiles: [path.resolve(__dirname, 'dotenv.js')],
testMatch: [
// '**/__tests__/**/*.[jt]s?(x)',
'**/?(*.)+(spec|test).[jt]s?(x)'
],
};

19
lerna.json Normal file
View File

@ -0,0 +1,19 @@
{
"packages": [
"packages/*"
],
"version": "independent",
"command": {
"bootstrap": {
"npmClientArgs": [
"--no-package-lock"
]
},
"publish": {
"allowBranch": "master",
"ignoreChanges": [
"*.md"
]
}
}
}

47
package.json Normal file
View File

@ -0,0 +1,47 @@
{
"name": "root",
"private": true,
"scripts": {
"bootstrap": "lerna bootstrap --no-ci",
"build": "npm run build-father-build && node packages/father-build/bin/father-build.js",
"build-father-build": "cd packages/father-build && npm run build",
"clean": "lerna clean",
"db:start": "docker-compose up -d",
"lint": "eslint --ext .ts,.tsx,.js \"packages/*/src/**.@(ts|tsx|js)\" --fix",
"test": "npm run lint && jest --clearCache && jest",
"release": "npm run build && lerna publish --yes --registry https://npm.pkg.github.com"
},
"devDependencies": {
"@koa/router": "^9.3.1",
"@types/jest": "^26.0.4",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa-mount": "^4.0.0",
"@types/koa__router": "^8.0.2",
"@types/lodash": "^4.14.158",
"@types/node": "^14.0.23",
"@types/supertest": "^2.0.10",
"@typescript-eslint/eslint-plugin": "^3.6.1",
"@typescript-eslint/parser": "^3.6.1",
"cross-env": "^7.0.2",
"dotenv": "^8.2.0",
"eslint": "^7.4.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"jest": "^26.1.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"lerna": "^3.22.0",
"mysql2": "^2.1.0",
"nodemon": "^2.0.4",
"path-to-regexp": "^6.1.0",
"pg": "^8.3.0",
"pg-hstore": "^2.3.3",
"sequelize": "^6.3.4",
"supertest": "^4.0.2",
"ts-jest": "^26.1.2",
"ts-node": "^8.10.2",
"typescript": "^3.9.6"
}
}

View File

@ -0,0 +1,24 @@
{
"name": "@nocobase/actions",
"version": "0.3.0-alpha.0",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
},
"license": "MIT",
"dependencies": {
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/resourcer": "^0.3.0-alpha.0"
},
"devDependencies": {
"koa": "^2.13.0",
"sequelize": "^6.3.4",
"typescript": "^3.9.6"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nocobase/nocobase.git",
"directory": "packages/actions"
}
}

View File

@ -0,0 +1,43 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('add', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('belongsToMany1', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
let post = await Post.create();
let tag1 = await Tag.create({name: 'tag1'});
let tag2 = await Tag.create({name: 'tag2'});
await request(http.createServer(app.callback())).post(`/posts/${post.id}/tags:add/${tag1.id}`);
await request(http.createServer(app.callback())).post(`/posts/${post.id}/tags:add/${tag2.id}`);
let [tag01, tag02] = await post.getTags();
expect(tag01.id).toBe(tag1.id);
expect(tag02.id).toBe(tag2.id);
});
});

View File

@ -0,0 +1,130 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('common', () => {
let db: Database;
let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
resourcer = config.resourcer;
resourcer.define({
name: 'posts',
actions: actions.common,
});
});
afterAll(async () => {
await db.close();
});
it('create', async () => {
const response = await request(http.createServer(app.callback()))
.post('/posts')
.send({
title: 'title1',
});
expect(response.body.title).toBe('title1');
});
it('update', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await request(http.createServer(app.callback()))
.put(`/posts/${post.id}`)
.send({
title: 'title2',
});
expect(response.body.title).toBe('title2');
});
it('get', async () => {
const Post = db.getModel('posts');
const post = await Post.create({title: 'title3'});
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}`);
expect(response.body.title).toBe('title3');
});
it('delete', async () => {
const Post = db.getModel('posts');
let post = await Post.create();
await request(http.createServer(app.callback()))
.delete(`/posts/${post.id}`);
post = await Post.findByPk(post.id);
expect(post).toBeNull();
});
describe('list', () => {
beforeAll(async () => {
const Post = db.getModel('posts');
const items = [];
for (let index = 0; index < 2; index++) {
items.push({
title: `title${index}`,
status: 'draft',
});
}
await Post.bulkCreate(items);
});
it('list1', async () => {
const Post = db.getModel('posts');
const response = await request(http.createServer(app.callback())).get('/posts');
expect(response.body.count).toBe(await Post.count());
});
it('list2', async () => {
const Post = db.getModel('posts');
const response = await request(http.createServer(app.callback())).get('/posts?filter[title]=title1');
expect(response.body.count).toBe(await Post.count({
where: {
title: 'title1',
},
}));
});
it('list3', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=draft&fields=title&page=1');
expect(response.body).toEqual({
count: 2,
page: 1,
per_page: 20,
rows: [ { title: 'title0' }, { title: 'title1' } ],
});
});
it('list4', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=draft&fields=title&page=1&perPage=1');
expect(response.body).toEqual({
count: 2,
page: 1,
per_page: 1,
rows: [ { title: 'title0' } ],
});
});
it('list5', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=draft&fields=title&page=2&per_page=1');
expect(response.body).toEqual({
count: 2,
page: 2,
per_page: 1,
rows: [ { title: 'title1' } ],
});
});
it('list6', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?fields=title&filter[customTitle]=title0&filter[status]=draft');
expect(response.body).toEqual({ count: 1, rows: [ { title: 'title0' } ] });
});
})
});

View File

@ -0,0 +1,55 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('create', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
describe('common', () => {
it('create', async () => {
const response = await request(http.createServer(app.callback()))
.post('/posts')
.send({
title: 'title1',
});
expect(response.body.title).toBe('title1');
});
});
describe('hasMany', () => {
it('create', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await request(http.createServer(app.callback()))
.post(`/posts/${post.id}/comments`)
.send({
content: 'content1',
});
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content1');
});
});
});

View File

@ -0,0 +1,95 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('destroy', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('common1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await request(http.createServer(app.callback()))
.delete(`/posts/${post.id}`);
// console.log(response.body);
expect(response.body).toBe(post.id);
});
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1122',
}
});
const response = await request(http.createServer(app.callback()))
.delete(`/users/${user.id}/profile`);
const profile = await user.getProfile();
expect(profile).toBeNull();
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{content: 'content111222'},
],
});
let [comment] = await post.getComments();
await request(http.createServer(app.callback()))
.delete(`/posts/${post.id}/comments/${comment.id}`);
const count = await post.countComments();
expect(count).toBe(0);
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
user: {name: 'name121234'},
});
await request(http.createServer(app.callback())).delete(`/posts/${post.id}/user:destroy`);
const user = await post.getUser();
expect(user).toBeNull();
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{name: 'tag112233'},
],
});
const [tag] = await post.getTags();
await request(http.createServer(app.callback()))
.delete(`/posts/${post.id}/tags:destroy/${tag.id}`);
const tags = await post.getTags();
expect(tags.length).toBe(0);
});
});

View File

@ -0,0 +1,111 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('get', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('common1', async () => {
const Post = db.getModel('posts');
const post = await Post.create({
title: 'title11112222'
});
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}`);
expect(response.body.title).toBe('title11112222');
});
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await request(http.createServer(app.callback()))
.get(`/users/${user.id}/profile?fields=email`);
expect(response.body).toEqual({});
});
it('hasOne2', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1',
},
});
const response = await request(http.createServer(app.callback()))
.get(`/users/${user.id}/profile?fields=email`);
expect(response.body).toEqual({ email: 'email1' });
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{content: 'content111222'},
],
});
const [comment] = await post.getComments();
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/comments/${comment.id}`);
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content111222');
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/user?fields=name`);
expect(response.body).toEqual({});
});
it('belongsTo2', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
user: {name: 'name121234'},
});
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/user?fields=name`);
expect(response.body).toEqual({name: 'name121234'});
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{name: 'tag112233'},
],
});
const [tag] = await post.getTags();
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/tags/${tag.id}?fields=name,posts.id`);
expect(response.body.posts[0].id).toBe(post.id);
expect(response.body.name).toBe('tag112233');
});
});

View File

@ -0,0 +1,75 @@
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import Koa from 'koa';
import { Options } from 'sequelize';
import bodyParser from 'koa-bodyparser';
import associated from '../middlewares/associated';
import actions from '..';
export const config: {
[key: string]: Options;
} = {
mysql: {
username: 'test',
password: 'test',
database: 'test',
host: '127.0.0.1',
port: 43306,
dialect: 'mysql',
},
postgres: {
username: 'test',
password: 'test',
database: 'test',
host: '127.0.0.1',
port: 45432,
dialect: 'postgres',
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
logging: false,
},
};
export function getConfig() {
const app = new Koa();
const database = new Database(config.postgres);
const resourcer = new Resourcer();
resourcer.use(associated);
resourcer.registerHandlers({...actions.associate, ...actions.common});
resourcer.define({
name: 'posts',
actions: actions.common,
});
resourcer.define({
type: 'hasOne',
name: 'users.profile',
actions: actions.associate,
});
resourcer.define({
type: 'hasMany',
name: 'posts.comments',
actions: actions.associate,
});
resourcer.define({
type: 'belongsTo',
name: 'posts.user',
actions: actions.associate,
});
resourcer.define({
type: 'belongsToMany',
name: 'posts.tags',
actions: actions.associate,
});
app.use(async (ctx, next) => {
ctx.db = database;
await next();
});
app.use(bodyParser());
app.use(resourcer.middleware());
return { app, database, resourcer };
}

View File

@ -0,0 +1,148 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('list', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
describe('common', () => {
beforeAll(async () => {
const Post = db.getModel('posts');
const items = [];
for (let index = 0; index < 2; index++) {
items.push({
title: `title${index}`,
status: 'common',
});
}
await Post.bulkCreate(items);
});
it('list1', async () => {
const Post = db.getModel('posts');
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=common');
expect(response.body.count).toBe(await Post.count({where: {status: 'common'}}));
});
it('list2', async () => {
const Post = db.getModel('posts');
const response = await request(http.createServer(app.callback())).get('/posts?filter[title]=title1');
expect(response.body.count).toBe(await Post.count({
where: {
title: 'title1',
},
}));
});
it('list3', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=common&fields=title&page=1');
expect(response.body).toEqual({
count: 2,
page: 1,
per_page: 20,
rows: [ { title: 'title0' }, { title: 'title1' } ],
});
});
it('list4', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=common&fields=title&sort=title&page=2&perPage=1');
expect(response.body).toEqual({
count: 2,
page: 2,
per_page: 1,
rows: [ { title: 'title1' } ],
});
});
it('list5', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?filter[status]=common&fields=title&page=2&per_page=1');
expect(response.body).toEqual({
count: 2,
page: 2,
per_page: 1,
rows: [ { title: 'title1' } ],
});
});
it('list6', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?fields=title&filter[customTitle]=title0&filter[status]=common');
expect(response.body).toEqual({ count: 1, rows: [ { title: 'title0' } ] });
});
});
describe('hasMany', () => {
it('list1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{content: 'content1', status: 'published'},
{content: 'content2', status: 'published'},
{content: 'content3', status: 'draft'},
{content: 'content4', status: 'published'},
{content: 'content5', status: 'draft'},
{content: 'content6', status: 'published'},
],
});
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/comments?page=2&perPage=2&sort=content&fields=content&filter[published]=1`);
expect(response.body).toEqual({
rows: [ { content: 'content4' }, { content: 'content6' } ],
count: 4,
page: 2,
per_page: 2
});
});
});
describe('belongsToMany', () => {
it('list1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{name: 'tag1', status: 'published'},
{name: 'tag2', status: 'draft'},
{name: 'tag3', status: 'published'},
{name: 'tag4', status: 'draft'},
{name: 'tag5', status: 'published'},
{name: 'tag6', status: 'draft'},
{name: 'tag7', status: 'published'},
],
});
const response = await request(http.createServer(app.callback()))
.get(`/posts/${post.id}/tags?page=2&perPage=2&sort=name&fields=name&filter[published]=1`);
expect(response.body).toEqual({
rows: [ { name: 'tag5' }, { name: 'tag7' } ],
count: 4,
page: 2,
per_page: 2
});
});
});
});

View File

@ -0,0 +1,73 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '../';
import { getConfig } from './index';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { Context } from '../actions';
import jsonReponse from '../middlewares/json-reponse';
describe('middleware', () => {
let db: Database;
let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.table({
name: 'posts',
tableName: 'actions__m__posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'status',
defaultValue: 'publish',
}
],
scopes: {
customTitle: (title, ctx: Context) => {
return {
where: {
title: title,
},
}
},
}
});
await db.sync({
force: true,
});
resourcer = config.resourcer;
resourcer.define({
name: 'posts',
middlewares: [
jsonReponse,
],
actions: actions.common,
});
});
afterAll(async () => {
await db.close();
});
it('create', async () => {
const response = await request(http.createServer(app.callback()))
.post('/posts')
.send({
title: 'title1',
});
expect(response.body.data.title).toBe('title1');
});
it('list', async () => {
const response = await request(http.createServer(app.callback())).get('/posts?fields=title&page=1');
expect(response.body).toEqual({
data: [ { title: 'title1' } ],
meta: { count: 1, page: 1, per_page: 20 }
});
});
});

View File

@ -0,0 +1,96 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('remove', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
await user.updateAssociations({
profile: {
email: 'email1122',
}
});
const response = await request(http.createServer(app.callback()))
.post(`/users/${user.id}/profile:remove`);
const profile = await user.getProfile();
expect(profile).toBeNull();
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{content: 'content111222'},
],
});
let [comment] = await post.getComments();
await request(http.createServer(app.callback()))
.post(`/posts/${post.id}/comments:remove/${comment.id}`);
const count = await post.countComments();
expect(count).toBe(0);
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
let post = await Post.create();
await post.updateAssociations({
user: {name: 'name121234'},
});
await request(http.createServer(app.callback())).post(`/posts/${post.id}/user:remove`);
post = await Post.findOne({
where: {
id: post.id,
}
});
const user = await post.getUser();
expect(user).toBeNull();
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{
name: 'tag112233',
posts_tags: {
test: 'test1',
}
},
],
});
const [tag] = await post.getTags();
await request(http.createServer(app.callback()))
.delete(`/posts/${post.id}/tags:remove/${tag.id}`);
const tags = await post.getTags();
expect(tags.length).toBe(0);
});
});

View File

@ -0,0 +1,61 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('set', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const User = db.getModel('users');
let post = await Post.create();
let user = await User.create();
await request(http.createServer(app.callback())).post(`/posts/${post.id}/user:set/${user.id}`);
post = await Post.findOne({
where: {
id: post.id,
}
});
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
});
it('belongsToMany1', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
let post = await Post.create();
let tag1 = await Tag.create({name: 'tag1'});
let tag2 = await Tag.create({name: 'tag2'});
await request(http.createServer(app.callback())).post(`/posts/${post.id}/tags:set/${tag1.id}`);
let [tag01] = await post.getTags();
expect(tag1.id).toBe(tag01.id);
expect(await post.countTags()).toBe(1);
await request(http.createServer(app.callback())).post(`/posts/${post.id}/tags:set/${tag2.id}`);
let [tag02] = await post.getTags();
expect(tag2.id).toBe(tag02.id);
expect(await post.countTags()).toBe(1);
});
});

View File

@ -0,0 +1,31 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'comments',
tableName: 'actions__comments',
fields: [
{
type: 'string',
name: 'content',
},
{
type: 'string',
name: 'status',
},
{
type: 'belongsTo',
name: 'post',
},
{
type: 'belongsTo',
name: 'user',
}
],
scopes: {
published: {
where: {
status: 'published'
}
}
},
} as TableOptions;

View File

@ -0,0 +1,42 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'posts',
tableName: 'actions__posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'string',
name: 'status',
defaultValue: 'publish',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'hasmany',
name: 'comments',
},
{
type: 'belongsToMany',
name: 'tags',
},
],
hooks: {
beforeCreate(model, options) {
},
},
scopes: {
customTitle: (title, ctx) => {
return {
where: {
title: title,
},
}
},
},
} as TableOptions;

View File

@ -0,0 +1,12 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'posts_tags',
tableName: 'actions__posts_tags',
fields: [
{
type: 'string',
name: 'test',
},
],
} as TableOptions;

View File

@ -0,0 +1,12 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'profiles',
tableName: 'actions__profiles',
fields: [
{
type: 'string',
name: 'email',
},
],
} as TableOptions;

View File

@ -0,0 +1,27 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'tags',
tableName: 'actions__tags',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'status',
},
{
type: 'belongsToMany',
name: 'posts',
},
],
scopes: {
published: {
where: {
status: 'published'
}
}
}
} as TableOptions;

View File

@ -0,0 +1,16 @@
import { TableOptions } from "@nocobase/database";
export default {
name: 'users',
tableName: 'actions__users',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasone',
name: 'profile',
},
],
} as TableOptions;

View File

@ -0,0 +1,106 @@
import Koa from 'koa';
import http from 'http';
import request from 'supertest';
import actions from '..';
import { getConfig } from './index';
import Database, { Model } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { resolve } from 'path';
describe('update', () => {
let db: Database;
// let resourcer: Resourcer;
let app: Koa;
beforeAll(async () => {
const config = getConfig();
app = config.app;
db = config.database;
db.import({
directory: resolve(__dirname, './tables'),
});
await db.sync({
force: true,
});
// resourcer = config.resourcer;
});
afterAll(async () => {
await db.close();
});
it('common1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
const response = await request(http.createServer(app.callback()))
.put(`/posts/${post.id}`).send({
title: 'title11112222'
});
expect(response.body.title).toBe('title11112222');
});
it('hasOne1', async () => {
const User = db.getModel('users');
const user = await User.create();
const response = await request(http.createServer(app.callback()))
.put(`/users/${user.id}/profile`).send({
email: 'email1122',
});
expect(response.body.email).toEqual('email1122');
});
it('hasMany1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
comments: [
{content: 'content111222'},
],
});
const [comment] = await post.getComments();
const response = await request(http.createServer(app.callback()))
.put(`/posts/${post.id}/comments/${comment.id}`).send({content: 'content111222333'});
expect(response.body.post_id).toBe(post.id);
expect(response.body.content).toBe('content111222333');
});
it('belongsTo1', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
user: {name: 'name121234'},
});
const response = await request(http.createServer(app.callback()))
.post(`/posts/${post.id}/user:update`).send({name: 'name1212345'});
expect(response.body.name).toEqual('name1212345');
});
it('belongsToMany', async () => {
const Post = db.getModel('posts');
const post = await Post.create();
await post.updateAssociations({
tags: [
{name: 'tag112233'},
],
});
const [tag] = await post.getTags();
let response = await request(http.createServer(app.callback()))
.post(`/posts/${post.id}/tags:update/${tag.id}`).send({
name: 'tag11223344',
posts_tags: {
test: 'test1',
},
});
const [tag1] = await post.getTags();
expect(tag1.posts_tags.test).toBe('test1');
expect(response.body.name).toBe('tag11223344');
response = await request(http.createServer(app.callback()))
.post(`/posts/${post.id}/tags:update/${tag.id}`).send({
posts_tags: {
test: 'test112233',
},
});
const [tag2] = await post.getTags();
expect(tag2.posts_tags.test).toBe('test112233');
});
});

View File

@ -0,0 +1,206 @@
import { Context, Next } from '.';
import { list, get, create, update, destroy } from './common';
import { HasOne, BelongsTo, BelongsToMany, HasMany, Model, Relation } from '@nocobase/database';
import { Op } from 'sequelize';
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function set(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { set: setAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](model);
await next();
}
/**
*
*
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function add(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const { add: addAccessor } = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
// const options = TargetModel.parseApiJson({
// fields,
// });
const model = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](model);
await next();
}
/**
*
*
* BlongsTo
* BlongsToMany
*
* @param ctx
* @param next
*/
export async function remove(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const {get: getAccessor, remove: removeAccessor, set: setAccessor} = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
ctx.body = await associated[setAccessor](null);
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
await associated[removeAccessor](model);
ctx.body = {id: model.id};
}
await next();
}
export async function toggle(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const {get: getAccessor, remove: removeAccessor, set: setAccessor, add: addAccessor} = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
const m1 = await associated[getAccessor]();
if (m1 && m1[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute] == resourceKey) {
ctx.body = await associated[setAccessor](null);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[setAccessor](m2);
}
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
if (model) {
ctx.body = await associated[removeAccessor](model);
} else {
const m2 = await TargetModel.findOne({
// ...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore
context: ctx,
});
ctx.body = await associated[addAccessor](m2);
}
}
await next();
}
export default {
list, // hasMany、belongsToMany
get, // 所有关系都有
create, // hasMany
update, // hasOne, hasMany, blongsToMany 中间表的数据更新
destroy, // 所有情况
set, // belongsTo、blongsToMany
add, // blongsToMany
remove, // belongsTo、blongsToMany
toggle, // blongsToMany
}

View File

@ -0,0 +1,358 @@
import { Context, Next } from '.';
import { Relation, Model, Field, HasOne, HasMany, BelongsTo, BelongsToMany } from '@nocobase/database';
import { Utils, Op, Sequelize } from 'sequelize';
import { isEmpty } from 'lodash';
/**
*
*
* - Signle
* - HasMany
* - BelongsToMany
*
* HasOne belongsTo list
*
* @param ctx
* @param next
*/
export async function list(ctx: Context, next: Next) {
const {
page,
perPage,
sort = [],
fields = [],
filter = {},
associated,
associatedName,
resourceName,
resourceField,
} = ctx.action.params;
const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({
sort,
page,
perPage,
filter,
fields,
context: ctx,
});
let data = {};
if (page || perPage) {
options.limit = 1*(perPage||20);
options.offset = options.limit * (page > 0 ? page - 1 : 0);
}
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const getAccessor = resourceField.getAccessors().get;
const countAccessor = resourceField.getAccessors().count;
options.scope = options.scopes||[];
const rows = await associated[getAccessor]({
joinTableAttributes: [],
...options,
context: ctx,
});
delete options.attributes;
delete options.limit;
delete options.offset;
delete options.order;
if (options.include) {
options.include = options.include.map(includeOptions => {
includeOptions.attributes = [];
return includeOptions;
});
}
const count = await associated[countAccessor]({ ...options, context: ctx });
data = {
rows,
count,
};
} else {
data = await Model.scope(options.scopes||[]).findAndCountAll({
...options,
// @ts-ignore hooks 里添加 context
context: ctx,
});
}
if (page || perPage) {
data['page'] = 1*(page||1);
data[Utils.underscoredIf('perPage', Model.options.underscored)] = 1*(perPage||20);
}
ctx.body = data;
await next();
}
/**
*
*
* Signle
* HasMany
*
* resource action HasOneBelongsToBelongsToMany
* 使 model.updateAssociations
*
* TODO
*
* @param ctx
* @param next
*/
export async function create(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
values,
} = ctx.action.params as { associated: Model, associatedName: string, resourceField: Relation, values: any };
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const create = resourceField.getAccessors().create;
const model: Model = await associated[create](values, { context: ctx });
await model.updateAssociations(values, { context: ctx });
ctx.body = model;
} else {
const { resourceName } = ctx.action.params;
const Model = ctx.db.getModel(resourceName);
// @ts-ignore
const model = await Model.create(values, { context: ctx });
// @ts-ignore
await model.updateAssociations(values, { context: ctx });
ctx.body = model;
}
await next();
}
/**
*
*
* @param ctx
* @param next
*/
export async function get(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const getAccessor = resourceField.getAccessors().get;
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
const model: Model = await associated[getAccessor]({ ...options, context: ctx });
ctx.body = model;
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
ctx.body = model;
}
} else {
const { resourceName, resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({
fields,
});
const data = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore hooks 里添加 context
context: ctx,
});
ctx.body = data;
}
await next();
}
/**
*
*
* TODO
*
* @param ctx
* @param next
*/
export async function update(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const {get: getAccessor, create: createAccessor, add: addAccessor} = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [], values } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
let model: Model = await associated[getAccessor]({ ...options, context: ctx });
if (model) {
// @ts-ignore
await model.update(values, { context: ctx });
} else if (!model && resourceField instanceof HasOne) {
model = await associated[createAccessor](values, { context: ctx });
}
await model.updateAssociations(values, { context: ctx });
ctx.body = model;
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
if (resourceField instanceof BelongsToMany) {
const throughName = resourceField.getThroughName();
if (typeof values[throughName] === 'object') {
const ThroughModel = resourceField.getThroughModel();
const throughValues = values[throughName];
const { foreignKey, sourceKey, otherKey } = resourceField.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: associated[sourceKey],
[otherKey]: resourceKey,
},
});
await through.update(throughValues);
await through.updateAssociations(throughValues);
delete values[throughName];
}
}
if (!isEmpty(values)) {
// @ts-ignore
await model.update(values, { context: ctx });
await model.updateAssociations(values, { context: ctx });
}
ctx.body = model;
}
} else {
const { resourceName, resourceKey, resourceKeyAttribute, fields = [], values } = ctx.action.params;
const Model = ctx.db.getModel(resourceName);
const options = Model.parseApiJson({
fields,
});
const model = await Model.findOne({
...options,
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey,
},
// @ts-ignore hooks 里添加 context
context: ctx,
});
// @ts-ignore
await model.update(values, { context: ctx });
// @ts-ignore
await model.updateAssociations(values, { context: ctx });
ctx.body = model;
}
await next();
}
/**
*
*
* Single
* HasOne
* HasMany
*
* TODO onUpdate/onDelete
*
* @param ctx
* @param next
*/
export async function destroy(ctx: Context, next: Next) {
const {
associated,
resourceField,
associatedName,
} = ctx.action.params as {
associated: Model,
associatedName: string,
resourceField: Relation,
values: any,
};
if (associated && resourceField) {
const AssociatedModel = ctx.db.getModel(associatedName);
if (!(associated instanceof AssociatedModel)) {
throw new Error(`${associatedName} associated model invalid`);
}
const {get: getAccessor, remove: removeAccessor, set: setAccessor} = resourceField.getAccessors();
const { resourceKey, resourceKeyAttribute, fields = [] } = ctx.action.params;
const TargetModel = ctx.db.getModel(resourceField.getTarget());
const options = TargetModel.parseApiJson({
fields,
});
if (resourceField instanceof HasOne || resourceField instanceof BelongsTo) {
const model: Model = await associated[getAccessor]({ ...options, context: ctx });
await associated[setAccessor](null);
ctx.body = await model.destroy();
} else if (resourceField instanceof HasMany || resourceField instanceof BelongsToMany) {
const [model]: Model[] = await associated[getAccessor]({
...options,
where: {
[resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey,
},
context: ctx,
});
await associated[removeAccessor](model);
ctx.body = await model.destroy();
}
} else {
const { resourceName, resourceKey, resourceKeyAttribute, values } = ctx.action.params;
const Model = ctx.db.getModel(resourceName);
const resourceKeys = resourceKey ? resourceKey.split(',') : values[`${resourceKeyAttribute || Model.primaryKeyAttribute}s`];
const data = await Model.destroy({
where: {
[resourceKeyAttribute || Model.primaryKeyAttribute]: {
[Op.in]: resourceKeys,
},
},
// @ts-ignore hooks 里添加 context
context: ctx,
});
ctx.body = data;
}
await next();
}
export default {
list, // single、hasMany、belongsToMany
create, // signle、hasMany
get, // all
update, // single、
destroy,
};

View File

@ -0,0 +1,13 @@
import Koa from 'koa';
import Database from '@nocobase/database';
import { Action } from '@nocobase/resourcer';
export type Next = () => Promise<any>;
export type Context = Koa.Context & {
db: Database;
action: Action;
};
export { default as common } from './common';
export { default as associate } from './associate';

View File

@ -0,0 +1,5 @@
import * as actions from './actions';
export * from './middleware';
export default actions;

View File

@ -0,0 +1,21 @@
import { Context, Next } from './actions';
import { Action } from '@nocobase/resourcer';
export async function middleware(ctx: Context, next: Next) {
await next();
if (ctx.action instanceof Action) {
const { rows, ...meta } = ctx.body;
if (rows) {
ctx.body = {
data: rows,
meta,
};
} else {
ctx.body = {
data: ctx.body,
};
}
}
}
export default middleware;

View File

@ -0,0 +1,59 @@
import { Context, Next, associate } from '../actions';
import { Action } from '@nocobase/resourcer';
import { HasOne, HasMany, BelongsTo, BelongsToMany, Model } from '@nocobase/database';
export async function associated(ctx: Context, next: Next) {
if (!(ctx.action instanceof Action)) {
return next();
}
const { associated, associatedName, associatedKey, resourceName } = ctx.action.params;
if (!associatedName || !associatedKey) {
return next();
}
if (associated) {
return next();
}
const Model = ctx.db.getModel(associatedName);
const field = ctx.db.getTable(associatedName).getField(resourceName);
let model: Model;
if (field instanceof HasOne) {
model = await Model.findOne({
where: {
[field.options.sourceKey]: associatedKey,
}
});
} else if (field instanceof HasMany) {
model = await Model.findOne({
where: {
[field.options.sourceKey]: associatedKey,
}
});
} else if (field instanceof BelongsTo) {
model = await Model.findOne({
where: {
[Model.primaryKeyAttribute]: associatedKey,
}
});
} else if (field instanceof BelongsToMany) {
model = await Model.findOne({
where: {
[field.options.sourceKey]: associatedKey,
}
});
}
if (model) {
ctx.action.setParam('associated', model);
ctx.action.setParam('resourceField', field);
}
await next();
}
export default associated;

View File

@ -0,0 +1,21 @@
import { Context, Next } from '../actions';
import { Action } from '@nocobase/resourcer';
export async function jsonResponse(ctx: Context, next: Next) {
await next();
if (ctx.action instanceof Action) {
const { rows, ...meta } = ctx.body;
if (rows) {
ctx.body = {
data: rows,
meta,
};
} else {
ctx.body = {
data: ctx.body,
};
}
}
}
export default jsonResponse;

4
packages/api/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
yarn-error.log
.env
src2

15
packages/api/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:stretch
WORKDIR /app
COPY . /app
EXPOSE 23000
# # Install app dependencies
# ENV NPM_CONFIG_LOGLEVEL warn
# RUN yarn install
# # Show current folder structure in logs
# RUN ls -al -R
# CMD [ "npm", "run", "serve" ]

View File

@ -0,0 +1,41 @@
version: "3"
networks:
backend:
driver: bridge
services:
app:
build:
context: ./
volumes:
- ./:/app
ports:
- "${HTTP_PORT}:23000"
command: [ "yarn", "start" ]
env_file:
- ./.env
networks:
- backend
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
restart: always
ports:
- "23306:3306"
networks:
- backend
postgres:
image: postgres:10
restart: always
ports:
- "25432:5432"
networks:
- backend
command: postgres -c wal_level=logical
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}

View File

@ -0,0 +1,42 @@
import Api from '../src';
import dotenv from 'dotenv';
const sync = {
force: true,
alter: {
drop: true,
},
};
dotenv.config();
const api = Api.create({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.platform === 'linux' ? process.env.DB_HOST : 'localhost',
port: process.platform === 'linux' ? parseInt(process.env.DB_PORT) : ( process.env.DB_DIALECT == 'postgres' ? 25432 : 23306 ),
dialect: process.env.DB_DIALECT as any,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
// logging: false,
define: {},
sync,
},
resourcer: {
prefix: '/api',
},
});
api
.plugins([
[require('../../plugin-collections/src/index').default, {}],
])
.then(() => {
api.listen(23001, () => {
console.log('http://localhost:23001/');
});
});

View File

@ -0,0 +1,6 @@
{
"watch": ["src", ".env", ".env.dev", "../plugin-collections/src"],
"ext": "ts",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node ./example/index.ts"
}

38
packages/api/package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "@nocobase/api",
"version": "0.3.0-alpha.0",
"main": "lib/index.js",
"license": "MIT",
"scripts": {
"start": "nodemon",
"sync": "ts-node ./src/sync.ts",
"serve": "pm2-runtime start pm2.json",
"build": "tsc --declaration",
"db:sync": "docker-compose run app bash -c 'yarn sync'",
"logs": "docker-compose logs app"
},
"dependencies": {
"@koa/cors": "^3.1.0",
"@koa/router": "^9.4.0",
"@nocobase/actions": "^0.3.0-alpha.0",
"@nocobase/database": "^0.3.0-alpha.0",
"@nocobase/resourcer": "^0.3.0-alpha.0",
"@types/koa": "^2.11.4",
"@types/koa-bodyparser": "^4.3.0",
"@types/koa__router": "^8.0.2",
"bcrypt": "^5.0.0",
"crypto-random-string": "^3.3.0",
"dotenv": "^8.2.0",
"koa": "^2.13.0",
"koa-bodyparser": "^4.3.0",
"mockjs": "^1.1.0",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"nodemon": "^2.0.4",
"pg": "^8.3.3",
"pg-hstore": "^2.3.3",
"pm2": "^4.4.1",
"ts-node": "^9.0.0",
"typescript": "^4.0.2"
}
}

14
packages/api/pm2.json Executable file
View File

@ -0,0 +1,14 @@
{
"apps": [{
"name": "nocobase-api",
"script": "lib/index.js",
"instances": 0,
"exec_mode": "cluster",
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
}]
}

53
packages/api/src/index.ts Normal file
View File

@ -0,0 +1,53 @@
import Koa from 'koa';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import actions from '@nocobase/actions';
export class Application extends Koa {
database: Database;
resourcer: Resourcer;
async plugins(plugins: any[]) {
await Promise.all(plugins.map(async (pluginOption) => {
let plugin: Function;
let options = {};
if (Array.isArray(pluginOption)) {
plugin = pluginOption.shift();
plugin = plugin.bind(this);
options = pluginOption.shift()||{};
} else if (typeof pluginOption === 'function') {
plugin = pluginOption.bind(this);
}
return await plugin(options);
}));
}
}
export default {
create(options: any): Application {
console.log(options);
const app = new Application();
const resourcer = new Resourcer();
const database = new Database(options.database);
app.database = database;
app.resourcer = resourcer;
resourcer.registerHandlers(actions.common);
app.use(async (ctx, next) => {
ctx.db = database;
ctx.database = database;
await next();
});
app.use(resourcer.middleware(options.resourcer || {
prefix: '/api',
}));
return app;
}
}

View File

@ -0,0 +1 @@
Used in bin/create-nocobase-app.js to determine if it is in the local debug state.

View File

@ -0,0 +1 @@
# @nocobase/create-nocobase-app

View File

@ -0,0 +1,3 @@
#!/usr/bin/env node
require('../lib/cli');

View File

@ -0,0 +1,16 @@
{
"name": "@nocobase/create-nocobase-app",
"version": "0.3.0-alpha.0",
"description": "create-nocobase-app",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"license": "MIT",
"scripts": {
},
"dependencies": {
"@umijs/utils": "3.2.23"
},
"bin": {
"create-nocobase-app": "bin/create-nocobase-app.js"
}
}

View File

@ -0,0 +1,15 @@
import { Generator } from '@umijs/utils';
import { join } from 'path';
export default class AppGenerator extends Generator {
async writing() {
this.copyDirectory({
context: {
version: require('../../package').version,
conventionRoutes: this.args.conventionRoutes,
},
path: join(__dirname, '../../templates/AppGenerator'),
target: this.cwd,
});
}
}

View File

View File

@ -0,0 +1,30 @@
import { chalk, yParser } from '@umijs/utils';
import { existsSync } from 'fs';
import { join } from 'path';
const args = yParser(process.argv.slice(2), {
alias: {
version: ['v'],
help: ['h'],
},
boolean: ['version'],
});
if (args.version && !args._[0]) {
args._[0] = 'version';
const local = existsSync(join(__dirname, '../.local'))
? chalk.cyan('@local')
: '';
const { name, version } = require('../package.json');
console.log(`${name}@${version}${local}`);
} else {
require('./')
.default({
cwd: process.cwd(),
args,
})
.catch((err: Error) => {
console.error(`Create failed, ${err.message}`);
console.error(err);
});
}

View File

@ -0,0 +1,19 @@
import { join } from 'path';
import { rimraf } from '@umijs/utils';
import { existsSync } from 'fs';
import runGenerator from './index';
const fixtures = join(__dirname, 'fixtures');
const cwd = join(fixtures, 'generate');
test('generate app', async () => {
await runGenerator({
cwd,
args: {
_: [],
$0: '',
},
});
expect(existsSync(join(cwd, 'src', 'pages', 'index.tsx'))).toEqual(true);
rimraf.sync(cwd);
});

View File

@ -0,0 +1,16 @@
import { yargs } from '@umijs/utils';
import AppGenerator from './AppGenerator/AppGenerator';
export default async ({
cwd,
args,
}: {
cwd: string;
args: yargs.Arguments;
}) => {
const generator = new AppGenerator({
cwd,
args,
});
await generator.run();
};

View File

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View File

@ -0,0 +1,14 @@
HTTP_PORT=23000
DB_DATABASE=test
DB_USER=test
DB_PASSWORD=test
# DB_HOST=mysql
# DB_PORT=3306
# DB_DIALECT=mysql
DB_HOST=postgres
DB_PORT=5432
DB_DIALECT=postgres
# DB_PORT=25432
# DB_HOST=localhost

View File

@ -0,0 +1,10 @@
export default {
entry: 'src/api',
target: 'node',
cjs: { type: 'babel', lazy: true },
include: 'api/*',
disableTypeCheck: true,
// pkgs: [
// 'api',
// ],
};

View File

@ -0,0 +1,20 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/npm-debug.log*
/yarn-error.log
/yarn.lock
/package-lock.json
# production
/dist
# misc
.DS_Store
# umi
/src/.umi
/src/.umi-production
/src/.umi-test
/.env.local

View File

@ -0,0 +1,8 @@
**/*.md
**/*.svg
**/*.ejs
**/*.html
package.json
.umi
.umi-production
.umi-test

View File

@ -0,0 +1,11 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80,
"overrides": [
{
"files": ".prettierrc",
"options": { "parser": "json" }
}
]
}

View File

@ -0,0 +1,12 @@
import { defineConfig } from 'umi';
export default defineConfig({
nodeModulesTransform: {
type: 'none',
},
{{ ^conventionRoutes }}
routes: [
{ path: '/', component: '@/pages/index' },
],
{{ /conventionRoutes }}
});

View File

@ -0,0 +1,15 @@
# NocoBase Application
## Getting Started
Install dependencies,
```bash
$ yarn install
```
Start the dev server,
```bash
$ yarn start
```

View File

@ -0,0 +1,5 @@
{
"watch": ["src/api", ".env"],
"ext": "ts",
"exec": "ts-node ./src/api/index.ts"
}

View File

@ -0,0 +1,38 @@
{
"private": true,
"scripts": {
"start": "concurrently \"nodemon\" \"umi dev\"",
"build": "father-build && umi build",
"postinstall": "umi generate tmp",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"test": "umi-test",
"test:coverage": "umi-test --coverage"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,less,md,json}": [
"prettier --write"
],
"*.ts?(x)": [
"prettier --parser=typescript --write"
]
},
"dependencies": {
"@ant-design/pro-layout": "^5.0.12",
"@umijs/preset-react": "1.x",
"@umijs/test": "^3.2.23",
"@nocobase/api": "^{{{ version }}}",
"@nocobase/father-build": "^{{{ version }}}",
"@nocobase/plugin-collections": "^{{{ version }}}",
"concurrently": "^5.3.0",
"lint-staged": "^10.0.7",
"nodemon": "^2.0.6",
"prettier": "^1.19.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"umi": "^3.2.23",
"yorkie": "^2.0.0"
}
}

View File

@ -0,0 +1,7 @@
const api = require('@nocobase/api');
require('dotenv').config();
api.listen(23000, () => {
console.log('http://localhost:23000/');
});

View File

@ -0,0 +1,42 @@
import Api from '@nocobase/api';
import dotenv from 'dotenv';
const sync = {
force: true,
alter: {
drop: true,
},
};
dotenv.config();
const api = Api.create({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.platform === 'linux' ? process.env.DB_HOST : 'localhost',
port: process.platform === 'linux' ? process.env.DB_PORT : ( process.env.DB_DIALECT == 'postgres' ? 25432 : 23306 ),
dialect: process.env.DB_DIALECT as any,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
// logging: false,
define: {},
sync,
},
resourcer: {
prefix: '/api',
},
});
api
.plugins([
[require('@nocobase/plugin-collections').default, {}],
])
.then(() => {
api.listen(23001, () => {
console.log('http://localhost:23001/');
});
});

View File

@ -0,0 +1,7 @@
.normal {
}
.title {
background: rgb(121, 242, 157);
}

View File

@ -0,0 +1,10 @@
import React from 'react';
import styles from './index.less';
export default () => {
return (
<div>
<h1 className={styles.title}>Page index</h1>
</div>
);
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": [ "es5", "es6" ],
"module": "commonjs",
"target": "es6",
"moduleResolution": "node",
"importHelpers": true,
"jsx": "react",
"esModuleInterop": true,
"sourceMap": true,
"baseUrl": "./",
"strict": true,
"paths": {
"@/*": ["src/*"],
"@@/*": ["src/.umi/*"]
},
"allowSyntheticDefaultImports": true
},
"include": [
"mock/**/*",
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts"
]
}

View File

@ -0,0 +1,8 @@
declare module '*.css';
declare module '*.less';
declare module "*.png";
declare module '*.svg' {
export function ReactComponent(props: React.SVGProps<SVGSVGElement>): React.ReactElement
const url: string
export default url
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": [ "es5", "es6" ],
"module": "commonjs",
"target": "es6",
"allowJs": true,
"declaration": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"sourceMap": false,
"baseUrl": "./",
"paths": {
}
},
"exclude": [
"node_modules"
]
}

View File

@ -0,0 +1,125 @@
import Database from '../src';
import path from 'path';
import dbDriven from './plugins/db-driven';
const sync = {
force: true,
alter: {
drop: true,
}
}
const db = new Database({
username: 'test',
password: 'test',
database: 'test',
host: '127.0.0.1',
port: 45432,
dialect: 'postgres',
logging: false,
define: {
},
sync,
});
(async () => {
const tables = db.import({
directory: path.resolve(__dirname, 'tables'),
});
await db.sync({ tables });
await db.plugin(dbDriven());
if (!sync.force) {
await db.sequelize.drop();
await db.sync();
}
const [Table, Field] = db.getModels(['tables', 'fields']);
const [table] = await Table.findOrCreate({
where: {
name: 'demos',
},
defaults: {
options: {
name: 'demos',
},
},
});
await db.getModel('demos').create({});
await Field.bulkCreate([
{
name: 'col1',
table_name: 'demos',
options: {
type: 'string',
name: 'col1',
},
},
{
name: 'col2',
table_name: 'demos',
options: {
type: 'string',
name: 'col2',
},
},
]);
await db.getModel('demos').create({
col1: 'col1',
col2: 'col2',
});
await table.createField({
name: 'col3',
options: {
type: 'string',
name: 'col3',
},
});
await db.getModel('demos').create({
col1: 'col1',
col2: 'col2',
col3: 'col3',
});
await table.createField({
name: 'col4',
options: {
type: 'string',
name: 'col4',
},
});
await db.getModel('demos').create({
col1: 'col1',
col2: 'col2',
col3: 'col3',
col4: 'col4',
});
await table.createField({
name: 'col5',
options: {
type: 'string',
name: 'col5',
},
});
await db.getModel('demos').create({
col1: 'col1',
col2: 'col2',
col3: 'col3',
col4: 'col4',
col5: 'col5',
});
await db.close();
})();

View File

@ -0,0 +1,25 @@
import path from 'path';
import Database, { Model } from '../../../src';
export default (options?: any) => {
return async (db: Database) => {
const tables = db.import({
directory: path.resolve(__dirname, 'tables'),
});
await db.sync({ tables });
const Table = db.getModel('tables');
const items = await Table.findAll();
await Promise.all(items.map(async item => {
const fields: Model[] = await item.getFields();
const table = db.table({
...item.options,
fields: fields.map(field => field.options),
});
}));
await db.sync({ tables: items.map(item => item.name) });
}
}

View File

@ -0,0 +1,78 @@
import Database, { TableOptions, Table } from '../../../../src';
export default (db: Database) => ({
name: 'fields',
tableName: 'nocobase_fields',
fields: [
{
type: 'string',
name: 'table_name',
},
{
type: 'string',
name: 'name',
index: {
fields: ['table_name', 'name'],
unique: true,
},
},
{
type: 'json',
name: 'options',
},
{
type: 'belongsTo',
name: 'table',
foreignKey: 'table_name',
targetKey: 'name',
},
],
hooks: {
async afterCreate(model: any) {
if (!model.table_name) {
return;
}
const table = db.getTable(model.table_name);
table.addField(model.options);
// console.log(table);
await table.sync({
force: false,
alter: {
drop: false,
}
});
},
async afterUpdate(model: any) {
if (!model.table_name) {
return;
}
const table = db.getTable(model.table_name);
table.addField(model.options);
await table.sync({
force: false,
alter: {
drop: false,
}
});
},
async afterBulkCreate(models) {
const tables = new Map<string, Table>();
for (const model of new Map<string, any>(Object.entries(models)).values()) {
if (!model.table_name) {
return;
}
const table = db.getTable(model.table_name);
table.addField(model.options);
tables.set(table.getName(), table);
}
for (const table of tables.values()) {
await table.sync({
force: false,
alter: {
drop: false,
}
});
}
},
}
} as TableOptions);

View File

@ -0,0 +1,53 @@
import { FindOrCreateOptions } from 'sequelize';
import Database, { TableOptions, Model } from '../../../../src';
export class Table extends Model {
}
export default (db: Database) => ({
name: 'tables',
tableName: 'nocobase_tables',
model: Table,
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'json',
name: 'options',
},
{
type: 'hasMany',
name: 'fields',
sourceKey: 'name',
foreignKey: 'table_name',
},
],
hooks: {
async afterCreate(model: Table) {
const fields: Model[] = await model.getFields();
const table = db.table({...model.options, fields: fields.map(field => field.options)});
await table.sync();
},
async afterUpdate(model: Table) {
const fields: Model[] = await model.getFields();
const table = db.table({...model.options, fields: fields.map(field => field.options)});
await table.sync({
force: false,
alter: {
drop: false,
}
});
},
async afterBulkCreate(models: Table[]) {
await Promise.all(models.map(async (model: any) => {
const fields: Model[] = await model.getFields();
const table = db.table({...model.options, fields: fields.map(field => field.options)});
await table.sync();
}));
},
},
} as TableOptions);

View File

@ -0,0 +1,26 @@
module.exports = {
name: 'bar',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'text',
length: 'long',
name: 'content',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'belongsToMany',
name: 'tags',
},
{
type: 'hasMany',
name: 'comments'
},
],
};

View File

@ -0,0 +1,19 @@
import { TableOptions } from '../../src';
export default {
name: 'comments',
fields: [
{
type: 'text',
name: 'content',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'belongsTo',
name: 'post',
},
],
} as TableOptions;

View File

@ -0,0 +1,3 @@
{
"name": "foo"
}

View File

@ -0,0 +1,28 @@
import { TableOptions } from '../../src';
export default {
name: 'posts',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'text',
length: 'long',
name: 'content',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'belongsToMany',
name: 'tags',
},
{
type: 'hasMany',
name: 'comments'
},
],
} as TableOptions;

View File

@ -0,0 +1,23 @@
import { TableOptions } from '../../src';
export default {
name: 'profiles',
fields: [
{
type: 'string',
name: 'realname',
},
{
type: 'string',
name: 'email',
},
{
type: 'string',
name: 'gender',
},
{
type: 'date',
name: 'birthday',
},
],
} as TableOptions;

View File

@ -0,0 +1,15 @@
import { TableOptions } from '../../src';
export default {
name: 'tags',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'belongsToMany',
name: 'posts',
},
],
} as TableOptions;

View File

@ -0,0 +1,34 @@
import { TableOptions } from '../../src';
export default {
name: 'users',
fields: [
{
type: 'string',
name: 'username',
unique: true,
},
{
type: 'string',
name: 'password',
// index: true,
},
{
type: 'string',
name: 'openid',
index: true,
},
{
type: 'hasOne',
name: 'profile',
},
{
type: 'hasMany',
name: 'posts',
},
{
type: 'hasMany',
name: 'comments',
}
],
} as TableOptions;

View File

@ -0,0 +1,23 @@
{
"name": "@nocobase/database",
"version": "0.3.0-alpha.0",
"description": "",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
"scripts": {
},
"license": "MIT",
"dependencies": {
"bcrypt": "^5.0.0",
"glob": "^7.1.6",
"sequelize": "^6.3.3"
},
"devDependencies": {
"typescript": "^3.9.6"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nocobase/nocobase.git",
"directory": "packages/database"
}
}

View File

@ -0,0 +1,694 @@
import { getDatabase } from './';
import {
HasMany, HasOne, Integer, BelongsTo, BelongsToMany
} from '../fields';
import { DataTypes } from 'sequelize';
describe('associations', () => {
describe('hasOne', () => {
it('shound be defaults', async () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasone',
name: 'foo',
},
],
});
const field: HasOne = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be ok when the association table is defined later', async () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'hasone',
name: 'foo',
},
],
});
db.table({
name: 'foos',
});
const field: HasOne = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be custom when target is defined', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasone',
name: 'foo2',
target: 'foos',
},
],
});
const field: HasOne = db.getTable('bars').getField('foo2');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be custom when sourceKey is defined', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasone',
name: 'foo',
sourceKey: 'sid',
},
],
});
const field: HasOne = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_sid');
expect(field.options.sourceKey).toBe('sid');
});
it('shound be integer type when the column of sourceKey does not exist', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasone',
name: 'foo',
sourceKey: 'sid',
},
],
});
const field: HasOne = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_sid');
expect(field.options.sourceKey).toBe('sid');
const sourceKeyColumn: Integer = db.getTable('bars').getField('sid');
expect(sourceKeyColumn).toBeInstanceOf(Integer);
expect(sourceKeyColumn.options.unique).toBe(true);
});
});
describe('hasMany', () => {
it('shound be defaults', async () => {
const db = getDatabase();
db.table({
name: 'foo',
});
db.table({
name: 'bar',
fields: [
{
type: 'hasMany',
name: 'foo',
},
],
});
const field: HasMany = db.getTable('bar').getField('foo');
expect(field.options.target).toBe('foo');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be ok when the association table is defined later', async () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'hasMany',
name: 'foos',
},
],
});
db.table({
name: 'foos',
});
const field: HasMany = db.getTable('bars').getField('foos');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be custom when target is defined', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasMany',
name: 'foo2',
target: 'foos',
},
],
});
const field: HasMany = db.getTable('bars').getField('foo2');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.sourceKey).toBe('id');
});
it('shound be custom when sourceKey is defined', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasMany',
name: 'foos',
sourceKey: 'sid',
},
],
});
const field: HasMany = db.getTable('bars').getField('foos');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_sid');
expect(field.options.sourceKey).toBe('sid');
});
it('shound be integer type when the column of sourceKey does not exist', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'bars',
fields: [
{
type: 'hasMany',
name: 'foos',
sourceKey: 'sid',
},
],
});
const field: HasMany = db.getTable('bars').getField('foos');
expect(field.options.target).toBe('foos');
expect(field.options.foreignKey).toBe('bar_sid');
expect(field.options.sourceKey).toBe('sid');
const sourceKeyColumn: Integer = db.getTable('bars').getField('sid');
expect(sourceKeyColumn).toBeInstanceOf(Integer);
expect(sourceKeyColumn.options.unique).toBe(true);
});
});
describe('belongsTo', () => {
it('shound be custom foreignKey', async () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'foo',
},
],
});
db.table({
name: 'foos',
});
const field: BelongsTo = db.getTable('bars').getField('foo');
expect(field.options.targetKey).toBe('id');
expect(field.options.foreignKey).toBe('foo_id');
});
it('shound be custom foreignKey', async () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'custom_foo',
target: 'foos',
},
],
});
db.table({
name: 'foos',
});
const field: BelongsTo = db.getTable('bars').getField('custom_foo');
expect(field.options.targetKey).toBe('id');
expect(field.options.foreignKey).toBe('custom_foo_id');
});
it('shound be custom primaryKey', () => {
const db = getDatabase();
db.table({
name: 'foos',
fields: [
{
type: 'integer',
name: 'fid',
primaryKey: true,
autoIncrement: true,
}
],
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'foo',
},
],
});
const field: BelongsTo = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.targetKey).toBe('fid');
expect(field.options.foreignKey).toBe('foo_fid');
});
it('shound be custom primaryKey', () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'foo',
},
],
});
db.table({
name: 'foos',
fields: [
{
type: 'integer',
name: 'fid',
primaryKey: true,
autoIncrement: true,
}
],
});
const field: BelongsTo = db.getTable('bars').getField('foo');
expect(field.options.target).toBe('foos');
expect(field.options.targetKey).toBe('fid');
expect(field.options.foreignKey).toBe('foo_fid');
});
it('shound throw error', () => {
const db = getDatabase();
db.table({
name: 'foos',
});
expect(() => {
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'foo',
targetKey: 'fid',
},
],
});
}).toThrow('Unknown attribute "fid" passed as targetKey, define this attribute on model "foos" first')
});
it('shound be ok when the association table is defined later', async () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'foo',
targetKey: 'fid',
},
],
});
db.table({
name: 'foos',
fields: [
{
type: 'integer',
name: 'fid',
},
],
});
const field: BelongsTo = db.getTable('bars').getField('foo');
expect(field.options.targetKey).toBe('fid');
expect(field.options.foreignKey).toBe('foo_fid');
});
it('shound work', async () => {
const db = getDatabase();
db.table({
name: 'rows',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'hasMany',
name: 'columns',
sourceKey: 'name',
}
],
});
db.table({
name: 'columns',
fields: [
{
type: 'belongsTo',
name: 'row',
targetKey: 'name',
},
{
type: 'string',
name: 'name',
}
],
});
const f1: BelongsTo = db.getTable('columns').getField('row');
expect(f1.options.foreignKey).toBe('row_name');
const f2: HasMany = db.getTable('rows').getField('columns');
expect(f2.options.foreignKey).toBe('row_name');
});
});
describe('belongsToMany', () => {
it('shound be defaults', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
fields: [
{
type: 'belongsToMany',
name: 'bars',
},
],
});
db.table({
name: 'bars_foos',
fields: [
{
type: 'string',
name: 'name',
},
],
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsToMany',
name: 'foos',
},
],
});
// console.log(db.getModel('bars_foos').rawAttributes);
// await db.sync({
// force: true,
// });
let field: BelongsToMany;
field = db.getTable('bars').getField('foos');
expect(field.options.target).toBe('foos');
expect(field.options.through).toBe('bars_foos');
expect(field.options.sourceKey).toBe('id');
expect(field.options.foreignKey).toBe('bar_id');
expect(field.options.targetKey).toBe('id');
expect(field.options.otherKey).toBe('foo_id');
field = db.getTable('foos').getField('bars');
expect(field.options.target).toBe('bars');
expect(field.options.through).toBe('bars_foos');
expect(field.options.sourceKey).toBe('id');
expect(field.options.foreignKey).toBe('foo_id');
expect(field.options.targetKey).toBe('id');
expect(field.options.otherKey).toBe('bar_id');
expect(db.getModel('foos').associations.bars).toBeDefined();
expect(db.getModel('bars').associations.foos).toBeDefined();
});
it('shound be correct when use custom primary key', async () => {
const db = getDatabase();
db.table({
name: 'foos',
fields: [
{
type: 'integer',
autoIncrement: true,
name: 'fid',
primaryKey: true,
},
{
type: 'belongsToMany',
name: 'bars',
},
],
});
db.table({
name: 'bars',
fields: [
{
type: 'integer',
autoIncrement: true,
name: 'bid',
primaryKey: true,
},
{
type: 'belongsToMany',
name: 'foos',
},
],
});
// await db.sync({force: true});
// await db.sequelize.close();
let field: BelongsToMany;
field = db.getTable('bars').getField('foos');
expect(field.options.target).toBe('foos');
expect(field.options.through).toBe('bars_foos');
expect(field.options.sourceKey).toBe('bid');
expect(field.options.foreignKey).toBe('bar_bid');
expect(field.options.targetKey).toBe('fid');
expect(field.options.otherKey).toBe('foo_fid');
field = db.getTable('foos').getField('bars');
expect(field.options.target).toBe('bars');
expect(field.options.through).toBe('bars_foos');
expect(field.options.sourceKey).toBe('fid');
expect(field.options.foreignKey).toBe('foo_fid');
expect(field.options.targetKey).toBe('bid');
expect(field.options.otherKey).toBe('bar_bid');
expect(db.getModel('foos').associations.bars).toBeDefined();
expect(db.getModel('bars').associations.foos).toBeDefined();
const { foos: barAssociation } = db.getModel('bars').associations as any;
expect(barAssociation.target.name).toBe('foos');
expect(barAssociation.through.model.name).toBe('bars_foos');
expect(barAssociation.sourceKey).toBe('bid');
expect(barAssociation.foreignKey).toBe('bar_bid');
expect(barAssociation.targetKey).toBe('fid');
expect(barAssociation.otherKey).toBe('foo_fid');
const { bars: fooAssociation } = db.getModel('foos').associations as any;
expect(fooAssociation.target.name).toBe('bars');
expect(fooAssociation.through.model.name).toBe('bars_foos');
expect(fooAssociation.sourceKey).toBe('fid');
expect(fooAssociation.foreignKey).toBe('foo_fid');
expect(fooAssociation.targetKey).toBe('bid');
expect(fooAssociation.otherKey).toBe('bar_bid');
});
it('through be defined after source and target', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
fields: [
{
type: 'belongsToMany',
name: 'bars',
},
],
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsToMany',
name: 'foos',
},
],
});
db.table({
name: 'bars_foos',
fields: [
{
type: 'string',
name: 'name',
},
],
});
// await db.sync({
// force: true,
// });
const Through = db.getModel('bars_foos');
expect(Through.rawAttributes.name).toBeDefined();
expect(Through.rawAttributes.foo_id).toBeDefined();
expect(Through.rawAttributes.bar_id).toBeDefined();
});
it('through be defined after source', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
fields: [
{
type: 'belongsToMany',
name: 'bars',
},
],
});
db.table({
name: 'bars_foos',
fields: [
{
type: 'string',
name: 'name',
},
],
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsToMany',
name: 'foos',
},
],
});
// await db.sync({
// force: true,
// });
const Through = db.getModel('bars_foos');
expect(Through.rawAttributes.name).toBeDefined();
expect(Through.rawAttributes.foo_id).toBeDefined();
expect(Through.rawAttributes.bar_id).toBeDefined();
});
it('#', () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'posts',
fields: [
{
type: 'string',
name: 'slug',
unique: true,
},
{
type: 'belongsToMany',
name: 'tags',
sourceKey: 'slug',
targetKey: 'name',
},
],
});
db.table({
name: 'tags',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'belongsToMany',
name: 'posts',
sourceKey: 'name',
targetKey: 'slug',
},
],
});
const f1: BelongsToMany = db.getTable('posts').getField('tags');
expect(f1.options).toEqual({
target: 'tags',
through: 'posts_tags',
sourceKey: 'slug',
foreignKey: 'post_slug',
type: 'BELONGSTOMANY',
name: 'tags',
targetKey: 'name',
otherKey: 'tag_name'
});
const f2: BelongsToMany = db.getTable('tags').getField('posts');
expect(f2.options).toEqual({
target: 'posts',
through: 'posts_tags',
sourceKey: 'name',
foreignKey: 'tag_name',
type: 'BELONGSTOMANY',
name: 'posts',
targetKey: 'slug',
otherKey: 'post_slug'
});
});
});
it('shound be defined', () => {
const db = getDatabase();
db.table({
name: 'bars',
fields: [
{
type: 'string',
name: 'foo_name',
},
{
type: 'belongsTo',
name: 'foo',
foreignKey: 'foo_name',
targetKey: 'name',
}
],
})
db.table({
name: 'foos',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'hasMany',
name: 'bars',
sourceKey: 'name',
foreignKey: 'foo_name'
},
],
});
});
});

View File

@ -0,0 +1,42 @@
import Database from '../database';
import { Options } from 'sequelize';
import path from 'path';
require('dotenv').config({
path: path.resolve(__dirname, '.env'),
})
export const config: {
[key: string]: Options;
} = {
mysql: {
username: 'test',
password: 'test',
database: 'test',
host: '127.0.0.1',
port: 43306,
dialect: 'mysql',
},
postgres: {
username: 'test',
password: 'test',
database: 'test',
host: '127.0.0.1',
port: 45432,
dialect: 'postgres',
define: {
hooks: {
beforeCreate(model, options) {
},
},
},
// logging: false,
},
};
export function getDatabase(options: Options = {}) {
// console.log(process.env.DIALECT);
const db = new Database({...config[process.env.DIALECT||'postgres'], ...options});
return db;
};

View File

@ -0,0 +1,970 @@
import { getDatabase } from '.';
import Database from '..';
import { Op, Sequelize } from 'sequelize';
import Model from '../model';
import { BelongsToMany } from '../fields';
import { Mode } from 'fs';
describe('actions', () => {
let db: Database;
beforeAll(async () => {
db = getDatabase({
logging: false,
});
db.table({
name: 'users',
tableName: 'user1234',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'hasone',
name: 'profile',
},
{
type: 'hasMany',
name: 'posts',
},
{
type: 'hasMany',
name: 'posts_title1',
target: 'posts',
scope: {
title: 'title1',
},
}
]
});
db.table({
name: 'profiles',
tableName: 'profile1234',
fields: [
{
type: 'string',
name: 'name',
},
]
});
db.table({
name: 'posts',
tableName: 'post123456',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'belongsToMany',
name: 'tags',
},
{
type: 'belongsToMany',
name: 'tags_name1',
target: 'tags',
scope: {
name: 'name1',
},
},
{
type: 'hasmany',
name: 'comments',
},
{
type: 'hasmany',
name: 'current_user_comments',
target: 'comments',
},
]
});
db.table({
name: 'posts_tags',
tableName: 'posts_tags1234',
fields: [
{
type: 'string',
name: 'name',
},
]
});
db.table({
name: 'tags',
tableName: 'tag1234',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'belongsToMany',
name: 'posts',
},
],
});
db.table({
name: 'comments',
tableName: 'comment1234',
fields: [
{
type: 'belongsTo',
name: 'user',
},
{
type: 'belongsTo',
name: 'post',
},
{
type: 'string',
name: 'content',
}
]
});
db.table({
name: 'tables',
fields: [
{
type: 'string',
name: 'name',
primaryKey: true,
autoIncrement: false,
},
{
type: 'hasMany',
name: 'fields',
}
],
});
db.table({
name: 'fields',
fields: [
{
type: 'belongsTo',
name: 'table',
},
{
type: 'string',
name: 'name',
}
],
});
db.table({
name: 'rows',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'hasMany',
name: 'columns',
sourceKey: 'name',
}
],
});
db.table({
name: 'columns',
fields: [
{
type: 'belongsTo',
name: 'row',
targetKey: 'name',
},
{
type: 'string',
name: 'name',
}
],
});
await db.sync({
force: true,
});
});
afterAll(async () => {
await db.close();
});
it('through attributes', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tag = await Tag.create();
await post.updateAssociations({
tags: [{
name: 'xxx',
posts_tags: {
name: 'name134',
}
}, {
id: tag.id,
posts_tags: {
name: 'name234',
}
}],
});
const PostTag = db.getModel('posts_tags');
const [t1, t2] = await PostTag.findAll({
where: {
post_id: post.id,
},
order: ['tag_id'],
});
expect(t1.name).toBe('name234');
expect(t2.name).toBe('name134');
});
describe('scope', () => {
it('scope', async () => {
const [User, Post, Comment] = db.getModels(['users', 'posts', 'comments']);
const user1 = await User.create();
const user2 =await User.create();
const user3 =await User.create();
const user4 =await User.create();
const post = await Post.create();
const comment = await Comment.create();
comment.updateAssociations({
post: post,
user: user1,
});
await post.updateAssociations({
comments: [
{
content: 'content1',
user: user1,
},
{
content: 'content2',
user: user2,
},
{
content: 'content3',
user: user3,
},
{
content: 'content4',
user: user4,
},
],
});
const comments = await post.getCurrent_user_comments();
// console.log(comments);
});
});
describe('findByApiJson', () => {
it('q', async () => {
db.getModel('tags').addScope('scopeName', (name, ctx) => {
expect(ctx.scopeName).toBe(name);
console.log(ctx);
return {
where: {
name: name,
}
}
});
const [User, Post] = db.getModels(['users', 'posts']);
const postData = [];
for (let index = 0; index < 20; index++) {
postData.push({
title: `title${index}`,
});
}
const user = await User.create({
name: 'name112233',
});
await Post.create({
title: 'xxxx',
});
const post = await Post.create({
title: 'title112233',
});
await user.updateAssociations({
posts: post,
});
await post.updateAssociations({
tags: [
{name: 'tag1'},
{name: 'tag2'},
{name: 'tag3'},
],
});
// where & include
const options = Post.parseApiJson({
filter: {
title: 'title112233',
user: { // belongsTo
name: 'name112233',
},
tags: { // belongsToMany
scopeName: 'tag3',
},
},
fields: [
'title',
'tags_count',
'tags.name',
'user.name'
],
sort: '-tags_count,tags.name,user.posts_count',
context: {
scopeName: 'tag3',
},
});
const { rows, count } = await Post.findAndCountAll({
...options,
// group: ['id'],
// limit: 20,
// offset: 20,
});
// console.log(JSON.stringify(rows[0].toJSON(), null, 2));
rows.forEach(post => {
// expect(post.toJSON()).toEqual({ title: 'title112233', 'tags_count': 3, user: { name: 'name112233', posts_count: 1 } });
expect(post.get('title')).toBe('title112233');
expect(post.user.get('name')).toBe('name112233');
});
// console.log(count);
// expect(count).toBe(1);
});
});
describe('query', () => {
it('to be explained', async () => {
const [User, Post] = db.getModels(['users', 'posts']);
const postData = [];
for (let index = 0; index < 20; index++) {
postData.push({
title: `title${index}`,
});
}
const user = await User.create();
let posts = await Post.bulkCreate(postData);
await user.updateAssociations({
posts: posts,
});
const userOne = await User.findOne({
attributes: {
exclude: ['updated_at'],
include: [
User.withCountAttribute('posts'),
User.withCountAttribute('posts_title1'),
],
},
where: {
id: user.id,
},
});
expect(userOne.get('posts_count')).toBe(20);
expect(userOne.get('posts_title1_count')).toBe(1);
});
it('to be explained', async () => {
const [User, Post] = db.getModels(['users', 'posts']);
const postData = [];
for (let index = 0; index < 20; index++) {
postData.push({
title: `title${index}`,
});
}
const user = await User.create();
let posts = await Post.bulkCreate(postData);
await user.updateAssociations({
posts: posts,
});
const userOne = await User.findOne({
attributes: {
exclude: ['updated_at'],
include: [
User.withCountAttribute({
association: 'posts',
alias: 'posts2_count'
}),
],
},
where: {
id: user.id,
},
});
expect(userOne.get('posts2_count')).toBe(20);
});
it('to be explained', async () => {
const [Tag, Post, User] = db.getModels(['tags', 'posts', 'users']);
const tagData = [];
for (let index = 0; index < 5; index++) {
tagData.push({
name: `name${index}`,
});
}
let post = await Post.create();
let tags = await Tag.bulkCreate(tagData);
await post.updateAssociations({
user: {
name: 'user1',
},
tags,
});
post = await Post.findOne({
attributes: {
include: [
'id',
Post.withCountAttribute('tags'),
Post.withCountAttribute('tags_name1'),
],
},
where: {
id: post.id,
},
include: [
{
association: 'user',
attributes: ['id', 'name',
User.withCountAttribute({
sourceAlias: 'user',
association: 'posts',
}),
],
},
{
association: 'tags',
attributes: ['id', 'name', Tag.withCountAttribute('posts')],
},
],
});
expect(post.get('tags_count')).toBe(5);
expect(post.get('tags_name1_count')).toBe(1);
expect(post.user.get('posts_count')).toBe(1);
post.tags.forEach(tag => {
expect(tag.get('posts_count')).toBe(1);
});
});
});
describe('hasOne', () => {
it('shoud associated id when association is integer', async () => {
const [User, Profile] = db.getModels(['users', 'profiles']);
const user = await User.create();
const profile = await Profile.create();
await user.updateAssociations({
// 关联 id
profile,
});
const userProfile = await user.getProfile();
expect(userProfile.id).toBe(profile.id);
});
it('shoud associated id when association is integer', async () => {
const [User, Profile] = db.getModels(['users', 'profiles']);
const user = await User.create();
const profile = await Profile.create();
await user.updateAssociations({
// 关联 id
profile: profile.id,
});
const userProfile = await user.getProfile();
expect(userProfile.id).toBe(profile.id);
});
it('shoud associated id when association is integer', async () => {
const [User, Profile] = db.getModels(['users', 'profiles']);
const user = await User.create();
const profile = await Profile.create();
await user.updateAssociations({
// 关联 id
profile: {
id: profile.id,
},
});
const userProfile = await user.getProfile();
expect(userProfile.id).toBe(profile.id);
});
it('shoud associated id when association is integer', async () => {
const [User, Profile] = db.getModels(['users', 'profiles']);
const user = await User.create();
const profile = await Profile.create();
await user.updateAssociations({
// 关联 id
profile: {
id: profile.id,
name: 'profile1',
},
});
const userProfile = await user.getProfile();
expect(userProfile.id).toBe(profile.id);
expect(userProfile.name).toBe('profile1');
});
it('shoud associated id when association is integer', async () => {
const [User] = db.getModels(['users']);
const user = await User.create();
await user.updateAssociations({
// 关联 id
profile: {
name: 'profile2',
},
});
const userProfile = await user.getProfile();
expect(userProfile.name).toBe('profile2');
});
});
describe('hasMany', () => {
it('@1', async () => {
const [Comment, Post] = db.getModels(['comments', 'posts']);
const comment = await Comment.create();
const post = await Post.create();
await post.updateAssociations({
comments: comment,
});
const count = await post.countComments();
expect(count).toBe(1);
});
it('@2', async () => {
const [Comment, Post] = db.getModels(['comments', 'posts']);
const comment = await Comment.create();
const post = await Post.create();
await post.updateAssociations({
comments: [comment],
});
const count = await post.countComments();
expect(count).toBe(1);
});
it('@3', async () => {
const [Comment, Post] = db.getModels(['comments', 'posts']);
const comment = await Comment.create();
const post = await Post.create();
await post.updateAssociations({
comments: [comment.id],
});
const count = await post.countComments();
expect(count).toBe(1);
});
it('@4', async () => {
const [Comment, Post] = db.getModels(['comments', 'posts']);
const comment = await Comment.create();
const post = await Post.create();
await post.updateAssociations({
comments: comment.id,
});
const count = await post.countComments();
expect(count).toBe(1);
});
it('@5', async () => {
const [Post] = db.getModels(['posts']);
const post = await Post.create();
await post.updateAssociations({
comments: {
content: 'content1',
},
});
const count = await post.countComments();
expect(count).toBe(1);
});
it('@6', async () => {
const [Post, Comment] = db.getModels(['posts', 'comments']);
const post = await Post.create();
const comment1 = await Comment.create();
const comment2 = await Comment.create();
await post.updateAssociations({
comments: [
{
content: 'content2',
},
{
content: 'content3',
},
{
id: comment1.id,
},
comment2,
],
});
const count = await post.countComments();
expect(count).toBe(4);
});
it('shoud work1', async () => {
const [Table, Field] = db.getModels(['tables', 'fields']);
const table = await Table.create({
name: 'examples',
});
await table.updateAssociations({
fields: [
{name: 'name'},
]
});
const field = await Field.findOne({
where: {
table_name: 'examples',
name: 'name',
},
});
expect(field).toBeDefined();
expect(field.get('name')).toBe('name');
});
it('shoud work2', async () => {
const [Row, Column] = db.getModels(['rows', 'columns']);
const row = await Row.create({
name: 'examples',
});
await row.updateAssociations({
columns: [
{name: 'name'},
]
});
const column = await Column.findOne({
where: {
row_name: 'examples',
name: 'name',
},
});
expect(column).toBeDefined();
expect(column.get('name')).toBe('name');
});
it('shoud work3', async () => {
const [Table, Field] = db.getModels(['tables', 'fields']);
const table = await Table.create({
name: 'abcdef',
});
const field = await Field.create({name: 'name123'});
await table.updateAssociations({
fields: [
{
id: field.id,
name: 'nam1234',
},
]
});
const f = await Field.findOne({
where: {
table_name: 'abcdef',
name: 'nam1234',
},
});
expect(f).toBeDefined();
expect(f.id).toBe(field.id);
const options = Table.parseApiJson({
fields: ['name', 'fields_count'],
});
const t = await Table.findOne(options);
expect(t.get('fields_count')).toBe(1);
});
});
describe('blongsTo', () => {
it('shoud associated id when association is integer', async () => {
const [User, Post] = db.getModels(['users', 'posts']);
const user = await User.create();
const post = await Post.create();
await post.updateAssociations({
// 关联 id
user,
});
expect(user.id).toBe(post.user_id);
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
});
it('shoud associated id when association is integer', async () => {
const [User, Post] = db.getModels(['users', 'posts']);
const user = await User.create();
const post = await Post.create();
await post.updateAssociations({
// 关联 id
user: user.id,
});
expect(user.id).toBe(post.user_id);
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
});
it('shoud associated id when association is object only id attribute', async () => {
const [User, Tag, Post] = db.getModels(['users', 'tags', 'posts']);
const user = await User.create();
const post = await Post.create();
await post.updateAssociations({
// 关联 id
user: {
id: user.id,
},
});
expect(user.id).toBe(post.user_id);
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
});
it('shoud associate and update other attributes', async () => {
const [User, Post] = db.getModels(['users', 'posts']);
const user = await User.create();
let post = await Post.create();
await post.updateAssociations({
// 关联并更新当前 id 的数据
user: {
id: user.id,
name: 'user1234',
},
});
expect(user.id).toBe(post.user_id);
const postUser = await post.getUser();
expect(user.id).toBe(postUser.id);
expect(postUser.name).toBe('user1234');
});
it('shoud work', async () => {
const [Post] = db.getModels(['posts']);
const post = await Post.create();
await post.updateAssociations({
// 新建并关联 user
user: {
name: 'user123456',
},
});
const postUser = await post.getUser();
expect(postUser.name).toBe('user123456');
});
it('shoud work', async () => {
const [Table, Field] = db.getModels(['tables', 'fields']);
const field = await Field.create({
name: 'fieldName',
});
await Table.create({name: 'demos'});
await field.updateAssociations({
table: 'demos',
});
expect(field.table_name).toBe('demos');
});
it('shoud work', async () => {
const [Row, Column] = db.getModels(['rows', 'columns']);
await Row.create({
name: 't1_examples',
});
const column = await Column.create();
await column.updateAssociations({
row: 't1_examples',
});
});
it('shoud work', async () => {
const [Row, Column] = db.getModels(['rows', 'columns']);
await Row.create({
name: 't2_examples',
});
const column = await Column.create();
await column.updateAssociations({
row: {
name: 't2_examples',
},
});
});
});
describe('belongsToMany', () => {
it('@', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tag = await Tag.create();
await post.updateAssociations({
tags: tag,
});
const count = await post.countTags();
expect(count).toBe(1);
});
it('@', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tag = await Tag.create();
await post.updateAssociations({
tags: [tag],
});
const count = await post.countTags();
expect(count).toBe(1);
});
it('@', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tag = await Tag.create();
await post.updateAssociations({
tags: tag.id,
});
const count = await post.countTags();
expect(count).toBe(1);
});
it('@', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tag = await Tag.create();
await post.updateAssociations({
tags: [tag.id],
});
const count = await post.countTags();
expect(count).toBe(1);
});
it('@', async () => {
const [Post, Tag] = db.getModels(['posts', 'tags']);
const post = await Post.create();
const tags = await Tag.bulkCreate([{}, {}]);
await post.updateAssociations({
tags: tags,
});
const count = await post.countTags();
expect(count).toBe(2);
});
it('@', async () => {
const [Post] = db.getModels(['posts']);
const post = await Post.create();
await post.updateAssociations({
tags: {
name: 'tag1'
},
});
const count = await post.countTags();
expect(count).toBe(1);
});
it('@', async () => {
const [Post] = db.getModels(['posts']);
const post = await Post.create();
await post.updateAssociations({
tags: [
{
name: 'tag2'
},
{
name: 'tag3'
},
],
});
const count = await post.countTags();
expect(count).toBe(2);
});
});
});
describe('belongsToMany', () => {
let db: Database;
let post: Model;
let tag1: Model;
let tag2: Model;
beforeAll(async () => {
db = getDatabase({
logging: false,
});
db.table({
name: 'posts',
tableName: 't333333_posts',
fields: [
{
type: 'string',
name: 'slug',
unique: true,
},
{
type: 'belongsToMany',
name: 'tags',
sourceKey: 'slug',
targetKey: 'name',
},
],
});
db.table({
name: 'tags',
tableName: 't333333_tags',
fields: [
{
type: 'string',
name: 'name',
unique: true,
},
{
type: 'belongsToMany',
name: 'posts',
sourceKey: 'name',
targetKey: 'slug',
},
],
});
await db.sync({force: true});
const [ Post, Tag ] = db.getModels(['posts', 'tags']);
post = await Post.create({slug: 'post1'});
tag1 = await Tag.create({name: 'tag1'});
tag2 = await Tag.create({name: 'tag2'});
});
afterAll(async () => {
await db.close();
});
it('@', async () => {
await post.updateAssociations({
tags: tag1.name,
});
expect(await post.countTags()).toBe(1);
});
it('@', async () => {
await post.updateAssociations({
tags: tag2.id,
});
expect(await post.countTags()).toBe(1);
});
it('@', async () => {
await post.updateAssociations({
tags: [tag1, tag2],
});
expect(await post.countTags()).toBe(2);
});
it('@', async () => {
await post.updateAssociations({
tags: {
name: 'tag2',
},
});
expect(await post.countTags()).toBe(1);
expect((await post.getTags())[0].id).toBe(tag2.id);
});
it('@', async () => {
await post.updateAssociations({
tags: [{
name: 'tag3',
}],
});
expect(await post.countTags()).toBe(1);
});
});

View File

@ -0,0 +1,3 @@
module.exports = function () {
return 'fn';
}

View File

@ -0,0 +1 @@
export default () => 'foo';

View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

View File

@ -0,0 +1,3 @@
module.exports = {
'foo': 'bar',
}

View File

@ -0,0 +1,3 @@
export default {
'foo': 'bar',
}

View File

@ -0,0 +1,172 @@
import { getDatabase } from './';
describe('db sync', () => {
describe('table.sync', () => {
it('shound be ok1', async () => {
const db = getDatabase({
logging: false,
});
const table = db.table({
name: 'foos',
});
await table.sync();
await db.close();
});
it('sync#belongsTo', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
tableName: 'foos1',
fields: [
{
type: 'belongsTo',
name: 'bar',
}
],
});
db.table({
name: 'bars',
tableName: 'bars1',
fields: [
{
type: 'hasMany',
name: 'foos',
}
],
});
await db.sequelize.drop();
await db.getTable('foos').sync({
force: false,
alter: {
drop: false,
}
});
await db.close();
});
it('sync#hasMany', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
tableName: 'foos2',
fields: [
{
type: 'belongsTo',
name: 'bar',
}
],
});
db.table({
name: 'bars',
tableName: 'bars2',
fields: [
{
type: 'hasMany',
name: 'foos',
}
],
});
await db.sequelize.drop();
await db.getTable('bars').sync({
force: false,
alter: {
drop: false,
}
});
await db.close();
});
it('sync#belongsToMany', async () => {
const db = getDatabase({
logging: false,
});
db.table({
name: 'foos',
fields: [
{
type: 'belongsToMany',
name: 'bars',
},
{
type: 'string',
name: 'name',
},
],
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsToMany',
name: 'foos',
},
{
type: 'string',
name: 'name',
}
],
});
await db.sequelize.drop();
await db.getTable('foos').sync({
force: false,
alter: {
drop: false,
}
});
await db.getModel('bars').create({
name: 'aa',
});
expect(await db.getModel('bars').count()).toBe(1);
db.getTable('bars').addField({
type: 'string',
name: 'col1',
});
await db.getTable('bars').sync({
force: false,
alter: {
drop: false,
}
});
await db.getModel('bars').create({
name: 'bb',
col1: 'val1'
});
expect(await db.getModel('bars').count()).toBe(2);
await db.close();
});
it('shound be ok2', async () => {
const db = getDatabase({
logging: false,
});
const table = db.table({
name: 'goos',
});
await table.sync({
force: true,
});
table.addField({
type: 'string',
name: 'col1',
});
await table.sync({
force: false,
alter: {
drop: false,
}
});
table.addField({
type: 'string',
name: 'col2',
});
await table.sync({
force: false,
alter: {
drop: false,
}
});
await db.close();
});
});
});

View File

@ -0,0 +1,300 @@
import { getDatabase } from './';
import Database from '../database';
import Table from '../table';
import Model from '../model';
describe('tables', () => {
let db: Database;
// beforeAll(() => {
// db = getDatabase();
// });
// afterAll(async () => {
// await db.sequelize.close();
// });
describe('options', () => {
it('shoud be defined', () => {
db = getDatabase();
db.table({
name: 'foo',
});
expect(db.getTable('foo')).toBeInstanceOf(Table);
expect(db.isDefined('foo')).toBe(true);
});
it('custom model test', () => {
class Abc extends Model {
public static database: Database;
static getModel(name: string) {
return this.database.getModel(name);
}
static test12345() {
return 'test12345';
}
}
db = getDatabase();
const table = db.table({
name: 'abc',
model: Abc,
});
expect(Abc.database).toBe(db);
expect(table.getModel().test12345()).toBe('test12345');
expect(Abc.getModel('abc').test12345()).toBe('test12345');
});
it('shoud tableName === name', async () => {
db = getDatabase();
db.table({
name: 'foos',
});
expect(db.getModel('foos').name).toBe('foos');
expect(db.getModel('foos').getTableName()).toBe('foos');
});
it('shoud be custom when tableName is defined', async () => {
db = getDatabase();
db.table({
name: 'bar',
tableName: 'bar_v2'
});
expect(db.getModel('bar').name).toBe('bar');
expect(db.getModel('bar').getTableName()).toBe('bar_v2');
});
it('shoud be custom when timestamps is defined', async () => {
db = getDatabase();
db.table({
name: 'baz',
createdAt: 'created',
updatedAt: 'updated',
});
expect(db.getModel('baz').rawAttributes.created).toBeDefined();
expect(db.getModel('baz').rawAttributes.updated).toBeDefined();
});
it('index shound be defined', async () => {
db = getDatabase();
db.table({
name: 'baz',
fields: [
{
type: 'string',
name: 'col1',
index: true,
},
{
type: 'string',
name: 'col2',
},
{
type: 'string',
name: 'col3',
index: {
fields: ['col2', 'col3'],
},
},
],
});
expect(db.getModel('baz').options.indexes).toStrictEqual(db.getTable('baz').getModelOptions().indexes);
expect(db.getTable('baz').getModelOptions().indexes).toStrictEqual([
{ fields: [ 'col1' ], name: 'baz_col1', type: '', parser: null },
{ fields: [ 'col2', 'col3' ], name: 'baz_col2_col3', type: '', parser: null }
]);
});
it('index shound be defined', async () => {
db = getDatabase();
db.table({
name: 'baz2',
indexes: [
{
fields: ['col1'],
},
{
fields: ['col2', 'col3'],
},
],
fields: [
{
type: 'string',
name: 'col1',
},
{
type: 'string',
name: 'col2',
},
{
type: 'string',
name: 'col3',
},
],
});
expect(db.getModel('baz2').options.indexes).toStrictEqual(db.getTable('baz2').getModelOptions().indexes);
expect(db.getTable('baz2').getModelOptions().indexes).toStrictEqual([
{ fields: [ 'col1' ], name: 'baz2_col1', type: '', parser: null },
{ fields: [ 'col2', 'col3' ], name: 'baz2_col2_col3', type: '', parser: null },
]);
});
});
describe('#extend()', () => {
it('shoud be extend', async () => {
db = getDatabase();
db.table({
name: 'baz',
});
expect(db.getModel('baz').rawAttributes.created_at).toBeDefined();
expect(db.getModel('baz').rawAttributes.updated_at).toBeDefined();
db.extend({
name: 'baz',
createdAt: 'created',
updatedAt: 'updated',
});
expect(db.getModel('baz').rawAttributes.created).toBeDefined();
expect(db.getModel('baz').rawAttributes.updated).toBeDefined();
});
it('shoud be extend', async () => {
db = getDatabase();
db.table({
name: 'foos',
});
db.table({
name: 'baz',
fields: [
{
type: 'string',
name: 'col1',
},
{
type: 'hasOne',
name: 'foo',
}
]
});
db.extend({
name: 'baz',
timestamps: false,
fields: [
{
type: 'string',
name: 'col2',
}
],
});
expect(db.getModel('baz').rawAttributes.col1).toBeDefined();
expect(db.getModel('baz').rawAttributes.col2).toBeDefined();
expect(db.getModel('baz').rawAttributes.created_at).toBeUndefined();
expect(db.getModel('baz').rawAttributes.updated_at).toBeUndefined();
expect(db.getModel('baz').rawAttributes.col2).toBeDefined();
expect(db.getModel('baz').associations.foo).toBeDefined();
// await db.sync({force: true});
// await db.sequelize.close();
});
});
describe('associations', () => {
beforeAll(() => {
db = getDatabase();
});
it('shound be undefined when target table does not exist', () => {
db.table({
name: 'bars',
fields: [
{
type: 'hasOne',
name: 'foo',
}
],
});
expect(db.getModel('bars').associations.foo).toBeUndefined();
});
it('shound be defined when target table exists', () => {
db.table({name: 'foos'});
expect(db.getModel('bars').associations.foo).toBeDefined();
});
describe('#setFields()', () => {
beforeAll(() => {
db = getDatabase();
db.table({
name: 'table1',
fields: [
{
type: 'hasOne',
name: 'table21',
target: 'table2',
},
{
type: 'hasOne',
name: 'table22',
target: 'table2',
},
]
});
db.table({
name: 'table2',
});
});
it('shound be defined', () => {
const table1 = db.getModel('table1');
expect(Object.keys(table1.associations).length).toBe(2);
expect(table1.associations.table21).toBeDefined();
expect(table1.associations.table22).toBeDefined();
});
it('shound be defined', () => {
db.getTable('table1').setFields([
{
type: 'string',
name: 'name',
},
{
type: 'hasOne',
name: 'table23',
target: 'table2',
},
]);
const table1 = db.getModel('table1');
expect(table1.rawAttributes.name).toBeDefined();
expect(Object.keys(table1.associations).length).toBe(1);
expect(table1.associations.table21).toBeUndefined();
expect(table1.associations.table22).toBeUndefined();
expect(table1.associations.table23).toBeDefined();
});
});
describe('#addField()', () => {
beforeAll(() => {
db = getDatabase();
db.table({
name: 'table1',
});
db.table({
name: 'table2',
});
});
it('shound be defined when the field be added after initialization', () => {
db.getTable('table1').addField({
type: 'hasOne',
name: 'table2',
target: 'table2',
});
expect(Object.keys(db.getModel('table1').associations).length).toBe(1);
expect(db.getModel('table1').associations.table2).toBeDefined();
});
it('shound be defined when continue to add', () => {
db.getTable('table1').addField({
type: 'hasOne',
name: 'table21',
target: 'table2',
});
expect(Object.keys(db.getModel('table1').associations).length).toBe(2);
expect(db.getModel('table1').associations.table2).toBeDefined();
expect(db.getModel('table1').associations.table21).toBeDefined();
});
});
});
});

View File

@ -0,0 +1,330 @@
import {
buildField,
Boolean,
Integer,
String,
Text,
HasOne,
HasMany,
BelongsTo,
BelongsToMany,
Float,
Double,
Real,
Decimal,
Column,
Time,
Date,
DateOnly,
Array,
Json,
Jsonb,
Password
} from '../fields';
import { DataTypes } from 'sequelize';
import { ABSTRACT } from 'sequelize/lib/data-types';
import { getDatabase } from '.';
import Database from '..';
describe('field types', () => {
const assertTypeInstanceOf = (expected, actual) => {
const db = getDatabase();
const table = db.table({
name: 'test',
});
const field = buildField({
type: actual,
name: 'test',
}, {
sourceTable: table,
database: db,
});
expect(field).toBeInstanceOf(expected);
if (field instanceof Column) {
const { type } = field.getAttributeOptions() as any;
if (actual instanceof ABSTRACT) {
expect(type).toBeInstanceOf(field.getDataType());
// postgres 的 text 不限制长度,无需参数
if (db.sequelize.getDialect() !== 'postgres' || field.getType() !== 'TEXT') {
// 非严谨比较undefined == null
expect(type).toEqual(actual);
}
} else if (typeof actual === 'function') {
expect(type).toBe(field.getDataType());
expect(type).toBe(actual);
} else if (typeof actual === 'string') {
expect(type).toBe(field.getDataType());
}
}
}
it('shound be boolean', () => {
assertTypeInstanceOf(Boolean, 'boolean');
assertTypeInstanceOf(Boolean, DataTypes.BOOLEAN);
});
it('shound be integer', () => {
assertTypeInstanceOf(Integer, 'int');
assertTypeInstanceOf(Integer, 'integer');
assertTypeInstanceOf(Integer, DataTypes.INTEGER);
assertTypeInstanceOf(Integer, DataTypes.INTEGER({
length: 5,
}));
});
it('shound be tiny integer', () => {
assertTypeInstanceOf(Integer, 'tinyint');
assertTypeInstanceOf(Integer, 'tinyInt');
assertTypeInstanceOf(Integer, 'tinyinteger');
assertTypeInstanceOf(Integer, 'tinyInteger');
assertTypeInstanceOf(Integer, DataTypes.TINYINT);
assertTypeInstanceOf(Integer, DataTypes.TINYINT({
length: 5,
}));
});
it('shound be small integer', () => {
assertTypeInstanceOf(Integer, 'smallint');
assertTypeInstanceOf(Integer, 'smallInt');
assertTypeInstanceOf(Integer, 'smallinteger');
assertTypeInstanceOf(Integer, 'smallInteger');
assertTypeInstanceOf(Integer, DataTypes.SMALLINT);
assertTypeInstanceOf(Integer, DataTypes.SMALLINT({
length: 5,
}));
});
it('shound be medium integer', () => {
assertTypeInstanceOf(Integer, 'mediumInt');
assertTypeInstanceOf(Integer, 'MediumInt');
assertTypeInstanceOf(Integer, 'MediumInteger');
assertTypeInstanceOf(Integer, 'MediumInteger');
assertTypeInstanceOf(Integer, DataTypes.MEDIUMINT);
assertTypeInstanceOf(Integer, DataTypes.MEDIUMINT({
length: 5,
}));
});
it('shound be big integer', () => {
assertTypeInstanceOf(Integer, 'bigint');
assertTypeInstanceOf(Integer, 'bigInt');
assertTypeInstanceOf(Integer, 'biginteger');
assertTypeInstanceOf(Integer, 'bigInteger');
assertTypeInstanceOf(Integer, DataTypes.BIGINT);
assertTypeInstanceOf(Integer, DataTypes.BIGINT({
length: 5,
}));
});
it('shound be float', () => {
assertTypeInstanceOf(Float, 'float');
assertTypeInstanceOf(Float, DataTypes.FLOAT({
length: 5,
}));
});
it('shound be double', () => {
assertTypeInstanceOf(Double, 'double');
assertTypeInstanceOf(Double, DataTypes.DOUBLE(10));
});
it('shound be real', () => {
assertTypeInstanceOf(Real, 'real');
assertTypeInstanceOf(Real, DataTypes.REAL({
length: 5,
}));
});
it('shound be decimal', () => {
assertTypeInstanceOf(Decimal, 'decimal');
assertTypeInstanceOf(Decimal, DataTypes.DECIMAL(5));
});
it('shound be string', () => {
assertTypeInstanceOf(String, 'string');
assertTypeInstanceOf(String, DataTypes.STRING);
assertTypeInstanceOf(String, DataTypes.STRING(100));
});
it('shound be text', () => {
assertTypeInstanceOf(Text, 'text');
assertTypeInstanceOf(Text, DataTypes.TEXT);
assertTypeInstanceOf(Text, DataTypes.TEXT({
length: 'long',
}));
});
it('shound be time', () => {
assertTypeInstanceOf(Time, 'time');
assertTypeInstanceOf(Time, DataTypes.TIME);
});
it('shound be date', () => {
assertTypeInstanceOf(Date, 'date');
assertTypeInstanceOf(Date, 'timestamp');
assertTypeInstanceOf(Date, DataTypes.DATE);
assertTypeInstanceOf(Date, DataTypes.DATE(2));
});
it('shound be dateonly', () => {
assertTypeInstanceOf(DateOnly, 'dateOnly');
assertTypeInstanceOf(DateOnly, 'dateonly');
assertTypeInstanceOf(DateOnly, DataTypes.DATEONLY);
});
it('shound be array', () => {
assertTypeInstanceOf(Array, 'array');
});
it('shound be json', () => {
assertTypeInstanceOf(Json, 'json');
assertTypeInstanceOf(Json, DataTypes.JSON);
});
it('shound be jsonb', () => {
assertTypeInstanceOf(Jsonb, 'jsonb');
assertTypeInstanceOf(Jsonb, DataTypes.JSONB);
});
it('shound be HasOne relationship', () => {
assertTypeInstanceOf(HasOne, 'hasone');
assertTypeInstanceOf(HasOne, 'hasOne');
assertTypeInstanceOf(HasOne, 'HasOne');
});
it('shound be HasMany relationship', () => {
assertTypeInstanceOf(HasMany, 'hasmany');
assertTypeInstanceOf(HasMany, 'hasMany');
assertTypeInstanceOf(HasMany, 'HasMany');
});
it('shound be BelongsTo relationship', () => {
assertTypeInstanceOf(BelongsTo, 'belongsto');
assertTypeInstanceOf(BelongsTo, 'belongsTo');
assertTypeInstanceOf(BelongsTo, 'BelongsTo');
});
it('shound be BelongsToMany relationship', () => {
assertTypeInstanceOf(BelongsToMany, 'belongstomany');
assertTypeInstanceOf(BelongsToMany, 'belongsToMany');
assertTypeInstanceOf(BelongsToMany, 'BelongsToMany');
});
describe('virtual', () => {
let db: Database;
beforeAll(async () => {
db = getDatabase();
db.table({
name: 'formula_tests',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'integer',
name: 'number1',
},
{
type: 'integer',
name: 'number2',
},
{
type: 'json',
name: 'meta',
},
{
type: 'formula',
name: 'formula1',
format: 'number',
formula: '{{ number1 + number2 }}',
},
{
type: 'formula',
name: 'formula2',
formula: '1{{ title }}2',
},
{
type: 'reference',
name: 'reference1',
dataIndex: 'key1',
source: 'meta',
},
{
type: 'reference',
name: 'reference2',
dataIndex: 'col2',
source: 'bar',
},
{
type: 'belongsTo',
name: 'bar',
},
],
});
db.table({
name: 'password_table',
fields: [
{
type: 'password',
name: 'password',
},
],
})
db.table({
name: 'bars',
tableName: 'formula_bars',
fields: [
{
type: 'string',
name: 'col2',
}
],
})
await db.sync({force: true});
});
afterAll(async () => {
await db.close();
});
it('pwd', async () => {
const Pwd = db.getModel('password_table');
const pwd = await Pwd.create({
password: '123456',
});
expect(Password.verify('123456', pwd.password)).toBeTruthy();
});
it('formula', async () => {
const [ Formula ] = db.getModels(['formula_tests']);
const formula = await Formula.create({
title: 'title1',
number1: 1,
number2: 2,
});
expect(formula.formula1).toBe(3);
expect(formula.formula2).toBe('1title12');
});
it('formula', async () => {
const [ Formula ] = db.getModels(['formula_tests']);
const formula = await Formula.create({
meta: {
key1: 'val1',
},
});
await formula.updateAssociations({
bar: {
col2: 'val2',
}
});
const f = await Formula.findOne({
where: {
id: formula.id,
},
include: {
association: 'bar',
}
});
expect(f.reference1).toBe('val1');
expect(f.reference2).toBe('val2');
});
});
});

View File

@ -0,0 +1,658 @@
import { requireModule, toWhere, toInclude } from '../utils';
import path from 'path';
import { Op } from 'sequelize';
import { getDatabase } from '.';
import Database from '../database';
import Model, { ModelCtor } from '../model';
describe('utils', () => {
describe('toWhere', () => {
it('Op.eq', () => {
const where = toWhere({
id: {
eq: 12
},
});
expect(where).toEqual({
id: {
[Op.eq]: 12,
},
});
});
it('Op.ilike', () => {
const where = toWhere({
id: {
ilike: 'val1'
},
});
expect(where).toEqual({
id: {
[Op.iLike]: 'val1',
},
});
});
it('Op.ilike', () => {
const where = toWhere({
'id.ilike': 'val1',
});
expect(where).toEqual({
id: {
[Op.iLike]: 'val1',
},
});
});
it('Op.is null', () => {
const where = toWhere({
'id.is': null,
});
expect(where).toEqual({
id: {
[Op.is]: null,
},
});
});
it('Op.in', () => {
const where = toWhere({
id: {
in: [12]
},
});
expect(where).toEqual({
id: {
[Op.in]: [12],
},
});
});
it('Op.in', () => {
const where = toWhere({
'id.in': [11, 12],
});
expect(where).toEqual({
id: {
[Op.in]: [11, 12],
},
});
});
describe('association', () => {
let db: Database;
let Foo: ModelCtor<Model>;
beforeAll(() => {
db = getDatabase();
db.table({
name: 'bazs',
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'baz',
}
]
});
db.table({
name: 'foos',
fields: [
{
type: 'hasMany',
name: 'bars',
}
],
});
Foo = db.getModel('foos');
});
const toWhereExpect = (options, logging = false) => {
const where = toWhere(options, {
associations: Foo.associations,
});
return expect(where);
}
it('association', () => {
toWhereExpect({
col1: 'val1',
bars: {
name: {
ilike: 'aa',
},
col2: {
lt: 2,
},
baz: {
col1: 12,
},
},
'bars.col3.ilike': 'aa',
}).toEqual({
col1: 'val1',
$__include: {
bars: {
name: {
[Op.iLike]: 'aa',
},
col2: {
[Op.lt]: 2,
},
col3: {
[Op.iLike]: 'aa',
},
$__include: {
baz: {
col1: 12
},
},
},
},
});
});
});
});
describe('toInclude', () => {
let db: Database;
let Foo: ModelCtor<Model>;
beforeAll(() => {
db = getDatabase();
db.table({
name: 'bazs',
});
db.table({
name: 'bays',
});
db.table({
name: 'bars',
fields: [
{
type: 'belongsTo',
name: 'baz',
},
{
type: 'belongsTo',
name: 'bay',
},
],
});
db.table({
name: 'foos',
fields: [
{
type: 'hasMany',
name: 'bars',
},
{
type: 'hasMany',
name: 'fozs',
},
{
type: 'hasMany',
name: 'coos',
},
],
});
db.table({
name: 'fozs',
});
db.table({
name: 'coos',
});
Foo = db.getModel('foos');
});
const toIncludeExpect = (options: any, logging = false) => {
const include = toInclude(options, {
Model: Foo,
associations: Foo.associations,
});
if (logging) {
console.log(JSON.stringify(include, null, 2));
}
return expect(include);
};
it('normal columns', () => {
toIncludeExpect({
fields: ['col1', 'col2'],
}).toEqual({ attributes: [ 'col1', 'col2' ] });
});
it('association count attribute', () => {
toIncludeExpect({
fields: ['col1', 'bars_count'],
}).toEqual({ attributes: [ 'col1', Foo.withCountAttribute('bars') ] });
});
it('association without attributes', () => {
toIncludeExpect({
fields: ['col1', 'bars'],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
}
],
});
});
it('association attributes', () => {
toIncludeExpect({
fields: ['col1', 'bars.col1', 'bars.col2'],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
attributes: [ 'col1', 'col2' ],
}
],
});
});
it('association attributes', () => {
toIncludeExpect({
fields: ['col1', ['bars', 'col1'], ['bars', 'col2']],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
attributes: [ 'col1', 'col2' ],
}
],
});
});
it('nested association', () => {
toIncludeExpect({
fields: ['col1', 'bars.baz'],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
attributes: [],
include: [
{
association: 'baz',
}
]
}
],
});
});
it.skip('nested association', () => {
// TODO输出 bars 的所有字段
toIncludeExpect({
fields: ['col1', 'bars', 'bars.baz'],
}, true).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
include: [
{
association: 'baz',
}
]
}
],
});
});
it('nested association', () => {
toIncludeExpect({
fields: ['col1', 'bars.col1', 'bars.col2', 'bars.baz'],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
attributes: ['col1', 'col2'],
include: [
{
association: 'baz',
}
]
}
],
});
});
it('nested association', () => {
// TODO输出 bars 的所有字段
toIncludeExpect({
fields: ['col1', 'bars.col1', 'bars.col2', 'bars.baz.col1', 'bars.baz.col2'],
}).toEqual({
attributes: [ 'col1' ],
include: [
{
association: 'bars',
attributes: ['col1', 'col2'],
include: [
{
association: 'baz',
attributes: ['col1', 'col2'],
}
]
}
],
});
});
it('append attributes', () => {
toIncludeExpect({
fields: {
appends: ['bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
},
});
});
it('append attributes', () => {
toIncludeExpect({
fields: {
appends: ['bars.col1', 'bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
},
include: [
{
association: 'bars',
attributes: {
include: ['col1'],
},
}
]
});
});
it('only & append attributes', () => {
toIncludeExpect({
fields: {
only: ['col1'],
appends: ['bars_count'],
},
}).toEqual({
attributes: ['col1', Foo.withCountAttribute('bars')],
});
});
it('only & append attributes', () => {
toIncludeExpect({
fields: {
only: ['col1', 'bars.col1'],
appends: ['bars_count'],
},
}).toEqual({
attributes: ['col1', Foo.withCountAttribute('bars')],
include: [
{
association: 'bars',
attributes: ['col1'],
}
],
});
});
it('excpet attributes', () => {
toIncludeExpect({
fields: {
except: ['col1'],
},
}).toEqual({
attributes: {
include: [],
exclude: ['col1'],
},
});
});
it('excpet attributes', () => {
toIncludeExpect({
fields: {
except: ['col1', 'bars.col1'],
},
}).toEqual({
attributes: {
include: [],
exclude: ['col1'],
},
include: [
{
association: 'bars',
attributes: {
include: [],
exclude: ['col1'],
}
}
]
});
});
it('excpet & append attributes', () => {
toIncludeExpect({
fields: {
except: ['col1'],
appends: ['bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
exclude: ['col1'],
},
});
});
describe('where options', () => {
it('where', () => {
toIncludeExpect({
filter: {
col1: 'val1',
},
fields: {
except: ['col1'],
appends: ['bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
exclude: ['col1'],
},
where: {
col1: 'val1',
},
});
});
it('where', () => {
toIncludeExpect({
filter: {
col1: 'val1',
bars: {
col1: 'val1',
}
},
fields: {
except: ['col1'],
appends: ['bars', 'bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
exclude: ['col1'],
},
where: {
col1: 'val1',
},
include: [
{
association: 'bars',
where: {
col1: 'val1',
}
}
]
});
});
it('where', () => {
toIncludeExpect({
filter: {
col1: 'val1',
bars: {
col1: 'val1',
}
},
fields: {
except: ['col1'],
appends: ['bars_count'],
},
}).toEqual({
attributes: {
include: [Foo.withCountAttribute('bars')],
exclude: ['col1'],
},
where: {
col1: 'val1',
},
include: [
{
association: 'bars',
where: {
col1: 'val1',
}
}
]
});
});
it('parseApiJson', () => {
const data = Foo.parseApiJson({
filter: {
col1: 'co2',
}
});
expect(data).toEqual({ where: { col1: 'co2' } });
});
it('parseApiJson', () => {
const data = Foo.parseApiJson({
fields: ['col1'],
});
expect(data).toEqual({ attributes: ['col1'] });
});
it('parseApiJson', () => {
const data = Foo.parseApiJson({
fields: ['col1'],
filter: {
col1: 'co2',
},
});
expect(data).toEqual({ attributes: ['col1'], where: { col1: 'co2' } });
});
it('parseApiJson', () => {
const data = Foo.parseApiJson({
fields: ['col1'],
filter: {
col1: 'val1',
bars: {
col1: 'val1',
}
},
});
expect(data).toEqual({
attributes: ['col1'],
where: { col1: 'val1' },
include: [
{
association: 'bars',
where: { col1: 'val1' },
}
],
});
});
it('parseApiJson', () => {
const data = Foo.parseApiJson({
fields: ['col1', 'bars.col1'],
filter: {
col1: 'val1',
bars: {
col1: 'val1',
}
},
});
expect(data).toEqual({
attributes: ['col1'],
where: { col1: 'val1' },
include: [
{
association: 'bars',
attributes: ['col1'],
where: { col1: 'val1' },
}
],
});
});
});
});
describe('requireModule', () => {
test('toBeTruthy', () => {
const r = requireModule(true);
expect(r).toBeTruthy();
});
test('toBeInstanceOf Function', () => {
const r = requireModule(() => {});
expect(r).toBeInstanceOf(Function);
});
test('toBeInstanceOf Function', () => {
const r = requireModule(path.resolve(__dirname, './modules/fn'));
expect(r).toBeInstanceOf(Function);
});
test('toBeInstanceOf Function', () => {
const r = requireModule(path.resolve(__dirname, './modules/fnts'));
expect(r).toBeInstanceOf(Function);
});
test('object', () => {
const r = requireModule(path.resolve(__dirname, './modules/obj'));
expect(r).toEqual({
'foo': 'bar',
});
});
test('object', () => {
const r = requireModule(path.resolve(__dirname, './modules/objts'));
expect(r).toEqual({
'foo': 'bar',
});
});
test('json', () => {
const r = requireModule(path.resolve(__dirname, './modules/json'));
expect(r).toEqual({
'foo': 'bar',
});
});
});
});

View File

@ -0,0 +1,245 @@
import {
Options,
Sequelize,
SyncOptions as SequelizeSyncOptions,
} from 'sequelize';
import glob from 'glob';
import Table, { TableOptions } from './table';
import { Model, ModelCtor } from './model';
import { requireModule } from './utils';
export interface SyncOptions extends SequelizeSyncOptions {
/**
* tables
*/
tables?: string[] | Table[] | Map<string, Table>;
}
export interface ImportOptions {
/**
*
*/
directory: string;
/**
* ['js', 'ts', 'json']
*/
extensions?: string[];
}
export default class Database {
public readonly sequelize: Sequelize;
/**
* Model
*/
public readonly associating = new Set<string>();
/**
*
*/
public readonly throughTables = new Map<string, Array<string>>();
protected tables = new Map<string, Table>();
protected options: Options;
constructor(options: Options) {
this.options = options;
this.sequelize = new Sequelize(options);
}
/**
* tables
*
* TODO: 配置的文件驱动现在会全部初始化
*
* @param {object} [options]
* @param {string} [options.directory]
* @param {array} [options.extensions = ['js', 'ts', 'json']]
*/
public import(options: ImportOptions): Map<string, Table> {
const { extensions = ['js', 'ts', 'json'], directory } = options;
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten);
const tables = new Map<string, Table>();
files.forEach((file: string) => {
if (file.endsWith('.d.ts')) {
return;
}
const options = requireModule(file);
const table = this.table(typeof options === 'function' ? options(this) : options);
tables.set(table.getName(), table);
});
return tables;
}
/**
*
*
* @param options
*/
public table(options: TableOptions): Table {
const { name } = options;
const table = new Table(options, { database: this });
this.tables.set(name, table);
// 在 source 或 target 之后定义 through需要更新 source 和 target 的 model
if (this.throughTables.has(name)) {
const [sourceTable, targetTable] = this.getTables(this.throughTables.get(name));
sourceTable && sourceTable.modelInit(true);
targetTable && targetTable.modelInit(true);
// this.throughTables.delete(name);
}
return table;
}
/**
* API
*
* @param options
*/
public extend(options: TableOptions): Table {
const { name } = options;
let table: Table;
if (this.tables.has(name)) {
table = this.tables.get(name);
table.extend(options);
} else {
table = this.table(options);
this.tables.set(name, table);
}
return table;
}
/**
*
*
* @param name
*/
public isDefined(name: string): boolean {
return this.sequelize.isDefined(name);
}
/**
* Model
*
* TODO: 动态初始化并加载配置
*
*
* @param name
*/
public getModel(name: string): ModelCtor<Model> {
return this.isDefined(name) ? this.sequelize.model(name) as any : undefined;
}
/**
* names Models
*
* @param names
*/
public getModels(names: string[]): Array<ModelCtor<Model>> {
return names.map(name => this.getModel(name));
}
/**
* table
*
* TODO:
* table Model
*
*
* @param name
*/
public getTable(name: string): Table {
return this.tables.has(name) ? this.tables.get(name) : undefined;
}
/**
* names table
*
* @param names
*/
public getTables(names: string[]): Array<Table> {
return names.map(name => this.getTable(name));
}
/**
*
*
* Model.init
*/
public associate() {
for (const name of this.associating) {
const Model: any = this.getModel(name);
Model.associate && Model.associate(this.sequelize.models);
}
}
/**
*
*
* TODO: 细节待定
*
* @param plugin
* @param options
*/
public async plugin(plugin: any, options = {}) {
await plugin(this, options);
}
/**
*
*
* @param options
*/
public async sync(options: SyncOptions = {}) {
const { tables = [], ...restOptions } = options;
let items: Array<any>;
if (tables instanceof Map) {
items = Array.from(tables.values());
} else {
items = tables;
}
/**
* sequelize.sync model
* Model.sync Model
* database.sync tables
*/
if (items.length > 0) {
// 指定 tables 时,新建 sequelize 实例来单独处理这些 tables 相关 models 的 sync
const sequelize = new Sequelize(this.options);
const names = new Set<string>();
for (const key in items) {
let table = items[key];
if (typeof table === 'string') {
table = this.getTable(table);
}
if (table instanceof Table) {
for (const name of table.getRelatedTableNames()) {
names.add(name);
}
}
}
for (const name of names) {
// @ts-ignore
sequelize.modelManager.addModel(this.getModel(name));
}
await sequelize.sync(restOptions);
await sequelize.close();
} else {
await this.sequelize.sync(restOptions);
}
}
/**
*
*/
public async close() {
return await this.sequelize.close();
}
}

View File

@ -0,0 +1,691 @@
import {
Utils,
DataType,
DataTypes,
Sequelize,
HasOneOptions,
HasManyOptions,
BelongsToOptions,
BelongsToManyOptions,
ThroughOptions,
} from 'sequelize';
import * as Options from './option-types';
import { getDataTypeKey } from '.';
import Table from '../table';
import Database from '../database';
import Model, { ModelCtor } from '../model';
import { template, isArray, map, get, toNumber } from 'lodash';
import bcrypt from 'bcrypt';
export interface IField {
}
export interface IFields {
[key: string]: IField;
}
export interface FieldContext {
sourceTable: Table;
database: Database,
}
export class Field implements IField {
public readonly options: any;
protected context: FieldContext;
constructor(options: any, context: FieldContext) {
const { type } = options;
this.options = {...options, type: getDataTypeKey(type)};
this.context = context;
}
public isMultipleColumns() {
return false;
}
public getType() {
return this.options.type;
}
public getOptions() {
return this.options;
}
}
export class Column extends Field {
public getDataType() {
const { type } = this.options;
if (DataTypes[type]) {
return DataTypes[type];
}
return DataTypes[(<typeof Column>this.constructor).name.toUpperCase()];
}
public getDataTypeInstance(options: any = {}): any {
const dataType = this.getDataType();
Object.keys(options).forEach(key => options[key] === undefined && delete options[key]);
return Object.keys(options).length > 0 ? dataType(options) : dataType;
}
public getAttributeOptions() {
return {
...this.options,
type: this.getDataTypeInstance(),
}
}
}
export class Boolean extends Column {
}
export class Number extends Column {
}
export class Integer extends Number {
public readonly options: Options.IntegerOptions;
public getDataType(): Function {
const { type } = this.options;
return {
INT: DataTypes.INTEGER,
INTEGER: DataTypes.INTEGER,
TINYINT: DataTypes.TINYINT,
TINYINTEGER: DataTypes.TINYINT,
SMALLINT: DataTypes.SMALLINT,
SMALLINTEGER: DataTypes.SMALLINT,
MEDIUMINT: DataTypes.MEDIUMINT,
MEDIUMINTEGER: DataTypes.MEDIUMINT,
BIGINT: DataTypes.BIGINT,
BIGINTEGER: DataTypes.BIGINT,
}[type as string] || DataTypes.INTEGER;
}
public getAttributeOptions() {
const { length, zerofill, unsigned, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length, zerofill, unsigned }),
}
}
}
export class Float extends Number {
public readonly options: Options.FloatOptions;
public getAttributeOptions() {
const { length, decimals, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length, decimals }),
}
}
}
export class Double extends Number {
public readonly options: Options.DoubleOptions;
public getAttributeOptions() {
const { length, decimals, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length, decimals }),
}
}
}
export class Decimal extends Number {
public readonly options: Options.DecimalOptions;
public getAttributeOptions() {
const { precision, scale, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ precision, scale }),
}
}
}
export class Real extends Number {
public readonly options: Options.RealOptions;
public getAttributeOptions() {
const { length, decimals, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length, decimals }),
}
}
}
export class String extends Column {
public readonly options: Options.StringOptions;
public getAttributeOptions() {
const { length, binary, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length, binary }),
}
}
}
export class Text extends Column {
public readonly options: Options.TextOptions;
public getDataTypeInstance(options: any = {}): any {
const { database } = this.context;
const dataType = this.getDataType();
Object.keys(options).forEach(key => options[key] === undefined && delete options[key]);
if (database.sequelize.getDialect() === 'postgres') {
return Object.keys(options).length > 0 ? dataType() : dataType;
}
return Object.keys(options).length > 0 ? dataType(options) : dataType;
}
public getAttributeOptions() {
const { length, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length }),
}
}
}
export class Time extends Column {
}
export class Date extends Column {
public readonly options: Options.DateOptions;
public getAttributeOptions() {
const { length, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ length }),
}
}
}
export class DateOnly extends Column {
}
export class Virtual extends Column {
}
export class Reference extends Virtual {
public getDataType() {
return DataTypes.VIRTUAL;
}
public getAttributeOptions() {
const { source, dataIndex, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ source, dataIndex }),
get() {
return get(this[source], dataIndex);
},
}
}
}
export class Formula extends Virtual {
public getDataType() {
return DataTypes.VIRTUAL;
}
public getAttributeOptions() {
const { sourceTable } = this.context;
const { formula, format = 'string', ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({ formula }),
get() {
const fields = sourceTable.getFields();
const data: any = {};
for (const [name, field] of fields) {
console.log(field.getType());
if (['formula', 'virtual'].indexOf((field.getType() as string).toLowerCase()) === -1) {
data[name] = this.getDataValue(name);
}
}
try {
const compiled = template(formula, {
interpolate: /{{([\s\S]+?)}}/g,
});
const value = compiled(data);
return format === 'number' ? toNumber(value) : value;
} catch (error) {
console.log(error);
return;
}
}
}
}
}
export class Password extends String {
public getDataType() {
return DataTypes.STRING;
}
public static verify(value: string, hash: string) {
return bcrypt.compareSync(value, hash);
}
public getAttributeOptions() {
const { name, length, binary, ...restOptions } = this.options;
return {
name,
...restOptions,
type: this.getDataTypeInstance({ length, binary }),
set(this: Model, value: string) {
this.setDataValue(name, bcrypt.hashSync(value, 10));
},
}
}
}
export class Array extends Column {
public readonly options: Options.ArrayOptions;
public getDataType() {
return DataTypes.JSON;
}
public getAttributeOptions() {
const { items, ...restOptions } = this.options;
return {
...restOptions,
type: this.getDataTypeInstance({items}),
}
}
}
export class Json extends Column {
}
export class Jsonb extends Column {
}
export interface HasOneAccessors {
get: string;
set: string;
create: string;
}
export interface HasManyAccessors {
get: string;
set: string;
addMultiple: string;
add: string;
create: string;
remove: string;
removeMultiple: string;
hasSingle: string;
hasAll: string;
count: string;
}
export interface BelongsToAccessors {
get: string;
set: string;
create: string;
}
export interface BelongsToManyAccessors {
get: string;
set: string;
addMultiple: string;
add: string;
create: string;
remove: string;
removeMultiple: string;
hasSingle: string;
hasAll: string;
count: string;
}
export abstract class Relation extends Field {
public getAssociationType() {
if (this instanceof HasOne) {
return 'hasOne';
}
if (this instanceof HasMany) {
return 'hasMany';
}
if (this instanceof BelongsTo) {
return 'belongsTo';
}
if (this instanceof BelongsToMany) {
return 'belongsToMany';
}
}
public getTarget() {
const { target, name } = this.options;
if (target) {
return target;
}
if (this instanceof HasMany) {
return name;
}
if (this instanceof BelongsToMany) {
return name;
}
return Utils.pluralize(name);
}
public getTargetModel() {
const { name } = this.options;
const { sourceTable } = this.context;
// @ts-ignore
return sourceTable.getModel().associations[name].target;
}
public getAccessors() {
const { name } = this.options;
const { sourceTable } = this.context;
// @ts-ignore
return sourceTable.getModel().associations[name].accessors;
}
public getAssociationOptions(): any {
const { name, ...restOptions } = this.options;
return {
as: name,
...restOptions,
}
}
public getAssociationArguments() {
return {
target: this.getTarget(),
type: this.getAssociationType(),
options: this.getAssociationOptions(),
}
};
}
class HasOneOrMany extends Relation {
constructor(options: Options.HasOneOptions | Options.HasManyOptions, context: FieldContext) {
const { sourceTable } = context;
let { foreignKey, sourceKey } = options;
const SourceModel = sourceTable.getModel();
if (!sourceKey) {
sourceKey = SourceModel.primaryKeyAttribute;
}
if (!SourceModel.rawAttributes[sourceKey]) {
sourceTable.addField({
type: 'integer',
name: sourceKey,
unique: true,
});
}
if (!foreignKey) {
foreignKey = Utils.underscoredIf(
Utils.camelize([
SourceModel.options.name.singular, sourceKey
].join('_')),
SourceModel.options.underscored
);
}
super({ sourceKey, foreignKey, ...options }, context);
}
}
export class HasOne extends HasOneOrMany {
public readonly options: Options.HasOneOptions;
constructor(options: Options.HasOneOptions, context: FieldContext) {
let { name, target } = options;
if (!target) {
target = Utils.pluralize(name);
}
super({target, ...options}, context);
}
public getAccessors(): HasOneAccessors {
return super.getAccessors();
}
public getAssociationOptions(): HasOneOptions {
const { name, ...restOptions }= this.options;
return {
as: name,
...restOptions,
}
}
}
export class HasMany extends HasOneOrMany {
public readonly options: Options.HasManyOptions;
constructor(options: Options.HasManyOptions, context: FieldContext) {
let { name, target } = options;
if (!target) {
target = name;
}
super({target, ...options}, context);
}
public getAssociationOptions(): HasManyOptions {
const { name, ...restOptions }= this.options;
return {
as: name,
...restOptions,
}
}
}
export class BelongsTo extends Relation {
public readonly options: Options.BelongsToOptions;
constructor(options: Options.BelongsToOptions, context: FieldContext) {
let { name, target } = options;
if (!target) {
target = Utils.pluralize(name);
}
super({target, ...options}, context);
this.updateOptionsAfterTargetModelBeDefined();
}
public getAccessors(): BelongsToAccessors {
return super.getAccessors();
}
public updateOptionsAfterTargetModelBeDefined() {
let { name, target, targetKey, foreignKey } = this.options;
const { database } = this.context;
const TargetModel = database.getModel(target);
if (!TargetModel) {
return;
}
if (!targetKey) {
targetKey = TargetModel.primaryKeyAttribute;
this.options.targetKey = targetKey;
}
if (!foreignKey) {
foreignKey = Utils.underscoredIf(
Utils.camelize([
name, targetKey
].join('_')),
TargetModel.options.underscored
);
this.options.foreignKey = foreignKey;
}
}
public getAssociationOptions(): BelongsToOptions {
const { name, ...restOptions }= this.options;
return {
as: name,
...restOptions,
}
}
}
export class BelongsToMany extends Relation {
public readonly options: Options.BelongsToManyOptions;
constructor(options: Options.BelongsToManyOptions, context: FieldContext) {
let { name, target, through, sourceKey, foreignKey, targetKey, otherKey } = options;
const { database, sourceTable } = context;
const SourceModel = sourceTable.getModel();
if (!target) {
target = name;
}
if (!through) {
through = Utils.underscoredIf(
Utils.camelize(
[SourceModel.name, target]
.map(name => name.toLowerCase())
.sort()
.join('_')
),
SourceModel.options.underscored
);
}
if (!sourceKey) {
sourceKey = SourceModel.primaryKeyAttribute;
}
if (!foreignKey) {
foreignKey = Utils.underscoredIf(
Utils.camelize([
SourceModel.options.name.singular, sourceKey
].join('_')),
SourceModel.options.underscored
);
}
super({
target,
through,
sourceKey,
foreignKey,
...options,
}, context);
this.updateOptionsAfterTargetModelBeDefined();
// through table 未特殊定义时,默认根据 through 信息配置 through table
// database.tables 里不会有 through table但 database.sequelize.models 有
database.throughTables.set(this.getThroughName(), [sourceTable.getName(), target]);
}
public getAccessors(): BelongsToManyAccessors {
return super.getAccessors();
}
public updateOptionsAfterTargetModelBeDefined() {
const { database } = this.context;
let { target, targetKey, otherKey } = this.options;
const TargetModel = database.getModel(target);
if (!TargetModel) {
return;
}
if (!targetKey) {
targetKey = TargetModel.primaryKeyAttribute;
this.options.targetKey = targetKey;
}
if (!otherKey) {
otherKey = Utils.underscoredIf(
Utils.camelize([
TargetModel.options.name.singular, targetKey
].join('_')),
TargetModel.options.underscored
);
this.options.otherKey = otherKey;
}
}
public getThroughName(): string {
// TODO name 必须是字符串
return this.options.through as string;
}
public getThroughModel(): ModelCtor<Model> {
const { through, target } = this.options;
const { database, sourceTable } = this.context;
const throughName = this.getThroughName();
if (database.sequelize.isDefined(throughName)) {
return database.getModel(throughName);
}
// 如果不存在 Through Model需要初始化一个不能用 Sequelize.Model
class ThroughModel extends Model {}
// TODO需要对接 through 的其他参数
ThroughModel.init({}, {
modelName: throughName,
tableName: throughName,
sequelize: database.sequelize,
indexes: [],
underscored: true,
});
return ThroughModel;
}
public getAssociationOptions(): BelongsToManyOptions {
const { name, ...restOptions }= this.options;
return {
as: name,
through: this.getThroughModel(),
...restOptions,
}
}
}

View File

@ -0,0 +1,124 @@
import * as Fields from './field-types';
import { IField, IFields } from './field-types';
import { FieldOptions } from './option-types';
import { ABSTRACT } from 'sequelize/lib/data-types';
/**
* Field Column Relation
*
* Column
*
* - Boolean
* - Number
* - Integer
* - Float
* - Double
* - Decimal
* - Real
* - String
* - Text
* - Array
* - Json
* - Jsonb
* - Time
* - Date
* - Dateonly
* - Virtual
* - Formula
*
* Relation:
*
* - HasOne
* - HasMany
* - BelongsTo
* - BelongsToMany
*/
export * from './option-types';
export * from './field-types';
/**
*
*/
const registeredFields = new Map<string, any>();
/**
*
*
* @param key
* @param field
*/
export function registerField(key: string, field: IField) {
registeredFields.set(key.toUpperCase(), field);
}
/**
*
*
* @param fields
*/
export function registerFields(fields: IFields) {
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
registerField(key, fields[key]);
}
}
}
export function getField(key: string) {
key = key.toUpperCase();
if (registeredFields.has(key)) {
return registeredFields.get(key);
}
return Fields.Column;
}
export function getDataTypeKey(type: any): string {
if (typeof type === 'string') {
return type.toUpperCase();
}
if (Object.prototype.hasOwnProperty.call(type, 'key')) {
return type.key.toUpperCase();
}
if (type instanceof ABSTRACT) {
return type.constructor.name.toUpperCase();
}
return type.toUpperCase();
}
/**
*
*
* @param options
* @param context
*/
export function buildField(options: FieldOptions, context: Fields.FieldContext) {
let { type, required } = options;
if (type instanceof ABSTRACT) {
options = {...type.options, ...options};
}
type = getDataTypeKey(type);
if (type !== 'VIRTUAL' && required) {
options.allowNull = false;
}
const Field = getField(type);
return new Field({type, ...options}, context);
}
registerFields({
...Fields,
// aliases
Int: Fields.Integer,
TinyInt: Fields.Integer,
TinyInteger: Fields.Integer,
SmallInt: Fields.Integer,
SmallInteger: Fields.Integer,
MediumInt: Fields.Integer,
MediumInteger: Fields.Integer,
BigInt: Fields.Integer,
BigInteger: Fields.Integer,
Timestamp: Fields.Date,
Creator: Fields.BelongsTo,
});

View File

@ -0,0 +1,214 @@
import {
Utils,
DataType,
DataTypes,
Sequelize,
ModelAttributeColumnOptions,
ThroughOptions,
StringDataTypeOptions,
IntegerDataTypeOptions,
NumberDataTypeOptions,
TextDataTypeOptions,
FloatDataTypeOptions,
DecimalDataTypeOptions,
DoubleDataTypeOptions,
RealDataTypeOptions,
DateDataTypeOptions,
HasOneOptions as SequelizeHasOneOptions,
HasManyOptions as SequelizeHasManyOptions,
BelongsToOptions as SequelizeBelongsToOptions,
BelongsToManyOptions as SequelizeBelongsToManyOptions,
ModelIndexesOptions,
} from 'sequelize';
export interface AbstractFieldOptions {
name: string;
[key: string]: any;
}
export interface AbstractColumnOptions extends AbstractFieldOptions, Omit<ModelAttributeColumnOptions, 'type'> {
dataType?: DataType;
index?: boolean | ModelIndexesOptions;
}
export interface AbstractRelationOptions extends AbstractFieldOptions {
target?: string;
}
export interface BooleanOptions extends AbstractColumnOptions {
type: 'boolean';
}
export interface NumberOptions extends AbstractColumnOptions {
// type: 'number' | typeof DataTypes.NUMBER | string;
}
export interface IntegerOptions extends IntegerDataTypeOptions, NumberOptions {
type: 'int' | 'integer' | typeof DataTypes.INTEGER |
'tinyint' | 'tinyInteger' | typeof DataTypes.TINYINT |
'smallint' | 'smallInteger' | typeof DataTypes.SMALLINT |
'mediumint' | 'mediumInteger' | typeof DataTypes.MEDIUMINT |
'bigint' | 'bigInteger' | typeof DataTypes.BIGINT;
}
export interface FloatOptions extends FloatDataTypeOptions, NumberOptions {
type: 'float' | typeof DataTypes.FLOAT;
}
export interface DoubleOptions extends DoubleDataTypeOptions, NumberOptions {
type: 'double' | typeof DataTypes.DOUBLE;
}
export interface DecimalOptions extends DecimalDataTypeOptions, NumberOptions {
type: 'decimal' | typeof DataTypes.DECIMAL;
}
export interface RealOptions extends RealDataTypeOptions, NumberOptions {
type: 'real' | typeof DataTypes.REAL;
}
export interface StringOptions extends StringDataTypeOptions, AbstractColumnOptions {
type: 'string' | typeof DataTypes.STRING;
}
export interface PasswordOptions extends Omit<StringOptions, 'type'> {
type: 'password';
}
export interface TextOptions extends TextDataTypeOptions, AbstractColumnOptions {
type: 'text' | typeof DataTypes.TEXT;
}
export interface TimeOptions extends DateDataTypeOptions, AbstractColumnOptions {
type: 'time' | typeof DataTypes.TIME;
}
export interface DateOptions extends DateDataTypeOptions, AbstractColumnOptions {
type: 'date' | typeof DataTypes.DATE;
}
export interface DateOnlyOptions extends AbstractColumnOptions {
type: 'dateonly' | typeof DataTypes.DATEONLY;
}
export interface ArrayOptions extends AbstractColumnOptions {
type: 'array' | typeof DataTypes.ARRAY;
items?: any;
}
export interface JsonOptions extends AbstractColumnOptions {
type: 'json' | 'jsonb' | typeof DataTypes.JSON | typeof DataTypes.JSONB;
fields?: any;
}
export interface VirtualOptions extends AbstractColumnOptions {
type: 'virtual';
}
export interface FormulaOptions extends AbstractColumnOptions {
type: 'formula';
formula: string;
format: 'string' | 'number';
}
export interface ReferenceOptions extends AbstractColumnOptions {
type: 'reference';
source: string;
dataIndex: string;
}
export interface HasOneOptions extends SequelizeHasOneOptions, AbstractRelationOptions {
type: 'hasOne' | 'hasone';
}
export interface HasManyOptions extends SequelizeHasManyOptions, AbstractRelationOptions {
type: 'hasMany' | 'hasmany';
/**
* The name of the field to use as the key for the association in the source table.
* Defaults to the primary key of the source table
*/
sourceKey?: string;
/**
* Defaults to source singular name + sourceKey
*/
foreignKey?: string;
}
export interface BelongsToOptions extends SequelizeBelongsToOptions, AbstractRelationOptions {
type: 'belongsTo' | 'belongsto';
/**
* The name of the field to use as the key for the association in the target table.
* Defaults to the primary key of the target table
*/
targetKey?: string;
/**
* Defaults to name + targetKey
*/
foreignKey?: string;
}
export interface BelongsToManyOptions extends Omit<SequelizeBelongsToManyOptions, 'through'>, AbstractRelationOptions {
type: 'belongsToMany' | 'belongstomany';
/**
* Defaults to the name of source + the name of target
*
* name
* users.belongsToMany(posts) -> through = posts_users
*/
through?: string;
/**
* Defaults to the primary key of the source table
*/
sourceKey?: string;
/**
* Defaults to the name of source + sourceKey
*/
foreignKey?: string;
/**
* Defaults to the primary key of the target table
*/
targetKey?: string;
/**
* Defaults to the name of target + targetKey
*/
otherKey?: string;
}
export type ColumnOptions = AbstractFieldOptions
| BooleanOptions
| NumberOptions
| IntegerOptions
| FloatOptions
| DoubleOptions
| DecimalOptions
| RealOptions
| StringOptions
| PasswordOptions
| TextOptions
| TimeOptions
| DateOptions
| DateOnlyOptions
| ArrayOptions
| JsonOptions
| VirtualOptions
| FormulaOptions
| ReferenceOptions;
export type ElementOptions = BooleanOptions
| IntegerOptions
| FloatOptions
| DoubleOptions
| DecimalOptions
| RealOptions
| StringOptions
| TextOptions
| TimeOptions
| DateOptions
| DateOnlyOptions
| ArrayOptions
| JsonOptions
| VirtualOptions;
export type RelationOptions = HasOneOptions | HasManyOptions | BelongsToOptions | BelongsToManyOptions;
export type FieldOptions = ColumnOptions | RelationOptions;

View File

@ -0,0 +1,9 @@
import Database from './database';
export * from './database';
export * from './model';
export * from './table';
export * from './fields';
export * from './utils';
export default Database;

View File

@ -0,0 +1,418 @@
import {
Model as SequelizeModel, Op, Sequelize, ProjectionAlias, Utils, SaveOptions,
} from 'sequelize';
import Database from './database';
import { HasOne, HasMany, BelongsTo, BelongsToMany, getDataTypeKey } from './fields';
import { toInclude } from './utils';
export interface ApiJsonOptions {
/**
*
*
*
* ['col', 'association.col1', 'association_count'],
*
*
* {
* only: ['col1'],
* appends: ['association_count'],
* }
*
*
* {
* except: ['col1'],
* appends: ['association_count'],
* }
*/
fields?: string[] | {
only?: string[];
appends?: string[];
} | {
except?: string[];
appends?: string[];
};
/**
*
*
*
* {
* col1: {
* $eq: 'val1'
* },
* }
*
* scope scope col scope
* {
* scope1: value
* }
*
* json &
* {
* 'association.col1': {
* $eq: 'val1'
* },
* }
*
* meta json
* {
* 'meta.key': {
* $eq: 'val1'
* },
* }
*
* json &
* {
* association: {
* col1: {
* $eq: 'val1'
* },
* },
* }
*/
filter?: any;
/**
*
*
* TODO
*
* ['col1', '-col2', 'association.col1', '-association.col2']
*/
sort?: any;
context?: any;
[key: string]: any;
}
export interface WithCountAttributeOptions {
/**
*
*/
association: string;
/**
* SourceModel
*
* include 使 include association
*
* include: {
* association: 'user', // Post.belongsTo(User)
* attributes: [
* User.withCountAttribute({
* association: 'posts',
* sourceAlias: 'user', // 内嵌时,需要指定 source 别名
* })
* ]
* }
*/
sourceAlias?: string;
where?: any;
/**
* association_count
*/
alias?: string;
[key: string]: any;
}
/**
* Model
*
* TODO: 自定义 model
*/
// @ts-ignore
export abstract class Model extends SequelizeModel {
/**
* ts
*/
[key: string]: any;
/**
* Model database
*
* Model.sequelize database public static readonly
*/
public static database: Database;
/**
* model 访 database
*/
get database(): Database {
// @ts-ignore
return this.constructor.database;
}
/**
* sub query
*
* TODO: 关联字段暂不支持主键以外的字段
*
* @param options
*/
static withCountAttribute(options?: string | WithCountAttributeOptions): (string | ProjectionAlias) {
if (typeof options === 'string') {
options = { association: options };
}
const { sourceAlias, association, where = {}, alias, ...restOptions } = options;
const associator = this.associations[association];
const table = this.database.getTable(this.name);
const field = table.getField(association);
const { targetKey, otherKey, foreignKey, sourceKey } = field.options as any;
if (associator.associationType === 'HasMany') {
where[foreignKey as string] = {
[Op.eq]: Sequelize.col(`${sourceAlias||this.name}.${sourceKey}`),
};
} else if (associator.associationType === 'BelongsToMany') {
where[targetKey] = {
// @ts-ignore
[Op.in]: Sequelize.literal(`(${associator.through.model.selectQuery({
attributes: [otherKey],
where: {
[foreignKey]: {
[Op.eq]: Sequelize.col(`${sourceAlias||this.name}.${sourceKey}`),
},
// @ts-ignore
...(associator.through.scope||{}),
},
})})`),
};
}
let countLiteral = 'count(*)';
if (this.database.sequelize.getDialect() === 'postgres') {
countLiteral = 'cast(count(*) as integer)';
}
const attribute = [
Sequelize.literal(
// @ts-ignore
`(${associator.target.selectQuery({
...restOptions,
attributes: [[Sequelize.literal(countLiteral), 'count']],
where: {
// @ts-ignore
...where, ...(associator.scope||{}),
},
})})`
),
alias || Utils.underscoredIf(`${association}Count`, this.options.underscored),
].filter(Boolean);
return attribute as ProjectionAlias;
}
/**
* Model SQL
*
* @param options
*/
static selectQuery(options = {}): string {
// @ts-ignore
return this.queryGenerator.selectQuery(
this.getTableName(),
options,
this,
).replace(/;$/, '');
}
static parseApiJson(options: ApiJsonOptions) {
const { fields, filter, sort, context, ...restOptions } = options;
const data = toInclude({fields, filter, sort}, {
Model: this,
associations: this.associations,
dialect: this.sequelize.getDialect(),
ctx: context,
});
if (data.attributes && data.attributes.length === 0) {
delete data.attributes;
}
return data;
}
/**
*
*
* TODO: 暂不支持除主键以外关联字段的更新
*
* @param data
*/
async updateAssociations(data: any, options?: SaveOptions & { context?: any }) {
const model = this;
const name = this.constructor.name;
const table = this.database.getTable(name);
for (const [key, association] of table.getAssociations()) {
if (!data[key]) {
continue;
}
let item = data[key];
const accessors = association.getAccessors();
if (association instanceof BelongsTo || association instanceof HasOne) {
if (typeof item === 'number' || typeof item === 'string') {
await model[accessors.set](item, options);
continue;
}
if (item instanceof SequelizeModel) {
await model[accessors.set](item, options);
continue;
}
if (typeof item !== 'object') {
continue;
}
const Target = association.getTargetModel();
const targetAttribute = association instanceof BelongsTo
? association.options.targetKey
: association.options.sourceKey;
if (item[targetAttribute]) {
await model[accessors.set](item[targetAttribute], options);
if (Object.keys(item).length > 1) {
const target = await Target.findOne({
where: {
[targetAttribute]: item[targetAttribute],
},
});
await target.update(item, options);
// @ts-ignore
await target.updateAssociations(item, options);
}
continue;
}
const t = await model[accessors.create](item, options);
await t.updateAssociations(item, options);
}
if (association instanceof HasMany || association instanceof BelongsToMany) {
if (!Array.isArray(item)) {
item = [item];
}
if (item.length === 0) {
continue;
}
await model[accessors.set](null, options);
const Target = association.getTargetModel();
await Promise.all(item.map(async value => {
let target: SequelizeModel;
let targetKey: string;
// 支持 number 和 string 类型的字段作为关联字段
if (typeof value === 'number' || typeof value === 'string') {
targetKey = (association instanceof BelongsToMany ? association.options.targetKey : Target.primaryKeyAttribute) as string;
let targetKeyType = getDataTypeKey(Target.rawAttributes[targetKey].type).toLocaleLowerCase();
if (targetKeyType === 'integer') {
targetKeyType = 'number';
}
let primaryKeyType = getDataTypeKey(Target.rawAttributes[Target.primaryKeyAttribute].type).toLocaleLowerCase();
if (primaryKeyType === 'integer') {
primaryKeyType = 'number';
}
if (typeof value === targetKeyType) {
target = await Target.findOne({
where: {
[targetKey] : value,
},
});
}
if (Target.primaryKeyAttribute !== targetKey && !target && typeof value === primaryKeyType) {
target = await Target.findOne({
where: {
[Target.primaryKeyAttribute] : value,
},
});
}
if (!target) {
console.log(targetKey);
throw new Error(`target [${value}] does not exist`);
}
return await model[accessors.add](target, options);
}
if (value instanceof SequelizeModel) {
if (association instanceof HasMany) {
return await model[accessors.add](value.getDataValue(Target.primaryKeyAttribute), options);
}
return await model[accessors.add](value, options);
}
if (typeof value !== 'object') {
return;
}
targetKey = association.options.targetKey as string;
// 如果有主键,直接查询主键
if (value[Target.primaryKeyAttribute]) {
target = await Target.findOne({
where: {
[Target.primaryKeyAttribute]: value[Target.primaryKeyAttribute],
},
});
}
// 如果主键和关系字段配置的不一样
else if (Target.primaryKeyAttribute !== targetKey && value[targetKey]) {
target = await Target.findOne({
where: {
[targetKey]: value[targetKey],
},
});
}
if (target) {
await model[accessors.add](target, options);
if (Object.keys(value).length > 1) {
await target.update(value, options);
// @ts-ignore
await target.updateAssociations(value, options);
}
if (association instanceof BelongsToMany) {
const ThroughModel = association.getThroughModel();
const throughName = association.getThroughName();
if (typeof value[throughName] === 'object') {
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: this.get(sourceKey),
[otherKey]: target.get(targetKey),
},
});
const throughValues = value[throughName];
await through.update(throughValues);
await through.updateAssociations(throughValues);
}
}
return;
}
const t = await model[accessors.create](value, options);
// console.log(t);
await model[accessors.add](t, options);
await t.updateAssociations(value, options);
if (association instanceof BelongsToMany) {
const ThroughModel = association.getThroughModel();
const throughName = association.getThroughName();
if (typeof value[throughName] === 'object') {
const { foreignKey, sourceKey, otherKey, targetKey } = association.options;
const through = await ThroughModel.findOne({
where: {
[foreignKey]: this.get(sourceKey),
[otherKey]: t.get(targetKey),
},
});
const throughValues = value[throughName];
await through.update(throughValues);
await through.updateAssociations(throughValues);
}
}
return;
}));
}
}
}
}
/**
* ModelCtor Model
*/
export type ModelCtor<M extends Model> = typeof Model & { new(): M } & { [key: string]: any };
export default Model;

Some files were not shown because too many files have changed in this diff Show More