first commit
This commit is contained in:
commit
e5d30b30ba
17
.eslintrc
Executable file
17
.eslintrc
Executable 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
23
.fatherrc.ts
Executable 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
11
.gitignore
vendored
Normal 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
38
docker-compose.yml
Normal 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
|
11
jest.config.js
Executable file
11
jest.config.js
Executable 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
19
lerna.json
Normal 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
47
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
24
packages/actions/package.json
Normal file
24
packages/actions/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
43
packages/actions/src/__tests__/add.test.ts
Normal file
43
packages/actions/src/__tests__/add.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
130
packages/actions/src/__tests__/common.test.ts
Normal file
130
packages/actions/src/__tests__/common.test.ts
Normal 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' } ] });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
55
packages/actions/src/__tests__/create.test.ts
Normal file
55
packages/actions/src/__tests__/create.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
95
packages/actions/src/__tests__/destroy.test.ts
Normal file
95
packages/actions/src/__tests__/destroy.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
111
packages/actions/src/__tests__/get.test.ts
Normal file
111
packages/actions/src/__tests__/get.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
75
packages/actions/src/__tests__/index.ts
Normal file
75
packages/actions/src/__tests__/index.ts
Normal 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 };
|
||||||
|
}
|
148
packages/actions/src/__tests__/list.test.ts
Normal file
148
packages/actions/src/__tests__/list.test.ts
Normal 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
73
packages/actions/src/__tests__/middleware.test.ts
Normal file
73
packages/actions/src/__tests__/middleware.test.ts
Normal 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 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
96
packages/actions/src/__tests__/remove.test.ts
Normal file
96
packages/actions/src/__tests__/remove.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
61
packages/actions/src/__tests__/set.test.ts
Normal file
61
packages/actions/src/__tests__/set.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
31
packages/actions/src/__tests__/tables/comments.ts
Normal file
31
packages/actions/src/__tests__/tables/comments.ts
Normal 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;
|
42
packages/actions/src/__tests__/tables/posts.ts
Normal file
42
packages/actions/src/__tests__/tables/posts.ts
Normal 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;
|
12
packages/actions/src/__tests__/tables/posts_tags.ts
Normal file
12
packages/actions/src/__tests__/tables/posts_tags.ts
Normal 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;
|
12
packages/actions/src/__tests__/tables/profiles.ts
Normal file
12
packages/actions/src/__tests__/tables/profiles.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { TableOptions } from "@nocobase/database";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'profiles',
|
||||||
|
tableName: 'actions__profiles',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'email',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as TableOptions;
|
27
packages/actions/src/__tests__/tables/tags.ts
Normal file
27
packages/actions/src/__tests__/tables/tags.ts
Normal 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;
|
16
packages/actions/src/__tests__/tables/users.ts
Normal file
16
packages/actions/src/__tests__/tables/users.ts
Normal 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;
|
106
packages/actions/src/__tests__/update.test.ts
Normal file
106
packages/actions/src/__tests__/update.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
206
packages/actions/src/actions/associate.ts
Normal file
206
packages/actions/src/actions/associate.ts
Normal 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
|
||||||
|
}
|
358
packages/actions/src/actions/common.ts
Normal file
358
packages/actions/src/actions/common.ts
Normal 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 层面一般不开放 HasOne、BelongsTo、BelongsToMany 的新增数据操作
|
||||||
|
* 如果需要这类操作建议使用 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,
|
||||||
|
};
|
13
packages/actions/src/actions/index.ts
Normal file
13
packages/actions/src/actions/index.ts
Normal 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';
|
5
packages/actions/src/index.ts
Normal file
5
packages/actions/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as actions from './actions';
|
||||||
|
|
||||||
|
export * from './middleware';
|
||||||
|
|
||||||
|
export default actions;
|
21
packages/actions/src/middleware.ts
Normal file
21
packages/actions/src/middleware.ts
Normal 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;
|
59
packages/actions/src/middlewares/associated.ts
Normal file
59
packages/actions/src/middlewares/associated.ts
Normal 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;
|
21
packages/actions/src/middlewares/json-reponse.ts
Normal file
21
packages/actions/src/middlewares/json-reponse.ts
Normal 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
4
packages/api/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
yarn-error.log
|
||||||
|
.env
|
||||||
|
src2
|
15
packages/api/Dockerfile
Normal file
15
packages/api/Dockerfile
Normal 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" ]
|
41
packages/api/docker-compose.yml
Normal file
41
packages/api/docker-compose.yml
Normal 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}
|
42
packages/api/example/index.ts
Normal file
42
packages/api/example/index.ts
Normal 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/');
|
||||||
|
});
|
||||||
|
});
|
6
packages/api/nodemon.json
Normal file
6
packages/api/nodemon.json
Normal 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
38
packages/api/package.json
Normal 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
14
packages/api/pm2.json
Executable 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
53
packages/api/src/index.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
1
packages/create-nocobase-app/.local
Executable file
1
packages/create-nocobase-app/.local
Executable file
@ -0,0 +1 @@
|
|||||||
|
Used in bin/create-nocobase-app.js to determine if it is in the local debug state.
|
1
packages/create-nocobase-app/README.md
Executable file
1
packages/create-nocobase-app/README.md
Executable file
@ -0,0 +1 @@
|
|||||||
|
# @nocobase/create-nocobase-app
|
3
packages/create-nocobase-app/bin/create-nocobase-app.js
Executable file
3
packages/create-nocobase-app/bin/create-nocobase-app.js
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/cli');
|
16
packages/create-nocobase-app/package.json
Executable file
16
packages/create-nocobase-app/package.json
Executable 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"
|
||||||
|
}
|
||||||
|
}
|
15
packages/create-nocobase-app/src/AppGenerator/AppGenerator.ts
Executable file
15
packages/create-nocobase-app/src/AppGenerator/AppGenerator.ts
Executable 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
0
packages/create-nocobase-app/src/app.ts
Executable file
0
packages/create-nocobase-app/src/app.ts
Executable file
30
packages/create-nocobase-app/src/cli.ts
Executable file
30
packages/create-nocobase-app/src/cli.ts
Executable 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);
|
||||||
|
});
|
||||||
|
}
|
0
packages/create-nocobase-app/src/fixtures/.gitkeep
Executable file
0
packages/create-nocobase-app/src/fixtures/.gitkeep
Executable file
19
packages/create-nocobase-app/src/index.test.ts
Executable file
19
packages/create-nocobase-app/src/index.test.ts
Executable 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);
|
||||||
|
});
|
16
packages/create-nocobase-app/src/index.ts
Executable file
16
packages/create-nocobase-app/src/index.ts
Executable 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();
|
||||||
|
};
|
16
packages/create-nocobase-app/templates/AppGenerator/.editorconfig
Executable file
16
packages/create-nocobase-app/templates/AppGenerator/.editorconfig
Executable 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
|
14
packages/create-nocobase-app/templates/AppGenerator/.env
Normal file
14
packages/create-nocobase-app/templates/AppGenerator/.env
Normal 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
|
10
packages/create-nocobase-app/templates/AppGenerator/.fatherrc.ts
Executable file
10
packages/create-nocobase-app/templates/AppGenerator/.fatherrc.ts
Executable file
@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
entry: 'src/api',
|
||||||
|
target: 'node',
|
||||||
|
cjs: { type: 'babel', lazy: true },
|
||||||
|
include: 'api/*',
|
||||||
|
disableTypeCheck: true,
|
||||||
|
// pkgs: [
|
||||||
|
// 'api',
|
||||||
|
// ],
|
||||||
|
};
|
20
packages/create-nocobase-app/templates/AppGenerator/.gitignore.tpl
Executable file
20
packages/create-nocobase-app/templates/AppGenerator/.gitignore.tpl
Executable 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
|
8
packages/create-nocobase-app/templates/AppGenerator/.prettierignore
Executable file
8
packages/create-nocobase-app/templates/AppGenerator/.prettierignore
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
**/*.md
|
||||||
|
**/*.svg
|
||||||
|
**/*.ejs
|
||||||
|
**/*.html
|
||||||
|
package.json
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
|
.umi-test
|
11
packages/create-nocobase-app/templates/AppGenerator/.prettierrc
Executable file
11
packages/create-nocobase-app/templates/AppGenerator/.prettierrc
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 80,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ".prettierrc",
|
||||||
|
"options": { "parser": "json" }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
12
packages/create-nocobase-app/templates/AppGenerator/.umirc.ts.tpl
Executable file
12
packages/create-nocobase-app/templates/AppGenerator/.umirc.ts.tpl
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
import { defineConfig } from 'umi';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
nodeModulesTransform: {
|
||||||
|
type: 'none',
|
||||||
|
},
|
||||||
|
{{ ^conventionRoutes }}
|
||||||
|
routes: [
|
||||||
|
{ path: '/', component: '@/pages/index' },
|
||||||
|
],
|
||||||
|
{{ /conventionRoutes }}
|
||||||
|
});
|
15
packages/create-nocobase-app/templates/AppGenerator/README.md
Executable file
15
packages/create-nocobase-app/templates/AppGenerator/README.md
Executable file
@ -0,0 +1,15 @@
|
|||||||
|
# NocoBase Application
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Install dependencies,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the dev server,
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ yarn start
|
||||||
|
```
|
0
packages/create-nocobase-app/templates/AppGenerator/mock/.gitkeep
Executable file
0
packages/create-nocobase-app/templates/AppGenerator/mock/.gitkeep
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src/api", ".env"],
|
||||||
|
"ext": "ts",
|
||||||
|
"exec": "ts-node ./src/api/index.ts"
|
||||||
|
}
|
38
packages/create-nocobase-app/templates/AppGenerator/package.json.tpl
Executable file
38
packages/create-nocobase-app/templates/AppGenerator/package.json.tpl
Executable 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"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
const api = require('@nocobase/api');
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
api.listen(23000, () => {
|
||||||
|
console.log('http://localhost:23000/');
|
||||||
|
});
|
@ -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/');
|
||||||
|
});
|
||||||
|
});
|
7
packages/create-nocobase-app/templates/AppGenerator/src/pages/index.less
Executable file
7
packages/create-nocobase-app/templates/AppGenerator/src/pages/index.less
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
.normal {
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
background: rgb(121, 242, 157);
|
||||||
|
}
|
10
packages/create-nocobase-app/templates/AppGenerator/src/pages/index.tsx
Executable file
10
packages/create-nocobase-app/templates/AppGenerator/src/pages/index.tsx
Executable 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>
|
||||||
|
);
|
||||||
|
}
|
26
packages/create-nocobase-app/templates/AppGenerator/tsconfig.json
Executable file
26
packages/create-nocobase-app/templates/AppGenerator/tsconfig.json
Executable 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"
|
||||||
|
]
|
||||||
|
}
|
8
packages/create-nocobase-app/templates/AppGenerator/typings.d.ts
vendored
Executable file
8
packages/create-nocobase-app/templates/AppGenerator/typings.d.ts
vendored
Executable 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
|
||||||
|
}
|
18
packages/create-nocobase-app/tsconfig.json
Normal file
18
packages/create-nocobase-app/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
125
packages/database/examples/index.ts
Normal file
125
packages/database/examples/index.ts
Normal 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();
|
||||||
|
})();
|
25
packages/database/examples/plugins/db-driven/index.ts
Normal file
25
packages/database/examples/plugins/db-driven/index.ts
Normal 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) });
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
@ -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);
|
26
packages/database/examples/tables/bar.js
Normal file
26
packages/database/examples/tables/bar.js
Normal 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'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
19
packages/database/examples/tables/comments.ts
Normal file
19
packages/database/examples/tables/comments.ts
Normal 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;
|
3
packages/database/examples/tables/foo.json
Normal file
3
packages/database/examples/tables/foo.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"name": "foo"
|
||||||
|
}
|
28
packages/database/examples/tables/posts.ts
Normal file
28
packages/database/examples/tables/posts.ts
Normal 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;
|
23
packages/database/examples/tables/profiles.ts
Normal file
23
packages/database/examples/tables/profiles.ts
Normal 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;
|
15
packages/database/examples/tables/tags.ts
Normal file
15
packages/database/examples/tables/tags.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { TableOptions } from '../../src';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'tags',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: 'string',
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'belongsToMany',
|
||||||
|
name: 'posts',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as TableOptions;
|
34
packages/database/examples/tables/users.ts
Normal file
34
packages/database/examples/tables/users.ts
Normal 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;
|
23
packages/database/package.json
Normal file
23
packages/database/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
694
packages/database/src/__tests__/associations.test.ts
Normal file
694
packages/database/src/__tests__/associations.test.ts
Normal 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'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
42
packages/database/src/__tests__/index.ts
Normal file
42
packages/database/src/__tests__/index.ts
Normal 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;
|
||||||
|
};
|
970
packages/database/src/__tests__/model.test.ts
Normal file
970
packages/database/src/__tests__/model.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
3
packages/database/src/__tests__/modules/fn.js
Normal file
3
packages/database/src/__tests__/modules/fn.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = function () {
|
||||||
|
return 'fn';
|
||||||
|
}
|
1
packages/database/src/__tests__/modules/fnts.ts
Normal file
1
packages/database/src/__tests__/modules/fnts.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default () => 'foo';
|
3
packages/database/src/__tests__/modules/json.json
Normal file
3
packages/database/src/__tests__/modules/json.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
3
packages/database/src/__tests__/modules/obj.js
Normal file
3
packages/database/src/__tests__/modules/obj.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
'foo': 'bar',
|
||||||
|
}
|
3
packages/database/src/__tests__/modules/objts.ts
Normal file
3
packages/database/src/__tests__/modules/objts.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
'foo': 'bar',
|
||||||
|
}
|
172
packages/database/src/__tests__/sync.test.ts
Normal file
172
packages/database/src/__tests__/sync.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
300
packages/database/src/__tests__/tables.test.ts
Normal file
300
packages/database/src/__tests__/tables.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
330
packages/database/src/__tests__/types.test.ts
Normal file
330
packages/database/src/__tests__/types.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
658
packages/database/src/__tests__/utils.test.ts
Normal file
658
packages/database/src/__tests__/utils.test.ts
Normal 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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
245
packages/database/src/database.ts
Normal file
245
packages/database/src/database.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
691
packages/database/src/fields/field-types.ts
Normal file
691
packages/database/src/fields/field-types.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
packages/database/src/fields/index.ts
Normal file
124
packages/database/src/fields/index.ts
Normal 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,
|
||||||
|
});
|
214
packages/database/src/fields/option-types.ts
Normal file
214
packages/database/src/fields/option-types.ts
Normal 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;
|
9
packages/database/src/index.ts
Normal file
9
packages/database/src/index.ts
Normal 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;
|
418
packages/database/src/model.ts
Normal file
418
packages/database/src/model.ts
Normal 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
Loading…
Reference in New Issue
Block a user