From 6d64344c315128981534ff3abbdc42adb3d100c8 Mon Sep 17 00:00:00 2001 From: Bosn Date: Thu, 23 Nov 2017 17:54:42 +0800 Subject: [PATCH] first submit --- .gitignore | 6 + .jshintrc | 77 +++ .travis.yml | 27 ++ README.md | 28 ++ config/config.dev.js | 24 + config/config.local.js | 24 + config/config.prod.js | 24 + config/index.js | 5 + dispatch.js | 30 ++ package.json | 65 +++ public/favicon.ico | Bin 0 -> 24838 bytes public/index.html | 12 + public/libs/README.md | 6 + public/libs/fetch.rap.js | 40 ++ public/libs/jquery.rap.js | 57 +++ public/libs/mock.rap.js | 19 + public/test/index.html | 5 + public/test/test.plugin.fetch.html | 14 + public/test/test.plugin.jquery.html | 14 + public/test/test.plugin.mock.html | 15 + public/test/test.request.js | 25 + rap2-delos.release | 6 + scripts/app.js | 48 ++ scripts/dev.js | 19 + scripts/init/bo.js | 102 ++++ scripts/init/delos.js | 152 ++++++ scripts/init/index.js | 10 + scripts/openChrome.applescript | 85 ++++ scripts/rap2_delos.sql | 305 ++++++++++++ scripts/worker.js | 21 + src/models/helper.js | 8 + src/models/index.js | 110 +++++ src/models/interface.js | 16 + src/models/logger.js | 18 + src/models/module.js | 12 + src/models/notification.js | 16 + src/models/organization.js | 13 + src/models/property.js | 19 + src/models/repository.js | 13 + src/models/sequelize.js | 58 +++ src/models/user.js | 12 + src/routes/account.js | 219 +++++++++ src/routes/analytics.js | 111 +++++ src/routes/counter.js | 14 + src/routes/index.js | 10 + src/routes/mock.js | 234 ++++++++++ src/routes/organization.js | 225 +++++++++ src/routes/repository.js | 700 ++++++++++++++++++++++++++++ src/routes/router.js | 35 ++ src/routes/utils/helper.js | 230 +++++++++ src/routes/utils/pagination.js | 148 ++++++ src/routes/utils/tree.js | 173 +++++++ test/helper.js | 98 ++++ test/index.js | 2 + test/test.account.js | 104 +++++ test/test.counter.js | 24 + test/test.interface.js | 128 +++++ test/test.js | 36 ++ test/test.mock.js | 90 ++++ test/test.module.js | 101 ++++ test/test.organization.js | 136 ++++++ test/test.property.js | 113 +++++ test/test.repository.js | 145 ++++++ 63 files changed, 4636 insertions(+) create mode 100644 .gitignore create mode 100644 .jshintrc create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 config/config.dev.js create mode 100644 config/config.local.js create mode 100644 config/config.prod.js create mode 100644 config/index.js create mode 100644 dispatch.js create mode 100644 package.json create mode 100644 public/favicon.ico create mode 100644 public/index.html create mode 100644 public/libs/README.md create mode 100644 public/libs/fetch.rap.js create mode 100644 public/libs/jquery.rap.js create mode 100644 public/libs/mock.rap.js create mode 100644 public/test/index.html create mode 100644 public/test/test.plugin.fetch.html create mode 100644 public/test/test.plugin.jquery.html create mode 100644 public/test/test.plugin.mock.html create mode 100644 public/test/test.request.js create mode 100644 rap2-delos.release create mode 100644 scripts/app.js create mode 100644 scripts/dev.js create mode 100644 scripts/init/bo.js create mode 100644 scripts/init/delos.js create mode 100644 scripts/init/index.js create mode 100644 scripts/openChrome.applescript create mode 100644 scripts/rap2_delos.sql create mode 100644 scripts/worker.js create mode 100644 src/models/helper.js create mode 100644 src/models/index.js create mode 100644 src/models/interface.js create mode 100644 src/models/logger.js create mode 100644 src/models/module.js create mode 100644 src/models/notification.js create mode 100644 src/models/organization.js create mode 100644 src/models/property.js create mode 100644 src/models/repository.js create mode 100644 src/models/sequelize.js create mode 100644 src/models/user.js create mode 100644 src/routes/account.js create mode 100644 src/routes/analytics.js create mode 100644 src/routes/counter.js create mode 100644 src/routes/index.js create mode 100644 src/routes/mock.js create mode 100644 src/routes/organization.js create mode 100644 src/routes/repository.js create mode 100644 src/routes/router.js create mode 100644 src/routes/utils/helper.js create mode 100644 src/routes/utils/pagination.js create mode 100644 src/routes/utils/tree.js create mode 100644 test/helper.js create mode 100644 test/index.js create mode 100644 test/test.account.js create mode 100644 test/test.counter.js create mode 100644 test/test.interface.js create mode 100644 test/test.js create mode 100644 test/test.mock.js create mode 100644 test/test.module.js create mode 100644 test/test.organization.js create mode 100644 test/test.property.js create mode 100644 test/test.repository.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcb195a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +bower_components +coverage +npm-debug.log +tmp \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..d1614dc --- /dev/null +++ b/.jshintrc @@ -0,0 +1,77 @@ +{ + // JSHint Default Configuration File (as on JSHint website) + // See http://jshint.com/docs/ for more details + + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : false, // true: Identifiers must be in camelCase + "curly" : false, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` and `--` + "quotmark" : false, // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // Unused variables: + // true : all variables, last function parameter + // "vars" : all variables only + // "strict" : all variables, all function parameters + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. + + // Relaxing + "asi" : true, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "esversion" : 6, // {int} Specify the ECMAScript version to which the code must adhere. + "moz" : true, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + "esnext" : true, + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : true, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "noyield" : false, // true: Tolerate generator functions with no yield statement in them. + "notypeof" : false, // true: Tolerate invalid typeof operator values + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "devel" : true, // Development/debugging (alert, confirm, etc) + "jquery" : true, // jQuery + "node" : true, // Node.js + "browser" : true, // Browser + + + // Custom Globals + "globals" : {} // additional predefined global variables +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c5157e8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: node_js + +services: + - mysql + +cache: + directories: + - node_modules + - $HOME/.npm + +notifications: + email: false + +node_js: + - '8' + +before_install: + - npm i -g npm@^5.5.1 + - npm i -g lerna@^2.5.1 + - mysql -e 'CREATE DATABASE IF NOT EXISTS RAP2_DELOS_APP DEFAULT CHARSET utf8 COLLATE utf8_general_ci' + +script: + - npm install + - npm run create-db + - npm run check + +after_success: diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a3763b --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# rap2-delos CE + +[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) + +RAP2 服务端。 + +http://rap2api.taobao.org + +## 本地开发 + +``` +npm run dev +``` + +## 测试 + +``` +npm run test +``` + +或者 + +``` +# 在任意文件变化后自动运行测试用例 +npm run watch-test +# 在任意文件变化后自动运行指定的测试用例 +npm run watch-test test/test.repository.js +``` diff --git a/config/config.dev.js b/config/config.dev.js new file mode 100644 index 0000000..cb084a4 --- /dev/null +++ b/config/config.dev.js @@ -0,0 +1,24 @@ +module.exports = { + version: '2.3', + serve: { + port: 8080 + }, + keys: ['some secret hurr'], + session: { + key: 'rap2:sess' + }, + db: { + dialect: 'mysql', + host: 'localhost', + port: '3306', + username: 'root', + password: '', // KeyCenter 配置项密文 + database: 'RAP2_DELOS_APP', + pool: { + max: 5, + min: 0, + idle: 10000 + }, + logging: false + } +} diff --git a/config/config.local.js b/config/config.local.js new file mode 100644 index 0000000..edf8fb9 --- /dev/null +++ b/config/config.local.js @@ -0,0 +1,24 @@ +module.exports = { + version: '2.3', + serve: { + port: 8080 + }, + keys: ['some secret hurr'], + session: { + key: 'rap2:sess' + }, + db: { + dialect: 'mysql', + host: 'localhost', + port: '3306', + username: 'root', + password: '', + database: 'RAP2_DELOS_APP_LOCAL', + pool: { + max: 5, + min: 0, + idle: 10000 + }, + logging: true + } +} diff --git a/config/config.prod.js b/config/config.prod.js new file mode 100644 index 0000000..b38f4cc --- /dev/null +++ b/config/config.prod.js @@ -0,0 +1,24 @@ +module.exports = { + version: '2.3', + serve: { + port: 8080 + }, + keys: ['some secret hurr'], + session: { + key: 'rap2:sess' + }, + db: { + dialect: 'mysql', + host: 'localhost', + port: '3306', + username: 'root', + password: '', + database: 'RAP2_DELOS_APP', + pool: { + max: 5, + min: 0, + idle: 10000 + }, + logging: true + } +} diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..4b818df --- /dev/null +++ b/config/index.js @@ -0,0 +1,5 @@ +// local or development or production +module.exports = + (process.env.NODE_ENV === 'local' && require('./config.local')) || + (process.env.NODE_ENV === 'development' && require('./config.dev')) || + require('./config.prod') diff --git a/dispatch.js b/dispatch.js new file mode 100644 index 0000000..7f65657 --- /dev/null +++ b/dispatch.js @@ -0,0 +1,30 @@ +// process.env.NODE_ENV = 'production' + +// http://gitlab.alibaba-inc.com/thx/rap2-dolores/commit/2fd70fdcaa9d179e9cf95e530e37f31f8488f432 +// https://nodejs.org/api/cluster.html +// https://github.com/node-modules/graceful/blob/master/example/express_with_cluster/dispatch.js +// http://gitlab.alibaba-inc.com/mm/fb/blob/master/dispatch.js + +let cluster = require('cluster') +let path = require('path') +let now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '') + +cluster.setupMaster({ + exec: path.join(__dirname, 'scripts/worker.js') +}) + +if (cluster.isMaster) { + require('os').cpus().forEach((cpu, index) => { + cluster.fork() + }) + cluster.on('listening', (worker, address) => { + console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} is now connected to ${address.address}:${address.port}.`) + }) + cluster.on('disconnect', (worker) => { + console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} has disconnected.`) + }) + cluster.on('exit', (worker, code, signal) => { + console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} died (${signal || code}). restarting...`) + cluster.fork() + }) +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d484064 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "rap2-delos", + "version": "1.0.0", + "repository": { + "url": "" + }, + "description": "", + "main": "dispatch.js", + "scripts": { + "create-db": "node scripts/init", + "dev": "NODE_ENV=development nodemon --watch scripts --watch src scripts/dev.js", + "dev-local": "NODE_ENV=local nodemon --watch scripts --watch src scripts/dev.js", + "start": "NODE_ENV=production node dispatch.js", + "check": "npm run linter && mocha", + "test": "NODE_ENV=development mocha", + "linter": "standard --fix", + "watch-test": "NODE_ENV=development nodemon --watch scripts --watch src --watch test ./node_modules/.bin/mocha --timeout 5000", + "watch-test-local": "NODE_ENV=local nodemon --watch scripts --watch src --watch test ./node_modules/.bin/mocha --timeout 5000" + }, + "author": "mozhi.gyy@alibaba-inc.com, bosn@outlook.com", + "license": "ISC", + "dependencies": { + "chalk": "^1.1.3", + "graceful": "^1.0.1", + "js-beautify": "^1.6.9", + "kcors": "^2.2.1", + "koa": "^2.2.0", + "koa-body": "^2.0.0", + "koa-logger": "^2.0.1", + "koa-router": "^7.1.1", + "koa-send": "^4.0.0", + "koa-session": "^5.0.0", + "koa-static": "^3.0.0", + "mockjs": "^1.0.1-beta3", + "moment": "^2.17.1", + "mysql": "^2.11.1", + "node-fetch": "^1.7.1", + "node-print": "0.0.4", + "sequelize": "^3.30.4", + "sequelize-cli": "^3.1.0", + "underscore": "^1.8.3", + "urllib": "^2.22.0" + }, + "devDependencies": { + "babel-eslint": "^7.2.3", + "chai": "^3.5.0", + "mocha": "^3.3.0", + "nodemon": "^1.11.0", + "npm-run-all": "^4.0.2", + "pre-commit": "^1.2.2", + "standard": "^10.0.2", + "supertest": "^3.0.0" + }, + "standard": { + "parser": "babel-eslint", + "globals": [], + "ignore": [] + }, + "pre-commit": [ + "linter" + ], + "engines": { + "install-node": "9.2.0" + } +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5c125de5d897c1ff5692a656485b3216123dcd89 GIT binary patch literal 24838 zcmeI4X^>UL6@VY56)S&I{`6Nu0RscWCdj@GJHx(%?6_-;yKy1n;EEf9f}pr1CW5HA zYt$%U#C=}?jWH&%G@BaHBxsWAoUb3}&6%Ei@4Ii_JRa1`RQ23*yU)_wJ$?H0>6gj0 z${d_I^w5kvTW3xYEc?FvyP3>p$!py@`@T`|dVepIsjbbvR}af%KKy7YuQ%SDC^zmNWPYR^7avI5P-@dKev}UZ^aDAOyci9Nn zwR4qEz~tSvrp|#ACvWzo9`3B;`}^{t18dxaH;?xT7#hmJiKAaI;|O=$yxzXNOHGw~ z^!5pE^SW`av%t_$22LFPsM^l%=PSp!3r`>9w%s+^ZQYnnTQ*Ggd9-1~kj_o$YdW@b ztCkJ(ZGYjusqV5L4{^)R9Gt@gzU1t|?xhE&c^q(|(R#oa*}Sj5c({A$mhrB8*Y@tc zr)K#C{KOp-eHl35ZWJ1&zkmI>9DL%!KJE@_!=W?aH;i?ZDb0O1HPFy6 zcV0Kf)eZ0BHmz9vowF7EA{z*aue9M)iJP&Zd)qYlfJ-c^sS1qY^?>s)!!Ta@x zr@Lz|80r)7<{QVk9Z$}5SDaVtz*Rc?oH5~Wcjoc^eA&EdJ^h@aZ-BvL{K2s_7Cvfr zFL&(R?D&(9OxsS%z_BzI9^Ai^AOF$PUpGk~oO(=OpMc3@Zh&KH1a9>G%%0rC)t@oQ z4d~M`hX+g^Wf8P>A&&qjq|tZe*44Laq7qVPK#QIc)s*Qj34P`NL`Q{xBI`SnR!RC? zlGdTvC%oVZ@0BgcH>}qc!uzul@{i@sH}L0|=eZBJ9qF!HHaw?`s0(_DJj(v`(memI z6jH}=BfGlSlRV4)ouv#h*65yRR>G zo;I#~BVK&l&{+H=_~Nq$d%bFLh7GE5pS&>Fr{RMe>)MM19~z6F1oQo_y>vtlpEZF# zIc82TpMc3z9;{Q)=zG5B#4+96yHCvYy8p4;C%6x`%y$2HccC9|#vGVD)**C0xX|R| z%h)}ze!Tnrvvb@RZ!GX@2lMEq`=`08b`9$%FnN@*zJLo2wD5?MbE&LN)Z>Kty*;m= zt{Cn0>Q3nk)`bR^{dVf!3ECg6Yz4YcskI>$XH*L8E)MsudhnkP0B>+M(XEcErHUBKi~ z1`fEP&WPhp{@Ew?cPlR(ma9iw8NbJWHqp=btCtM*FnP*@ZwwlJ&-Y|LEjgvJzUtPc zz5CrWNBRV8d0-bpWAl<=zM1PU8lJseDxBK^QuuCj2fg{&2#*IG5ezf1B(o%lU+OZx7So4D?yi2*h zFBkr5pG3AJs83uy!~C3mQZLp~ss7-N9oAY>t)!eC#s)CrPukK!(!G*)H?v(~JCoj# zfvgTxMV{4?zL1neQ;ITVBAdFDf`1yG$o{g7^1sR_n{RZ7tnXio?tM%240}(z9xFY0 zlz{^-G*RET;-`7`>e0b{{`!2kM)t7Si9ZqD$~wh*hyGC>z~qs@0T&u*;h}hiKGEga zHkJ;%7aNc^o_0(>Z{Gp069H;TwPTUnvvX0SJ+kGGZ0lFBWocl>kaa)AoiMta+x_-J-?#KHFnJ*! zwD1V?)4s#|?O)DlMBhVv4IgZs?d>b<6%xK3<{o91H?-%8?PK!_fm#3d>{{gQ z?*8`b{G6?bZKdO{_9IVlz{R$PcGjeL|3*|@upby()_Lf^eQ&XQe)CjsbJ3Uolrgt< zweld3GH|fZpn(=1@PencO_a_)v6tU?WV-w8wfXLbOGae0{<*C?Ead$6v+> z|EQKThJTmwXK!c6AOD+FgtDv7i<48{-OPce!KDVkzR+XKOcREPha(;$}iUb!*)f-Fb}Y4@r9z-_{OIg z`xn^T#ZtEPv_T$M*Sr+=Z{q#~8$|7Y{0!*2u${D*Jj%dfOrS~FzpH*_|55J!7kl4w z?LT!7T(!3!632pmZh?dh`n-z$_ts42pn6;c`}hx;TSYd0idsqal5&0uGV=UM{c9xQ z1KK6&TS+a^H|6B_hPo1W3 zh+Dun!`UkP%H3}*@IE18q{7&MH2f3?T6o}Jf+xI@fh=SyUOArw`*w1_-PUlHZTHc@ z--yqIxPtI}IjPRzLIZ8cPv4P=>?A&=E~~0)>&J#V;TwAR*6}`01iu~U$@prtzW6YS ze}E>gUX+0YuF}B+Uhw2x7a7Q+oOzMNFHTNN<)40Rzg#`pABKF18@l}5A>RL`?Ri;Z zC8ExD$)im1@R{N7(wIog8$Yn(6%q$yd9(zKe};OnH%;mWBs7)>ls~T3Wi6!Xqw6+dpJLVS1P| z9qV%io-nE*rYcPxiS31>U_>mbPTXxkC*!?*zefr#2vF|qr8{|4|u^7-pD|f z&OPc->UKu)=iHgIpysp;Lsbyj}GJWoBkufOA={CRTUjr%af zc5pUH9{pg?M5%+)oN`q9yBbBt@+3xHV)qGm8b)Cp-w7~CwEhtBUk0rbjrqM zTb|tQ3-5-pw^cul`T+X&s?O;?V(FD!(Q9Qg@(LTCNz{0-vBM^SX5lti3|GpxFn4;Ax6pGc~t)R!Bo${lYH(* z!F&5X*?S&}YoDCyzwv1H+XI(+rL`;RN9}iLxlfr-r&vGG8OQa@=>+a)+Ij)sd_{wu z1Am(+3-RFr4&N8N6+hqo19S#;SA1-hG>07p3}&*j4CR+rqdV)^6n; z_vFr!(a%-=#=kb{pYmNL@6|DWkw~%E2V2jYl*e1}c{e$fib?(O+hs}eoBLRo&9(;J}YV}0Mi;LZAe{U$(s= zT<-IaV$Z+q-P!~3{HxN>Kbw30jXzM&I(S<6Ksx^}HvU2Vntb!etSsm0>)j}Me^+L5{2yz--)?W`Q?az z!WLG4UNP}+#C+NKH+ZG-Q=E>IPp%LuKLx$$8NAOGr(#~P>!EA zDYlpXDR=xM?Xv5(-qp74Cw3LzBeASHSBY`OezkbOyjP!G%WSymju_C$VBl--z + + + + + + RAP2 Delos + + + https://rap2.alibaba-inc.com + + diff --git a/public/libs/README.md b/public/libs/README.md new file mode 100644 index 0000000..a10cdb8 --- /dev/null +++ b/public/libs/README.md @@ -0,0 +1,6 @@ +## RAP2 提供两种拦截方式 + +1. 引入 `libs/jquery.rap.js` + 用复写的 `jQuery.ajax` 拦截与 RAP2 接口设置匹配的 ajax 请求,然后转发至 RAP2。 +2. 引入 `libs/mock.rap.js` + 由 Mock 拦截与 RAP2 接口设置匹配的 ajax 请求,然后直接返回响应数据,不会转发至 RAP2。 diff --git a/public/libs/fetch.rap.js b/public/libs/fetch.rap.js new file mode 100644 index 0000000..d906b20 --- /dev/null +++ b/public/libs/fetch.rap.js @@ -0,0 +1,40 @@ +;(function (RAP, fetch) { + if (!fetch) { + console.warn('当前环境不支持 fetch') + return + } + if (!RAP) { + console.warn('请先引入 RAP 插件') + return + } + + let next = fetch + let find = (settings) => { + for (let repositoryId in RAP.interfaces) { + for (let itf of RAP.interfaces[repositoryId]) { + if (itf.method.toUpperCase() === settings.method.toUpperCase() && itf.url === settings.url) { + return Object.assign({}, itf, { repositoryId }) + } + } + } + } + window.fetch = function (url, settings) { + // ajax(settings) + if (typeof url === 'object') { + settings = Object.assign({ method: 'GET' }, url) + } else { + // ajax(url) ajax(url, settings) + settings = Object.assign({ method: 'GET' }, settings, { url }) + } + + var match = find(settings) + if (!match) return next.call(window, url, settings) + + let redirect = `${RAP.protocol}://${RAP.host}/app/mock/${match.repositoryId}/${match.method}/${match.url}` + settings.credentials = 'include' + settings.method = 'GET' + settings.dataType = 'jsonp' + console.log(`Fetch ${match.method} ${match.url} => ${redirect}`) + return next.call(window, redirect, settings) + } +})(window.RAP, window.fetch) diff --git a/public/libs/jquery.rap.js b/public/libs/jquery.rap.js new file mode 100644 index 0000000..0eea30e --- /dev/null +++ b/public/libs/jquery.rap.js @@ -0,0 +1,57 @@ +;(function (RAP, jQuery) { + if (!jQuery) { + console.warn('请先引入 jQuery') + return + } + if (!RAP) { + console.warn('请先引入 RAP 插件') + return + } + + // 示例:检测重复接口 + let counter = {} + for (let repositoryId in RAP.interfaces) { + for (let itf of RAP.interfaces[repositoryId]) { + let key = `${itf.method} ${itf.url}` + counter[key] = [...(counter[key] || []), itf] + } + } + for (let key in counter) { + if (counter[key].length > 1) { + console.group('警告:检测到重复接口 ' + key) + counter[key].forEach(itf => { + console.warn(`#${itf.id} ${itf.method} ${itf.url}`) + }) + console.groupEnd('警告:检测到重复接口 ' + key) + } + } + + let next = jQuery.ajax + let find = (settings) => { + for (let repositoryId in RAP.interfaces) { + for (let itf of RAP.interfaces[repositoryId]) { + if (itf.method.toUpperCase() === settings.method.toUpperCase() && itf.url === settings.url) { + return Object.assign({}, itf, { repositoryId }) + } + } + } + } + jQuery.ajax = function (url, settings) { + // ajax(settings) + if (typeof url === 'object') { + settings = Object.assign({ method: 'GET' }, url) + } else { + // ajax(url) ajax(url, settings) + settings = Object.assign({ method: 'GET' }, settings, { url }) + } + + var match = find(settings) + if (!match) return next.call(jQuery, url, settings) + + let redirect = `${RAP.protocol}://${RAP.host}/app/mock/${match.repositoryId}/${match.method}/${match.url}` + settings.method = 'GET' + settings.dataType = 'jsonp' + console.log(`jQuery ${match.method} ${match.url} => ${redirect}`) + return next.call(jQuery, redirect, settings) + } +})(window.RAP, window.jQuery) diff --git a/public/libs/mock.rap.js b/public/libs/mock.rap.js new file mode 100644 index 0000000..4201310 --- /dev/null +++ b/public/libs/mock.rap.js @@ -0,0 +1,19 @@ +;(function (RAP, Mock) { + if (!RAP) { + console.warn('请先引入 RAP 插件') + return + } + if (!Mock) { + console.warn('请先引入 Mock') + return + } + + for (let repositoryId in RAP.interfaces) { + for (let itf of RAP.interfaces[repositoryId]) { + Mock.mock(itf.url, itf.method.toLowerCase(), (settings) => { + console.log(`Mock ${itf.method} ${itf.url} =>`, itf.response) + return Mock.mock(itf.response) + }) + } + } +})(window.RAP, window.Mock) diff --git a/public/test/index.html b/public/test/index.html new file mode 100644 index 0000000..1fcd053 --- /dev/null +++ b/public/test/index.html @@ -0,0 +1,5 @@ + diff --git a/public/test/test.plugin.fetch.html b/public/test/test.plugin.fetch.html new file mode 100644 index 0000000..766f172 --- /dev/null +++ b/public/test/test.plugin.fetch.html @@ -0,0 +1,14 @@ + + +
+ + diff --git a/public/test/test.plugin.jquery.html b/public/test/test.plugin.jquery.html new file mode 100644 index 0000000..bbbaa04 --- /dev/null +++ b/public/test/test.plugin.jquery.html @@ -0,0 +1,14 @@ + + +
+ + diff --git a/public/test/test.plugin.mock.html b/public/test/test.plugin.mock.html new file mode 100644 index 0000000..f679c98 --- /dev/null +++ b/public/test/test.plugin.mock.html @@ -0,0 +1,15 @@ + + +
+ + + diff --git a/public/test/test.request.js b/public/test/test.request.js new file mode 100644 index 0000000..1bc45c5 --- /dev/null +++ b/public/test/test.request.js @@ -0,0 +1,25 @@ +/* global $ */ +const appendData = (repositoryId, itf, data) => { + $('#result').append(`
+ #${repositoryId} #${itf.id} ${itf.name} ${itf.method} ${itf.url} +
${JSON.stringify(data, null, 2)}
+
`) +} +const doRequest = (RAP, fetch) => { // eslint-disable-line no-unused-vars + for (let repositoryId in RAP.interfaces) { + RAP.interfaces[repositoryId].forEach(itf => { + if (fetch) { + fetch(itf.url, { method: itf.method }) + .then(res => res.json()) + .then(data => { + appendData(repositoryId, itf, data) + }) + return + } + $.ajax({ url: itf.url, method: itf.method, dataType: 'json' }) + .done(data => { + appendData(repositoryId, itf, data) + }) + }) + } +} diff --git a/rap2-delos.release b/rap2-delos.release new file mode 100644 index 0000000..a6cd8ec --- /dev/null +++ b/rap2-delos.release @@ -0,0 +1,6 @@ +# 1. 你可以直接编辑本文件的内容,或者通过工具来帮你校验合法性和自动生成,请点击:http://aliwing.alibaba-inc.com/apprelease/home.htm +# 2. 更多关于Release文件的规范和约定,请点击: http://docs.alibaba-inc.com/pages/viewpage.action?pageId=252891532 + +# 构建源码语言类型 +code.language=nodejs + diff --git a/scripts/app.js b/scripts/app.js new file mode 100644 index 0000000..0caa1db --- /dev/null +++ b/scripts/app.js @@ -0,0 +1,48 @@ +const debug = true +const Koa = require('koa') +const session = require('koa-session') +const logger = require('koa-logger') +const serve = require('koa-static') +const body = require('koa-body') +const cors = require('kcors') +const router = require('../src/routes') +const config = require('../config') + +const app = new Koa() +app.counter = { users: {}, mock: 0 } + +app.keys = config.keys +app.use(session(config.session, app)) +if (debug) app.use(logger()) +app.use(async(ctx, next) => { + await next() + if (ctx.path === '/favicon.ico') return + ctx.session.views = (ctx.session.views || 0) + 1 + if (ctx.session.fullname) ctx.app.counter.users[ctx.session.fullname] = true +}) +app.use(cors({ + credentials: true +})) +app.use(async(ctx, next) => { + await next() + if (typeof ctx.body === 'object' && ctx.body.data !== undefined) { + ctx.type = 'json' + // ctx.body.path = ctx.path + ctx.body = JSON.stringify(ctx.body, null, 2) + } +}) +app.use(async(ctx, next) => { + await next() + if (ctx.request.query.callback) { + let body = typeof ctx.body === 'object' ? JSON.stringify(ctx.body, null, 2) : ctx.body + ctx.body = ctx.request.query.callback + '(' + body + ')' + ctx.type = 'application/x-javascript' + } +}) + +app.use(serve('public')) +app.use(serve('test')) +app.use(body()) +app.use(router.routes()) + +module.exports = app diff --git a/scripts/dev.js b/scripts/dev.js new file mode 100644 index 0000000..9a1d43d --- /dev/null +++ b/scripts/dev.js @@ -0,0 +1,19 @@ +const start = () => { + let execSync = require('child_process').execSync + let app = require('./app') + let port = 8080 + let url = `http://localhost:${port}` // /api.html + let open = false + console.log('----------------------------------------') + app.listen(port, () => { + console.log(`rap2-dolores is running as ${url}`) + if (!open) return + try { + execSync(`osascript openChrome.applescript ${url}`, { cwd: __dirname, stdio: 'ignore' }) + } catch (e) { + execSync(`open ${url}`) + } + }) +} + +start() diff --git a/scripts/init/bo.js b/scripts/init/bo.js new file mode 100644 index 0000000..6d1094f --- /dev/null +++ b/scripts/init/bo.js @@ -0,0 +1,102 @@ +// let Random = require('mockjs').Random +const { mock } = require('mockjs') + +const scopes = ['request', 'response'] +const methods = ['GET', 'POST', 'PUT', 'DELETE'] +const types = ['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp'] +const values = ['@INT', '@FLOAT', '@TITLE', '@NAME'] + +let USER_ID = 100000000 +let ORGANIZATION_ID = 1 +let REPOSITORY_ID = 1 +let MODULE_ID = 1 +let INTERFACE_ID = 1 +let PROPERTY_ID = 1 + +module.exports = { + BO_ADMIN: { id: USER_ID++, fullname: 'admin', email: 'admin@rap2.com', password: 'admin' }, + BO_MOZHI: { id: USER_ID++, fullname: '墨智', email: 'mozhi@rap2.com', password: 'mozhi' }, + BO_USER_COUNT: 10, + BO_USER_FN: () => mock({ + id: USER_ID++, + empId: 'bo@natural', + fullname: '@cname', + email: '@email', + password: '@word(6)' + }), + BO_ORGANIZATION_COUNT: 3, + BO_ORGANIZATION_FN: (source) => { + return Object.assign( + mock({ + id: ORGANIZATION_ID++, + name: '组织@ctitle(5)', + description: '@cparagraph', + logo: '@url', + creatorId: undefined, + owner: undefined, + members: '' + }), + source + ) + }, + BO_REPOSITORY_COUNT: 3, + BO_REPOSITORY_FN: (source) => { + return Object.assign( + mock({ + id: REPOSITORY_ID++, + name: '仓库@ctitle', + description: '@cparagraph', + logo: '@url' + }), + source + ) + }, + BO_MODULE_COUNT: 3, + BO_MODULE_FN: (source) => { + return Object.assign( + mock({ + id: MODULE_ID++, + name: '模块@ctitle(4)', + description: '@cparagraph', + repositoryId: undefined, + creatorId: undefined + }), + source + ) + }, + BO_INTERFACE_COUNT: 3, + BO_INTERFACE_FN: (source) => { + return Object.assign( + mock({ + id: INTERFACE_ID++, + name: '接口@ctitle(4)', + url: '/@word(5)/@word(5)/@word(5).json', + 'method|1': methods, + description: '@cparagraph', + creatorId: undefined, + lockerId: undefined, + repositoryId: undefined, + moduleId: undefined + }), + source + ) + }, + BO_PROPERTY_COUNT: 6, + BO_PROPERTY_FN: (source) => { + return Object.assign( + mock({ + id: PROPERTY_ID++, + 'scope|1': scopes, + name: '@word(6)', + 'type|1': types, + 'value|1': values, + description: '@csentence', + creatorId: undefined, + repositoryId: undefined, + moduleId: undefined, + interfaceId: undefined + }), + source + ) + } +} diff --git a/scripts/init/delos.js b/scripts/init/delos.js new file mode 100644 index 0000000..24b3ec3 --- /dev/null +++ b/scripts/init/delos.js @@ -0,0 +1,152 @@ +const sequelize = require('../../src/models/sequelize') +const { User, Organization, Repository, Module, Interface, Property } = require('../../src/models/index') +const { BO_ADMIN, BO_MOZHI } = require('./bo') +const { BO_USER_FN, BO_ORGANIZATION_FN, BO_REPOSITORY_FN, BO_MODULE_FN, BO_INTERFACE_FN, BO_PROPERTY_FN } = require('./bo') +const { BO_USER_COUNT, BO_ORGANIZATION_COUNT, BO_REPOSITORY_COUNT, BO_MODULE_COUNT, BO_INTERFACE_COUNT, BO_PROPERTY_COUNT } = require('./bo') +const EMPTY_WHERE = { where: {} } + +async function init () { + await sequelize.drop() + await sequelize.sync({ + force: true, + logging: console.log + }) + await User.destroy(EMPTY_WHERE) + await Organization.destroy(EMPTY_WHERE) + await Repository.destroy(EMPTY_WHERE) + await Module.destroy(EMPTY_WHERE) + await Interface.destroy(EMPTY_WHERE) + await Property.destroy(EMPTY_WHERE) + + // 用户 + await User.create(BO_ADMIN) + await User.create(BO_MOZHI) + for (let i = 0; i < BO_USER_COUNT; i++) { + await User.create(BO_USER_FN()) + } + + let users = await User.findAll() + + // 用户 admin 仓库 + for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) { + let repository = await Repository.create( + BO_REPOSITORY_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id }) + ) + await repository.setMembers( + users.filter(user => user.id !== BO_ADMIN.id) + ) + await initRepository(repository) + } + + // 用户 mozhi 的仓库 + for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) { + let repository = await Repository.create( + BO_REPOSITORY_FN({ creatorId: BO_MOZHI.id, ownerId: BO_MOZHI.id }) + ) + await repository.setMembers( + users.filter(user => user.id !== BO_MOZHI.id) + ) + await initRepository(repository) + } + + // 团队 + for (let BO_ORGANIZATION_INDEX = 0; BO_ORGANIZATION_INDEX < BO_ORGANIZATION_COUNT; BO_ORGANIZATION_INDEX++) { + let organization = await Organization.create( + BO_ORGANIZATION_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id }) + ) + await organization.setMembers( + users.filter(user => user.id !== BO_ADMIN.id) + ) + // 团队的仓库 + for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) { + let repository = await Repository.create( + BO_REPOSITORY_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id, organizationId: organization.id }) + ) + await repository.setMembers( + users.filter(user => user.id !== BO_ADMIN.id) + ) + await initRepository(repository) + } + } +} + +async function initRepository (repository) { + // 模块 + for (let BO_MODULE_INDEX = 0; BO_MODULE_INDEX < BO_MODULE_COUNT; BO_MODULE_INDEX++) { + let mod = await Module.create( + BO_MODULE_FN({ creatorId: repository.creatorId, repositoryId: repository.id }) + ) + await repository.addModule(mod) + // 接口 + for (let BO_INTERFACE_INDEX = 0; BO_INTERFACE_INDEX < BO_INTERFACE_COUNT; BO_INTERFACE_INDEX++) { + let itf = await Interface.create( + BO_INTERFACE_FN({ creatorId: mod.creatorId, repositoryId: repository.id, moduleId: mod.id }) + ) + await mod.addInterface(itf) + // 属性 + for (let BO_PROPERTY_INDEX = 0; BO_PROPERTY_INDEX < BO_PROPERTY_COUNT; BO_PROPERTY_INDEX++) { + let prop = await Property.create( + BO_PROPERTY_FN({ creatorId: itf.creatorId, repositoryId: repository.id, moduleId: mod.id, interfaceId: itf.id }) + ) + await itf.addProperty(prop) + } + } + } +} + +async function after () { + let exclude = ['password', 'createdAt', 'updatedAt', 'deletedAt'] + let repositories = await Repository.findAll({ + attributes: { exclude: [] }, + include: [ + { model: User, as: 'creator', attributes: { exclude }, required: true }, + { model: User, as: 'owner', attributes: { exclude }, required: true }, + { model: Organization, as: 'organization', attributes: { exclude }, required: false }, + { model: User, as: 'locker', attributes: { exclude }, required: false }, + { model: User, as: 'members', attributes: { exclude }, through: { attributes: [] }, required: true }, + { model: Module, + as: 'modules', + attributes: { exclude }, + // through: { attributes: [] }, + include: [ + { + model: Interface, + as: 'interfaces', + attributes: { exclude }, + // through: { attributes: [] }, + include: [ + { + model: Property, + as: 'properties', + attributes: { exclude }, + // through: { attributes: [] }, + required: true + } + ], + required: true + } + ], + required: true + } + ], + offset: 0, + limit: 100 + }) + // console.log(JSON.stringify(repositories, null, 2)) + console.log(repositories.map(item => item.toJSON())) + + let admin = await User.findById(BO_ADMIN.id) + // for (let k in admin) console.log(k) + let owned = await admin.getOwnedOrganizations() + console.log(owned.map(item => item.toJSON())) + + let mozhi = await User.findById(BO_MOZHI.id) + for (let k in mozhi) console.log(k) + let joined = await mozhi.getJoinedOrganizations() + console.log(joined.map(item => item.toJSON())) +} + +module.exports = { + init, + after +} diff --git a/scripts/init/index.js b/scripts/init/index.js new file mode 100644 index 0000000..f2214e1 --- /dev/null +++ b/scripts/init/index.js @@ -0,0 +1,10 @@ +const { init, after } = require('./delos') +/** + * initialize database + */ +async function main () { + await init() + await after() +} + +main() diff --git a/scripts/openChrome.applescript b/scripts/openChrome.applescript new file mode 100644 index 0000000..0f56027 --- /dev/null +++ b/scripts/openChrome.applescript @@ -0,0 +1,85 @@ +(* +Copyright (c) 2015-present, Facebook, Inc. +All rights reserved. + +This source code is licensed under the BSD-style license found in the +-- LICENSE file in the root directory of this source tree. An additional grant +of patent rights can be found in the PATENTS file in the same directory. +*) + +property targetTab: null +property targetTabIndex: -1 +property targetWindow: null + +on run argv + set theURL to item 1 of argv + + tell application "Chrome" + + if (count every window) = 0 then + make new window + end if + + -- 1: Looking for tab running debugger + -- then, Reload debugging tab if found + -- then return + set found to my lookupTabWithUrl(theURL) + if found then + set targetWindow's active tab index to targetTabIndex + tell targetTab to reload + tell targetWindow to activate + set index of targetWindow to 1 + return + end if + + -- 2: Looking for Empty tab + -- In case debugging tab was not found + -- We try to find an empty tab instead + set found to my lookupTabWithUrl("chrome://newtab/") + if found then + set targetWindow's active tab index to targetTabIndex + set URL of targetTab to theURL + tell targetWindow to activate + return + end if + + -- 3: Create new tab + -- both debugging and empty tab were not found + -- make a new tab with url + tell window 1 + activate + make new tab with properties {URL:theURL} + end tell + end tell +end run + +-- Function: +-- Lookup tab with given url +-- if found, store tab, index, and window in properties +-- (properties were declared on top of file) +on lookupTabWithUrl(lookupUrl) + tell application "Chrome" + -- Find a tab with the given url + set found to false + set theTabIndex to -1 + repeat with theWindow in every window + set theTabIndex to 0 + repeat with theTab in every tab of theWindow + set theTabIndex to theTabIndex + 1 + if (theTab's URL as string) contains lookupUrl then + -- assign tab, tab index, and window to properties + set targetTab to theTab + set targetTabIndex to theTabIndex + set targetWindow to theWindow + set found to true + exit repeat + end if + end repeat + + if found then + exit repeat + end if + end repeat + end tell + return found +end lookupTabWithUrl diff --git a/scripts/rap2_delos.sql b/scripts/rap2_delos.sql new file mode 100644 index 0000000..99219eb --- /dev/null +++ b/scripts/rap2_delos.sql @@ -0,0 +1,305 @@ +-- MySQL dump 10.13 Distrib 5.7.12, for osx10.9 (x86_64) +-- +-- Host: localhost Database: RAP2_DELOS_APP_LOCAL +-- ------------------------------------------------------ +-- Server version 5.7.12 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `interfaces` +-- + +DROP TABLE IF EXISTS `interfaces`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `interfaces` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `name` varchar(256) NOT NULL COMMENT '-', + `url` varchar(256) NOT NULL COMMENT '-', + `method` varchar(32) NOT NULL COMMENT '-', + `description` text COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `lockerId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_moduleId` (`moduleId`), + KEY `idx_creatorId` (`creatorId`), + KEY `idx_lockerId` (`lockerId`), + KEY `idx_repositoryId` (`repositoryId`), + CONSTRAINT `interfaces_ibfk_1` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `interfaces_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `interfaces_ibfk_3` FOREIGN KEY (`lockerId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `interfaces_ibfk_4` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='接口'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `loggers` +-- + +DROP TABLE IF EXISTS `loggers`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `loggers` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `type` varchar(32) NOT NULL COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `userId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `organizationId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `interfaceId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_userId` (`userId`), + KEY `idx_repositoryId` (`repositoryId`), + KEY `idx_organizationId` (`organizationId`), + KEY `idx_moduleId` (`moduleId`), + KEY `idx_interfaceId` (`interfaceId`), + CONSTRAINT `loggers_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `loggers_ibfk_2` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `loggers_ibfk_3` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `loggers_ibfk_4` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `loggers_ibfk_5` FOREIGN KEY (`interfaceId`) REFERENCES `interfaces` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='操作日志'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `modules` +-- + +DROP TABLE IF EXISTS `modules`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `modules` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `name` varchar(256) NOT NULL COMMENT '-', + `description` text COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_repositoryId` (`repositoryId`), + KEY `idx_creatorId` (`creatorId`), + CONSTRAINT `modules_ibfk_1` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `modules_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='模块'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `notifications` +-- + +DROP TABLE IF EXISTS `notifications`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `notifications` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `fromId` bigint(11) DEFAULT NULL COMMENT '-', + `toId` bigint(11) NOT NULL COMMENT '-', + `type` varchar(128) NOT NULL COMMENT '-', + `param1` varchar(128) DEFAULT NULL COMMENT '-', + `param2` varchar(128) DEFAULT NULL COMMENT '-', + `param3` varchar(128) DEFAULT NULL COMMENT '-', + `readed` tinyint(1) NOT NULL COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`) +) COMMENT='消息'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `organizations` +-- + +DROP TABLE IF EXISTS `organizations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `organizations` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `name` varchar(256) NOT NULL COMMENT '-', + `description` text COMMENT '-', + `logo` varchar(256) DEFAULT NULL COMMENT '-', + `visibility` tinyint(1) NOT NULL DEFAULT '1' COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `ownerId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_creatorId` (`creatorId`), + CONSTRAINT `organizations_ibfk_1` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='团队'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `repositories_collaborators` +-- + +DROP TABLE IF EXISTS `repositories_collaborators`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `repositories_collaborators` ( + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned NOT NULL COMMENT '-', + `collaboratorId` bigint(11) unsigned NOT NULL COMMENT '-', + PRIMARY KEY (`repositoryId`,`collaboratorId`), + KEY `idx_collaboratorId` (`collaboratorId`), + CONSTRAINT `repositories_collaborators_ibfk_1` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `repositories_collaborators_ibfk_2` FOREIGN KEY (`collaboratorId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) COMMENT='协同仓库'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `organizations_members` +-- + +DROP TABLE IF EXISTS `organizations_members`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `organizations_members` ( + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `userId` bigint(11) unsigned NOT NULL COMMENT '-', + `organizationId` bigint(11) unsigned NOT NULL COMMENT '-', + PRIMARY KEY (`userId`,`organizationId`), + KEY `idx_organizationId` (`organizationId`), + CONSTRAINT `organizations_members_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `organizations_members_ibfk_2` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) COMMENT='用户'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `properties` +-- + +DROP TABLE IF EXISTS `properties`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `properties` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `scope` varchar(32) NOT NULL DEFAULT 'response' COMMENT '-', + `name` varchar(256) NOT NULL COMMENT '-', + `type` varchar(32) NOT NULL COMMENT '-', + `rule` varchar(128) DEFAULT NULL COMMENT '-', + `value` text COMMENT '-', + `description` text COMMENT '-', + `parentId` bigint(11) NOT NULL DEFAULT '-1' COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `interfaceId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_interfaceId` (`interfaceId`), + KEY `idx_creatorId` (`creatorId`), + KEY `idx_moduleId` (`moduleId`), + KEY `idx_repositoryId` (`repositoryId`), + CONSTRAINT `properties_ibfk_1` FOREIGN KEY (`interfaceId`) REFERENCES `interfaces` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `properties_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `properties_ibfk_3` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `properties_ibfk_4` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='属性'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `repositories` +-- + +DROP TABLE IF EXISTS `repositories`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `repositories` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `name` varchar(256) NOT NULL COMMENT '-', + `description` text COMMENT '-', + `logo` varchar(256) DEFAULT NULL COMMENT '-', + `visibility` tinyint(1) NOT NULL DEFAULT '1' COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + `ownerId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `organizationId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + `lockerId` bigint(11) unsigned DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + KEY `idx_ownerId` (`ownerId`), + KEY `idx_organizationId` (`organizationId`), + KEY `idx_creatorId` (`creatorId`), + CONSTRAINT `repositories_ibfk_1` FOREIGN KEY (`ownerId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `repositories_ibfk_2` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `repositories_ibfk_3` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE +) COMMENT='仓库'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `repositories_members` +-- + +DROP TABLE IF EXISTS `repositories_members`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `repositories_members` ( + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `userId` bigint(11) unsigned NOT NULL COMMENT '-', + `repositoryId` bigint(11) unsigned NOT NULL COMMENT '-', + PRIMARY KEY (`userId`,`repositoryId`), + KEY `idx_repositoryId` (`repositoryId`), + CONSTRAINT `repositories_members_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT `repositories_members_ibfk_2` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) COMMENT='用户'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `users` ( + `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识', + `fullname` varchar(32) NOT NULL COMMENT '-', + `password` varchar(32) DEFAULT NULL COMMENT '-', + `email` varchar(128) NOT NULL COMMENT '-', + `createdAt` datetime NOT NULL COMMENT '-', + `updatedAt` datetime NOT NULL COMMENT '-', + `deletedAt` datetime DEFAULT NULL COMMENT '-', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_email` (`email`), + UNIQUE KEY `uk_users_email_unique` (`email`) +) COMMENT='用户'; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2017-05-19 23:47:54 diff --git a/scripts/worker.js b/scripts/worker.js new file mode 100644 index 0000000..68119ac --- /dev/null +++ b/scripts/worker.js @@ -0,0 +1,21 @@ +const start = () => { + // https://github.com/node-modules/graceful + const graceful = require('graceful') + const now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '') + const app = require('./app') + const { serve: { port } } = require('../config') + const server = app.listen(port, () => { + console.log(`[${now()}] worker#${process.pid} rap2-dolores is running as ${port}`) + }) + + graceful({ + servers: [server], + killTimeout: '10s', + error: function (err, throwErrorCount) { + if (err.message) err.message += ` (uncaughtException throw ${throwErrorCount} times on pid:${process.pid})` + console.error(`[${now()}] worker#${process.pid}] ${err.message}`) + } + }) +} + +start() diff --git a/src/models/helper.js b/src/models/helper.js new file mode 100644 index 0000000..77a44c3 --- /dev/null +++ b/src/models/helper.js @@ -0,0 +1,8 @@ +const Sequelize = require('sequelize') +module.exports = { + id: { type: Sequelize.BIGINT(11).UNSIGNED, primaryKey: true, allowNull: false, autoIncrement: true, comment: '唯一标识' }, + include: [], + exclude: { + generalities: ['createdAt', 'updatedAt', 'deletedAt', 'reserve'] + } +} diff --git a/src/models/index.js b/src/models/index.js new file mode 100644 index 0000000..45cd004 --- /dev/null +++ b/src/models/index.js @@ -0,0 +1,110 @@ +// TODO 2.2 如何缓存重复查询?https://github.com/rfink/sequelize-redis-cache +const Helper = require('./helper') +const User = require('./user') +const Organization = require('./organization') +const Repository = require('./repository') +const Module = require('./module') +const Interface = require('./interface') +const Property = require('./property') +const Logger = require('./logger') +const Notification = require('./notification') + +// http://docs.sequelizejs.com/manual/tutorial/associations.html + +User.OwnedOrganizations = User.hasMany(Organization, { foreignKey: 'ownerId', constraints: false, as: 'ownedOrganizations' }) +User.JoinedOrganizations = User.belongsToMany(Organization, { through: 'organizations_members', as: 'joinedOrganizations' }) + +User.OwnedRepositories = User.hasMany(Repository, { foreignKey: 'ownerId', constraints: false, as: 'ownedRepositories' }) +User.JoinedRepositories = User.belongsToMany(Repository, { through: 'repositories_members', as: 'joinedRepositories' }) + +Organization.Creator = Organization.belongsTo(User, { foreignKey: 'creatorId', as: 'creator' }) // 创建者 +Organization.Owner = Organization.belongsTo(User, { foreignKey: 'ownerId', constraints: false, as: 'owner' }) // 所有者 +Organization.Members = Organization.belongsToMany(User, { through: 'organizations_members', as: 'members' }) // 团队成员 +Organization.Repositories = Organization.hasMany(Repository, { foreignKey: 'organizationId', constraints: false, as: 'repositories' }) + +Repository.Creator = Repository.belongsTo(User, { foreignKey: 'creatorId', constraints: false, as: 'creator' }) // 创建者 +Repository.Owner = Repository.belongsTo(User, { foreignKey: 'ownerId', constraints: false, as: 'owner' }) // 所有者 +Repository.Organization = Repository.belongsTo(Organization, { foreignKey: 'organizationId', constraints: false, as: 'organization' }) // 所属团队 +Repository.Locker = Repository.belongsTo(User, { foreignKey: 'lockerId', constraints: false, as: 'locker' }) // 锁定者 +Repository.Members = Repository.belongsToMany(User, { through: 'repositories_members', as: 'members' }) // 仓库成员 +Repository.Modules = Repository.hasMany(Module, { foreignKey: 'repositoryId', constraints: false, as: 'modules' }) +Repository.Interfaces = Repository.hasMany(Interface, { foreignKey: 'repositoryId', constraints: false, as: 'interfaces' }) + +Repository.Collaborators = Repository.belongsToMany(Repository, { through: 'repositories_collaborators', as: 'collaborators' }) // 仓库共享 + +Module.Creator = Module.belongsTo(User, { foreignKey: 'creatorId', as: 'creator' }) // 创建者 +Module.Repository = Module.belongsTo(Repository, { foreignKey: 'repositoryId', as: 'repository' }) +Module.Interfaces = Module.hasMany(Interface, { foreignKey: 'moduleId', constraints: false, as: 'interfaces' }) + +Interface.Creator = Interface.belongsTo(User, { foreignKey: 'creatorId', as: 'creator' }) // 创建者 +Interface.Locker = Interface.belongsTo(User, { foreignKey: 'lockerId', as: 'locker' }) // 锁定者 +Interface.Module = Interface.belongsTo(Module, { foreignKey: 'moduleId', as: 'module' }) +Interface.Repository = Interface.belongsTo(Repository, { foreignKey: 'repositoryId', as: 'repository' }) +Interface.Properties = Interface.hasMany(Property, { foreignKey: 'interfaceId', constraints: false, as: 'properties' }) + +Property.Creator = Property.belongsTo(User, { foreignKey: 'creatorId', as: 'creator' }) // 创建者 +Property.Interface = Property.belongsTo(Interface, { foreignKey: 'interfaceId', as: 'interface' }) +Property.Module = Property.belongsTo(Module, { foreignKey: 'moduleId', as: 'module' }) +Property.Repository = Property.belongsTo(Repository, { foreignKey: 'repositoryId', as: 'repository' }) + +Logger.Creator = Logger.belongsTo(User, { foreignKey: 'creatorId', as: 'creator' }) +Logger.User = Logger.belongsTo(User, { foreignKey: 'userId', as: 'user' }) +Logger.Repository = Logger.belongsTo(Repository, { foreignKey: 'repositoryId', as: 'repository' }) +Logger.Organization = Logger.belongsTo(Organization, { foreignKey: 'organizationId', as: 'organization' }) +Logger.Module = Logger.belongsTo(Module, { foreignKey: 'moduleId', as: 'module' }) +Logger.Interface = Logger.belongsTo(Interface, { foreignKey: 'interfaceId', as: 'interface' }) + +const QueryInclude = { + User: { model: User, as: 'user', attributes: { exclude: ['password', ...Helper.exclude.generalities] }, required: true }, + Creator: { model: User, as: 'creator', attributes: { exclude: ['password', ...Helper.exclude.generalities] }, required: true }, + Owner: { model: User, as: 'owner', attributes: { exclude: ['password', ...Helper.exclude.generalities] }, required: true }, + Locker: { model: User, as: 'locker', attributes: { exclude: ['password', ...Helper.exclude.generalities] }, required: false }, + Members: { model: User, as: 'members', attributes: { exclude: ['password', ...Helper.exclude.generalities] }, through: { attributes: [] }, required: false }, + Repository: { model: Repository, as: 'repository', attributes: { exclude: [] }, paranoid: false, required: false }, + Organization: { model: Organization, as: 'organization', attributes: { exclude: [] }, paranoid: false, required: false }, + Module: { model: Module, as: 'module', attributes: { exclude: [] }, paranoid: false, required: false }, + Interface: { model: Interface, as: 'interface', attributes: { exclude: [] }, paranoid: false, required: false }, + Collaborators: { model: Repository, as: 'collaborators', attributes: { exclude: [] }, through: { attributes: [] }, required: false }, + RepositoryHierarchy: { + model: Module, + as: 'modules', + attributes: { exclude: [] }, + required: false, + separate: true, + order: [ + ['priority', 'ASC'] + ], + include: [{ + model: Interface, + as: 'interfaces', + attributes: { exclude: [] }, + required: false, + separate: true, + order: [ + ['priority', 'ASC'] + ], + include: [{ + model: User, + as: 'locker', + attributes: { exclude: ['password', ...Helper.exclude.generalities] }, + required: false + }, { + model: Property, + as: 'properties', + attributes: { exclude: [] }, + required: false, + separate: true + }] + }] + }, + Properties: { + model: Property, + as: 'properties', + attributes: { exclude: [] }, + required: false + } +} + +module.exports = { + User, Organization, Repository, Module, Interface, Property, Logger, Notification, Helper, QueryInclude +} diff --git a/src/models/interface.js b/src/models/interface.js new file mode 100644 index 0000000..8bfee2a --- /dev/null +++ b/src/models/interface.js @@ -0,0 +1,16 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +const methods = ['GET', 'POST', 'PUT', 'DELETE'] +module.exports = sequelize.define('interface', { + id, + name: { type: Sequelize.STRING(256), allowNull: false, comment: '接口名称' }, + url: { type: Sequelize.STRING(256), allowNull: false, comment: '接口地址' }, + method: { type: Sequelize.ENUM(...methods), allowNull: false, comment: '接口类型' }, + description: { type: Sequelize.TEXT, allowNull: true, comment: '接口描述' }, + // DONE 2.2 支持接口排序 + priority: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: false, defaultValue: 1, comment: '接口优先级' } +}, { + paranoid: true, + comment: '接口' +}) diff --git a/src/models/logger.js b/src/models/logger.js new file mode 100644 index 0000000..eee1ab6 --- /dev/null +++ b/src/models/logger.js @@ -0,0 +1,18 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +const types = ['create', 'update', 'delete', 'lock', 'unlock', 'join', 'exit'] +// DONE 2.3 需要加 creator 吗?Xxx 把 Xxx 加入了 仓库Xxx 或 团队Xxx。 +module.exports = sequelize.define('logger', { + id, + type: { type: Sequelize.ENUM(...types), allowNull: false, comment: '操作类型' }, + creatorId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: true, comment: '创建者' }, + userId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: false, comment: '涉及用户' }, + organizationId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: true, comment: '涉及组织' }, + repositoryId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: true, comment: '涉及仓库' }, + moduleId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: true, comment: '涉及模块' }, + interfaceId: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: true, comment: '涉及接口' } +}, { + paranoid: true, + comment: '操作日志' +}) diff --git a/src/models/module.js b/src/models/module.js new file mode 100644 index 0000000..76a3b9b --- /dev/null +++ b/src/models/module.js @@ -0,0 +1,12 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +module.exports = sequelize.define('module', { + id, + name: { type: Sequelize.STRING(256), allowNull: false, comment: '模块名称' }, + description: { type: Sequelize.TEXT, comment: '模块描述' }, + priority: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: false, defaultValue: 1, comment: '接口优先级' } +}, { + paranoid: true, + comment: '模块' +}) diff --git a/src/models/notification.js b/src/models/notification.js new file mode 100644 index 0000000..f996b8a --- /dev/null +++ b/src/models/notification.js @@ -0,0 +1,16 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +module.exports = sequelize.define('notification', { + id, + fromId: { type: Sequelize.BIGINT(11), allowNull: true, comment: '发送者' }, + toId: { type: Sequelize.BIGINT(11), allowNull: false, comment: '接受者' }, + type: { type: Sequelize.STRING(128), allowNull: false, comment: '消息类型' }, + param1: { type: Sequelize.STRING(128), allowNull: true, comment: '参数1' }, + param2: { type: Sequelize.STRING(128), allowNull: true, comment: '参数2' }, + param3: { type: Sequelize.STRING(128), allowNull: true, comment: '参数3' }, + readed: { type: Sequelize.BOOLEAN, allowNull: false, defautValue: false, comment: '是否已读' } +}, { + paranoid: true, + comment: '消息' +}) diff --git a/src/models/organization.js b/src/models/organization.js new file mode 100644 index 0000000..f5c41b2 --- /dev/null +++ b/src/models/organization.js @@ -0,0 +1,13 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +module.exports = sequelize.define('organization', { + id, + name: { type: Sequelize.STRING(256), allowNull: false, comment: '团队名称' }, + description: { type: Sequelize.TEXT, allowNull: true, comment: '团队描述' }, + logo: { type: Sequelize.STRING(256), allowNull: true, comment: '团队标志' }, + visibility: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true, comment: '是否公开' } +}, { + paranoid: true, + comment: '团队' +}) diff --git a/src/models/property.js b/src/models/property.js new file mode 100644 index 0000000..0532d5f --- /dev/null +++ b/src/models/property.js @@ -0,0 +1,19 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +const SCOPES = ['request', 'response'] +const TYPES = ['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp'] +module.exports = sequelize.define('property', { + id, + scope: { type: Sequelize.ENUM(...SCOPES), allowNull: false, defaultValue: 'response', comment: '属性归属' }, + name: { type: Sequelize.STRING(256), allowNull: false, comment: '属性名称' }, + type: { type: Sequelize.ENUM(...TYPES), allowNull: false, comment: '属性值类型' }, + rule: { type: Sequelize.STRING(128), allowNull: true, comment: '属性值生成规则' }, + value: { type: Sequelize.TEXT, allowNull: true, comment: '属性值' }, + description: { type: Sequelize.TEXT, allowNull: true, comment: '属性描述' }, + parentId: { type: Sequelize.BIGINT(11), allowNull: false, defaultValue: -1, comment: '父属性' }, + priority: { type: Sequelize.BIGINT(11).UNSIGNED, allowNull: false, defaultValue: 1, comment: '接口优先级' } +}, { + paranoid: true, + comment: '属性' +}) diff --git a/src/models/repository.js b/src/models/repository.js new file mode 100644 index 0000000..97e03ed --- /dev/null +++ b/src/models/repository.js @@ -0,0 +1,13 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +module.exports = sequelize.define('repository', { + id, + name: { type: Sequelize.STRING(256), allowNull: false, comment: '仓库名称' }, + description: { type: Sequelize.TEXT, allowNull: true, comment: '仓库描述' }, + logo: { type: Sequelize.STRING(256), allowNull: true, comment: '仓库标志' }, + visibility: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: true, comment: '是否公开' } +}, { + paranoid: true, + comment: '仓库' +}) diff --git a/src/models/sequelize.js b/src/models/sequelize.js new file mode 100644 index 0000000..6f14239 --- /dev/null +++ b/src/models/sequelize.js @@ -0,0 +1,58 @@ +// require('colors') +const Sequelize = require('sequelize') +const config = require('../../config') +const chalk = require('chalk') +const now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '') +const logging = process.env.NODE_ENV === 'development' + ? (sql) => { + sql = sql.replace('Executing (default): ', '') + console.log(`${chalk.bold('SQL')} ${now()} ${chalk.gray(sql)}`) + } + : console.log + +const sequelize = new Sequelize({ + database: config.db.database, + username: config.db.username, + password: config.db.password, + host: config.db.host, + port: config.db.port, + dialect: config.db.dialect, + pool: config.db.pool, + logging: config.db.logging ? logging : false +}) + +sequelize.authenticate() + .then((/* err */) => { + // console.log('Connection has been established successfully.'); + console.log('----------------------------------------') + console.log('DATABASE √') + console.log(' HOST %s', config.db.host) + console.log(' PORT %s', config.db.port) + console.log(' DATABASE %s', config.db.database) + console.log('----------------------------------------') + }) + .catch(err => { + console.log('Unable to connect to the database:', err) + }) + +module.exports = sequelize + +// module.exports = { +// Sequelize, +// sequelize, +// id: { type: Sequelize.BIGINT(11).UNSIGNED, primaryKey: true, allowNull: false, autoIncrement: true }, +// attributes: { +// create_date: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW, comment: '创建时间' }, +// update_date: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.NOW, comment: '更新时间' }, +// delete_date: { type: Sequelize.DATE, allowNull: true, comment: '删除时间' }, +// reserve: { type: Sequelize.STRING, allowNull: true, comment: '备用' } +// }, +// options: { +// // freezeTableName: true, +// // createdAt: 'create_date', +// // updatedAt: 'update_date', +// // deletedAt: 'delete_date', +// paranoid: true +// }, +// exclude: ['password', 'create_date', 'delete_date', 'reserve'] // 'update_date', +// } diff --git a/src/models/user.js b/src/models/user.js new file mode 100644 index 0000000..2fcab87 --- /dev/null +++ b/src/models/user.js @@ -0,0 +1,12 @@ +const Sequelize = require('sequelize') +const sequelize = require('./sequelize') +const { id } = require('./helper') +module.exports = sequelize.define('user', { + id, + fullname: { type: Sequelize.STRING(32), allowNull: false, comment: '姓名' }, + password: { type: Sequelize.STRING(32), allowNull: true, comment: '密码' }, + email: { type: Sequelize.STRING(128), allowNull: false, unique: true, comment: '邮箱' } +}, { + paranoid: true, + comment: '用户' +}) diff --git a/src/routes/account.js b/src/routes/account.js new file mode 100644 index 0000000..f8d30c9 --- /dev/null +++ b/src/routes/account.js @@ -0,0 +1,219 @@ +let router = require('./router') +let Pagination = require('./utils/pagination') +let { User, Notification, Logger, QueryInclude } = require('../models') + +router.get('/app/get', async (ctx, next) => { + let data = {} + let query = ctx.query + let hooks = { + user: User + } + for (let name in hooks) { + if (!query[name]) continue + data[name] = await hooks[name].findById(query[name], { + attributes: { exclude: [] } + }) + } + ctx.body = { + data: Object.assign({}, ctx.body && ctx.body.data, data) + } + + return next() +}) + +router.get('/account/count', async(ctx, next) => { + ctx.body = { + data: await User.count() + } +}) + +router.get('/account/list', async(ctx, next) => { + let where = {} + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { fullname: { $like: `%${name}%` } } + ] + }) + } + let options = { where } + let total = await User.count(options) + let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 10) + ctx.body = { + data: await User.findAll(Object.assign(options, { + attributes: QueryInclude.User.attributes, + offset: pagination.start, + limit: pagination.limit, + order: [ + ['id', 'DESC'] + ] + })), + pagination: pagination + } +}) + +router.get('/account/info', async(ctx, next) => { + ctx.body = { + data: ctx.session.id ? await User.findById(ctx.session.id, { + attributes: QueryInclude.User.attributes + }) : null + } +}) +router.post('/account/login', async(ctx, next) => { + let { email, password } = ctx.request.body + let result = await User.findOne({ + attributes: QueryInclude.User.attributes, + where: { email, password } + }) + if (result) { + ctx.session.id = result.id + ctx.session.fullname = result.fullname + ctx.session.email = result.email + ctx.app.counter.users[result.fullname] = true + } + ctx.body = { + data: result + } +}) + +router.get('/account/logout', async(ctx, next) => { + delete ctx.app.counter.users[ctx.session.email] + let id = ctx.session.id + Object.assign(ctx.session, { id: undefined, fullname: undefined, email: undefined }) + ctx.body = { + data: await { id } + } +}) + +router.post('/account/register', async(ctx, next) => { + // TODO 2.4 empId 可能为空,需要重新梳理用户注册流程 + let { fullname, email, password } = ctx.request.body + console.log(fullname) + console.log(email) + console.log(password) + let exists = await User.findAll({ + where: { email } + }) + if (exists && exists.length) { + ctx.body = { + data: { + isOk: false, + errMsg: '该邮件已被注册,请更换再试。' + } + } + return + } + + // login automatically after register + let result = await User.create({ fullname, email, password }) + + if (result) { + ctx.session.id = result.id + ctx.session.fullname = result.fullname + ctx.session.email = result.email + ctx.app.counter.users[result.fullname] = true + } + + ctx.body = { + data: { + id: result.id, + fullname: result.fullname, + email: result.email + } + } +}) + +router.get('/account/remove', async(ctx, next) => { + ctx.body = { + data: await User.destroy({ + where: { id: ctx.query.id } + }) + } +}) + +// TODO 2.3 账户设置 +router.get('/account/setting', async(ctx, next) => { + ctx.body = { + data: {} + } +}) +router.post('/account/setting', async(ctx, next) => { + ctx.body = { + data: {} + } +}) + +// TODO 2.3 账户通知 +let NOTIFICATION_EXCLUDE_ATTRIBUTES = [] +router.get('/account/notification/list', async(ctx, next) => { + let total = await Notification.count() + let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 10) + ctx.body = { + data: await Notification.findAll({ + attributes: { exclude: NOTIFICATION_EXCLUDE_ATTRIBUTES }, + offset: pagination.start, + limit: pagination.limit, + order: [ + ['id', 'DESC'] + ] + }), + pagination: pagination + } +}) +router.get('/account/notification/unreaded', async(ctx, next) => { + ctx.body = { + data: [] + } +}) +router.post('/account/notification/unreaded', async(ctx, next) => { + ctx.body = { + data: 0 + } +}) +router.post('/account/notification/read', async(ctx, next) => { + ctx.body = { + data: 0 + } +}) + +// TODO 2.3 账户日志 +router.get('/account/logger', async(ctx, next) => { + let auth = await User.findById(ctx.session.id) + let repositories = [...await auth.getOwnedRepositories({}), ...await auth.getJoinedRepositories({})] + let organizations = [...await auth.getOwnedOrganizations({}), ...await auth.getJoinedOrganizations({})] + + let where = { + $or: [ + { userId: ctx.session.id }, + { repositoryId: repositories.map(item => item.id) }, + { organizationId: organizations.map(item => item.id) } + ] + } + let total = await Logger.count({ where }) + let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let logs = await Logger.findAll({ + where, + attributes: {}, + include: [ + Object.assign({}, QueryInclude.Creator, { required: false }), + QueryInclude.User, + QueryInclude.Organization, + QueryInclude.Repository, + QueryInclude.Module, + QueryInclude.Interface + ], + offset: pagination.start, + limit: pagination.limit, + order: [ + ['id', 'DESC'] + ], + paranoid: false + }) + ctx.body = { + data: logs, + pagination: pagination + } +}) + +module.exports = router diff --git a/src/routes/analytics.js b/src/routes/analytics.js new file mode 100644 index 0000000..9e11238 --- /dev/null +++ b/src/routes/analytics.js @@ -0,0 +1,111 @@ +const router = require('./router') +const moment = require('moment') +const Sequelize = require('sequelize') +const SELECT = { type: Sequelize.QueryTypes.SELECT } +const sequelize = require('../models/sequelize') +const YYYY_MM_DD = 'YYYY-MM-DD' + +// 最近 30 天新建仓库数 +router.get('/app/analytics/repositories/created', async (ctx, next) => { + let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD) + let end = moment().startOf('day').format(YYYY_MM_DD) + let sql = ` + SELECT + DATE(createdAt) AS label, + COUNT(*) as value + FROM + RAP2_DELOS_APP.repositories + WHERE + createdAt >= '${start}' AND createdAt <= '${end}' + GROUP BY label + ORDER BY label ASC; + ` + let result = await sequelize.query(sql, SELECT) + result = result.map(item => ({ + label: moment(item.label).format(YYYY_MM_DD), + value: item.value + })) + ctx.body = { + data: result + } +}) + +// 最近 30 天活跃仓库数 +router.get('/app/analytics/repositories/updated', async (ctx, next) => { + let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD) + let end = moment().startOf('day').format(YYYY_MM_DD) + let sql = ` + SELECT + DATE(updatedAt) AS label, + COUNT(*) as value + FROM + RAP2_DELOS_APP.repositories + WHERE + updatedAt >= '${start}' AND updatedAt <= '${end}' + GROUP BY label + ORDER BY label ASC; + ` + let result = await sequelize.query(sql, SELECT) + result = result.map(item => ({ + label: moment(item.label).format(YYYY_MM_DD), + value: item.value + })) + ctx.body = { + data: result + } +}) + +// 最近 30 天活跃用户 +router.get('/app/analytics/users/activation', async (ctx, next) => { + let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD) + let end = moment().startOf('day').format(YYYY_MM_DD) + let sql = ` + SELECT + loggers.userId AS userId, + users.empId AS empId, + users.fullname AS fullname, + COUNT(*) AS value + FROM + loggers + LEFT JOIN + (users) ON (loggers.userId = users.id) + WHERE + loggers.updatedAt >= '${start}' AND loggers.updatedat <= '${end}' + GROUP BY loggers.userId + ORDER BY value DESC + LIMIT 10 + ` + let result = await sequelize.query(sql, SELECT) + ctx.body = { + data: result + } +}) + +// 最近 30 天活跃仓库 +router.get('/app/analytics/repositories/activation', async (ctx, next) => { + let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD) + let end = moment().startOf('day').format(YYYY_MM_DD) + let sql = ` + SELECT + loggers.repositoryId AS repositoryId, + repositories.name, + COUNT(*) AS value + FROM + loggers + LEFT JOIN + (repositories) ON (loggers.repositoryId = repositories.id) + WHERE + loggers.repositoryId IS NOT NULL + AND loggers.updatedAt >= '${start}' + AND loggers.updatedat <= '${end}' + GROUP BY loggers.repositoryId + ORDER BY value DESC + LIMIT 10 + ` + let result = await sequelize.query(sql, SELECT) + ctx.body = { + data: result + } +}) + +// TODO 2.3 支持 start、end diff --git a/src/routes/counter.js b/src/routes/counter.js new file mode 100644 index 0000000..f8c11e5 --- /dev/null +++ b/src/routes/counter.js @@ -0,0 +1,14 @@ +const router = require('./router') +const config = require('../../config') + +router.get('/app/counter', async(ctx, next) => { + ctx.body = { + data: { + version: config.version, + users: Object.keys(ctx.app.counter.users).length, + mock: ctx.app.counter.mock + } + } +}) + +module.exports = router diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..e0231e6 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,10 @@ +let router = require('./router') + +require('./counter') +require('./account') +require('./organization') +require('./repository') +require('./mock') +require('./analytics') + +module.exports = router diff --git a/src/routes/mock.js b/src/routes/mock.js new file mode 100644 index 0000000..6bdaf7a --- /dev/null +++ b/src/routes/mock.js @@ -0,0 +1,234 @@ +const { URL } = require('url') +const router = require('./router') +const { Repository, Interface, Property, QueryInclude } = require('../models') +const attributes = { exclude: [] } +const Tree = require('./utils/tree') +const pt = require('node-print').pt +const beautify = require('js-beautify').js_beautify + +// 检测是否存在重复接口,会在返回的插件 JS 中提示。同时也会在编辑器中提示。 +const parseDuplicatedInterfaces = (repository) => { + let counter = {} + for (let itf of repository.interfaces) { + let key = `${itf.method} ${itf.url}` + counter[key] = [...(counter[key] || []), { id: itf.id, method: itf.method, url: itf.url }] + } + let duplicated = [] + for (let key in counter) { + if (counter[key].length > 1) { + duplicated.push(counter[key]) + } + } + return duplicated +} +const generatePlugin = (protocol, host, repository) => { + // DONE 2.3 protocol 错误,应该是 https + let duplicated = parseDuplicatedInterfaces(repository) + let editor = `${protocol}://rap2.alibaba-inc.com/repository/editor?id=${repository.id}` + let result = ` +/** + * 仓库 #${repository.id} ${repository.name} + * 在线编辑 ${editor} + * 仓库数据 ${protocol}://${host}/repository/get?id=${repository.id} + * 请求地址 ${protocol}://${host}/app/mock/${repository.id}/:method/:url + * 或者 ${protocol}://${host}/app/mock/template/:interfaceId + * 或者 ${protocol}://${host}/app/mock/data/:interfaceId + */ +;(function(){ + let repositoryId = ${repository.id} + let interfaces = [ + ${repository.interfaces.map(itf => + `{ id: ${itf.id}, name: '${itf.name}', method: '${itf.method}', url: '${itf.url}', + request: ${JSON.stringify(itf.request)}, + response: ${JSON.stringify(itf.response)} }` + ).join(',\n ')} + ] + ${duplicated.length ? `console.warn('检测到重复接口,请访问 ${editor} 修复警告!')\n` : ''} + let RAP = window.RAP || { + protocol: '${protocol}', + host: '${host}', + interfaces: {} + } + RAP.interfaces[repositoryId] = interfaces + window.RAP = RAP +})();` + return beautify(result, { indent_size: 2 }) +} + +router.get('/app/plugin/:repositories', async (ctx, next) => { + let repositoryIds = new Set(ctx.params.repositories.split(',').map(item => +item).filter(item => item)) // _.uniq() => Set + let result = [] + for (let id of repositoryIds) { + let repository = await Repository.findById(id, { + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators + ] + }) + if (!repository) continue + if (repository.collaborators) { + repository.collaborators.map(item => { + repositoryIds.add(item.id) + }) + } + console.log(repositoryIds) + repository.interfaces = await Interface.findAll({ + attributes: { exclude: [] }, + where: { + repositoryId: repository.id + }, + include: [ + QueryInclude.Properties + ] + }) + repository.interfaces.forEach(itf => { + itf.request = Tree.ArrayToTreeToTemplate(itf.properties.filter(item => item.scope === 'request')) + itf.response = Tree.ArrayToTreeToTemplate(itf.properties.filter(item => item.scope === 'response')) + }) + // 修复 协议总是 http + // https://lark.alipay.com/login-session/unity-login/xp92ap + let protocol = ctx.headers['x-client-scheme'] || ctx.protocol + result.push(generatePlugin(protocol, ctx.host, repository)) + } + + ctx.type = 'application/x-javascript' + ctx.body = result.join('\n') +}) + +// /app/mock/:repository/:method/:url +// X DONE 2.2 支持 GET POST PUT DELETE 请求 +// DONE 2.2 忽略请求地址中的前缀斜杠 +// DONE 2.3 支持所有类型的请求,这样从浏览器中发送跨越请求时不需要修改 method +router.all('/app/mock/(\\d+)/(\\w+)/(.+)', async (ctx, next) => { + ctx.app.counter.mock++ + + let [ repositoryId, method, url ] = [ctx.params[0], ctx.params[1], ctx.params[2]] + + let urlWithoutPrefixSlash = /(\/)?(.*)/.exec(url)[2] + let urlWithoutSearch + try { + let urlParts = new URL(url) + urlWithoutSearch = `${urlParts.origin}${urlParts.pathname}` + } catch (e) { + urlWithoutSearch = url + } + // console.log([urlWithoutPrefixSlash, '/' + urlWithoutPrefixSlash, urlWithoutSearch]) + // DONE 2.3 腐烂的 KISSY + // KISSY 1.3.2 会把路径中的 // 替换为 /。在浏览器端拦截跨域请求时,需要 encodeURIComponent(url) 以防止 http:// 被替换为 http:/。但是同时也会把参数一起编码,导致 route 的 url 部分包含了参数。 + // 所以这里重新解析一遍!!! + + let repository = await Repository.findById(repositoryId) + let collaborators = await repository.getCollaborators() + + let itf = await Interface.findOne({ + attributes, + where: { + repositoryId: [repositoryId, ...collaborators.map(item => item.id)], + method, + url: [urlWithoutPrefixSlash, '/' + urlWithoutPrefixSlash, urlWithoutSearch] + } + }) + + if (!itf) { + ctx.body = {} + return + } + + let interfaceId = itf.id + let properties = await Property.findAll({ + attributes, + where: { interfaceId, scope: 'response' } + }) + properties = properties.map(item => item.toJSON()) + // pt(properties) + + // DONE 2.2 支持引用请求参数 + let requestProperties = await Property.findAll({ + attributes, + where: { interfaceId, scope: 'request' } + }) + requestProperties = requestProperties.map(item => item.toJSON()) + let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties) + Object.assign(requestData, ctx.query) + + let data = Tree.ArrayToTreeToTemplateToData(properties, requestData) + ctx.type = 'json' + ctx.body = JSON.stringify(data, null, 2) +}) + +// DONE 2.2 支持获取请求参数的模板、数据、Schema +router.get('/app/mock/template/:interfaceId', async (ctx, next) => { + ctx.app.counter.mock++ + let { interfaceId } = ctx.params + let { scope = 'response' } = ctx.query + let properties = await Property.findAll({ + attributes, + where: { interfaceId, scope } + }) + pt(properties.map(item => item.toJSON())) + let template = Tree.ArrayToTreeToTemplate(properties) + ctx.type = 'json' + ctx.body = Tree.stringifyWithFunctonAndRegExp(template) + // ctx.body = template + // ctx.body = JSON.stringify(template, null, 2) +}) + +router.get('/app/mock/data/:interfaceId', async (ctx, next) => { + ctx.app.counter.mock++ + let { interfaceId } = ctx.params + let { scope = 'response' } = ctx.query + let properties = await Property.findAll({ + attributes, + where: { interfaceId, scope } + }) + properties = properties.map(item => item.toJSON()) + // pt(properties) + + // DONE 2.2 支持引用请求参数 + let requestProperties = await Property.findAll({ + attributes, + where: { interfaceId, scope: 'request' } + }) + requestProperties = requestProperties.map(item => item.toJSON()) + let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties) + Object.assign(requestData, ctx.query) + + let data = Tree.ArrayToTreeToTemplateToData(properties, requestData) + ctx.type = 'json' + ctx.body = JSON.stringify(data, null, 2) +}) + +router.get('/app/mock/schema/:interfaceId', async (ctx, next) => { + ctx.app.counter.mock++ + let { interfaceId } = ctx.params + let { scope = 'response' } = ctx.query + let properties = await Property.findAll({ + attributes, + where: { interfaceId, scope } + }) + pt(properties.map(item => item.toJSON())) + properties = properties.map(item => item.toJSON()) + let schema = Tree.ArrayToTreeToTemplateToJSONSchema(properties) + ctx.type = 'json' + ctx.body = Tree.stringifyWithFunctonAndRegExp(schema) +}) + +router.get('/app/mock/tree/:interfaceId', async (ctx, next) => { + ctx.app.counter.mock++ + let { interfaceId } = ctx.params + let { scope = 'response' } = ctx.query + let properties = await Property.findAll({ + attributes, + where: { interfaceId, scope } + }) + pt(properties.map(item => item.toJSON())) + properties = properties.map(item => item.toJSON()) + let tree = Tree.ArrayToTree(properties) + ctx.type = 'json' + ctx.body = Tree.stringifyWithFunctonAndRegExp(tree) +}) diff --git a/src/routes/organization.js b/src/routes/organization.js new file mode 100644 index 0000000..31ee46b --- /dev/null +++ b/src/routes/organization.js @@ -0,0 +1,225 @@ +let _ = require('underscore') +let router = require('./router') +let Pagination = require('./utils/pagination') +let { User, Organization, Repository, Module, Interface, Property, QueryInclude, Logger } = require('../models') + +Organization.hook('afterCreate', async(instance, options) => { + await Logger.create({ + userId: instance.creatorId, + type: 'create', + organizationId: instance.id + }) +}) + +router.get('/app/get', async (ctx, next) => { + let data = {} + let query = ctx.query + let hooks = { + organization: Organization + } + for (let name in hooks) { + if (!query[name]) continue + data[name] = await hooks[name].findById(query[name], { + attributes: { exclude: [] } + }) + } + ctx.body = { + data: Object.assign({}, ctx.body && ctx.body.data, data) + } + + return next() +}) + +router.get('/organization/count', async(ctx, next) => { + ctx.body = { + data: await Organization.count() + } +}) +router.get('/organization/list', async(ctx, next) => { + let where = {} + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + let total = await Organization.count({ + where, + include: [ + QueryInclude.Creator, + QueryInclude.Owner + ] + }) + let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let organizations = await Organization.findAll({ + where, + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Members + ], + offset: pagination.start, + limit: pagination.limit, + order: [['updatedAt', 'DESC']] + }) + ctx.body = { + data: organizations, + pagination: pagination + } +}) +router.get('/organization/owned', async(ctx, next) => { + let where = {} + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + + let auth = await User.findById(ctx.session.id) + let options = { + where, + attributes: { exclude: [] }, + include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members], + order: [['updatedAt', 'DESC']] + } + let owned = await auth.getOwnedOrganizations(options) + ctx.body = { + data: owned, + pagination: null + } +}) +router.get('/organization/joined', async(ctx, next) => { + let where = {} + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + + let auth = await User.findById(ctx.session.id) + let options = { + where, + attributes: { exclude: [] }, + include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members], + order: [['updatedAt', 'DESC']] + } + let joined = await auth.getJoinedOrganizations(options) + // await auth.getOwnedOrganizations() + // await auth.getJoinedOrganizations() + ctx.body = { + data: joined, + pagination: null + } +}) +router.get('/organization/get', async(ctx, next) => { + let organization = await Organization.findById(ctx.query.id, { + attributes: { exclude: [] }, + include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members] + }) + ctx.body = { + data: organization + } +}) +router.post('/organization/create', async(ctx, next) => { + let creatorId = ctx.session.id + let body = Object.assign({}, ctx.request.body, { creatorId, ownerId: creatorId }) + let created = await Organization.create(body) + if (body.memberIds) { + let members = await User.findAll({ where: { id: body.memberIds } }) + await created.setMembers(members) + } + let filled = await Organization.findById(created.id, { + attributes: { exclude: [] }, + include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members] + }) + ctx.body = { + data: filled + } +}) +router.post('/organization/update', async(ctx, next) => { + let body = Object.assign({}, ctx.request.body) + delete body.creatorId + // DONE 2.2 支持转移团队 + // delete body.ownerId + let updated = await Organization.update(body, { where: { id: body.id } }) + if (body.memberIds) { + let reloaded = await Organization.findById(body.id) + let members = await User.findAll({ where: { id: body.memberIds } }) + ctx.prevAssociations = await reloaded.getMembers() + await reloaded.setMembers(members) + ctx.nextAssociations = await reloaded.getMembers() + } + ctx.body = { + data: updated[0] + } + return next() +}, async(ctx, next) => { + let { id } = ctx.request.body + // 团队改 + await Logger.create({ + userId: ctx.session.id, + type: 'update', + organizationId: id + }) + // 加入 & 退出 + if (!ctx.prevAssociations || !ctx.nextAssociations) return + let prevIds = ctx.prevAssociations.map(item => item.id) + let nextIds = ctx.nextAssociations.map(item => item.id) + let joined = _.difference(nextIds, prevIds) + let exited = _.difference(prevIds, nextIds) + let creatorId = ctx.session.id + for (let userId of joined) { + await Logger.create({ creatorId, userId, type: 'join', organizationId: id }) + } + for (let userId of exited) { + await Logger.create({ creatorId, userId, type: 'exit', organizationId: id }) + } +}) +router.post('/organization/transfer', async(ctx, next) => { + let { id, ownerId } = ctx.request.body + let body = { ownerId } + let result = await Organization.update(body, { where: { id } }) + ctx.body = { + data: result[0] + } +}) +router.get('/organization/remove', async(ctx, next) => { + let { id } = ctx.query + let result = await Organization.destroy({ where: { id } }) + let repositories = await Repository.findAll({ + where: { organizationId: id } + }) + if (repositories.length) { + let ids = repositories.map(item => item.id) + await Repository.destroy({ where: { id: ids } }) + await Module.destroy({ where: { repositoryId: ids } }) + await Interface.destroy({ where: { repositoryId: ids } }) + await Property.destroy({ where: { repositoryId: ids } }) + } + ctx.body = { + data: result + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let { id } = ctx.query + await Logger.create({ + userId: ctx.session.id, + type: 'delete', + organizationId: id + }) +}) + +module.exports = router diff --git a/src/routes/repository.js b/src/routes/repository.js new file mode 100644 index 0000000..d2fbf9c --- /dev/null +++ b/src/routes/repository.js @@ -0,0 +1,700 @@ +// TODO 2.1 大数据测试,含有大量模块、接口、属性的仓库 +const router = require('./router') +const _ = require('underscore') +const Pagination = require('./utils/pagination') +const { User, Organization, Repository, Module, Interface, Property, QueryInclude, Logger } = require('../models') +const Tree = require('./utils/tree') +const { initRepository, initModule } = require('./utils/helper') + +router.get('/app/get', async (ctx, next) => { + let data = {} + let query = ctx.query + let hooks = { + repository: Repository, + module: Module, + interface: Interface, + property: Property + } + for (let name in hooks) { + if (!query[name]) continue + data[name] = await hooks[name].findById(query[name]) + } + ctx.body = { + data: Object.assign({}, ctx.body && ctx.body.data, data) + } + + return next() +}) + +router.get('/repository/count', async(ctx, next) => { + ctx.body = { + data: await Repository.count() + } +}) + +router.get('/repository/list', async(ctx, next) => { + let where = {} + let { name, user, organization } = ctx.query + if (user) Object.assign(where, { ownerId: user, organizationId: null }) + if (organization) Object.assign(where, { organizationId: organization }) + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + let total = await Repository.count({ + where, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker + ] + }) + let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let repositories = await Repository.findAll({ + where, + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators + ], + offset: pagination.start, + limit: pagination.limit, + order: [['updatedAt', 'DESC']] + }) + ctx.body = { + data: repositories, + pagination: pagination + } +}) +router.get('/repository/owned', async(ctx, next) => { + let where = {} // { organizationId: null } // 彻底废弃个人仓库&团队仓库概念,一个仓库必须属于某个用户,可选属于某个团队 + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + + let auth = await User.findById(ctx.query.user || ctx.session.id) + // let total = await auth.countOwnedRepositories({ where }) + // let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let repositories = await auth.getOwnedRepositories({ + where, + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators + ], + // offset: pagination.start, + // limit: pagination.limit, + order: [['updatedAt', 'DESC']] + }) + ctx.body = { + data: repositories, + pagination: null + } +}) +router.get('/repository/joined', async(ctx, next) => { + let where = {} + let { name } = ctx.query + if (name) { + Object.assign(where, { + $or: [ + { name: { $like: `%${name}%` } }, + { id: name } // name => id + ] + }) + } + + let auth = await User.findById(ctx.query.user || ctx.session.id) + // let total = await auth.countJoinedRepositories({ where }) + // let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100) + let repositories = await auth.getJoinedRepositories({ + where, + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.Collaborators + ], + // offset: pagination.start, + // limit: pagination.limit, + order: [['updatedAt', 'DESC']] + }) + ctx.body = { + data: repositories, + pagination: null + } +}) +router.get('/repository/get', async(ctx, next) => { + let repository = await Repository.findById(ctx.query.id, { + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.RepositoryHierarchy, + QueryInclude.Collaborators + ] + }) + ctx.body = { + data: repository + } +}) +router.post('/repository/create', async(ctx, next) => { + let creatorId = ctx.session.id + let body = Object.assign({}, ctx.request.body, { creatorId, ownerId: creatorId }) + let created = await Repository.create(body) + if (body.memberIds) { + let members = await User.findAll({ where: { id: body.memberIds } }) + await created.setMembers(members) + } + if (body.collaboratorIds) { + let collaborators = await Repository.findAll({ where: { id: body.collaboratorIds } }) + await created.setCollaborators(collaborators) + } + await initRepository(created) + ctx.body = { + data: await Repository.findById(created.id, { + attributes: { exclude: [] }, + include: [ + QueryInclude.Creator, + QueryInclude.Owner, + QueryInclude.Locker, + QueryInclude.Members, + QueryInclude.Organization, + QueryInclude.RepositoryHierarchy, + QueryInclude.Collaborators + ] + }) + } + return next() +}, async(ctx, next) => { + await Logger.create({ + userId: ctx.session.id, + type: 'create', + repositoryId: ctx.body.data.id + }) +}) +router.post('/repository/update', async(ctx, next) => { + let body = Object.assign({}, ctx.request.body) + delete body.creatorId + // DONE 2.2 支持转移仓库 + // delete body.ownerId + delete body.organizationId + let result = await Repository.update(body, { where: { id: body.id } }) + if (body.memberIds) { + let reloaded = await Repository.findById(body.id) + let members = await User.findAll({ where: { id: body.memberIds } }) + ctx.prevAssociations = await reloaded.getMembers() + await reloaded.setMembers(members) + ctx.nextAssociations = await reloaded.getMembers() + } + if (body.collaboratorIds) { + let reloaded = await Repository.findById(body.id) + let collaborators = await Repository.findAll({ where: { id: body.collaboratorIds } }) + await reloaded.setCollaborators(collaborators) + } + ctx.body = { + data: result[0] + } + return next() +}, async(ctx, next) => { + let { id } = ctx.request.body + await Logger.create({ + userId: ctx.session.id, + type: 'update', + repositoryId: id + }) + // 加入 & 退出 + if (!ctx.prevAssociations || !ctx.nextAssociations) return + let prevIds = ctx.prevAssociations.map(item => item.id) + let nextIds = ctx.nextAssociations.map(item => item.id) + let joined = _.difference(nextIds, prevIds) + let exited = _.difference(prevIds, nextIds) + let creatorId = ctx.session.id + for (let userId of joined) { + await Logger.create({ creatorId, userId, type: 'join', repositoryId: id }) + } + for (let userId of exited) { + await Logger.create({ creatorId, userId, type: 'exit', repositoryId: id }) + } +}) +router.post('/repository/transfer', async(ctx, next) => { + let { id, ownerId, organizationId } = ctx.request.body + let body = {} + if (ownerId) body.ownerId = ownerId // 转移给其他用户 + if (organizationId) { + body.organizationId = organizationId // 转移给其他团队,同时转移给该团队拥有者 + body.ownerId = (await Organization.findById(organizationId)).ownerId + } + let result = await Repository.update(body, { where: { id } }) + ctx.body = { + data: result[0] + } +}) +router.get('/repository/remove', async(ctx, next) => { + let { id } = ctx.query + let result = await Repository.destroy({ where: { id } }) + await Module.destroy({ where: { repositoryId: id } }) + await Interface.destroy({ where: { repositoryId: id } }) + await Property.destroy({ where: { repositoryId: id } }) + ctx.body = { + data: result + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let { id } = ctx.query + await Logger.create({ + userId: ctx.session.id, + type: 'delete', + repositoryId: id + }) +}) + +// TOEO 锁定/解锁仓库 待测试 +router.post('/repository/lock', async (ctx, next) => { + let user = ctx.session.id + if (!user) { + ctx.body = { data: 0 } + return + } + let { id } = ctx.request.body + let result = await Repository.update({ lockerId: user }, { + where: { id } + }) + ctx.body = { data: result[0] } +}) +router.post('/repository/unlock', async (ctx, next) => { + if (!ctx.session.id) { + ctx.body = { data: 0 } + return + } + let { id } = ctx.request.body + let result = await Repository.update({ lockerId: null }, { + where: { id } + }) + ctx.body = { data: result[0] } +}) + +// 模块 +router.get('/module/count', async (ctx, next) => { + ctx.body = { + data: await Module.count() + } +}) +router.get('/module/list', async (ctx, next) => { + let where = {} + let { repositoryId, name } = ctx.query + if (repositoryId) where.repositoryId = repositoryId + if (name) where.name = { $like: `%${name}%` } + ctx.body = { + data: await Module.findAll({ + attributes: { exclude: [] }, + where + }) + } +}) +router.get('/module/get', async (ctx, next) => { + ctx.body = { + data: await Module.findById(ctx.query.id, { + attributes: { exclude: [] } + }) + } +}) +router.post('/module/create', async (ctx, next) => { + let creatorId = ctx.session.id + let body = Object.assign(ctx.request.body, { creatorId }) + body.priority = (await Module.count()) + 1 + let created = await Module.create(body) + await initModule(created) + ctx.body = { + data: await Module.findById(created.id) + } + return next() +}, async(ctx, next) => { + let mod = ctx.body.data + await Logger.create({ + userId: ctx.session.id, + type: 'create', + repositoryId: mod.repositoryId, + moduleId: mod.id + }) +}) +router.post('/module/update', async (ctx, next) => { + let body = ctx.request.body + let result = await Module.update(body, { + where: { id: body.id } + }) + ctx.body = { + data: result[0] + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let mod = ctx.request.body + await Logger.create({ + userId: ctx.session.id, + type: 'update', + repositoryId: mod.repositoryId, + moduleId: mod.id + }) +}) +router.get('/module/remove', async (ctx, next) => { + let { id } = ctx.query + let result = await Module.destroy({ where: { id } }) + await Interface.destroy({ where: { moduleId: id } }) + await Property.destroy({ where: { moduleId: id } }) + ctx.body = { + data: result + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let { id } = ctx.query + let mod = await Module.findById(id, { paranoid: false }) + await Logger.create({ + userId: ctx.session.id, + type: 'delete', + repositoryId: mod.repositoryId, + moduleId: mod.id + }) +}) +router.post('/module/sort', async (ctx, next) => { + let { ids } = ctx.request.body + for (let index = 0; index < ids.length; index++) { + await Module.update({ priority: index + 1 }, { + where: { id: ids[index] } + }) + } + ctx.body = { + data: ids.length + } +}) + +// +router.get('/interface/count', async (ctx, next) => { + ctx.body = { + data: await Interface.count() + } +}) +router.get('/interface/list', async (ctx, next) => { + let where = {} + let { repositoryId, moduleId, name } = ctx.query + if (repositoryId) where.repositoryId = repositoryId + if (moduleId) where.moduleId = moduleId + if (name) where.name = { $like: `%${name}%` } + ctx.body = { + data: await Interface.findAll({ + attributes: { exclude: [] }, + where + }) + } +}) +router.get('/interface/get', async (ctx, next) => { + let { id, repositoryId, method, url } = ctx.query + + let itf + if (id) { + itf = await Interface.findById(id, { + attributes: { exclude: [] } + }) + } else if (repositoryId && method && url) { + // 同 /app/mock/:repository/:method/:url + let urlWithoutPrefixSlash = /(\/)?(.*)/.exec(url)[2] + let repository = await Repository.findById(repositoryId) + let collaborators = await repository.getCollaborators() + + itf = await Interface.findOne({ + attributes: { exclude: [] }, + where: { + repositoryId: [repositoryId, ...collaborators.map(item => item.id)], + method, + url: [urlWithoutPrefixSlash, '/' + urlWithoutPrefixSlash] + } + }) + } + itf = itf.toJSON() + + let scopes = ['request', 'response'] + for (let i = 0; i < scopes.length; i++) { + let properties = await Property.findAll({ + attributes: { exclude: [] }, + where: { interfaceId: itf.id, scope: scopes[i] } + }) + properties = properties.map(item => item.toJSON()) + itf[scopes[i] + 'Properties'] = Tree.ArrayToTree(properties).children + } + + ctx.type = 'json' + ctx.body = Tree.stringifyWithFunctonAndRegExp({ + data: itf + }) +}) +router.post('/interface/create', async (ctx, next) => { + let creatorId = ctx.session.id + let body = Object.assign(ctx.request.body, { creatorId }) + body.priority = (await Interface.count()) + 1 + let created = await Interface.create(body) + // await initInterface(created) + ctx.body = { + data: await Interface.findById(created.id) + } + return next() +}, async(ctx, next) => { + let itf = ctx.body.data + await Logger.create({ + userId: ctx.session.id, + type: 'create', + repositoryId: itf.repositoryId, + moduleId: itf.moduleId, + interfaceId: itf.id + }) +}) +router.post('/interface/update', async (ctx, next) => { + let body = ctx.request.body + let result = await Interface.update(body, { + where: { id: body.id } + }) + ctx.body = { + data: result[0] + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let itf = ctx.request.body + await Logger.create({ + userId: ctx.session.id, + type: 'update', + repositoryId: itf.repositoryId, + moduleId: itf.moduleId, + interfaceId: itf.id + }) +}) +router.get('/interface/remove', async (ctx, next) => { + let { id } = ctx.query + let result = await Interface.destroy({ where: { id } }) + await Property.destroy({ where: { interfaceId: id } }) + ctx.body = { + data: result + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let { id } = ctx.query + let itf = await Interface.findById(id, { paranoid: false }) + await Logger.create({ + userId: ctx.session.id, + type: 'delete', + repositoryId: itf.repositoryId, + moduleId: itf.moduleId, + interfaceId: itf.id + }) +}) +router.post('/interface/lock', async (ctx, next) => { + if (!ctx.session.id) { + ctx.body = { data: 0 } + return + } + + let { id } = ctx.request.body + let itf = await Interface.findById(id) + if (itf.lockerId) { // DONE 2.3 BUG 接口可能被不同的人重复锁定。如果已经被锁定,则忽略。 + ctx.body = { + data: 0 + } + return + } + + let result = await Interface.update({ lockerId: ctx.session.id }, { + where: { id } + }) + ctx.body = { + data: result[0] + } + return next() +}) +router.post('/interface/unlock', async (ctx, next) => { + if (!ctx.session.id) { + ctx.body = { data: 0 } + return + } + + let { id } = ctx.request.body + let itf = await Interface.findById(id) + if (itf.lockerId !== ctx.session.id) { // DONE 2.3 BUG 接口可能被其他人解锁。如果不是同一个用户,则忽略。 + ctx.body = { data: 0 } + return + } + + let result = await Interface.update({ lockerId: null }, { + where: { id } + }) + ctx.body = { + data: result[0] + } + return next() +}) +router.post('/interface/sort', async (ctx, next) => { + let { ids } = ctx.request.body + for (let index = 0; index < ids.length; index++) { + await Interface.update({ priority: index + 1 }, { + where: { id: ids[index] } + }) + } + ctx.body = { + data: ids.length + } +}) + +// +router.get('/property/count', async (ctx, next) => { + ctx.body = { + data: await Property.count() + } +}) +router.get('/property/list', async (ctx, next) => { + let where = {} + let { repositoryId, moduleId, interfaceId, name } = ctx.query + if (repositoryId) where.repositoryId = repositoryId + if (moduleId) where.moduleId = moduleId + if (interfaceId) where.interfaceId = interfaceId + if (name) where.name = { $like: `%${name}%` } + ctx.body = { + data: await Property.findAll({ where }) + } +}) +router.get('/property/get', async (ctx, next) => { + let { id } = ctx.query + ctx.body = { + data: await Property.findById(id, { + attributes: { exclude: [] } + }) + } +}) +router.post('/property/create', async (ctx, next) => { + let creatorId = ctx.session.id + let body = Object.assign(ctx.request.body, { creatorId }) + let created = await Property.create(body) + ctx.body = { + data: await Property.findById(created.id, { + attributes: { exclude: [] } + }) + } +}) +router.post('/property/update', async (ctx, next) => { + let properties = ctx.request.body // JSON.parse(ctx.request.body) + properties = Array.isArray(properties) ? properties : [properties] + let result = 0 + for (let item of properties) { + let property = _.pick(item, Object.keys(Property.attributes)) + let affected = await Property.update(property, { + where: { id: property.id } + }) + result += affected[0] + } + ctx.body = { + data: result + } +}) +router.post('/properties/update', async (ctx, next) => { + let { itf } = ctx.query + let properties = ctx.request.body // JSON.parse(ctx.request.body) + properties = Array.isArray(properties) ? properties : [properties] + + // 删除不在更新列表中的属性 + // DONE 2.2 清除幽灵属性:子属性的父属性不存在(原因:前端删除父属性后,没有一并删除后代属性,依然传给了后端) + // SELECT * FROM properties WHERE parentId!=-1 AND parentId NOT IN (SELECT id FROM properties) + /* 查找和删除脚本 + SELECT * FROM properties + WHERE + deletedAt is NULL AND + parentId != - 1 AND + parentId NOT IN ( + SELECT * FROM ( + SELECT id FROM properties WHERE deletedAt IS NULL + ) as p + ) + */ + let existingProperties = properties.filter(item => !item.memory) + let result = await Property.destroy({ + where: { + id: { $notIn: existingProperties.map(item => item.id) }, + interfaceId: itf + } + }) + // 更新已存在的属性 + for (let item of existingProperties) { + let affected = await Property.update(item, { + where: { id: item.id } + }) + result += affected[0] + } + // 插入新增加的属性 + let newProperties = properties.filter(item => item.memory) + let memoryIdsMap = {} + for (let item of newProperties) { + let created = await Property.create(Object.assign({}, item, { + id: null, + parentId: -1, + priority: item.priority || ((await Property.count()) + 1) + })) + memoryIdsMap[item.id] = created.id + item.id = created.id + result += 1 + } + // 同步 parentId + for (let item of newProperties) { + let parentId = memoryIdsMap[item.parentId] || item.parentId + await Property.update({ parentId }, { + where: { id: item.id } + }) + } + ctx.body = { + data: result + } + return next() +}, async(ctx, next) => { + if (ctx.body.data === 0) return + let itf = await Interface.findById(ctx.query.itf, { + attributes: { exclude: [] } + }) + await Logger.create({ + userId: ctx.session.id, + type: 'update', + repositoryId: itf.repositoryId, + moduleId: itf.moduleId, + interfaceId: itf.id + }) +}) +router.get('/property/remove', async (ctx, next) => { + let { id } = ctx.query + ctx.body = { + data: await Property.destroy({ + where: { id } + }) + } +}) + +module.exports = router diff --git a/src/routes/router.js b/src/routes/router.js new file mode 100644 index 0000000..e24ab3e --- /dev/null +++ b/src/routes/router.js @@ -0,0 +1,35 @@ +let Router = require('koa-router') +const fetch = require('node-fetch') +let router = new Router() + +// index +router.get('/', (ctx, next) => { + ctx.body = 'Hello RAP!' +}) + +// env +router.get('/env', (ctx, next) => { + ctx.body = process.env.NODE_ENV +}) + +// fix preload +router.get('/check.node', (ctx, next) => { + ctx.body = 'success' +}) +router.get('/status.taobao', (ctx, next) => { + ctx.body = 'success' +}) +router.get('/test/test.status', (ctx, next) => { + ctx.body = 'success' +}) + +// proxy +router.get('/proxy', async(ctx, next) => { + let { target } = ctx.query + console.log(` <=> ${target}`) + let json = await fetch(target).then(res => res.json()) + ctx.type = 'json' + ctx.body = json +}) + +module.exports = router diff --git a/src/routes/utils/helper.js b/src/routes/utils/helper.js new file mode 100644 index 0000000..0b1e6bb --- /dev/null +++ b/src/routes/utils/helper.js @@ -0,0 +1,230 @@ +let { Module, Interface, Property } = require('../../models') + +const genExampleModule = (extra) => Object.assign({ + name: '示例模块', + description: '示例模块', + creatorId: undefined, + repositoryId: undefined +}, extra) +const genExampleInterface = (extra) => Object.assign({ + name: '示例接口', + url: `/example/${Date.now()}`, + method: 'GET', + description: '示例接口描述', + creatorId: undefined, + lockerId: null, + moduleId: undefined, + repositoryId: undefined +}, extra) +const genExampleProperty = (extra) => Object.assign({ + scope: undefined, + name: 'foo', + type: 'String', + rule: '', + value: '@ctitle', + description: { request: '请求属性示例', response: '响应属性示例' }[extra.scope], + parentId: -1, + creatorId: undefined, + interfaceId: undefined, + moduleId: undefined, + repositoryId: undefined +}, extra) + +// 初始化仓库 +const initRepository = async (repository) => { + let mod = await Module.create(genExampleModule({ + creatorId: repository.creatorId, + repositoryId: repository.id + })) + await initModule(mod) +} +// 初始化模块 +const initModule = async (mod) => { + let itf = await Interface.create(genExampleInterface({ + creatorId: mod.creatorId, + moduleId: mod.id, + repositoryId: mod.repositoryId + })) + await initInterface(itf) +} +// 初始化接口 +const initInterface = async (itf) => { + let { creatorId, repositoryId, moduleId } = itf + let interfaceId = itf.id + await Property.create(genExampleProperty({ + scope: 'request', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + // TODO 2.1 完整的 Mock 示例:无法模拟所有 Mock 规则 + await Property.create(genExampleProperty({ + scope: 'response', + name: 'string', + type: 'String', + rule: '1-10', + value: '★', + description: '字符串属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'number', + type: 'Number', + rule: '1-100', + value: '1', + description: '数字属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'boolean', + type: 'Boolean', + rule: '1-2', + value: 'true', + description: '布尔属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'regexp', + type: 'RegExp', + rule: '', + value: '/[a-z][A-Z][0-9]/', + description: '正则属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'function', + type: 'Function', + rule: '', + value: '() => Math.random()', + description: '函数属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + let array = await Property.create(genExampleProperty({ + scope: 'response', + name: 'array', + type: 'Array', + rule: '1-10', + value: '', + description: '数组属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'foo', + type: 'Number', + rule: '+1', + value: 1, + description: '数组元素示例', + parentId: array.id, + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'bar', + type: 'String', + rule: '1-10', + value: '★', + description: '数组元素示例', + parentId: array.id, + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'items', + type: 'Array', + rule: '', + value: `[1, true, 'hello', /\\w{10}/]`, + description: '自定义数组元素示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + let object = await Property.create(genExampleProperty({ + scope: 'response', + name: 'object', + type: 'Object', + rule: '', + value: '', + description: '对象属性示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'foo', + type: 'Number', + rule: '+1', + value: 1, + description: '对象属性示例', + parentId: object.id, + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'bar', + type: 'String', + rule: '1-10', + value: '★', + description: '对象属性示例', + parentId: object.id, + creatorId, + repositoryId, + moduleId, + interfaceId + })) + await Property.create(genExampleProperty({ + scope: 'response', + name: 'placeholder', + type: 'String', + rule: '', + value: '@title', + description: '占位符示例', + creatorId, + repositoryId, + moduleId, + interfaceId + })) +} + +module.exports = { + genExampleModule, + genExampleInterface, + initRepository, + initModule, + initInterface +} diff --git a/src/routes/utils/pagination.js b/src/routes/utils/pagination.js new file mode 100644 index 0000000..47d0f4f --- /dev/null +++ b/src/routes/utils/pagination.js @@ -0,0 +1,148 @@ +/* + Pagination + + Pure paging implementation reference. + 纯粹的分页参考实现。 + + 属性 + data 数据 + total 总条数 + cursor 当前页数,第几页,从 1 开始计算 + limit 分页大小 + pages 总页数 + start 当前页的起始下标 + end 当前页的结束下标 + hasPrev 是否有前一页 + hasNext 是否有下一页 + hasFirst 是否有第一页 + hasLast 是否有最后一页 + prev 前一页 + next 后一页 + first 第一页 + last 最后一页 + focus 当前页的当前焦点下标 + 方法 + calc() 计算分页状态,当属性值发生变化时,方法 calc() 被调用。 + moveTo(cursor) 移动到指定页 + moveToPrev() 移动到前一页 + moveToNext() 移动到下一页 + moveToFirst() 移动到第一页 + moveToLast() 移动到最后一页 + fetch(arr) 获取当前页的数据,或者用当前状态获取参数 arr 的子集 + setData(data) 更新数据集合 + setTotal(total) 更新总条数 + setCursor(cursor) 更新当前页数 + setFocus(focus) 设置当前焦点 + setLimit(limit) 设置分页大小 + get(focus) 获取一条数据 + toString() 友好打印 + toHTML(url) 生成分页栏 + +*/ + +/* + new Pagination( data, cursor, limit ) + new Pagination( total, cursor, limit ) +*/ +function Pagination (data, cursor, limit) { + this.data = (typeof data === 'number' || typeof data === 'string') ? undefined : data + this.total = this.data ? this.data.length : parseInt(data, 10) + this.cursor = parseInt(cursor, 10) + this.limit = parseInt(limit, 10) + this.calc() +} +Pagination.prototype = { + calc: function () { + if (this.total && parseInt(this.total, 10) > 0) { + this.limit = this.limit < 1 ? 1 : this.limit + + this.pages = (this.total % this.limit === 0) ? this.total / this.limit : this.total / this.limit + 1 + this.pages = parseInt(this.pages, 10) + this.cursor = (this.cursor > this.pages) ? this.pages : this.cursor + this.cursor = (this.cursor < 1) ? this.pages > 0 ? 1 : 0 : this.cursor + + this.start = (this.cursor - 1) * this.limit + this.start = (this.start < 0) ? 0 : this.start // 从 0 开始计数 + this.end = (this.start + this.limit > this.total) ? this.total : this.start + this.limit + this.end = (this.total < this.limit) ? this.total : this.end + + this.hasPrev = (this.cursor > 1) + this.hasNext = (this.cursor < this.pages) + this.hasFirst = this.hasPrev + this.hasLast = this.hasNext + + this.prev = this.hasPrev ? this.cursor - 1 : 0 + this.next = this.hasNext ? this.cursor + 1 : 0 + this.first = this.hasFirst ? 1 : 0 + this.last = this.hasLast ? this.pages : 0 + + this.focus = this.focus ? this.focus : 0 + this.focus = this.focus % this.limit + this.start + this.focus = this.focus > this.end - 1 ? this.end - 1 : this.focus + } else { + this.pages = this.cursor = this.start = this.end = 0 + this.hasPrev = this.hasNext = this.hasFirst = this.hasLast = false + this.prev = this.next = this.first = this.last = 0 + this.focus = 0 + } + + return this + }, + moveTo: function (cursor) { + this.cursor = parseInt(cursor, 10) + return this.calc() + }, + moveToPrev: function () { + return this.moveTo(this.cursor - 1) + }, + moveToNext: function () { + return this.moveTo(this.cursor + 1) + }, + moveToFirst: function () { + return this.moveTo(1) + }, + moveToLast: function () { + return this.moveTo(this.pages) + }, + fetch: function (arr) { + return (arr || this.data).slice(this.start, this.end) + }, + setData: function (data) { + this.data = data + this.total = data.length + return this.calc() + }, + setTotal: function (total) { + this.total = parseInt(total, 10) + return this.calc() + }, + setCursor: function (cursor) { + this.cursor = parseInt(cursor, 10) + return this.calc() + }, + setFocus: function (focus) { + this.focus = parseInt(focus, 10) + if (this.focus < 0) this.focus += this.total + if (this.focus >= this.total) this.focus -= this.total + this.cursor = parseInt(this.focus / this.limit, 10) + 1 + return this.calc() + }, + setLimit: function (limit) { + this.limit = parseInt(limit, 10) + return this.calc() + }, + get: function (focus) { + if (focus !== undefined) return this.data[focus % this.data.length] + else return this.data[this.focus] + }, + toString: function () { + return JSON.stringify(this, null, 4) + } +} +Pagination.prototype.to = Pagination.prototype.moveTo +Pagination.prototype.toPrev = Pagination.prototype.moveToPrev +Pagination.prototype.toNext = Pagination.prototype.moveToNext +Pagination.prototype.toFirst = Pagination.prototype.moveToFirst +Pagination.prototype.toLast = Pagination.prototype.moveToLast + +module.exports = Pagination diff --git a/src/routes/utils/tree.js b/src/routes/utils/tree.js new file mode 100644 index 0000000..aa2e425 --- /dev/null +++ b/src/routes/utils/tree.js @@ -0,0 +1,173 @@ +const vm = require('vm') +const _ = require('underscore') +const Mock = require('mockjs') +const { RE_KEY } = require('mockjs/src/mock/constant') + +const Tree = {} + +Tree.ArrayToTree = (list) => { + let result = { + name: 'root', + children: [], + depth: 0 + } + + let mapped = {} + list.forEach(item => { mapped[item.id] = item }) + + function _parseChildren (parentId, children, depth) { + for (let id in mapped) { + let item = mapped[id] + if (typeof parentId === 'function' ? parentId(item.parentId) : item.parentId === parentId) { + children.push(item) + item.depth = depth + 1 + item.children = _parseChildren(item.id, [], item.depth) + } + } + return children + } + + _parseChildren( + (parentId) => { + // 忽略 parentId 为 0 的根属性(历史遗留),现为 -1 + if (parentId === -1) return true + }, + result.children, + result.depth + ) + + return result +} + +// TODO 2.x 和前端重复了 +Tree.TreeToTemplate = (tree) => { + function parse (item, result) { + let rule = item.rule ? ('|' + item.rule) : '' + let value = item.value + switch (item.type) { + case 'String': + result[item.name + rule] = item.value + break + case 'Number': + if (value === '') value = 1 + let parsed = parseFloat(value) + if (!isNaN(parsed)) value = parsed + result[item.name + rule] = value + break + case 'Boolean': + if (value === 'true') value = true + if (value === 'false') value = false + if (value === '0') value = false + value = !!value + result[item.name + rule] = value + break + case 'Function': + case 'RegExp': + try { + result[item.name + rule] = eval('(' + item.value + ')') // eslint-disable-line no-eval + } catch (e) { + console.warn(`TreeToTemplate ${e.message}: ${item.type} { ${item.name}${rule}: ${item.value} }`) // TODO 2.2 怎么消除异常值? + result[item.name + rule] = item.value + } + break + case 'Object': + if (item.value) { + try { + result[item.name + rule] = eval(`(${item.value})`) // eslint-disable-line no-eval + } catch (e) { + result[item.name + rule] = item.value + } + } else { + result[item.name + rule] = {} + item.children.forEach((child) => { + parse(child, result[item.name + rule]) + }) + } + break + case 'Array': + if (item.value) { + try { + result[item.name + rule] = eval(`(${item.value})`) // eslint-disable-line no-eval + } catch (e) { + result[item.name + rule] = item.value + } + } else { + result[item.name + rule] = item.children.length ? [{}] : [] + item.children.forEach((child) => { + parse(child, result[item.name + rule][0]) + }) + } + break + } + } + let result = {} + tree.children.forEach((child) => { + parse(child, result) + }) + return result +} + +Tree.TemplateToData = (template) => { + // 数据模板 template 中可能含有攻击代码,例如死循环,所以在沙箱中生成最终数据 + // https://nodejs.org/dist/latest-v7.x/docs/api/vm.html + const sandbox = { Mock, template, data: {} } + const script = new vm.Script('data = Mock.mock(template)') + const context = new vm.createContext(sandbox) // eslint-disable-line new-cap + try { + script.runInContext(context, { timeout: 1000 }) // 每次 Mock.mock() 最多执行 1s + // DONE 2.1 __root__ + let data = sandbox.data + let keys = Object.keys(data) + if (keys.length === 1 && keys[0] === '__root__') data = data.__root__ + return data + } catch (err) { + console.error(err) + return {} + } +} + +Tree.ArrayToTreeToTemplate = (list) => { + let tree = Tree.ArrayToTree(list) + let template = Tree.TreeToTemplate(tree) + return template +} + +Tree.ArrayToTreeToTemplateToData = (list, extra) => { + let tree = Tree.ArrayToTree(list) + let template = Tree.TreeToTemplate(tree) + let data + + if (extra) { + // DONE 2.2 支持引用请求参数 + let keys = Object.keys(template).map(item => item.replace(RE_KEY, '$1')) + let extraKeys = _.difference(Object.keys(extra), keys) + let scopedData = Tree.TemplateToData( + Object.assign({}, _.pick(extra, extraKeys), template) + ) + data = _.pick(scopedData, keys) + } else { + data = Tree.TemplateToData(template) + } + + return data +} + +Tree.ArrayToTreeToTemplateToJSONSchema = (list) => { + let tree = Tree.ArrayToTree(list) + let template = Tree.TreeToTemplate(tree) + let schema = Mock.toJSONSchema(template) + return schema +} + +// TODO 2.2 执行 JSON.stringify() 序列化时会丢失正则和函数。需要转为字符串或者函数。 +// X Function.protytype.toJSON = Function.protytype.toString +// X RegExp.protytype.toJSON = RegExp.protytype.toString +Tree.stringifyWithFunctonAndRegExp = (json) => { + return JSON.stringify(json, (k, v) => { + if (typeof v === 'function') return v.toString() + if (v !== undefined && v !== null && v.exec) return v.toString() + else return v + }, 2) +} + +module.exports = Tree diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 0000000..6a8f784 --- /dev/null +++ b/test/helper.js @@ -0,0 +1,98 @@ +/* global before, after */ +const Random = require('mockjs').Random +module.exports = { + mockUsers: () => [{}, {}, {}, {}, {}].map(item => ( + { + fullname: Random.cname(), + email: Random.email(), + password: Random.word(6) + } + )), + mockRepository: () => ( + { + name: '测试用例_临时_' + Random.ctitle(6) + Math.random(), + description: Random.cparagraph(), + logo: Random.url() + } + ), + prepare: (request, should, users, repository) => { + users.forEach((item, index) => { + before(done => { + request.post('/account/register').send(item).expect(200) + .end((err, res) => { + should.not.exist(err) + Object.assign(item, res.body.data) + done() + }) + }) + }) + before(done => { + request.post('/account/login') + .send({ email: users[0].email, password: users[0].password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data } = res.body + data.should.be.a('object').have.all.keys({ id: users[0].id, fullname: users[0].fullname, email: users[0].email }) + done() + }) + }) + if (repository) { + before(done => { + request.post('/repository/create') + .send( + Object.assign(repository, { + organizationId: undefined, + memberIds: users.slice(2).map(item => item.id) + }) + ) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + Object.assign(repository, res.body.data) + done() + }) + }) + after(done => { + request.get('/repository/remove') + .query({ id: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + } + after(done => { + request.get('/account/logout') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + users.forEach((item, index) => { + after(done => { + request.get('/account/remove').query({ id: users[index].id }).expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + }) + }, + keys: { + pagination: ['cursor', 'limit', 'total'] + }, + excludes: { + user: ['password', 'create_date', 'update_date', 'delete_date', 'reserve'], + organization: [], + repository: ['create_date', 'update_date', 'delete_date', 'reserve'] + } +} diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..6a72a27 --- /dev/null +++ b/test/index.js @@ -0,0 +1,2 @@ +// clear console +process.stdout.write(process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H') diff --git a/test/test.account.js b/test/test.account.js new file mode 100644 index 0000000..d1a01b8 --- /dev/null +++ b/test/test.account.js @@ -0,0 +1,104 @@ +/* global describe, it */ +const app = require('../scripts/app') +const request = require('supertest').agent(app.listen()) +const should = require('chai').should() +const Random = require('mockjs').Random + +describe('Account', () => { + let user = { fullname: Random.cname(), email: Random.email(), password: Random.word(6) } + let validUser = (user) => { + user.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + } + let validPagination = (pagination) => { + pagination.should.be.a('object').contain.all.keys(['cursor', 'limit', 'total']) + } + it('/account/register', (done) => { + request.post('/account/register') + .send(user) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + console.log(err) + should.not.exist(err) + validUser(res.body.data) + user.id = res.body.data.id + done() + }) + }) + it('/account/count', (done) => { + request.get('/account/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number').above(0) + done() + }) + }) + it('/account/list', (done) => { + request.get('/account/list') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data, pagination } = res.body + data.should.be.a('array').have.length.above(0) + data.forEach(item => { + validUser(item) + }) + validPagination(pagination) + done() + }) + }) + it('/account/login', done => { + request.post('/account/login') + .send({ email: user.email, password: user.password }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validUser(res.body.data) + done() + }) + }) + it('/account/info', done => { + request.get('/account/info') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validUser(res.body.data) + done() + }) + }) + it('/account/logout', done => { + request.get('/account/logout') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.be.a('object').have.all.keys({ id: user.id }) + done() + }) + }) + it('/account/info', done => { + request.get('/account/info') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + should.not.exist(res.body.data) + done() + }) + }) + it('/account/remove', (done) => { + request.get('/account/remove') + .query({ id: user.id }) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +}) diff --git a/test/test.counter.js b/test/test.counter.js new file mode 100644 index 0000000..771cf26 --- /dev/null +++ b/test/test.counter.js @@ -0,0 +1,24 @@ +/* global describe, it */ +const app = require('../scripts/app') +const request = require('supertest').agent(app.listen()) +const should = require('chai').should() +const { mockUsers, prepare } = require('./helper') + +describe('Counter', () => { + let users = mockUsers() + prepare(request, should, users) + + it('/app/counter', done => { + request.get('/app/counter') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { version, users, mock } = res.body.data + version.should.be.a('string').not.eq('') + users.should.be.a('number').above(0) + mock.should.be.a('number') + done() + }) + }) +}) diff --git a/test/test.interface.js b/test/test.interface.js new file mode 100644 index 0000000..1bf6e6e --- /dev/null +++ b/test/test.interface.js @@ -0,0 +1,128 @@ +/* global describe, it, before */ +let app = require('../scripts/app') +let request = require('supertest').agent(app.listen()) +let should = require('chai').should() +let Random = require('mockjs').Random +const { Interface } = require('../src/models') +const { mockUsers, mockRepository, prepare } = require('./helper') + +describe('Interface', () => { + let users = mockUsers() + let repository = mockRepository() + prepare(request, should, users, repository) + + let itf = {} + before(done => { + itf = { + name: '测试用例_临时_' + Random.ctitle(6) + Math.random(), + url: Random.url(), + method: Random.pick(['GET', 'POST', 'PUT', 'DELETE']), + description: Random.cparagraph(), + lockerId: null, + repositoryId: repository.id, + moduleId: repository.modules[0].id + } + done() + }) + let validInterface = (itf, extras = []) => { + itf.should.be.a('object').have.all.keys( + Object.keys(Interface.attributes).concat(extras) + ) + itf.creatorId.should.be.a('number') + itf.repositoryId.should.be.a('number') + itf.moduleId.should.be.a('number') + } + + it('/interface/create', done => { + request.post('/interface/create') + .send(itf) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validInterface(res.body.data) + itf = res.body.data + done() + }) + }) + it('/interface/count', done => { + request.get('/interface/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number').above(0) + done() + }) + }) + it('/interface/list', done => { + request.get('/interface/list') + .query({ moduleId: repository.modules[0].id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data } = res.body + data.should.be.a('array').have.length.within(1, 2) + data.forEach(item => { + validInterface(item) + }) + done() + }) + }) + it('/interface/get', done => { + request.get('/interface/get') + .query({ id: 1 }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validInterface(res.body.data, ['requestProperties', 'responseProperties']) + done() + }) + }) + it('/interface/update', done => { + request.post('/interface/update') + .send({ id: itf.id, name: Random.ctitle(6) }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/interface/lock', done => { + request.post('/interface/lock') + .send({ id: itf.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/interface/unlock', done => { + request.post('/interface/unlock') + .send({ id: itf.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/interface/remove', done => { + request.get('/interface/remove') + .query({ id: itf.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +}) diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..2ac4919 --- /dev/null +++ b/test/test.js @@ -0,0 +1,36 @@ +/* global describe, it */ +let app = require('../scripts/app') +let request = require('supertest').agent(app.listen()) +let should = require('chai').should() + +describe('App', () => { + it('/', (done) => { + request.get('/') + .expect('Content-Type', /html/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + it('/check.node', (done) => { + request.get('/check.node') + .expect('Content-Type', /text/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.text.should.eq('success') + done() + }) + }) + it('/status.taobao', (done) => { + request.get('/status.taobao') + .expect('Content-Type', /text/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.text.should.eq('success') + done() + }) + }) +}) diff --git a/test/test.mock.js b/test/test.mock.js new file mode 100644 index 0000000..05272d4 --- /dev/null +++ b/test/test.mock.js @@ -0,0 +1,90 @@ +/* global describe, it, before */ +let app = require('../scripts/app') +let request = require('supertest').agent(app.listen()) +let should = require('chai').should() +const { mockUsers, mockRepository, prepare } = require('./helper') + +describe('Mock', () => { + let users = mockUsers() + let repository = mockRepository() + prepare(request, should, users, repository) + + let interfaces + before(done => { + request.get('/interface/list') + .query({ repositoryId: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + interfaces = res.body.data + done() + }) + }) + it('/app/plugin/:repository', done => { + request.get(`/app/plugin/${repository.id}`) + .expect('Content-Type', /javascript/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + it('/app/plugin/:repository,:repository', done => { + request.get(`/app/plugin/${repository.id},${repository.id}`) + .expect('Content-Type', /javascript/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + it('/app/mock/:repository/:method/:url', done => { + request.get(`/app/mock/${interfaces[0].repositoryId}/${interfaces[0].method}/${interfaces[0].url}`) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + it('/app/mock/template/:interfaceId', done => { + request.get(`/app/mock/template/${interfaces[0].id}`) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + it('/app/mock/data/:interfaceId', done => { + request.get(`/app/mock/data/${interfaces[0].id}`) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + done() + }) + }) + /** + it('/app/get', done => { + request.get('/app/get') + .query({ user: 100000000, organization: 1, repository: 1, module: 1, interface: 1, property: 1 }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { user, organization, repository, property } = res.body.data + let mod = res.body.data.module + let itf = res.body.data.interface + user.should.be.a('object') + organization.should.be.a('object') + repository.should.be.a('object') + mod.should.be.a('object') + itf.should.be.a('object') + property.should.be.a('object') + done() + }) + }) + */ +}) diff --git a/test/test.module.js b/test/test.module.js new file mode 100644 index 0000000..3b4b1d7 --- /dev/null +++ b/test/test.module.js @@ -0,0 +1,101 @@ +/* global describe, it, before */ +let app = require('../scripts/app') +let request = require('supertest').agent(app.listen()) +let should = require('chai').should() +let Random = require('mockjs').Random +const { Module } = require('../src/models') +const { mockUsers, mockRepository, prepare } = require('./helper') + +describe('Module', () => { + let users = mockUsers() + let repository = mockRepository() + prepare(request, should, users, repository) + + let mod = {} + before(done => { + mod = { + name: Random.ctitle(6), + description: Random.cparagraph(), + repositoryId: repository.id + } + done() + }) + let validModule = (mod) => { + mod.should.be.a('object').have.all.keys( + Object.keys(Module.attributes) + ) + mod.creatorId.should.be.a('number') + mod.repositoryId.should.be.a('number') + } + + it('/module/create', done => { + request.post('/module/create') + .send(mod) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validModule(res.body.data) + mod = res.body.data + done() + }) + }) + it('/module/count', done => { + request.get('/module/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number').above(0) + done() + }) + }) + it('/module/list', done => { + request.get('/module/list') + .query({ repositoryId: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data } = res.body + data.should.be.a('array').have.length.within(1, 2) + data.forEach(item => { + validModule(item) + }) + done() + }) + }) + it('/module/get', done => { + request.get('/module/get') + .query({ id: mod.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validModule(res.body.data) + done() + }) + }) + it('/module/update', done => { + request.post('/module/update') + .send(Object.assign({}, mod, { name: Random.ctitle(6) + Math.random() })) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/module/remove', done => { + request.get('/module/remove') + .query({ id: mod.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +}) diff --git a/test/test.organization.js b/test/test.organization.js new file mode 100644 index 0000000..c1145f6 --- /dev/null +++ b/test/test.organization.js @@ -0,0 +1,136 @@ +/* global describe, it, before */ +const app = require('../scripts/app') +const request = require('supertest').agent(app.listen()) +const should = require('chai').should() +const Random = require('mockjs').Random +const { Organization } = require('../src/models') +const { mockUsers, prepare, keys } = require('./helper') + +describe('Organization', () => { + let users = mockUsers() + prepare(request, should, users) + + let organization + before(done => { + organization = { + name: Random.ctitle(6) + Math.random(), + description: Random.cparagraph(), + logo: Random.url(), + memberIds: users.slice(2).map(item => item.id) + } + done() + }) + let validOrganization = (organization) => { + organization.should.be.a('object').have.all.keys( + [...Object.keys(Organization.attributes), 'creator', 'owner', 'members'] + ) + let { creator, owner, members } = organization + creator.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + members.should.be.a('array').have.length.within(3, 3) + members.forEach((user, index) => { + owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + }) + } + let validPagination = (pagination) => { + pagination.should.be.a('object').contain.all.keys(keys.pagination) + } + it('/organization/create', done => { + request.post('/organization/create') + .send(organization) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validOrganization(res.body.data) + organization = res.body.data + done() + }) + }) + it('/organization/count', done => { + request.get('/organization/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number').above(0) + done() + }) + }) + it('/organization/list', done => { + request.get('/organization/list') + .query({ name: organization.name, cursor: 1, limit: 1 }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data, pagination } = res.body + data.should.be.a('array').have.length.within(1, 1) + data.forEach(item => { + validOrganization(item) + }) + validPagination(pagination) + done() + }) + }) + it('/organization/owned', done => { + request.get('/organization/owned') + .query({ name: organization.name }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data, pagination } = res.body + data.should.be.a('array').have.length.within(1, 1) + data.forEach(item => { + validOrganization(item) + }) + should.not.exist(pagination) + done() + }) + }) + it('/organization/get', done => { + request.get('/organization/get') + .query({ id: organization.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validOrganization(res.body.data) + done() + }) + }) + it('/organization/update', done => { + request.post('/organization/update') + .send(Object.assign({}, organization, { name: Random.ctitle(6) + Math.random() })) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/organization/transfer', done => { + request.post('/organization/transfer') + .send({ id: organization.id, ownerId: users[1].id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/organization/remove', done => { + request.get('/organization/remove') + .query({ id: organization.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +}) diff --git a/test/test.property.js b/test/test.property.js new file mode 100644 index 0000000..885f2c6 --- /dev/null +++ b/test/test.property.js @@ -0,0 +1,113 @@ +/* global describe, it, before */ +let app = require('../scripts/app') +let request = require('supertest').agent(app.listen()) +let should = require('chai').should() +let Random = require('mockjs').Random +const { Property } = require('../src/models') +const { mockUsers, mockRepository, prepare } = require('./helper') + +describe('Property', () => { + let users = mockUsers() + let repository = mockRepository() + prepare(request, should, users, repository) + + let mod = {} + let itf = {} + let property = {} + before(done => { + mod = repository.modules[0] + itf = mod.interfaces[0] + property = { + scope: Random.pick(['request', 'response']), + name: Random.word(6), + type: Random.pick(['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp']), + rule: '', + value: Random.pick(['@INT', '@FLOAT', '@TITLE', '@NAME']), + description: Random.cparagraph(), + parentId: -1, + repositoryId: repository.id, + moduleId: mod.id, + interfaceId: itf.id + } + done() + }) + let validProperty = (property) => { + property.should.be.a('object').have.all.keys( + Object.keys(Property.attributes) + ) + property.creatorId.should.be.a('number') + property.repositoryId.should.be.a('number') + property.moduleId.should.be.a('number') + } + it('/property/create', done => { + request.post('/property/create') + .send(property) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validProperty(res.body.data) + property = res.body.data + done() + }) + }) + + it('/property/count', done => { + request.get('/property/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number') + done() + }) + }) + it('/property/list', done => { + request.get('/property/list') + .query({ interfaceId: itf.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data } = res.body + data.should.be.a('array').have.length.within(1, 15) + data.forEach(item => { + validProperty(item) + }) + done() + }) + }) + it('/property/get', done => { + request.get('/property/get') + .query({ id: property.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validProperty(res.body.data) + done() + }) + }) + it('/property/update', done => { + request.post('/property/update') + .send({ id: property.id, name: Random.word(6) }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/property/remove', done => { + request.get('/property/remove') + .query({ id: property.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +}) diff --git a/test/test.repository.js b/test/test.repository.js new file mode 100644 index 0000000..7076936 --- /dev/null +++ b/test/test.repository.js @@ -0,0 +1,145 @@ +/* global describe, it, before */ +const app = require('../scripts/app') +const request = require('supertest').agent(app.listen()) +const should = require('chai').should() +const Random = require('mockjs').Random +const { Repository } = require('../src/models') +const { mockUsers, prepare, keys } = require('./helper') + +describe('Repository', () => { + let users = mockUsers() + prepare(request, should, users) + + let repository = {} + before(done => { + repository = { + name: `测试用例_临时仓库_${Random.ctitle(6)}_${Date.now()}`, + description: Random.cparagraph(), + logo: Random.url(), + organizationId: undefined, + memberIds: users.slice(2).map(item => item.id) + } + done() + }) + let validRepository = (repository, deep) => { + repository.should.be.a('object').have.all.keys( + [...Object.keys(Repository.attributes), 'creator', 'owner', 'members', 'locker', 'organization', 'collaborators'] + .concat(deep ? ['modules'] : []) + ) + let { creator, owner, members } = repository + creator.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + members.should.be.a('array').have.length.within(3, 3) + members.forEach((user, index) => { + owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email']) + }) + } + let validPagination = (pagination) => { + pagination.should.be.a('object').contain.all.keys(keys.pagination) + } + + it('/repository/create', done => { + request.post('/repository/create') + .send(repository) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validRepository(res.body.data, true) + repository = res.body.data + done() + }) + }) + it('/repository/count', done => { + request.get('/repository/count') + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.to.be.a('number').above(0) + done() + }) + }) + it('/repository/list', done => { + request.get('/repository/list') + .query({ name: repository.name, cursor: 1, limit: 1 }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + let { data, pagination } = res.body + data.should.be.a('array').have.length.within(1, 1) + data.forEach(item => { + validRepository(item) + }) + validPagination(pagination) + done() + }) + }) + it('/repository/get', done => { + request.get('/repository/get') + .query({ id: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + validRepository(res.body.data, true) + done() + }) + }) + it('/repository/update', done => { + request.post('/repository/update') + .send(Object.assign({}, repository, { name: `测试用例_临时仓库_${Random.ctitle(6)}_${Date.now()}` })) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/repository/lock', done => { + request.post('/repository/lock') + .send({ id: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/repository/unlock', done => { + request.post('/repository/unlock') + .send({ id: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/repository/transfer', done => { + request.post('/repository/transfer') + .send({ id: repository.id, ownerId: users[1].id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) + it('/repository/remove', done => { + request.get('/repository/remove') + .query({ id: repository.id }) + .expect('Content-Type', /json/) + .expect(200) + .end((err, res) => { + should.not.exist(err) + res.body.data.should.eq(1) + done() + }) + }) +})