[
  {
    "path": ".dockerignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# misc\n.DS_Store\n.env\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# css\nsrc/**/*.css\nbuild\nbuild.zip\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/---------.md",
    "content": "---\nname: 产品功能需求及建议\nabout: 为RAP2提供新功能的建议\n\n---\n\n**您提出的功能是否和您遇到的问题有关，请描述该问题**\n\n\n**您是否有建议的实现方案**\n\n\n**其它帮助我们理解您需求的描述、截图**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug--.md",
    "content": "---\nname: Bug反馈\nabout: 为了更快速的定位您的问题，请提供详细的BUG描述。\n\n---\n\n**BUG描述**\n\n\n**复现步骤**\n\n\n**期望结果**\n\n\n**实际结果**\n\n\n**截图**\n\n\n**环境**\n* 是否是自建服务器？如果为自建，请提供操作系统版本\n* 如果是前端错误，请提供浏览器版本\n* 其它可能帮助我们排查问题的环境信息\n\n**附加信息**\n"
  },
  {
    "path": ".gitignore",
    "content": ".nyc_output\n/dist\n/bin\n.DS_Store\nnode_modules\nbower_components\ncoverage\nnpm-debug.log\ntmp\npackage-lock.json\ndump.rdb\n/docker/mysql/volume\n.idea"
  },
  {
    "path": ".jshintrc",
    "content": "{\n    // JSHint Default Configuration File (as on JSHint website)\n    // See http://jshint.com/docs/ for more details\n\n    \"maxerr\"        : 50,       // {int} Maximum error before stopping\n\n    // Enforcing\n    \"bitwise\"       : true,     // true: Prohibit bitwise operators (&, |, ^, etc.)\n    \"camelcase\"     : false,    // true: Identifiers must be in camelCase\n    \"curly\"         : false,    // true: Require {} for every new block or scope\n    \"eqeqeq\"        : true,     // true: Require triple equals (===) for comparison\n    \"forin\"         : true,     // true: Require filtering for..in loops with obj.hasOwnProperty()\n    \"freeze\"        : true,     // true: prohibits overwriting prototypes of native objects such as Array, Date etc.\n    \"immed\"         : false,    // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`\n    \"latedef\"       : false,    // true: Require variables/functions to be defined before being used\n    \"newcap\"        : false,    // true: Require capitalization of all constructor functions e.g. `new F()`\n    \"noarg\"         : true,     // true: Prohibit use of `arguments.caller` and `arguments.callee`\n    \"noempty\"       : true,     // true: Prohibit use of empty blocks\n    \"nonbsp\"        : true,     // true: Prohibit \"non-breaking whitespace\" characters.\n    \"nonew\"         : false,    // true: Prohibit use of constructors for side-effects (without assignment)\n    \"plusplus\"      : false,    // true: Prohibit use of `++` and `--`\n    \"quotmark\"      : false,    // Quotation mark consistency:\n                                //   false    : do nothing (default)\n                                //   true     : ensure whatever is used is consistent\n                                //   \"single\" : require single quotes\n                                //   \"double\" : require double quotes\n    \"undef\"         : true,     // true: Require all non-global variables to be declared (prevents global leaks)\n    \"unused\"        : true,     // Unused variables:\n                                //   true     : all variables, last function parameter\n                                //   \"vars\"   : all variables only\n                                //   \"strict\" : all variables, all function parameters\n    \"strict\"        : true,     // true: Requires all functions run in ES5 Strict Mode\n    \"maxparams\"     : false,    // {int} Max number of formal params allowed per function\n    \"maxdepth\"      : false,    // {int} Max depth of nested blocks (within functions)\n    \"maxstatements\" : false,    // {int} Max number statements per function\n    \"maxcomplexity\" : false,    // {int} Max cyclomatic complexity per function\n    \"maxlen\"        : false,    // {int} Max number of characters per line\n    \"varstmt\"       : false,    // true: Disallow any var statements. Only `let` and `const` are allowed.\n\n    // Relaxing\n    \"asi\"           : true,     // true: Tolerate Automatic Semicolon Insertion (no semicolons)\n    \"boss\"          : false,     // true: Tolerate assignments where comparisons would be expected\n    \"debug\"         : false,     // true: Allow debugger statements e.g. browser breakpoints.\n    \"eqnull\"        : false,     // true: Tolerate use of `== null`\n    \"esversion\"     : 6,         // {int} Specify the ECMAScript version to which the code must adhere.\n    \"moz\"           : true,     // true: Allow Mozilla specific syntax (extends and overrides esnext features)\n    \"esnext\"        : true,\n                                 // (ex: `for each`, multiple try/catch, function expression…)\n    \"evil\"          : false,     // true: Tolerate use of `eval` and `new Function()`\n    \"expr\"          : true,      // true: Tolerate `ExpressionStatement` as Programs\n    \"funcscope\"     : false,     // true: Tolerate defining variables inside control statements\n    \"globalstrict\"  : false,     // true: Allow global \"use strict\" (also enables 'strict')\n    \"iterator\"      : false,     // true: Tolerate using the `__iterator__` property\n    \"lastsemic\"     : false,     // true: Tolerate omitting a semicolon for the last statement of a 1-line block\n    \"laxbreak\"      : false,     // true: Tolerate possibly unsafe line breakings\n    \"laxcomma\"      : false,     // true: Tolerate comma-first style coding\n    \"loopfunc\"      : false,     // true: Tolerate functions being defined in loops\n    \"multistr\"      : false,     // true: Tolerate multi-line strings\n    \"noyield\"       : false,     // true: Tolerate generator functions with no yield statement in them.\n    \"notypeof\"      : false,     // true: Tolerate invalid typeof operator values\n    \"proto\"         : false,     // true: Tolerate using the `__proto__` property\n    \"scripturl\"     : false,     // true: Tolerate script-targeted URLs\n    \"shadow\"        : false,     // true: Allows re-define variables later in code e.g. `var x=1; x=2;`\n    \"sub\"           : false,     // true: Tolerate using `[]` notation when it can still be expressed in dot notation\n    \"supernew\"      : false,     // true: Tolerate `new function () { ... };` and `new Object;`\n    \"validthis\"     : false,     // true: Tolerate using this in a non-constructor function\n\n    // Environments\n    \"devel\"         : true,     // Development/debugging (alert, confirm, etc)\n    \"jquery\"        : true,     // jQuery\n    \"node\"          : true,     // Node.js\n    \"browser\"       : true,     // Browser\n\t\n\n    // Custom Globals\n    \"globals\"       : {}        // additional predefined global variables\n}\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"semi\": false,\n  \"trailingComma\": \"all\",\n  \"singleQuote\": true,\n  \"printWidth\": 100,\n  \"tabWidth\": 2\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\nservices:\n  - mysql\n  - redis-server\n\ncache:\n  directories:\n    - node_modules\n    - $HOME/.npm\n\nnotifications:\n  email: false\n\nnode_js:\n  - '10.1.0'\n\nbefore_install:\n  - npm i -g npm@^5.5.1\n  - mysql -e 'CREATE DATABASE IF NOT EXISTS RAP2_DELOS_APP DEFAULT CHARSET utf8 COLLATE utf8_general_ci'\n\nscript:\n  - npm install\n  - npm run build\n  - npm run create-db\n  - npm run test\n  - npm run check\n\nafter_success:\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  // 使用 IntelliSense 以学习相关的 Node.js 调试属性。\n  // 悬停以查看现有属性的描述。\n  // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n\n    {\n      \"type\": \"node\",\n      \"request\": \"attach\",\n      \"name\": \"Attach by Process ID\",\n      \"processId\": \"${command:PickProcess}\",\n      \"protocol\": \"inspector\"\n    }\n  ]\n}\n"
  },
  {
    "path": "APP-META/docker-config/environment/cai/conf/nginx-proxy.conf",
    "content": "server {\n  listen              80 default_server;\n  server_name         www.taobao.com;\n\n  location / {\n      proxy_pass   http://127.0.0.1:6001;\n      proxy_set_header Host            $host;\n      proxy_set_header X-Forwarded-For $remote_addr;\n  }\n\n  error_page 400 403 405 408 410 411 412 413 414 415 /error.html;\n  error_page 501 502 503 506 /error.html;\n  error_page 404 /404.json;\n  error_page 500 /500.json;\n}\n\nserver {\n    listen              80;\n    server_name         status.taobao.com;\n\n    tmd off;\n\n    location            = /nginx_status {\n        stub_status     on;\n    }\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "# BUILDING\nFROM node:lts-alpine AS builder\n\n# base on work of llitfkitfk@gmail.com\nLABEL maintainer=\"chibing.fy@alibaba-inc.com\"\n\nWORKDIR /app\n\n# cache dependencies\nCOPY package.json ./\n\n# 在国内打开下面一行加速\n#RUN npm config set registry https://registry.npm.taobao.org/\n\n# instal dependencies\nRUN npm install typescript -g && \\\n    npm install\n\n# build\nCOPY . ./\nRUN npm run build\n\n# RUNNING\nFROM node:lts-alpine\n\n# base on work of llitfkitfk@gmail.com\nLABEL maintainer=\"chibing.fy@alibaba-inc.com\"\n# use China mirror of: https://github.com/jgm/pandoc/releases/download/2.7.3/pandoc-2.7.3-linux.tar.gz\nRUN wget http://rap2-taobao-org.oss-cn-beijing.aliyuncs.com/pandoc-2.7.3-linux.tar.gz && \\\n    tar -xf pandoc-2.7.3-linux.tar.gz && \\\n    cp pandoc-2.7.3/bin/* /usr/bin/ && \\\n    pandoc -v && \\\n    rm -rf pandoc-2.7.3-linux.tar.gz pandoc-2.7.3\n\nWORKDIR /app\nCOPY --from=builder /app/public .\nCOPY --from=builder /app/dist .\nCOPY --from=builder /app/node_modules ./node_modules\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 THX\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# RAP2-DELOS 开源社区版本 (后端 API 服务器)\n\n**尊敬的用户：**\n\n由于近期安全审查发现数据风险严重隐患，经慎重评估，我们决定对 rap2.taobao.org 官方体验系统进行下线处理。\n\n> [!WARNING]\n> **下线时间：2025年11月30日 24:00**\n请各位用户在此之前完成以下操作：\n- 导出所需的 Mock 数据和配置\n- 私有化搭建 rap2 系统到自己的服务器\n- 迁移相关数据到自己的系统\n- 更新项目中的接口调用地址\n\n**给您带来的不便，敬请谅解。**\n\n> 阿里妈妈 THX 团队新项目 GoGoCode：https://github.com/thx/gogocode 给批量修改项目代码减轻痛苦！\n\nRAP2 是在 RAP1 基础上重做的新项目，它能给你提供方便的接口文档管理、Mock、导出等功能，包含两个组件(对应两个 Github Repository)。\n\n- rap2-delos: 后端数据 API 服务器，基于 Koa + MySQL[link](http://github.com/thx/rap2-delos)\n- rap2-dolores: 前端静态资源，基于 React [link](http://github.com/thx/rap2-dolores)\n\n\n**Rap 官方服务站点，无需安装直接体验: [rap2.taobao.org](http://rap2.taobao.org)**\n\n注意：本工具为开发工具，相关API未做任何XSS等安全验证，请勿在生产环境依赖RAP的任何服务！！！\n\n**有急事来官方钉钉群，响应更迅速: 31626736 (二群，一群已满）**\n\n2019-10-31：现已支持 Docker 一键部署，欢迎大家体验&反馈\n\n2019-09-27：更新的用户请注意按照下面指引安装 pandoc 以启用文档导出功能\n\n\n## 推荐使用 Docker 快速部署\n\n### 安装 Docker\n\n国内用户可参考 [https://get.daocloud.io/](https://get.daocloud.io/) 安装 Docker 以及 Docker Compose (Linux 用户需要单独安装)，建议按照链接指引配置 Docker Hub 的国内镜像提高加载速度。\n\n### 配置项目\n\n在任意地方建立目录 rap\n\n把本仓库中的 [docker-compose.yml](https://raw.githubusercontent.com/thx/rap2-delos/master/docker-compose.yml) 放到 rap 目录中\n\nRap 前端服务的端口号默认为 3000，你可以在 docker-compose.yml 中按照注释自定义\n\n在 rap 目录下执行下面的命令：\n\n```sh\n# 拉取镜像并启动\ndocker-compose up -d\n\n# 启动后，第一次运行需要手动初始化mysql数据库\n# ⚠️注意: 只有第一次该这样做\ndocker-compose exec delos node scripts/init\n\n# 部署成功后 访问\nhttp://localhost:3000 # 前端（可自定义端口号）\nhttp://localhost:38080 # 后端\n\n# 如果访问不了可能是数据库没有链接上，关闭 rap 服务\ndocker-compose down\n# 再重新运行\ndocker-compose up -d\n# 如果 Sequelize 报错可能是数据库表发生了变化，运行下面命令同步\ndocker-compose exec delos node scripts/updateSchema\n```\n\n**⚠️注意：第一次运行后 rap 目录下会被自动创建一个 docker 目录，里面存有 rap 的数据库数据，可千万不要删除。**\n\n### 镜像升级\n\nRap 经常会进行 bugfix 和功能升级，用 Docker 可以很方便地跟随主项目升级\n\n```sh\n# 拉取一下最新的镜像\ndocker-compose pull\n# 暂停当前应用\ndocker-compose down\n# 重新构建并启动\ndocker-compose up -d --build\n# 有时表结构会发生变化，执行下面命令同步\ndocker-compose exec delos node scripts/updateSchema\n# 清空不被使用的虚悬镜像\ndocker image prune -f\n```\n\n## 手动部署\n\n### 环境要求\n\n- Node.js 8.9.4+\n- MySQL 5.7+\n- Redis 4.0+\n- pandoc 2.73 (供文档生成使用)\n\n### 开发模式\n\n#### 安装 MySQL 和 Redis 服务器\n\n请自行查找搭建方法，mysql/redis 配置在 config.\\*.ts 文件中，在不修改任何配置的情况下，\nredis 会通过默认端口 + 本机即可正常访问，确保 redis-server 打开即可。\n\n注意：修改 cofig 文件后需要重新 `npm run build` 才能生效\n\n#### 安装 pandoc\n\n我们使用 pandoc 来生成 Rap 的离线文档，安装 Pandoc 最通用的办法是在 pandoc 的 [release 页面](https://github.com/jgm/pandoc/releases/tag/2.7.3)下载对应平台的二进制文件安装即可。\n\n其中 linux 版本最好放在`/usr/local/bin/pandoc` 让终端能直接找到，并执行 `chmod +x /usr/local/bin/pandoc` 给调用权限。\n\n测试在命令行执行命令 `pandoc -h` 有响应即可。\n\n#### 启动redis-server\n\n```sh\nredis-server\n```\n\n后台执行可以使用 nohup 或 pm2，这里推荐使用 pm2，下面命令会安装 pm2，并通过 pm2 来启动 redis 缓存服务\n\n```bash\nnpm install -g pm2\nnpm run start:redis\n```\n\n#### 先创建创建数据库\n\n```bash\nmysql -e 'CREATE DATABASE IF NOT EXISTS RAP2_DELOS_APP DEFAULT CHARSET utf8 COLLATE utf8_general_ci'\n```\n\n#### 初始化\n\n```bash\nnpm install\n```\n\nconfirm configurations in /config/config.dev.js (used in development mode)，确认/config/config.dev.js 中的配置(.dev.js 后缀表示用于开发模式)。\n\n#### 安装 && TypeScript 编译\n\n```bash\nnpm install -g typescript\nnpm run build\n```\n\n#### 初始化数据库表\n\n```bash\nnpm run create-db\n```\n\n#### 执行 mocha 测试用例和 js 代码规范检查\n\n```bash\nnpm run check\n```\n\n#### 启动开发模式的服务器 监视并在发生代码变更时自动重启\n```bash\nnpm run dev\n```\n\n### 生产模式\n\n```sh\n# 1. 修改/config/config.prod.js中的服务器配置\n# 2. 启动生产模式服务器\nnpm start\n\n```\n\n## 社区贡献\n\n- [rap2-javabean 自动从 Rap 接口生成 Java Bean](https://github.com/IndiraFinish/rap2-javabean)\n- [rap2-generator 把 Java Bean 生成到 Rap](https://github.com/kings1990/rap2-generator)\n- [yapix 一键生成接口文档, 上传到yapi, rap2, eolinker等](https://github.com/jetplugins/yapix)\n\n## Author\n\n- 版权: 阿里妈妈前端团队\n- 作者:\n  - RAP2 2017/10 前版本作者为[墨智(@Nuysoft)](https://github.com/nuysoft/), [mockjs](mockjs.com)的作者。\n  - 2017/10 之后版本开发者\n    - [霍雍(Bosn)](http://github.com/bosn/)，[RAP1](http://github.com/thx/RAP)作者，RAP 最早的创始人。\n    - [承虎(alvarto)](http://github.com/alvarto/)\n    - [池冰(bigfengyu)](https://github.com/bigfengyu)\n\n### Tech Arch\n\n- 前端架构(rap2-dolores)\n  - React / Redux / Saga / Router\n  - Mock.js\n  - SASS / Bootstrap 4 beta\n  - server: nginx\n- 后端架构(rap2-delos)\n  - Koa\n  - Sequelize\n  - MySQL\n  - Server\n  - server: node\n\n### 旧版本升级\n    \n    -数据库数据迁移  RAP2 2.4迁移到2.8\n      由于数据库表有主外键，按以下顺序插入数据\n      1.Users\n      2.Organizations\n      3.Repositories\n      4.repositories_members（备注：将createdAt、updatedAt两个字段必填去除）\n      5.organizations_members\n      6.Modules\n      7.Interfaces\n      8.Loggers\n      9.Properties（备注：将数据scope字段的所有''值替换成'String'）\n      default_val和repositories_collaborators表无数据无需处理\n      \n    \n"
  },
  {
    "path": "database/history/patch-null-type.sql",
    "content": "ALTER TABLE `Properties`\n  MODIFY COLUMN `type` enum('String','Number','Boolean','Object','Array','Function','RegExp','Null') NOT NULL;"
  },
  {
    "path": "database/history/v2.5_change.sql",
    "content": "ALTER TABLE repositories_collaborators\n\tDROP COLUMN createdat ;\n\nALTER TABLE repositories_collaborators\n\tDROP COLUMN updatedat ;\n\nALTER TABLE repositories_members \n\tDROP COLUMN createdat ;\n\nALTER TABLE repositories_members \n\tDROP COLUMN updatedat ;"
  },
  {
    "path": "database/history/v2.6_change_20180705.sql",
    "content": "ALTER TABLE `properties`\n  ADD COLUMN `pos` INT(10) NULL DEFAULT 2;\n\nALTER TABLE `interfaces`\n  ADD COLUMN `status` INT(10) NULL DEFAULT 200;"
  },
  {
    "path": "database/history/v2.8.0_token.sql",
    "content": "# 给 Repositories 表添加 token 列\n# 2019-11-11\nALTER TABLE Repositories \nADD COLUMN token VARCHAR(32) NULL;"
  },
  {
    "path": "database/history/v2018_07_27.sql",
    "content": "ALTER TABLE Interfaces\n  MODIFY COLUMN `method` VARCHAR(256) NOT NULL;"
  },
  {
    "path": "database/history/v2018_08_27_add_property_required.sql",
    "content": "ALTER TABLE `Properties`\n  ADD COLUMN `required` TINYINT(1) NOT NULL DEFAULT 0;"
  },
  {
    "path": "database/history/v2019_09_26_default_val_table.sql",
    "content": "CREATE TABLE `default_val` (\n\t`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',\n\t`createdat` datetime NOT NULL COMMENT '创建时间',\n\t`updatedat` datetime NOT NULL COMMENT '修改时间',\n\t`name` varchar(256) NOT NULL COMMENT '名字',\n\t`rule` varchar(512) NOT NULL COMMENT '规则',\n\t`value` text NOT NULL COMMENT '值',\n\t`repositoryid` bigint unsigned NOT NULL COMMENT 'FK',\n\t`deletedat` datetime NOT NULL COMMENT '删除时间',\n\tPRIMARY KEY (`id`)\n) DEFAULT CHARACTER SET=utf8mb4 COMMENT='默认值';"
  },
  {
    "path": "database/recreate_db.sql",
    "content": "DROP DATABASE RAP2_DELOS_APP;\nCREATE DATABASE IF NOT EXISTS RAP2_DELOS_APP\n\tDEFAULT CHARSET utf8\n\tCOLLATE utf8_general_ci;"
  },
  {
    "path": "database/v2018_05_15.sql",
    "content": "ALTER TABLE Interfaces\nMODIFY priority BIGINT(11) NOT NULL DEFAULT 1;\n\nALTER TABLE Properties\nMODIFY priority BIGINT(11) NOT NULL DEFAULT 1;\n\nALTER TABLE Modules\nMODIFY priority BIGINT(11) NOT NULL DEFAULT 1;"
  },
  {
    "path": "database/v2020_07_06_history_log.sql",
    "content": "CREATE TABLE `history_log` (\n  `id` int NOT NULL AUTO_INCREMENT,\n  `entityType` int NOT NULL,\n  `entityId` int NOT NULL,\n  `changeLog` text COLLATE utf8mb4_unicode_ci NOT NULL,\n  `relatedJSONData` text COLLATE utf8mb4_unicode_ci,\n  `userId` bigint(11) unsigned NOT NULL,\n  `createdAt` datetime NOT NULL,\n  `updatedAt` datetime NOT NULL,\n  `deletedAt` datetime DEFAULT NULL,\n  CONSTRAINT `hisotry_log_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `Users` (`id`) ON UPDATE CASCADE,\n  PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;\n"
  },
  {
    "path": "database/v2020_07_21_body_option_save.sql",
    "content": "ALTER TABLE `interfaces`\n  ADD COLUMN `bodyOption` VARCHAR(255) NULL;"
  },
  {
    "path": "database/v2020_07_21_merge_PR.sql",
    "content": "ALTER TABLE `RAP2_DELOS_APP`.`Properties`\nMODIFY COLUMN `scope` enum('request','response','script') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL\n  DEFAULT 'response' COMMENT 'property owner' AFTER `id`;"
  },
  {
    "path": "docker-compose.yml",
    "content": "# mail@dongguochao.com\n# llitfkitfk@gmail.com\n# chibing.fy@alibaba-inc.com\n\nversion: \"3\"\n\nservices:\n  # frontend\n  dolores:\n    image: rapteam/rap2-dolores:latest\n    ports:\n      #冒号前可以自定义前端端口号，冒号后不要动\n      - 3000:38081\n\n  # backend\n  delos:\n    image: rapteam/rap2-delos:latest\n    ports:\n      # 这里的配置不要改哦\n      - 38080:38080\n    environment:\n      - SERVE_PORT=38080\n      # if you have your own mysql, config it here, and disable the 'mysql' config blow\n      - MYSQL_URL=mysql # links will maintain /etc/hosts, just use 'container_name'\n      - MYSQL_PORT=3306\n      - MYSQL_USERNAME=root\n      - MYSQL_PASSWD=\n      - MYSQL_SCHEMA=rap2\n\n      # redis config\n      - REDIS_URL=redis\n      - REDIS_PORT=6379\n\n      # production / development\n      - NODE_ENV=production\n    ###### 'sleep 30 && node scripts/init' will drop the tables\n    ###### RUN ONLY ONCE THEN REMOVE 'sleep 30 && node scripts/init'\n    command: /bin/sh -c 'node dispatch.js'\n    # init the databases\n    # command: sleep 30 && node scripts/init && node dispatch.js\n    # without init\n    # command: node dispatch.js\n    depends_on:\n      - redis\n      - mysql\n\n  redis:\n    image: redis:4\n\n  # disable this if you have your own mysql\n  mysql:\n    image: mysql:5.7\n    # expose 33306 to client (navicat)\n    #ports:\n    #   - 33306:3306\n    volumes:\n      # change './docker/mysql/volume' to your own path\n      # WARNING: without this line, your data will be lost.\n      - \"./docker/mysql/volume:/var/lib/mysql\"\n    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --init-connect='SET NAMES utf8mb4;' --innodb-flush-log-at-trx-commit=0\n    environment:\n      MYSQL_ALLOW_EMPTY_PASSWORD: \"true\"\n      MYSQL_DATABASE: \"rap2\"\n      MYSQL_USER: \"root\"\n      MYSQL_PASSWORD: \"\"\n"
  },
  {
    "path": "docs/Schedule.md",
    "content": "## 2017.05.15～05.26 计划\n1. RAP1 数据迁移测试\n2. 发布线上服务，自测在项目中的体验\n3. 编写公开 API 的文档（注释）\n4. 其他参与的同学参照 v2.1 需求和约定 http://gitlab.alibaba-inc.com/thx/rap2-delos/blob/master/docs/Design.md，和仓库中的 TODO 任务注释\n5. 陪产假期间在家办公，业务支持直接电话我\n\n> TODO 消消乐\n\n## 2017.06.12～06.16 部署\n\n**线上 RAP1 数据迁移基本完成，正在测试迁移数据。**\n\n### 服务端 rap2-delos\n1. 正式发布部署和迁移\n\n### 前端 rap2-dolores\n1. 正式发布部署和测试\n\n## 2017.06.05～06.09 部署\n\n**v2.1 开发完成，部署基本完成。**\n\n### 服务端 rap2-delos\n1. 正式发布部署\n    1. 接入 KeyCenter\n2. 优化 分离拥有的仓库和加入的仓库\n3. 优化 分离拥有的组织和加入的组织\n4. 新增 fetch 拦截插件\n\n### 前端 rap2-dolores\n1. 正式发布部署\n    1. 接入 线上统一登录\n    2. 接入 线上域名\n2. 协同 服务端的『分离拥有的仓库和加入的仓库』\n3. 协同 服务端的『分离拥有的组织和加入的组织』\n4. 新增 支持查看其他用户的仓库\n5. 其他零散代码、视觉、交互优化\n    1. 视觉 增加拥有者 icon\n    2. 交互 首页 新用户显示引导文案『新建仓库』\n\n## 2017.05.31～06.02\n\n### 服务端 rap2-delos **2.1 开发完成**\n1. 修复 数字属性、布尔属性、数组属性的解析和初始化\n2. 修复 当前端发送 JSONP 请求，并且响应内容是字符串时，字符串响应再次执行 JSON.stringify()，导致响应内容格式错误\n3. 增加 JSONSchema 接口 /app/mock/schema/:interfaceId\n4. 完善 RAP1 迁移脚本\n    1. 修正 类型 array<number|string|object|boolean> => Array\n    2. 修正 模拟值 @mock=function(){} => Function\n    3. 修正 顺序值 $order => Array|+1: []\n5. 完善 仓库接口测试页面，支持动态仓库 id\n6. 新增 支持虚拟属性 __root__\n7. 正式发布部署（未完）\n    1. 调整 Dockerfile 配置\n    2. 接入 VIPServer\n    3. 数据库上线\n    4. 接入 KeyCenter（未完）\n\n### 前端 rap2-dolores\n1. 增加 生成规则帮助链接\n2. 增加 访问不存在仓库的编辑器时提示 404\n3. 协同 后端的『修复 数字属性、布尔属性、数组属性的解析和初始化』\n4. 增加 公开接口\n5. 代码优化\n    1. 删除 遗留的无效注释\n    2. 删除 不再使用的 Fetching 组件\n    3. 增加 RModal 重定位截流\n    4. 完善 登陆时只使用 email 和 password，丢弃其他属性（非 BUS SSO 场景）\n    5. 删除 遗留的 corporation、product、grouping 代码\n    6. 完善 补全团队列表的 propTypes\n    7. 修复 不解析原始类型的初始值\n    8. 部署 暂时访问 daily 环境，上线后再恢复\n6. 视觉优化\n    1. 增加 自动获得焦点：组织、仓库、模块、接口、属性、导入器、注册、登陆\n    2. 视觉 润色首页日志格式\n    3. 视觉 仓库列表和团队列表的最小高度为 10rem，增大没有找到匹配数据时的字号\n    4. 恢复 团队成员头像\n    5. 增加 协同仓库的帮助信息\n    6. 增加 组件 Popover 支持自定义 width\n    7. 视觉 组件 MembersInput 默认底部外边距 10px\n    8. 视觉 润色表单 input 的宽度\n    9. 视觉 移除 .rapfont，统一改用 react-icons\n    10. 视觉 润色接口编辑器\n    11. 新增 仓库编辑器初始加载时显示动画\n7. 修复 属性类型 Number 并且初始值为 '' 时，被解析为随机字符串\n8. 完善 删除团队、仓库、模块、接口时的确认提示\n9. 新增 导入器支持格式化输入的 JSON\n10. 修复 导入器重复调用 handleAddMemoryProperty() 丢失临时属性\n\n\n## 2017.05.22～05.26\n\n### 服务端 rap2-delos\n1. 支持 迁移 RAP1 数据（开发和本地调试完成，待线上验证）\n2. 完善 jQuery 插件、Mock 插件、插件文档 public/libs/README.md\n3. 增加 检测和提示仓库中的重复接口\n4. 修复 初始化新模块时创建了重复的示例接口\n5. 支持 仓库协同（即 RAP1 的项目路由，用于指定与哪些项目共享 mock 数据）\n6. 修复 creatorId 必须是当前登录用户，不需要前端传入\n7. 修复 测试用例创建的临时仓库没有及时移除\n8. 重构 IDB 结构设计\n  1. 清理 历史遗留表 user、repository、module、interface、property、organization、organization_members、logger、notification\n  2. 新建 仓库协同表 repositories_collaborators\n  3. 新建 账户通知表 notifications\n  4. 新增 字段 organizations.visibility，用于支持私有团队（待前端支持）\n  5. 新增 字段 repositories.visibility，用于支持私有仓库（待前端支持）\n9. 调整 接口 /app/get 的位置，从 routes/mock.js 分散到 routes/account|organization|repository.js\n10. 修复 当创建者或拥有者已经不存在时，仓库列表和组织列表的总记录数错误\n11. 支持 转移团队 /organization/transfer（待前端支持）\n12. 支持 转移仓库 /repository/transfer（待前端支持）\n13. 优化 获取单个仓库完整数据的性能\n14. 完善 示例接口初始化时填充更多的 Mock 规则示例\n\n### 前端 rap2-dolores\n1. 修复 /app/plugin/:repositories 接收到无效 repositoryId 时报错\n2. 调整 导航栏，我的仓库=>仓库，团队仓库=>组织\n3. 增加 仓库/全部仓库\n4. 增加 检测和提示仓库中的重复接口\n5. 修复 『我创建和加入的团队』不应该有分页\n6. 协同 后端的『仓库协同』\n7. 视觉 润色仓库编辑器\n8. 修复 仓库编辑权限的判断逻辑\n\n## 2017.05.15～05.19\n\n### 服务端 rap2-delos\n1. 修复 团队测试用例的用户 id 不存在\n2. 修复 接口 /repository/joined 不排除自己拥有的项目\n3. 完善 模拟数据接口 /app/mock/:repository/:method/:url\n  1. 支持响应多个仓库的数据\n  2. 支持不同的 http method\n  3. 完善相应的测试用例\n  4. 支持过滤重复仓库 id\n  5. 完善注释内容，增加关于直接通过 interface id 获取模板和数据的说明\n  6. 增加请求属性和响应属性的 Mock 模板\n4. 重构 IDB 结构设计，为迁移 RAP1 数据做准备\n  1. 清理历史遗留表 corporation、product、grouping、project、page、action\n  2. 清理历史遗留字段 property.template、property.page、property.project、module.project\n  3. 利用 Sequelize 重构所有表之间的关联关系（代码更精简）\n  4. 修改所有外键的命名，风格统一为 modelId（减少歧义）\n  5. 调整所有涉及的模型、路由、测试用例和初始数据\n5. 清理 历史 API 示例 HTML（已经全部改为测试用例）\n6. 新增 前端插件适配 jQuery、Mock（待测试）\n7. 完善 SQL 日志格式\n8. 完善 生成数据模板时的异常日志格式\n9. 完善 测试用例：用户、组织、仓库\n\n### 前端 rap2-dolores\n1. 新增 测试器 Tester（未完）\n2. 引入 react-icons，因为 iconfont 的质量参差不齐，在 React 中使用不方便\n3. 完善 仓库列表、组织列表的视觉：废弃 table 布局，类型文案改为 <select>\n4. 修复 组件 Popover 定位错误\n5. 修复 进入仓库编辑器时先展示上一次编辑过的仓库，直到当前仓库的数据返回后才会更新\n6. 修复 仓库数据返回之前，会展示不完整的静态内容\n7. 引入 RCodeMirror，作为接口属性导入工具的编辑器\n8. 协同 后端的『重构 IDB 结构设计』\n9. 优化 没有搜索到匹配的组织、仓库时的提示\n\n## 2017.05.12\n\n### 服务端 rap2-delos\n1. 新增 Sequelize 持久化时字段值校验\n2. 重构 初始数据，测试用户从 100000000 开始\n3. 新增 按 name 或 id 搜索用户、仓库、组织\n4. 修复 插件中的接口字段\n5. 新增 属性增加生成规则字段 rule\n6. 新增 所有响应 JSON 增加字段 update_date，前端可以当作版本号进行版本检测\n7. 新增 用户日志 /account/logger（未完）\n8. 修复 更新仓库、组织时意外变更 owner 和 creator\n9. 修复 仓库的字段 members 没有成员时返回 [null] 导致前端渲染报错\n10. 完善 格式化 /app/mock/:repository/:url 的响应，方便前端阅读和调试\n\n### 前端 rap2-dolores\n1. 重构 整站交互和样式，导航固定为首页、我的仓库、团队仓库、状态\n2. 重构 组织相关功能，代码和逻辑更清爽条理\n3. 修正 全局计数器 fetching\n4. 新增 成员输入组件 MembersInput\n5. 重构 仓库相关功能，代码和逻辑更清爽条理\n6. 放弃 样式组件 .panel，改为 .card\n7. 新增 用户日志（未完）\n8. 修复 组件 build 后初始 state 为 null\n9. 新增 表单验证（未完）\n10. 引入 NProgress\n11. 完善 表单输入一律禁用 spellcheck\n12. 完善 整站开屏动画\n13. 完善 预览 JSON 模板和 JSON 数据\n14. 新增 RModal 组件，用于替换繁琐的 DialogController\n15. 修复 接口测试时仓库 id 错误\n\n## 2017.04.28\n\n### 服务端 rap2-delos\n1. 重构整个项目：目录结构、依赖包、代码规范 standardjs、pre-commit 检测\n2. 增加测试用例 Account、Organization、Worksapce、Mock\n3. v2.1 需求和约定 http://gitlab.alibaba-inc.com/thx/rap2-delos/blob/master/docs/Design.md\n4. 安全修复：属性为死循环函数导致服务宕掉\n\n### 前端 rap2-dolores\n1. 接入集团统一登陆\n2. 增加代码规范 standardjs、pre-commit 检测\n3. 支持编辑模块、页面、接口\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rap2-delos\",\n  \"version\": \"2.9.0\",\n  \"repository\": {\n    \"url\": \"https://github.com/thx/rap2-delos\"\n  },\n  \"description\": \"\",\n  \"main\": \"dist/dispatch.js\",\n  \"scripts\": {\n    \"build\": \"rimraf -rf dist/ && tsc\",\n    \"test\": \"cross-env NODE_ENV=development cross-env TEST_MODE=true nyc mocha test/**/*.js\",\n    \"check\": \"echo \\\"Checking...\\\" && tsc && npm run lint\",\n    \"dev\": \"cross-env NODE_ENV=development nodemon --watch scripts --watch dist dist/scripts/dev.js\",\n    \"create-db\": \"cross-env NODE_ENV=development node dist/scripts/initSchema\",\n    \"start\": \"cross-env NODE_ENV=production pm2 start dist/dispatch.js --name=rap-server-delos\",\n    \"lint\": \"echo \\\"TSLint checking...\\\" && tslint -c tslint.json --fix 'src/**/*.ts' 'src/**/*.tsx'\",\n    \"start:redis\": \"pm2 start redis-server --name redis-server\",\n    \"clean\": \"pm2 delete all\",\n    \"build-docker\": \"docker build --rm -f \\\"Dockerfile\\\" -t rapteam/rap2-delos:latest .\"\n  },\n  \"mocha\": {\n    \"timeout\": 8000,\n    \"slow\": 200,\n    \"exit\": true,\n    \"allowUncaught\": true\n  },\n  \"author\": \"bosn, nuysoft\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"@types/treeify\": \"^1.0.0\",\n    \"chalk\": \"^3.0.0\",\n    \"cross-env\": \"^6.0.3\",\n    \"graceful\": \"^1.0.2\",\n    \"is-md5\": \"^0.0.2\",\n    \"js-beautify\": \"^1.10.3\",\n    \"json5\": \"^2.1.1\",\n    \"kcors\": \"^2.2.2\",\n    \"koa\": \"^2.11.0\",\n    \"koa-body\": \"^4.1.1\",\n    \"koa-generic-session\": \"^2.0.4\",\n    \"koa-logger\": \"^3.2.1\",\n    \"koa-redis\": \"^4.0.1\",\n    \"koa-router\": \"^8.0.8\",\n    \"koa-send\": \"^5.0.0\",\n    \"koa-static\": \"^5.0.0\",\n    \"lodash\": \"^4.17.15\",\n    \"mariadb\": \"^2.2.0\",\n    \"md5\": \"^2.2.1\",\n    \"mockjs\": \"1.1.0\",\n    \"moment\": \"^2.24.0\",\n    \"mysql\": \"^2.18.1\",\n    \"mysql2\": \"^2.1.0\",\n    \"nanoid\": \"^2.1.11\",\n    \"node-fetch\": \"^2.6.0\",\n    \"node-print\": \"0.0.4\",\n    \"node-schedule\": \"^1.3.2\",\n    \"nodemailer\": \"^6.4.10\",\n    \"notevil\": \"^1.3.3\",\n    \"path-to-regexp\": \"^3.1.0\",\n    \"redis\": \"^3.0.2\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"request\": \"^2.88.2\",\n    \"request-promise\": \"^4.2.5\",\n    \"sequelize\": \"^5.22.3\",\n    \"sequelize-typescript\": \"^1.1.0\",\n    \"svg-captcha\": \"^1.4.0\",\n    \"treeify\": \"^1.1.0\",\n    \"underscore\": \"^1.9.1\",\n    \"urllib\": \"^2.34.1\",\n    \"vm2\": \"^3.8.4\"\n  },\n  \"devDependencies\": {\n    \"@types/chai\": \"^4.2.10\",\n    \"@types/json5\": \"^0.0.30\",\n    \"@types/kcors\": \"^2.2.3\",\n    \"@types/koa\": \"^2.11.2\",\n    \"@types/koa-generic-session\": \"^1.0.3\",\n    \"@types/koa-logger\": \"^3.1.1\",\n    \"@types/koa-redis\": \"^4.0.0\",\n    \"@types/koa-router\": \"^7.4.0\",\n    \"@types/koa-static\": \"^4.0.1\",\n    \"@types/lodash\": \"^4.14.149\",\n    \"@types/mocha\": \"^8.0.0\",\n    \"@types/mockjs\": \"^1.0.2\",\n    \"@types/nanoid\": \"^2.1.0\",\n    \"@types/node\": \"^13.7.7\",\n    \"@types/node-schedule\": \"^1.3.0\",\n    \"@types/nodemailer\": \"^6.4.0\",\n    \"@types/redis\": \"^2.8.16\",\n    \"@types/request\": \"^2.48.4\",\n    \"@types/request-promise\": \"^4.1.45\",\n    \"@types/sequelize\": \"^4.28.8\",\n    \"@types/underscore\": \"^1.9.4\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"chai\": \"^4.2.0\",\n    \"mocha\": \"^8.0.1\",\n    \"nodemon\": \"^2.0.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"nyc\": \"^15.0.0\",\n    \"pre-commit\": \"^1.2.2\",\n    \"rimraf\": \"^3.0.2\",\n    \"source-map-support\": \"^0.5.16\",\n    \"standard\": \"^14.3.1\",\n    \"supertest\": \"^4.0.2\",\n    \"tslint\": \"^6.0.0\",\n    \"typescript\": \"^3.8.3\"\n  },\n  \"pre-commit\": [\n    \"check\"\n  ]\n}\n"
  },
  {
    "path": "public/404.json",
    "content": "{ \"isOk\": false, \"errMsg\": \"找不到接口，请检查您的配置。 Can not find your API, please check your configurations.\"}"
  },
  {
    "path": "public/500.json",
    "content": "{ \"isOk\": false, \"errMsg\": \"服务器内部错误，请联系管理员。 Server side internal error occurred, please contact the administrator of this system.\"}"
  },
  {
    "path": "public/error.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"shortcut icon\" href=\"favicon.ico\">\n    <title>RAP2 Delos</title>\n  </head>\n  <body>\n    Opps, 发生了错误.... 请联系管理员....\n  </body>\n</html>\n"
  },
  {
    "path": "public/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"shortcut icon\" href=\"favicon.ico\">\n    <title>RAP2 Delos</title>\n  </head>\n  <body>\n    RAP2后端服务已启动，请从前端服务(rap2-dolores)访问。\n    RAP2 back-end server is started, please visit via front-end service (rap2-dolores).\n  </body>\n</html>\n"
  },
  {
    "path": "public/libs/README.md",
    "content": "## RAP2 提供两种拦截方式\n\n1. 引入 `libs/jquery.rap.js`\n  用复写的 `jQuery.ajax` 拦截与 RAP2 接口设置匹配的 ajax 请求，然后转发至 RAP2。\n2. 引入 `libs/mock.rap.js`\n\t由 Mock 拦截与 RAP2 接口设置匹配的 ajax 请求，然后直接返回响应数据，不会转发至 RAP2。\n"
  },
  {
    "path": "public/libs/fetch.rap.js",
    "content": ";(function (RAP, fetch) {\n  if (!fetch) {\n    console.warn('当前环境不支持 fetch')\n    return\n  }\n  if (!RAP) {\n    console.warn('请先引入 RAP 插件')\n    return\n  }\n\n  let next = fetch\n  let find = (settings) => {\n    for (let repositoryId in RAP.interfaces) {\n      for (let itf of RAP.interfaces[repositoryId]) {\n        if (itf.method.toUpperCase() === settings.method.toUpperCase() && itf.url === settings.url) {\n          return Object.assign({}, itf, { repositoryId })\n        }\n      }\n    }\n  }\n  window.fetch = function (url, settings) {\n    // ajax(settings)\n    if (typeof url === 'object') {\n      settings = Object.assign({ method: 'GET' }, url)\n    } else {\n      // ajax(url) ajax(url, settings)\n      settings = Object.assign({ method: 'GET' }, settings, { url })\n    }\n\n    var match = find(settings)\n    if (!match) return next.call(window, url, settings)\n\n    let redirect = `${RAP.protocol}://${RAP.host}/app/mock/${match.repositoryId}/${match.method}/${match.url}`\n    settings.credentials = 'include'\n    settings.method = 'GET'\n    settings.dataType = 'jsonp'\n    console.log(`Fetch ${match.method} ${match.url} => ${redirect}`)\n    return next.call(window, redirect, settings)\n  }\n})(window.RAP, window.fetch)\n"
  },
  {
    "path": "public/libs/jquery.rap.js",
    "content": ";(function (RAP, jQuery) {\n  if (!jQuery) {\n    console.warn('请先引入 jQuery')\n    return\n  }\n  if (!RAP) {\n    console.warn('请先引入 RAP 插件')\n    return\n  }\n\n  // 示例：检测重复接口\n  let counter = {}\n  for (let repositoryId in RAP.interfaces) {\n    for (let itf of RAP.interfaces[repositoryId]) {\n      let key = `${itf.method} ${itf.url}`\n      counter[key] = [...(counter[key] || []), itf]\n    }\n  }\n  for (let key in counter) {\n    if (counter[key].length > 1) {\n      console.group('警告：检测到重复接口 ' + key)\n      counter[key].forEach(itf => {\n        console.warn(`#${itf.id} ${itf.method} ${itf.url}`)\n      })\n      console.groupEnd('警告：检测到重复接口 ' + key)\n    }\n  }\n\n  let next = jQuery.ajax\n  let find = (settings) => {\n    for (let repositoryId in RAP.interfaces) {\n      for (let itf of RAP.interfaces[repositoryId]) {\n        if (itf.method.toUpperCase() === settings.method.toUpperCase() && itf.url === settings.url) {\n          return Object.assign({}, itf, { repositoryId })\n        }\n      }\n    }\n  }\n  jQuery.ajax = function (url, settings) {\n    // ajax(settings)\n    if (typeof url === 'object') {\n      settings = Object.assign({ method: 'GET' }, url)\n    } else {\n      // ajax(url) ajax(url, settings)\n      settings = Object.assign({ method: 'GET' }, settings, { url })\n    }\n\n    var match = find(settings)\n    if (!match) return next.call(jQuery, url, settings)\n\n    let redirect = `${RAP.protocol}://${RAP.host}/app/mock/${match.repositoryId}/${match.method}/${match.url}`\n    settings.method = 'GET'\n    settings.dataType = 'jsonp'\n    console.log(`jQuery ${match.method} ${match.url} => ${redirect}`)\n    return next.call(jQuery, redirect, settings)\n  }\n})(window.RAP, window.jQuery)\n"
  },
  {
    "path": "public/libs/mock.rap.js",
    "content": ";(function (RAP, Mock) {\n  if (!RAP) {\n    console.warn('请先引入 RAP 插件')\n    return\n  }\n  if (!Mock) {\n    console.warn('请先引入 Mock')\n    return\n  }\n\n  for (let repositoryId in RAP.interfaces) {\n    for (let itf of RAP.interfaces[repositoryId]) {\n      Mock.mock(itf.url, itf.method.toLowerCase(), (settings) => {\n        console.log(`Mock ${itf.method} ${itf.url} =>`, itf.response)\n        return Mock.mock(itf.response)\n      })\n    }\n  }\n})(window.RAP, window.Mock)\n"
  },
  {
    "path": "public/test/index.html",
    "content": "<ul>\n  <li><a href='./test.plugin.jquery.html'>jQuery</a></li>\n  <li><a href='./test.plugin.mock.html'>Mock</a></li>\n  <li><a href='./test.plugin.fetch.html'>Fetch</a></li>\n</ul>\n"
  },
  {
    "path": "public/test/test.plugin.fetch.html",
    "content": "<script src=\"//g.alicdn.com/thx/brix-deps/jquery@jquery/1.12.4/dist/jquery.js\"></script>\n<script src=\"//g.alicdn.com/thx/brix-deps/nuysoft@Mock/1.0.1-beta3/dist/mock.js\"></script>\n<div id=\"result\"></div>\n<script src=\"./test.request.js\"></script>\n<script>\n  const id = /id=(\\d+)/.exec(location.search) || ['', 1]\n  if (id) {\n    $.getScript(`/app/plugin/${id[1]}`).then(() => {\n      $.getScript(`/libs/fetch.rap.js`).then(() => {\n        doRequest(window.RAP, window.fetch)\n      })\n    })\n  }\n</script>\n"
  },
  {
    "path": "public/test/test.plugin.jquery.html",
    "content": "<script src=\"//g.alicdn.com/thx/brix-deps/jquery@jquery/1.12.4/dist/jquery.js\"></script>\n<script src=\"//g.alicdn.com/thx/brix-deps/nuysoft@Mock/1.0.1-beta3/dist/mock.js\"></script>\n<div id=\"result\"></div>\n<script src=\"./test.request.js\"></script>\n<script>\n  const id = /id=(\\d+)/.exec(location.search) || ['', 1]\n  if (id) {\n    $.getScript(`/app/plugin/${id[1]}`).then(() => {\n      $.getScript(`/libs/jquery.rap.js`).then(() => {\n        doRequest(window.RAP)\n      })\n    })\n  }\n</script>\n"
  },
  {
    "path": "public/test/test.plugin.mock.html",
    "content": "<script src=\"//g.alicdn.com/thx/brix-deps/jquery@jquery/1.12.4/dist/jquery.js\"></script>\n<script src=\"//g.alicdn.com/thx/brix-deps/nuysoft@Mock/1.0.1-beta3/dist/mock.js\"></script>\n<div id=\"result\"></div>\n<script src=\"./test.request.js\"></script>\n<script>\n  const id = /id=(\\d+)/.exec(location.search) || ['', 1]\n  if (id) {\n    $.getScript(`/app/plugin/${id[1]}`).then(() => {\n      $.getScript(`/libs/mock.rap.js`).then(() => {\n        doRequest(window.RAP)\n      })\n    })\n  }\n</script>\n\n"
  },
  {
    "path": "public/test/test.request.js",
    "content": "/* global $ */\nconst appendData = (repositoryId, itf, data) => {\n  $('#result').append(`<div>\n    <strong>#${repositoryId} #${itf.id} ${itf.name} ${itf.method} ${itf.url}</strong>\n    <pre>${JSON.stringify(data, null, 2)}</pre>\n  </div>`)\n}\nconst doRequest = (RAP, fetch) => { // eslint-disable-line no-unused-vars\n  for (let repositoryId in RAP.interfaces) {\n    RAP.interfaces[repositoryId].forEach(itf => {\n      if (fetch) {\n        fetch(itf.url, { method: itf.method })\n          .then(res => res.json())\n          .then(data => {\n            appendData(repositoryId, itf, data)\n          })\n        return\n      }\n      $.ajax({ url: itf.url, method: itf.method, dataType: 'json' })\n        .done(data => {\n          appendData(repositoryId, itf, data)\n        })\n    })\n  }\n}\n"
  },
  {
    "path": "release/v2.6_20180705.md",
    "content": "# V2.6 更新说明 2018-7-5\n\n* 数据库字段变更请执行 `datbase/v2.6***`\n* 因字段变更，需要清空redis缓存\n  1. 运行redis-cli\n  2. 执行 `flushall` 命令清空缓存"
  },
  {
    "path": "src/config/config.dev.ts",
    "content": "import { IConfigOptions } from '../types'\n\nconst config: IConfigOptions = {\n  version: 'v2.9.0',\n  serve: {\n    port: (process.env.SERVE_PORT && parseInt(process.env.SERVE_PORT)) || 8080,\n    path: '',\n  },\n  keys: [\"some secret hurr\"],\n  session: {\n    key: 'rap2:sess',\n  },\n  db: {\n    // dialect: 'mysql',\n    // host: process.env.MYSQL_URL || 'tddl.daily2.alibaba.net',\n    dialect: \"mysql\",\n    host: process.env.MYSQL_URL ?? \"localhost\",\n    port: (process.env.MYSQL_PORT && parseInt(process.env.MYSQL_PORT)) || 3306,\n    username: process.env.MYSQL_USERNAME ?? \"root\",\n    password: process.env.MYSQL_PASSWD ?? \"\",\n    database: process.env.MYSQL_SCHEMA ?? \"RAP2_DELOS_APP\",\n    pool: {\n      max: 10,\n      min: 0,\n      idle: 10000,\n    },\n    logging: false,\n    dialectOptions: {\n      connectTimeout: 20000\n    }\n  },\n  redis: {},\n  mail: {\n    host: process.env.MAIL_HOST ?? \"smtp.aliyun.com\",\n    port: process.env.MAIL_PORT ?? 465,\n    secure: process.env.MAIL_SECURE ?? true,\n    auth: {\n      user: process.env.MAIL_USER ?? \"rap2org@service.alibaba.com\",\n      pass: process.env.MAIL_PASS ?? \"\"\n    }\n  },\n  mailSender: process.env.MAIL_SENDER ?? 'rap2org@service.alibaba.com',\n}\n\nexport default config\n"
  },
  {
    "path": "src/config/config.local.ts",
    "content": "import { IConfigOptions } from \"../types\"\n\nlet config: IConfigOptions = {\n  version: '2.9.0',\n  serve: {\n    port: (process.env.SERVE_PORT && parseInt(process.env.SERVE_PORT)) || 8080,\n    path: '',\n  },\n  keys: [\"some secret hurr\"],\n  session: {\n    key: 'rap2:sess',\n  },\n  db: {\n    dialect: \"mysql\",\n    host: process.env.MYSQL_URL || \"localhost\",\n    port: (process.env.MYSQL_PORT && parseInt(process.env.MYSQL_PORT)) || 3306,\n    username: process.env.MYSQL_USERNAME || \"root\",\n    password: process.env.MYSQL_PASSWD || \"\",\n    database: process.env.MYSQL_SCHEMA || \"RAP2_DELOS_APP_LOCAL\",\n    pool: {\n      max: 5,\n      min: 0,\n      idle: 10000,\n    },\n    logging: false\n  },\n  redis: {},\n  mail: {\n    host: process.env.MAIL_HOST ?? \"smtp-mail.outlook.com\",\n    port: process.env.MAIL_PORT ?? 587,\n    secure: process.env.MAIL_SECURE ?? false,\n    auth: {\n      user: process.env.MAIL_USER ?? \"\",\n      pass: process.env.MAIL_PASS ?? \"\"\n    }\n  },\n  mailSender: process.env.MAIL_SENDER ?? \"\"\n}\n\nexport default config"
  },
  {
    "path": "src/config/config.prod.ts",
    "content": "import { IConfigOptions } from '../types'\n\n// 先从环境变量取配置\nlet config: IConfigOptions = {\n  version: '2.9.0',\n  serve: {\n    port: (process.env.SERVE_PORT && parseInt(process.env.SERVE_PORT)) || 8080,\n    path: '',\n  },\n  keys: [\"some secret hurr\"],\n  session: {\n    key: 'rap2:sess',\n  },\n  db: {\n    dialect: 'mysql',\n    host: process.env.MYSQL_URL || 'localhost',\n    port: (process.env.MYSQL_PORT && parseInt(process.env.MYSQL_PORT)) || 3306,\n    username: process.env.MYSQL_USERNAME || 'root',\n    password: process.env.MYSQL_PASSWD || '',\n    database: process.env.MYSQL_SCHEMA || 'rap',\n    pool: {\n      max: 80,\n      min: 0,\n      idle: 20000,\n      acquire: 20000,\n    },\n    logging: false,\n  },\n  redis: {\n    host: process.env.REDIS_URL || 'localhost',\n    port: (process.env.REDIS_PORT && parseInt(process.env.REDIS_PORT)) || 6379,\n    password: process.env.REDIS_PWD || undefined,\n  },\n  mail: {\n    host: process.env.MAIL_HOST ?? 'smtp.aliyun.com',\n    port: process.env.MAIL_PORT ?? 465,\n    secure: process.env.MAIL_SECURE ?? true,\n    auth: {\n      user: process.env.MAIL_USER ?? 'rap2org@service.alibaba.com',\n      pass: process.env.MAIL_PASS ?? '',\n    },\n  },\n  mailSender: process.env.MAIL_SENDER ?? \"rap2org@service.alibaba.com\"\n}\n\nexport default config\n"
  },
  {
    "path": "src/config/index.ts",
    "content": "import { IConfigOptions } from \"../types\"\n\n// local or development or production\nlet configObj: IConfigOptions =\n    (process.env.NODE_ENV === 'local' && require('./config.local')).default ||\n    (process.env.NODE_ENV === 'development' && require('./config.dev')).default ||\n    require('./config.prod').default\n\nexport default configObj"
  },
  {
    "path": "src/dispatch.ts",
    "content": "import * as cluster from 'cluster'\nimport * as path from 'path'\n\nlet now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '')\n\ncluster.setupMaster({\n  exec: path.join(__dirname, 'scripts/worker.js'),\n})\n\nif (cluster.isMaster) {\n  require('os').cpus().forEach(() => {\n    cluster.fork()\n  })\n  cluster.on('listening', (worker, address) => {\n    console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} is now connected to ${address.address}:${address.port}.`)\n  })\n  cluster.on('disconnect', (worker) => {\n    console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} has disconnected.`)\n  })\n  cluster.on('exit', (worker, code, signal) => {\n    console.error(`[${now()}] master#${process.pid} worker#${worker.process.pid} died (${signal || code}). restarting...`)\n    cluster.fork()\n  })\n}\n"
  },
  {
    "path": "src/helpers/dedent.ts",
    "content": "/**\n * copy from https://github.com/MartinKolarik/dedent-js/blob/master/src/index.ts\n * 解决多行字符串缩进时的空格问题\n */\n\nexport default function dedent(\n  templateStrings: TemplateStringsArray | string,\n  ...values: any[]\n) {\n  let matches = []\n  let strings =\n    typeof templateStrings === 'string'\n      ? [templateStrings]\n      : templateStrings.slice()\n\n  // 1. Remove trailing whitespace.\n  strings[strings.length - 1] = strings[strings.length - 1].replace(\n    /\\r?\\n([\\t ]*)$/,\n    ''\n  )\n\n  // 2. Find all line breaks to determine the highest common indentation level.\n  for (let i = 0; i < strings.length; i++) {\n    let match\n\n    if ((match = strings[i].match(/\\n[\\t ]+/g))) {\n      matches.push(...match)\n    }\n  }\n\n  // 3. Remove the common indentation from all strings.\n  if (matches.length) {\n    let size = Math.min(...matches.map(value => value.length - 1))\n    let pattern = new RegExp(`\\n[\\t ]{${size}}`, 'g')\n\n    for (let i = 0; i < strings.length; i++) {\n      strings[i] = strings[i].replace(pattern, '\\n')\n    }\n  }\n\n  // 4. Remove leading whitespace.\n  strings[0] = strings[0].replace(/^\\r?\\n/, '')\n\n  // 5. Perform interpolation.\n  let string = strings[0]\n\n  for (let i = 0; i < values.length; i++) {\n    string += values[i] + strings[i + 1]\n  }\n\n  return string\n}\n"
  },
  {
    "path": "src/helpers/pandoc.ts",
    "content": "import { spawnSync } from 'child_process'\n\nexport default function pandoc(from: string, to: string, ...args: string[]) {\n  const option = ['-f', from, '-t', to].concat(args)\n  return function converter(input: any) {\n    let pandoc\n    input = Buffer.from(input)\n    try {\n      pandoc = spawnSync('pandoc', option, { input, timeout: 20000 })\n    } catch (err) {\n      console.error(err)\n      console.error(pandoc.stderr)\n    }\n    if (pandoc.stderr && pandoc.stderr.length) {\n      console.error(pandoc.output[2])\n      throw new Error(pandoc.output[2].toString())\n    }\n    return pandoc.stdout\n  }\n}"
  },
  {
    "path": "src/models/bo/defaultVal.ts",
    "content": "import { Table, Column, Model, AutoIncrement, PrimaryKey, ForeignKey } from 'sequelize-typescript'\nimport { Repository } from '../../models'\n\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true, tableName: 'default_val' })\nexport default class DefaultVal extends Model<DefaultVal> {\n\n  @PrimaryKey\n  @AutoIncrement\n  @Column\n  id: number\n\n  @Column\n  name: string\n\n  @Column\n  rule: string\n\n  @Column\n  value: string\n\n  @ForeignKey(() => Repository)\n  @Column\n  repositoryId: number\n\n}\n\n"
  },
  {
    "path": "src/models/bo/historyLog.ts",
    "content": "import { Table, Column, Model, AutoIncrement, PrimaryKey, DataType, AllowNull, ForeignKey, BelongsTo } from 'sequelize-typescript'\nimport { ENTITY_TYPE } from '../../routes/utils/const'\nimport { User } from '../../models'\n\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true, tableName: 'history_log' })\nexport default class HistoryLog extends Model<HistoryLog> {\n\n  @AllowNull(false)\n  @PrimaryKey\n  @AutoIncrement\n  @Column\n  id: number\n\n  /**\n   * ENTITY_TYPE.INTERFACE: 接口级日志\n   * ENTITY_TYPE.REPOSITORY: 接口被删除、模块被移除等超出接口级的日志\n   */\n  @AllowNull(false)\n  @Column({ type: DataType.INTEGER }) // for extension, type to INT, code as enum\n  entityType: ENTITY_TYPE\n\n  @AllowNull(false)\n  @Column\n  entityId: number\n\n  /**\n   * 文本日志，支持Markdown，可能存在不同版本，以MarkDown输出即可\n   */\n  @AllowNull(false)\n  @Column({ type: DataType.TEXT })\n  changeLog: string\n\n\n  /**\n   * 可空，当发现变更较大（删除、修改字段较多、或整个接口的删除时），记录对应实体的Model JSON用于恢复。\n   */\n  @Column({ type: DataType.TEXT })\n  relatedJSONData: string\n\n  @AllowNull(false)\n  @ForeignKey(() => User)\n  @Column\n  userId: number\n\n  @BelongsTo(() => User, 'userId')\n  user: User\n\n  jsonDataIsNull?: boolean\n\n}\n\nexport const LOG_SEPERATOR = '.|.'\nexport const LOG_SUB_SEPERATOR = '@|@'\n\n"
  },
  {
    "path": "src/models/bo/interface.ts",
    "content": "import { Table, Column, Model, HasMany, AutoIncrement, PrimaryKey, AllowNull, DataType, Default, BelongsTo, ForeignKey, BeforeBulkDestroy, BeforeBulkCreate, BeforeBulkUpdate, BeforeCreate, BeforeUpdate, BeforeDestroy } from 'sequelize-typescript'\nimport { User, Module, Repository, Property } from '../'\nimport RedisService, { CACHE_KEY } from '../../service/redis'\nimport * as Sequelize from 'sequelize'\nimport { BODY_OPTION } from '../../routes/utils/const'\n\nconst Op = Sequelize.Op\n\nenum methods { GET = 'GET', POST = 'POST', PUT = 'PUT', DELETE = 'DELETE' }\n\nexport enum MoveOp {\n  MOVE = 1,\n  COPY = 2\n}\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Interface extends Model<Interface> {\n\n  /** hooks */\n  @BeforeCreate\n  @BeforeUpdate\n  @BeforeDestroy\n  static async deleteCache(instance: Interface) {\n    await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, instance.repositoryId)\n  }\n\n  @BeforeBulkCreate\n  @BeforeBulkUpdate\n  @BeforeBulkDestroy\n  static async bulkDeleteCache(options: any) {\n    let id: number = options && options.attributes && options.attributes.id\n    if (!id) {\n      id = options.where && +options.where.id\n    }\n    if (options.where && options.where[Op.and]) {\n      const arr = options.where[Op.and]\n      if (arr && arr[1] && arr[1].id) {\n        id = arr[1].id\n      }\n    }\n    if ((id as any) instanceof Array) {\n      id = (id as any)[0]\n    }\n    if (id) {\n      const itf = await Interface.findByPk(id)\n      await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, itf.repositoryId)\n    }\n  }\n\n  public static METHODS = methods\n\n  public request?: object\n  public response?: object\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  name: string\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  url: string\n\n  @AllowNull(false)\n  @Column({ comment: 'API method' })\n  method: string\n\n  @Column({ type: DataType.STRING(255) })\n  bodyOption?: BODY_OPTION\n\n  @Column(DataType.TEXT)\n  description: string\n\n  @AllowNull(false)\n  @Default(1)\n  @Column(DataType.BIGINT())\n  priority: number\n\n  @Default(200)\n  @Column\n  status: number\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @ForeignKey(() => User)\n  @Column\n  lockerId: number\n\n  @ForeignKey(() => Module)\n  @Column\n  moduleId: number\n\n  @ForeignKey(() => Repository)\n  @Column\n  repositoryId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => User, 'lockerId')\n  locker: User\n\n  @BelongsTo(() => Module, 'moduleId')\n  module: Module\n\n  @BelongsTo(() => Repository, 'repositoryId')\n  repository: Repository\n\n  @HasMany(() => Property, 'interfaceId')\n  properties: Property[]\n\n}\n\n"
  },
  {
    "path": "src/models/bo/logger.ts",
    "content": "import {  Table, Column, Model,  AutoIncrement, PrimaryKey, AllowNull, DataType, BelongsTo, ForeignKey } from 'sequelize-typescript'\nimport { User, Repository, Organization, Module, Interface } from '../'\n\nenum types {\n  CREATE = 'create', UPDATE = 'update', DELETE = 'delete',\n  LOCK = 'lock', UNLOCK = 'unlock', JOIN = 'join', EXIT = 'exit',\n}\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Logger extends Model<Logger> {\n  public static TYPES = types\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column({\n    type: DataType.ENUM(types.CREATE, types.UPDATE, types.DELETE, types.LOCK, types.UNLOCK, types.JOIN, types.EXIT),\n    comment: 'operation type',\n  })\n  type: string\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @AllowNull(false)\n  @ForeignKey(() => User)\n  @Column\n  userId: number\n\n  @ForeignKey(() => Organization)\n  @Column\n  organizationId: number\n\n  @ForeignKey(() => Repository)\n  @Column\n  repositoryId: number\n\n  @ForeignKey(() => Module)\n  @Column\n  moduleId: number\n\n  @ForeignKey(() => Interface)\n  @Column\n  interfaceId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => User, 'userId')\n  user: User\n\n  @BelongsTo(() => Repository, 'repositoryId')\n  repository: Repository\n\n  @BelongsTo(() => Organization, 'organizationId')\n  organization: Organization\n\n  @BelongsTo(() => Module, 'moduleId')\n  module: Module\n\n  @BelongsTo(() => Interface, 'interfaceId')\n  interface: Interface\n\n}"
  },
  {
    "path": "src/models/bo/module.ts",
    "content": "import { Table, Column, Model, HasMany, AutoIncrement, PrimaryKey, AllowNull, DataType, Default, BelongsTo, ForeignKey, BeforeCreate, BeforeUpdate, BeforeDestroy, BeforeBulkCreate, BeforeBulkDestroy, BeforeBulkUpdate } from 'sequelize-typescript'\nimport { User, Repository, Interface } from '../'\nimport RedisService, { CACHE_KEY } from '../../service/redis'\nimport * as Sequelize from 'sequelize'\n\nconst Op = Sequelize.Op\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Module extends Model<Module> {\n/** hooks */\n  @BeforeCreate\n  @BeforeUpdate\n  @BeforeDestroy\n  static async deleteCache(instance: Module) {\n     await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, instance.repositoryId)\n  }\n\n  @BeforeBulkCreate\n  @BeforeBulkUpdate\n  @BeforeBulkDestroy\n  static async bulkDeleteCache(options: any) {\n    let id: number = options && options.attributes && options.attributes.id\n    if (!id) {\n      id = options.where && +options.where.id\n    }\n    if (options.where && options.where[Op.and]) {\n      const arr = options.where[Op.and]\n      if (arr && arr[1] && arr[1].id) {\n        id = arr[1].id\n      }\n    }\n    if ((id as any) instanceof Array) {\n      id = (id as any)[0]\n    }\n    if (id) {\n      const mod = await Module.findByPk(id)\n      await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, mod.repositoryId)\n    }\n  }\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  name: string\n\n\n  @AllowNull(false)\n  @Column(DataType.TEXT)\n  description: string\n\n  @AllowNull(false)\n  @Default(1)\n  @Column(DataType.BIGINT())\n  priority: number\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @ForeignKey(() => Repository)\n  @Column\n  repositoryId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => Repository, 'repositoryId')\n  repository: Repository\n\n  @HasMany(() => Interface, 'moduleId')\n  interfaces: Interface[]\n}"
  },
  {
    "path": "src/models/bo/notification.ts",
    "content": "import { Table, Column, Model, AutoIncrement, PrimaryKey, AllowNull, DataType, Default } from 'sequelize-typescript'\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Notification extends Model<Notification> {\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @Column({ comment: 'sender' })\n  fromId: number\n\n  @AllowNull(false)\n  @Column({ comment: 'receiver' })\n  toId: number\n\n  @AllowNull(false)\n  @Column({ comment: 'msg type' })\n  type: string\n\n  @Column(DataType.STRING(128))\n  param1: string\n\n  @Column(DataType.STRING(128))\n  param2: string\n\n  @Column(DataType.STRING(128))\n  param3: string\n\n  @AllowNull(false)\n  @Default(false)\n  @Column\n  readed: boolean\n}"
  },
  {
    "path": "src/models/bo/organization.ts",
    "content": "import { Table, Column, Model, HasMany, AutoIncrement, PrimaryKey, AllowNull, DataType, Default, BelongsTo, BelongsToMany, ForeignKey, AfterCreate } from 'sequelize-typescript'\nimport { User, Repository, OrganizationsMembers, Logger } from '../'\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Organization extends Model<Organization> {\n\n\n  @AfterCreate\n  static async createLog(instance: Organization) {\n    await Logger.create({\n      userId: instance.creatorId,\n      type: 'create',\n      organizationId: instance.id\n    })\n  }\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  name: string\n\n  @Column(DataType.TEXT)\n  description: string\n\n  @Column(DataType.STRING(256))\n  logo: string\n\n  @AllowNull(false)\n  @Default(true)\n  @Column({ comment: 'true:public, false:private' })\n  visibility: boolean\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @ForeignKey(() => User)\n  @Column\n  ownerId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => User, 'ownerId')\n  owner: User\n\n  @BelongsToMany(() => User, () => OrganizationsMembers)\n  members: User[]\n\n  @HasMany(() => OrganizationsMembers)\n  organizationMembersList: OrganizationsMembers[]\n\n  @HasMany(() => Repository, 'organizationId')\n  repositories: Repository[]\n}\n"
  },
  {
    "path": "src/models/bo/organizationsMembers.ts",
    "content": "import { Table, Column, Model, ForeignKey, PrimaryKey } from 'sequelize-typescript'\nimport { User,  Organization } from '../'\n\n@Table({ freezeTableName: true, timestamps: true, tableName: 'organizations_members' })\nexport default class OrganizationsMembers extends Model<OrganizationsMembers> {\n    @ForeignKey(() => User)\n    @PrimaryKey\n    @Column\n    userId: number\n\n    @ForeignKey(() => Organization)\n    @PrimaryKey\n    @Column\n    organizationId: number\n}"
  },
  {
    "path": "src/models/bo/property.ts",
    "content": "import { Table, Column, Model, AutoIncrement, PrimaryKey, AllowNull, DataType, Default, BelongsTo, ForeignKey } from 'sequelize-typescript'\nimport { User, Interface, Module, Repository } from '../'\n\nexport enum SCOPES { REQUEST = 'request', RESPONSE = 'response', SCRIPT = 'script' }\nexport enum TYPES { STRING = 'String', NUMBER = 'Number', BOOLEAN = 'Boolean', OBJECT = 'Object', ARRAY = 'Array', FUNCTION = 'Function', REGEXP = 'RegExp', Null = 'Null' }\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Property extends Model<Property> {\n  public static TYPES = TYPES\n  public static SCOPES = SCOPES\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  static attributes: any\n\n\n  @AllowNull(false)\n  @Default(SCOPES.RESPONSE)\n  @Column({\n    type: DataType.ENUM(SCOPES.REQUEST, SCOPES.RESPONSE),\n    comment: 'property owner',\n  })\n  scope: string\n\n  @AllowNull(false)\n  @Column({\n    type: DataType.ENUM(TYPES.STRING, TYPES.NUMBER, TYPES.BOOLEAN, TYPES.OBJECT, TYPES.ARRAY, TYPES.FUNCTION, TYPES.REGEXP, TYPES.Null),\n    comment: 'property type',\n  })\n  /** Data Type */\n  type: string\n\n  @AllowNull(false)\n  @Default(2)\n  @Column({ type: DataType.INTEGER }) // for better extension\n  /** request params type (position) */\n  pos: POS_TYPE\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  name: string\n\n  @Column({ type: DataType.STRING(128), comment: 'property generation rules' })\n  rule: string\n\n  @Column({ type: DataType.TEXT, comment: 'value of this property' })\n  value: string\n\n  @Column(DataType.TEXT)\n  description: string\n\n  @AllowNull(false)\n  @Default(-1)\n  @Column({ comment: 'parent property ID' })\n  parentId: number\n\n  @AllowNull(false)\n  @Default(1)\n  @Column(DataType.BIGINT())\n  priority: number\n\n  @ForeignKey(() => Interface)\n  @Column\n  interfaceId: number\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @ForeignKey(() => Module)\n  @Column\n  moduleId: number\n\n  @ForeignKey(() => Repository)\n  @Column\n  repositoryId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => Interface, 'interfaceId')\n  interface: Interface\n\n  @BelongsTo(() => Module, 'moduleId')\n  module: Module\n\n  @BelongsTo(() => Repository, 'repositoryId')\n  repository: Repository\n\n  @Column\n  /** 是否为必填选项 */\n  required: boolean\n\n}\n\n\n/**\n * 参数类型\n */\nexport enum POS_TYPE {\n  QUERY = 2,\n  HEADER = 1,\n  BODY = 3,\n  PRE_REQUEST_SCRIPT = 4,\n  TEST = 5,\n}"
  },
  {
    "path": "src/models/bo/repositoriesCollaborators.ts",
    "content": "import { Table, Column, Model, ForeignKey, PrimaryKey } from 'sequelize-typescript'\nimport { Repository } from '../'\n\n@Table({ freezeTableName: true, timestamps: true, tableName: 'repositories_collaborators' })\nexport default class RepositoriesCollaborators extends Model<RepositoriesCollaborators> {\n    @ForeignKey(() => Repository)\n    @PrimaryKey\n    @Column\n    repositoryId: number\n\n    @ForeignKey(() => Repository)\n    @PrimaryKey\n    @Column\n    collaboratorId: number\n}"
  },
  {
    "path": "src/models/bo/repositoriesMembers.ts",
    "content": "import { Table, Column, Model, ForeignKey, PrimaryKey } from 'sequelize-typescript'\nimport { Repository, User } from '../'\n\n@Table({ freezeTableName: true, timestamps: false, tableName: 'repositories_members' })\nexport default class RepositoriesMembers extends Model<RepositoriesMembers> {\n  @ForeignKey(() => User)\n  @PrimaryKey\n  @Column\n  userId: number\n\n  @ForeignKey(() => Repository)\n  @PrimaryKey\n  @Column\n  repositoryId: number\n}"
  },
  {
    "path": "src/models/bo/repository.ts",
    "content": "import { Table, Column, Model, HasMany, AutoIncrement, PrimaryKey, AllowNull, DataType, Default, BelongsTo, BelongsToMany, ForeignKey, BeforeUpdate, BeforeCreate, BeforeDestroy, BeforeBulkCreate, BeforeBulkUpdate, BeforeBulkDestroy } from 'sequelize-typescript'\nimport { User, Organization, Module, Interface, RepositoriesCollaborators } from '../'\nimport RedisService, { CACHE_KEY } from '../../service/redis'\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class Repository extends Model<Repository> {\n\n  /** hooks */\n  @BeforeCreate\n  @BeforeUpdate\n  @BeforeDestroy\n  static async cleanCache(instance: Repository) {\n    await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, instance.id)\n  }\n\n  @BeforeBulkCreate\n  @BeforeBulkUpdate\n  @BeforeBulkDestroy\n  static async bulkDeleteCache(options: any) {\n    const id = options && options.attributes && options.attributes.id\n    if (id) {\n     await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, id)\n    }\n  }\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column(DataType.STRING(256))\n  name: string\n\n  @Column(DataType.TEXT)\n  description: string\n\n  @Column(DataType.STRING(256))\n  logo: string\n\n  @Column(DataType.STRING(32))\n  token: string\n\n  @AllowNull(false)\n  @Default(true)\n  @Column({ comment: 'true:public, false:private' })\n  visibility: boolean\n\n  @ForeignKey(() => User)\n  @Column\n  ownerId: number\n\n  @ForeignKey(() => Organization)\n  @Column\n  organizationId: number\n\n  @ForeignKey(() => User)\n  @Column\n  creatorId: number\n\n  @ForeignKey(() => User)\n  @Column\n  lockerId: number\n\n  @BelongsTo(() => User, 'creatorId')\n  creator: User\n\n  @BelongsTo(() => User, 'ownerId')\n  owner: User\n\n  @BelongsTo(() => Organization, 'organizationId')\n  organization: Organization\n\n  @BelongsTo(() => User, 'lockerId')\n  locker: User\n\n  @BelongsToMany(() => User, 'repositories_members', 'repositoryId', 'userId')\n  members: User[]\n\n  @HasMany(() => Module, 'repositoryId')\n  modules: Module[]\n\n  @HasMany(() => Module, 'repositoryId')\n  interfaces: Interface[]\n\n  @BelongsToMany(() => Repository, () => RepositoriesCollaborators, 'repositoryId', 'collaboratorId')\n  collaborators: Repository[]\n\n  @BelongsToMany(() => Repository, () => RepositoriesCollaborators, 'collaboratorId')\n  repositories: Repository[]\n\n}"
  },
  {
    "path": "src/models/bo/user.ts",
    "content": "import { Table, Column, Model, HasMany, AutoIncrement, PrimaryKey, AllowNull, DataType, Unique, BelongsToMany } from 'sequelize-typescript'\nimport { Organization, Repository, OrganizationsMembers, RepositoriesMembers } from '../'\n\n@Table({ paranoid: true, freezeTableName: false, timestamps: true })\nexport default class User extends Model<User> {\n\n  @AutoIncrement\n  @PrimaryKey\n  @Column\n  id: number\n\n  @AllowNull(false)\n  @Column(DataType.STRING(32))\n  fullname: string\n\n  @Column(DataType.STRING(32))\n  password: string\n\n  @AllowNull(false)\n  @Unique\n  @Column(DataType.STRING(128))\n  email: string\n\n  @HasMany(() => Organization, 'ownerId')\n  ownedOrganizations: Organization[]\n\n  @BelongsToMany(() => Organization, () => OrganizationsMembers)\n  joinedOrganizations: Organization[]\n\n  @HasMany(() => Repository, 'ownerId')\n  ownedRepositories: Repository[]\n\n  @BelongsToMany(() => Repository, () => RepositoriesMembers)\n  joinedRepositories: Repository[]\n\n}"
  },
  {
    "path": "src/models/helper.ts",
    "content": "export let Helper: any =  {\n  include: [],\n  exclude: {\n    generalities: ['createdAt', 'updatedAt', 'deletedAt', 'reserve'],\n  },\n}\n"
  },
  {
    "path": "src/models/index.ts",
    "content": "export { default as QueryInclude } from './util/queryInclude'\nexport { default as Interface } from './bo/interface'\nexport { default as Logger } from './bo/logger'\nexport { default as Module } from './bo/module'\nexport { default as Notification } from './bo/notification'\nexport { default as Organization } from './bo/organization'\nexport { default as Property } from './bo/property'\nexport { default as Repository } from './bo/repository'\nexport { default as User } from './bo/user'\nexport { default as OrganizationsMembers } from './bo/organizationsMembers'\nexport { default as RepositoriesCollaborators } from './bo/repositoriesCollaborators'\nexport { default as RepositoriesMembers } from './bo/repositoriesMembers'\nexport { default as DefaultVal } from './bo/defaultVal'\nexport { default as HistoryLog } from './bo/historyLog'\n"
  },
  {
    "path": "src/models/sequelize.ts",
    "content": "import { Sequelize } from 'sequelize-typescript'\nimport config from '../config'\nimport MigrateService from '../service/migrate'\n\nconst chalk = require('chalk')\nconst now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '')\nconst logging = process.env.NODE_ENV === 'development'\n  ? (sql: string) => {\n    sql = sql.replace('Executing (default): ', '')\n    console.log(`${chalk.bold('SQL')} ${now()} ${chalk.gray(sql)}`)\n  }\n  : console.log\n\nconst sequelize = new Sequelize({\n  database: config.db.database,\n  dialect: config.db.dialect,\n  username: config.db.username,\n  password: config.db.password,\n  host: config.db.host,\n  port: config.db.port,\n  pool: config.db.pool,\n  logging: config.db.logging ? logging : false,\n  dialectOptions: config.db.dialectOptions,\n})\n\nsequelize.addModels([__dirname + '/bo'])\nsequelize.authenticate()\n  .then((/* err */) => {\n    console.log('----------------------------------------')\n    console.log('DATABASE √')\n    console.log('    HOST     %s', config.db.host)\n    console.log('    PORT     %s', config.db.port)\n    console.log('    DATABASE %s', config.db.database)\n    console.log('----------------------------------------')\n    MigrateService.checkAndFix()\n  })\n  // sequelize.sync()\n  .catch(err => {\n    console.log('Unable to connect to the database:', err)\n  })\n\nexport default sequelize\n"
  },
  {
    "path": "src/models/util/helper.ts",
    "content": "declare interface IHelper {\n  include: string[],\n  exclude: {\n    generalities: string[],\n  }\n}\nexport let Helper: IHelper =  {\n  include: [],\n  exclude: {\n    generalities: ['createdAt', 'updatedAt', 'deletedAt', 'reserve'],\n  },\n}\n"
  },
  {
    "path": "src/models/util/queryInclude.ts",
    "content": "// TODO 2.2 如何缓存重复查询？https://github.com/rfink/sequelize-redis-cache\nimport { Helper } from './helper'\nimport User from '../bo/user'\nimport Repository from '../bo/repository'\nimport Module from '../bo/module'\nimport Organization from '../bo/organization'\nimport Interface from '../bo/interface'\nimport Property from '../bo/property'\nimport { IncludeOptions } from 'sequelize'\n\ndeclare interface IQueryInclude {\n  [key: string]: IncludeOptions\n}\n\nconst QueryInclude: IQueryInclude = {\n  User: {\n    model: User,\n    as: 'user',\n    attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n    required: true,\n  },\n  UserForSearch: {\n    model: User,\n    as: 'user',\n    attributes: { include: ['id', 'fullname'] },\n    required: true,\n  },\n  Creator: {\n    model: User,\n    as: 'creator',\n    attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n    required: true,\n  },\n  Owner: {\n    model: User,\n    as: 'owner',\n    attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n    required: true,\n  },\n  Locker: {\n    model: User,\n    as: 'locker',\n    attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n    required: false,\n  },\n  Members: {\n    model: User,\n    as: 'members',\n    attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n    through: { attributes: [] },\n    required: false,\n  },\n  Repository: {\n    model: Repository,\n    as: 'repository',\n    attributes: { exclude: [] },\n    paranoid: false,\n    required: false,\n  },\n  Organization: {\n    model: Organization,\n    as: 'organization',\n    attributes: { exclude: [] },\n    paranoid: false,\n    required: false,\n  },\n  Module: {\n    model: Module,\n    as: 'module',\n    attributes: { exclude: [] },\n    paranoid: false,\n    required: false,\n  },\n  Interface: {\n    model: Interface,\n    as: 'interface',\n    attributes: { exclude: [] },\n    paranoid: false,\n    required: false,\n  },\n  Collaborators: {\n    model: Repository,\n    as: 'collaborators',\n    attributes: { exclude: [] },\n    through: { attributes: [] },\n    required: false,\n  },\n  RepositoryHierarchy: {\n    model: Module,\n    as: 'modules',\n    attributes: { exclude: [] },\n    required: false,\n    include: [\n      {\n        model: Interface,\n        as: 'interfaces',\n        attributes: { exclude: [] },\n        required: false,\n        include: [\n          {\n            model: User,\n            as: 'locker',\n            attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n            required: false,\n          },\n          {\n            model: Property,\n            as: 'properties',\n            attributes: { exclude: [] },\n            required: false,\n          },\n        ],\n      },\n    ],\n  },\n  RepositoryHierarchyExcludeProperty: {\n    model: Module,\n    as: 'modules',\n    attributes: { exclude: [] },\n    required: false,\n    include: [\n      {\n        model: Interface,\n        as: 'interfaces',\n        attributes: { exclude: [] },\n        required: false,\n        include: [\n          {\n            model: User,\n            as: 'locker',\n            attributes: { exclude: ['password', ...Helper.exclude.generalities] },\n            required: false,\n          },\n        ],\n      },\n    ],\n  },\n  Properties: {\n    model: Property,\n    as: 'properties',\n    attributes: { exclude: [] },\n    required: false,\n  },\n}\n\nexport default QueryInclude"
  },
  {
    "path": "src/routes/account.ts",
    "content": "import * as svgCaptcha from 'svg-captcha'\nimport { User, Notification, Logger, Organization, Repository } from '../models'\nimport router from './router'\nimport { Model } from 'sequelize-typescript'\nimport Pagination from './utils/pagination'\nimport { QueryInclude } from '../models'\nimport { Op } from 'sequelize'\nimport MailService from '../service/mail'\nimport * as md5 from 'md5'\nimport { isLoggedIn } from './base'\nimport { AccessUtils } from './utils/access'\nimport { COMMON_ERROR_RES } from './utils/const'\nimport * as moment from 'moment'\nimport RedisService, { CACHE_KEY, DEFAULT_CACHE_VAL } from '../service/redis'\n\n\n\n\nrouter.get('/app/get', async (ctx, next) => {\n  let data: any = {}\n  let query = ctx.query\n  let hooks: any = {\n    user: User,\n  }\n  for (let name in hooks) {\n    if (!query[name]) continue\n    data[name] = await hooks[name].findByPk(query[name], {\n      attributes: { exclude: [] }\n    })\n  }\n  ctx.body = {\n    data: Object.assign({}, ctx.body && ctx.body.data, data),\n  }\n\n  return next()\n})\n\nrouter.get('/account/count', async (ctx) => {\n  ctx.body = {\n    data: await User.count(),\n  }\n})\n\nrouter.get('/account/list', isLoggedIn, async (ctx) => {\n  // if (!AccessUtils.isAdmin(ctx.session.id)) {\n  //   ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n  //   return\n  // }\n  let where = {}\n  let { name } = ctx.query\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { fullname: { [Op.like]: `%${name}%` } },\n        { email: name },\n      ],\n    })\n  }\n  let options = { where }\n  let total = await User.count(options)\n  let limit = Math.min(+ctx.query.limit ?? 10, 100)\n  let pagination = new Pagination(total, ctx.query.cursor || 1, limit)\n  ctx.body = {\n    data: await User.findAll({\n      ...options, ...{\n        attributes: ['id', 'fullname', 'email'],\n        offset: pagination.start,\n        limit: pagination.limit,\n        order: [['id', 'DESC']],\n      }\n    }),\n    pagination: pagination\n  }\n})\n\nrouter.get('/account/info', async (ctx) => {\n  ctx.body = {\n    data: ctx.session.id ? await User.findByPk(ctx.session.id, {\n      attributes: QueryInclude.User.attributes\n    }) : undefined\n  }\n})\n\nrouter.post('/account/login', async (ctx) => {\n  let { email, password, captcha } = ctx.request.body\n  let result, errMsg\n  if (process.env.TEST_MODE !== 'true' &&\n    (!captcha || !ctx.session.captcha || captcha.trim().toLowerCase() !== ctx.session.captcha.toLowerCase())) {\n    errMsg = '错误的验证码'\n  } else {\n    result = await User.findOne({\n      attributes: QueryInclude.User.attributes,\n      where: { email, password: md5(md5(password)) },\n    })\n    if (result) {\n      ctx.session.id = result.id\n      ctx.session.fullname = result.fullname\n      ctx.session.email = result.email\n      let app: any = ctx.app\n      app.counter.users[result.fullname] = true\n    } else {\n      errMsg = '账号或密码错误'\n    }\n  }\n  ctx.body = {\n    data: result ? result : { errMsg },\n  }\n})\n\nrouter.get('/captcha_data', ctx => {\n  ctx.body = {\n    data: JSON.stringify(ctx.session)\n  }\n})\n\nrouter.get('/account/logout', async (ctx) => {\n  let app: any = ctx.app\n  delete app.counter.users[ctx.session.email]\n  let id = ctx.session.id\n  Object.assign(ctx.session, { id: undefined, fullname: undefined, email: undefined })\n  ctx.body = {\n    data: await { id },\n  }\n})\n\nrouter.post('/account/register', async (ctx) => {\n  let { fullname, email, password } = ctx.request.body\n  let exists = await User.findAll({\n    where: { email },\n  })\n  if (exists && exists.length) {\n    ctx.body = {\n      data: {\n        isOk: false,\n        errMsg: '该邮件已被注册，请更换再试。',\n      },\n    }\n    return\n  }\n\n  // login automatically after register\n  let result = await User.create({ fullname, email, password: md5(md5(password)) })\n\n  if (result) {\n    ctx.session.id = result.id\n    ctx.session.fullname = result.fullname\n    ctx.session.email = result.email\n    let app: any = ctx.app\n    app.counter.users[result.fullname] = true\n  }\n\n  ctx.body = {\n    data: {\n      id: result.id,\n      fullname: result.fullname,\n      email: result.email,\n    },\n  }\n})\n\nrouter.post('/account/update', async (ctx) => {\n  const { password } = ctx.request.body\n  let errMsg = ''\n  let isOk = false\n\n  if (!ctx.session || !ctx.session.id) {\n    errMsg = '登陆超时'\n  } else if (password.length < 6) {\n    errMsg = '密码长度过短'\n  } else {\n    const user = await User.findByPk(ctx.session.id)\n    user.password = md5(md5(password))\n    await user.save()\n    isOk = true\n  }\n  ctx.body = {\n    data: {\n      isOk,\n      errMsg\n    }\n  }\n})\n\nrouter.get('/account/remove', isLoggedIn, async (ctx) => {\n  if (!AccessUtils.isAdmin(ctx.session.id)) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  if (process.env.TEST_MODE === 'true') {\n    ctx.body = {\n      data: await User.destroy({\n        where: { id: ctx.query.id },\n      }),\n    }\n  } else {\n    ctx.body = {\n      data: {\n        isOk: false,\n        errMsg: 'access forbidden',\n      },\n    }\n  }\n})\n\n// TODO 2.3 账户设置\nrouter.get('/account/setting', async (ctx) => {\n  ctx.body = {\n    data: {},\n  }\n})\n\nrouter.post('/account/setting', async (ctx) => {\n  ctx.body = {\n    data: {},\n  }\n})\n\nrouter.post('/account/fetchUserSettings', isLoggedIn, async (ctx) => {\n  const keys: CACHE_KEY[] = ctx.request.body.keys\n  if (!keys || !keys.length) {\n    ctx.body = {\n      isOk: false,\n      errMsg: 'error'\n    }\n    return\n  }\n\n  const data: { [key: string]: string } = {}\n\n  for (const key of keys) {\n    data[key] = await RedisService.getCache(key, ctx.session.id) || DEFAULT_CACHE_VAL[key]\n  }\n\n  ctx.body = {\n    isOk: true,\n    data,\n  }\n})\n\nrouter.post('/account/updateUserSetting/:key', isLoggedIn, async (ctx) => {\n  const key: CACHE_KEY = ctx.params.key as CACHE_KEY\n  const value: string = ctx.request.body.value\n  await RedisService.setCache(key, value, ctx.session.id, 10 * 365 * 24 * 60 * 60)\n  ctx.body = {\n    isOk: true,\n  }\n})\n\n// TODO 2.3 账户通知\nlet NOTIFICATION_EXCLUDE_ATTRIBUTES: any = []\nrouter.get('/account/notification/list', async (ctx) => {\n  let total = await Notification.count()\n  let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 10)\n  ctx.body = {\n    data: await Notification.findAll({\n      attributes: { exclude: NOTIFICATION_EXCLUDE_ATTRIBUTES },\n      offset: pagination.start,\n      limit: pagination.limit,\n      order: [\n        ['id', 'DESC'],\n      ],\n    }),\n    pagination: pagination,\n  }\n})\n\nrouter.get('/account/notification/unreaded', async (ctx) => {\n  ctx.body = {\n    data: [],\n  }\n})\n\nrouter.post('/account/notification/unreaded', async (ctx) => {\n  ctx.body = {\n    data: 0,\n  }\n})\n\nrouter.post('/account/notification/read', async (ctx) => {\n  ctx.body = {\n    data: 0,\n  }\n})\n\n// TODO 2.3 账户日志\nrouter.get('/account/logger', async (ctx) => {\n  if (!ctx.session.id) {\n    ctx.body = {\n      data: {\n        isOk: false,\n        errMsg: 'not login'\n      }\n    }\n    return\n  }\n  let auth = await User.findByPk(ctx.session.id)\n  let repositories: Model<Repository>[] = [...(<Model<Repository>[]>await auth.$get('ownedRepositories')), ...(<Model<Repository>[]>await auth.$get('joinedRepositories'))]\n  let organizations: Model<Organization>[] = [...(<Model<Organization>[]>await auth.$get('ownedOrganizations')), ...(<Model<Organization>[]>await auth.$get('joinedOrganizations'))]\n\n  let where: any = {\n    [Op.or]: [\n      { userId: ctx.session.id },\n      { repositoryId: repositories.map(item => item.id) },\n      { organizationId: organizations.map(item => item.id) },\n    ],\n  }\n  let total = await Logger.count({ where })\n  let pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100)\n  let logs = await Logger.findAll({\n    where,\n    attributes: {},\n    include: [\n      Object.assign({}, QueryInclude.Creator, { required: false }),\n      QueryInclude.User,\n      QueryInclude.Organization,\n      QueryInclude.Repository,\n      QueryInclude.Module,\n      QueryInclude.Interface,\n    ],\n    offset: pagination.start,\n    limit: pagination.limit,\n    order: [\n      ['id', 'DESC'],\n    ],\n    paranoid: false,\n  } as any)\n\n  ctx.body = {\n    data: logs,\n    pagination,\n  }\n})\n\nrouter.get('/captcha', async (ctx) => {\n  const captcha = svgCaptcha.create()\n  ctx.session.captcha = captcha.text\n  ctx.set('Content-Type', 'image/svg+xml')\n  ctx.body = captcha.data\n})\n\nrouter.get('/worker', async (ctx) => {\n  ctx.body = process.env.NODE_APP_INSTANCE || 'NOT FOUND'\n})\n\nrouter.post('/account/reset', async (ctx) => {\n  const email = ctx.request.body.email\n  const password = ctx.request.body.password\n  if (password && ctx.session.resetCode && password === ctx.session.resetCode + '') {\n    const newPassword = String(Math.floor(Math.random() * 99999999))\n    const user = await User.findOne({ where: { email } })\n    if (!user) {\n      ctx.body = {\n        data: {\n          isOk: false,\n          errMsg: '您的邮箱没被注册过。',\n        }\n      }\n      return\n    }\n    user.password = md5(md5(newPassword))\n    await user.save()\n    ctx.body = {\n      data: {\n        isOk: true,\n        data: newPassword,\n      }\n    }\n  } else {\n    const resetCode = ctx.session.resetCode = Math.floor(Math.random() * 999999)\n    MailService.send(email, 'RAP重置账户验证码', `您的验证码为：${resetCode}`)\n    ctx.body = {\n      data: {\n        isOk: true,\n      }\n    }\n  }\n})\n\nrouter.post('/account/findpwd', async (ctx) => {\n  let { email, captcha } = ctx.request.body\n  let user, errMsg\n  if (process.env.TEST_MODE !== 'true' &&\n    (!captcha || !ctx.session.captcha || captcha.trim().toLowerCase() !== ctx.session.captcha.toLowerCase())) {\n    errMsg = '错误的验证码'\n  } else {\n    user = await User.findOne({\n      attributes: QueryInclude.User.attributes,\n      where: { email },\n    })\n    if (user) {\n      // 截取ID最后两位*日期字符串 作为返回链接的过期校验\n      let idstr = user.id.toString()\n      let timeCode = (parseInt(moment().add(60, 'minutes').format('YYMMDDHHmmss')) * parseInt(idstr.substr(idstr.length - 2))).toString()\n      let token = md5(user.email + user.id + timeCode + String(Math.floor(Math.random() * 99999999)))\n      await RedisService.setCache(CACHE_KEY.PWDRESETTOKEN_GET, token, user.id)\n      let link = `${ctx.headers.origin}/account/resetpwd?code=${timeCode}&email=${email}&token=${token}`\n      let content = MailService.mailFindpwdTemp.replace(/{=EMAIL=}/g, user.email).replace(/{=URL=}/g, link).replace(/{=NAME=}/g, user.fullname)\n      MailService.send(email, \"RAP2：重新设置您的密码\", content)\n    } else {\n      errMsg = '账号不存在'\n    }\n  }\n  ctx.body = {\n    data: !errMsg ? { isOk: true } : { isOk: false, errMsg }\n  }\n})\n\nrouter.post('/account/findpwd/reset', async (ctx) => {\n  let { code, email, captcha, token, password } = ctx.request.body\n  let user, errMsg\n  if (!code || !email || !captcha || !token || !password) {\n    errMsg = '参数错误'\n  }\n  else if (password.length < 6) {\n    errMsg = '密码长度过短'\n  }\n  else if (process.env.TEST_MODE !== 'true' &&\n    (!captcha || !ctx.session.captcha || captcha.trim().toLowerCase() !== ctx.session.captcha.toLowerCase())) {\n    errMsg = '错误的验证码'\n  } else {\n    user = await User.findOne({\n      attributes: QueryInclude.User.attributes,\n      where: { email },\n    })\n    if (!user) {\n      errMsg = '您的邮箱没被注册过，或用户已被锁定'\n    }\n    else {\n      const tokenCache = await RedisService.getCache(CACHE_KEY.PWDRESETTOKEN_GET, user.id)\n      if (!tokenCache || tokenCache !== token) {\n        errMsg = \"参数错误\"\n      }\n      else {\n        RedisService.delCache(CACHE_KEY.PWDRESETTOKEN_GET, user.id)\n        let idstr = user.id.toString()\n        let timespan = parseInt(code) / parseInt(idstr.substr(idstr.length - 2))\n        if (timespan < parseInt(moment().format('YYMMDDHHmmss'))) {\n          errMsg = \"此链接已超时，请重新发送重置密码邮件\"\n        }\n        else {\n          user.password = md5(md5(password))\n          await user.save()\n        }\n      }\n    }\n  }\n  ctx.body = {\n    data: !errMsg ? { isOk: true } : { isOk: false, errMsg }\n  }\n})\n\nrouter.post('/account/updateAccount', async ctx => {\n  try {\n    const { password, fullname } = ctx.request.body as { password: string, fullname: string }\n    if (!ctx.session?.id) {\n      throw new Error('需先登录才能操作')\n    }\n    const user = await User.findByPk(ctx.session.id)\n    if (password) {\n      user.password = md5(md5(password))\n    }\n    if (fullname) {\n      user.fullname = fullname\n    }\n    await user.save()\n    ctx.body = {\n      isOk: true\n    }\n  } catch (ex) {\n    ctx.body = {\n      isOk: false,\n      errMsg: ex.message,\n    }\n  }\n})\n"
  },
  {
    "path": "src/routes/analytics.ts",
    "content": "import router from './router'\nimport Repository from \"../models/bo/repository\"\nimport Logger from \"../models/bo/logger\"\nimport User from \"../models/bo/user\"\nconst moment = require('moment')\nconst Sequelize = require('sequelize')\nconst SELECT = { type: Sequelize.QueryTypes.SELECT }\nimport sequelize from '../models/sequelize'\nimport { isLoggedIn } from './base'\nconst YYYY_MM_DD = 'YYYY-MM-DD'\n\n// 最近 30 天新建仓库数\nrouter.get('/app/analytics/repositories/created', isLoggedIn, async (ctx) => {\n  let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD)\n  let end = moment().startOf('day').format(YYYY_MM_DD)\n  let sql = `\n    SELECT\n        DATE(createdAt) AS label,\n        COUNT(*) as value\n    FROM\n        ${Repository.getTableName()}\n    WHERE\n        createdAt >= '${start}' AND createdAt <= '${end}'\n    GROUP BY label\n    ORDER BY label ASC;\n  `\n  let result: any = await sequelize.query(sql, SELECT)\n  result = result.map((item: any) => ({\n    label: moment(item.label).format(YYYY_MM_DD),\n    value: item.value,\n  }))\n  ctx.body = {\n    data: result,\n  }\n})\n\n// 最近 30 天活跃仓库数\nrouter.get('/app/analytics/repositories/updated', isLoggedIn, async (ctx) => {\n  let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD)\n  let end = moment().startOf('day').format(YYYY_MM_DD)\n  let sql = `\n    SELECT\n        DATE(updatedAt) AS label,\n        COUNT(*) as value\n    FROM\n        ${Repository.getTableName()}\n    WHERE\n        updatedAt >= '${start}' AND updatedAt <= '${end}'\n    GROUP BY label\n    ORDER BY label ASC;\n  `\n  let result: any = await sequelize.query(sql, SELECT)\n  result = result.map((item: any) => ({\n    label: moment(item.label).format(YYYY_MM_DD),\n    value: item.value,\n  }))\n  ctx.body = {\n    data: result,\n  }\n})\n\n// 最近 30 天活跃用户\nrouter.get('/app/analytics/users/activation', isLoggedIn, async (ctx) => {\n  let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD)\n  let end = moment().startOf('day').format(YYYY_MM_DD)\n  let sql = `\n    SELECT\n        loggers.userId AS userId,\n        users.fullname AS fullname,\n        COUNT(*) AS value\n    FROM\n        ${Logger.getTableName()} loggers\n            LEFT JOIN\n        ${User.getTableName()} users ON (loggers.userId = users.id)\n    WHERE\n        loggers.updatedAt >= '${start}' AND loggers.updatedat <= '${end}'\n    GROUP BY loggers.userId\n    ORDER BY value DESC\n    LIMIT 10\n  `\n  let result = await sequelize.query(sql, SELECT)\n  ctx.body = {\n    data: result,\n  }\n})\n\n// 最近 30 天活跃仓库\nrouter.get('/app/analytics/repositories/activation', isLoggedIn, async (ctx) => {\n  let start = moment().startOf('day').subtract(30, 'days').format(YYYY_MM_DD)\n  let end = moment().startOf('day').format(YYYY_MM_DD)\n  let sql = `\n    SELECT\n        loggers.repositoryId AS repositoryId,\n        repositories.name,\n        COUNT(*) AS value\n    FROM\n        ${Logger.getTableName()} loggers\n    LEFT JOIN\n        ${Repository.getTableName()} repositories\n        ON (loggers.repositoryId = repositories.id)\n    WHERE\n        loggers.repositoryId IS NOT NULL\n            AND loggers.updatedAt >= '${start}'\n            AND loggers.updatedat <= '${end}'\n    GROUP BY loggers.repositoryId\n    ORDER BY value DESC\n    LIMIT 10\n  `\n  let result = await sequelize.query(sql, SELECT)\n  ctx.body = {\n    data: result,\n  }\n})\n\n// TODO 2.3 支持 start、end"
  },
  {
    "path": "src/routes/base.ts",
    "content": "import * as _ from 'lodash'\nimport { ParameterizedContext } from 'koa'\nconst inTestMode = process.env.TEST_MODE === 'true'\n\n\nexport async function isLoggedIn(ctx: ParameterizedContext<any, any>, next: () => Promise<any>) {\n  if (!inTestMode && (!ctx.session || !ctx.session.id)) {\n    ctx.body = {\n      isOk: false,\n      errMsg: 'need login',\n    }\n  } else {\n    await next()\n  }\n}"
  },
  {
    "path": "src/routes/counter.ts",
    "content": "import router from './router'\nimport config from '../config'\n\nrouter.get('/app/counter', async(ctx) => {\n  let app: any = ctx.app\n  ctx.body = {\n    data: {\n      version: config.version,\n      users: Object.keys(app.counter.users).length,\n      mock: app.counter.mock,\n    },\n  }\n})"
  },
  {
    "path": "src/routes/export.ts",
    "content": "import router from './router'\nimport { COMMON_ERROR_RES } from './utils/const'\nimport PostmanService from '../service/export/postman'\nimport MarkdownService from '../service/export/markdown'\n// import PDFService from '../service/export/pdf'\nimport * as moment from 'moment'\nimport DocxService from '../service/export/docx'\nimport { AccessUtils, ACCESS_TYPE } from './utils/access'\nimport { Repository } from '../models/'\n\nrouter.get('/export/postman', async ctx => {\n  const repoId = +ctx.query.id\n  if (\n    !(await AccessUtils.canUserAccess(\n      ACCESS_TYPE.REPOSITORY_GET,\n      ctx.session.id,\n      repoId\n    ))\n  ) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  if (!(repoId > 0)) {\n    ctx.data = COMMON_ERROR_RES.ERROR_PARAMS\n  }\n  const repository = await Repository.findByPk(repoId)\n  ctx.body = await PostmanService.export(repoId)\n  ctx.set(\n    'Content-Disposition',\n    `attachment; filename=\"RAP-${encodeURI(\n      repository.name\n    )}-${repoId}-${encodeURI('POSTMAN')}-${moment().format('YYYYMMDDHHmmss')}.json\"`\n  )\n  ctx.set(\n    'Content-type',\n    'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n  )\n})\n\nrouter.get('/export/markdown', async ctx => {\n  const repoId = +ctx.query.id\n  if (\n    !(await AccessUtils.canUserAccess(\n      ACCESS_TYPE.REPOSITORY_GET,\n      ctx.session.id,\n      repoId\n    ))\n  ) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  if (!(repoId > 0)) {\n    ctx.data = COMMON_ERROR_RES.ERROR_PARAMS\n  }\n  ctx.body = await MarkdownService.export(repoId, ctx.query.origin as string)\n})\n\nrouter.get('/export/docx', async ctx => {\n  const repoId = +ctx.query.id\n  if (\n    !(await AccessUtils.canUserAccess(\n      ACCESS_TYPE.REPOSITORY_GET,\n      ctx.session.id,\n      repoId\n    ))\n  ) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  if (!(repoId > 0)) {\n    ctx.data = COMMON_ERROR_RES.ERROR_PARAMS\n  }\n  const repository = await Repository.findByPk(repoId)\n  ctx.body = await DocxService.export(repoId, ctx.query.origin as string)\n  ctx.set(\n    'Content-Disposition',\n    `attachment; filename=\"RAP-${encodeURI(\n      repository.name\n    )}-${repoId}-${encodeURI('接口文档')}-${moment().format('YYYYMMDDHHmmss')}.docx\"`\n  )\n  ctx.set(\n    'Content-type',\n    'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\n  )\n})\n\n// router.get('/export/pdf', async ctx => {\n//   const repoId = +ctx.query.id\n//   if (\n//     !(await AccessUtils.canUserAccess(\n//       ACCESS_TYPE.REPOSITORY,\n//       ctx.session.id,\n//       repoId\n//     ))\n//   ) {\n//     ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n//     return\n//   }\n//   if (!(repoId > 0)) {\n//     ctx.data = COMMON_ERROR_RES.ERROR_PARAMS\n//   }\n//   const repository = await Repository.findByPk(repoId)\n//   ctx.body = await PDFService.export(repoId, ctx.query.origin)\n//   ctx.set(\n//     'Content-Disposition',\n//     `attachment; filename=\"RAP-${encodeURI(\n//       repository.name\n//     )}-${repoId}-${encodeURI('接口文档')}.pdf\"`\n//   )\n//   ctx.set(\n//     'Content-type',\n//     'application/pdf'\n//   )\n// })\n"
  },
  {
    "path": "src/routes/index.ts",
    "content": "import router from './router'\n\nrequire('./counter')\nrequire('./account')\nrequire('./organization')\nrequire('./repository')\nrequire('./mock')\nrequire('./analytics')\nrequire('./export')\n\nexport default router\n"
  },
  {
    "path": "src/routes/migration.ts",
    "content": "// import router from './router'\n// import migration from '../scripts/migration/migration'\n\n// // TODO 2.3 迁移期间采用单 worker 机制，迁移后恢复\n// const TASKS: { [key: number]: number } = {}\n// const doit = async (repositoryId: number) => {\n//   if (repositoryId in TASKS) return\n\n//   let stage = await migration.stage(repositoryId)\n//   if (stage === 4) {\n//     TASKS[repositoryId] = 100\n//     return\n//   }\n\n//   await migration.repository(repositoryId)\n//   await migration.lock(repositoryId)\n\n//   TASKS[repositoryId] = 0\n//   for (let i = 0; i < 100; i++) {\n//     const percent = await new Promise<number>((resolve) => {\n//       setTimeout(() => {\n//         resolve(i + 1)\n//       }, Math.random() * 500)\n//     })\n//     TASKS[repositoryId] = percent\n//   }\n// }\n\n// router.post('/app/migrate', async (ctx) => {\n//   let { repositoryId } = ctx.request.body\n//   if (!repositoryId) return\n//   doit(repositoryId)\n//   ctx.type = 'json'\n//   ctx.body = {\n//     percent: TASKS[repositoryId] || 0\n//   }\n// })\n\n// router.get('/app/migrate/progress/:repositoryId', async (ctx) => {\n//   let { repositoryId } = ctx.params\n//   let stage = await migration.stage(repositoryId)\n//   let percent = stage === 4 ? 100 : TASKS[repositoryId]\n//   ctx.body = {\n//     percent\n//   }\n// })\n"
  },
  {
    "path": "src/routes/mock.ts",
    "content": "import router from './router'\nimport { Repository, Interface, Property } from '../models'\nimport { QueryInclude } from '../models'\nimport Tree from './utils/tree'\nimport { MockService } from '../service/mock'\n\nconst attributes: any = { exclude: [] }\nconst pt = require('node-print').pt\nconst beautify = require('js-beautify').js_beautify\n\n// 检测是否存在重复接口，会在返回的插件 JS 中提示。同时也会在编辑器中提示。\nconst parseDuplicatedInterfaces = (repository: Repository) => {\n  let counter: any = {}\n  for (let itf of repository.interfaces) {\n    let key = `${itf.method} ${itf.url}`\n    counter[key] = [...(counter[key] || []), { id: itf.id, method: itf.method, url: itf.url }]\n  }\n  let duplicated = []\n  for (let key in counter) {\n    if (counter[key].length > 1) {\n      duplicated.push(counter[key])\n    }\n  }\n  return duplicated\n}\nconst generatePlugin = (protocol: any, host: any, repository: Repository) => {\n  // DONE 2.3 protocol 错误，应该是 https\n  let duplicated = parseDuplicatedInterfaces(repository)\n  let editor = `${protocol}://rap2.taobao.org/repository/editor?id=${repository.id}` // [TODO] replaced by cur domain\n  let result = `\n/**\n * 仓库    #${repository.id} ${repository.name}\n * 在线编辑 ${editor}\n * 仓库数据 ${protocol}://${host}/repository/get?id=${repository.id}\n * 请求地址 ${protocol}://${host}/app/mock/${repository.id}/:method/:url\n *    或者 ${protocol}://${host}/app/mock/template/:interfaceId\n *    或者 ${protocol}://${host}/app/mock/data/:interfaceId\n */\n;(function(){\n  let repositoryId = ${repository.id}\n  let interfaces = [\n    ${repository.interfaces.map((itf: Interface) =>\n    `{ id: ${itf.id}, name: '${itf.name}', method: '${itf.method}', url: '${itf.url}',\n      request: ${JSON.stringify(itf.request)},\n      response: ${JSON.stringify(itf.response)} }`\n  ).join(',\\n    ')}\n  ]\n  ${duplicated.length ? `console.warn('检测到重复接口，请访问 ${editor} 修复警告！')\\n` : ''}\n  let RAP = window.RAP || {\n    protocol: '${protocol}',\n    host: '${host}',\n    interfaces: {}\n  }\n  RAP.interfaces[repositoryId] = interfaces\n  window.RAP = RAP\n})();`\n  return beautify(result, { indent_size: 2 })\n}\n\nrouter.get('/app/plugin/:repositories', async (ctx) => {\n  let repositoryIds = new Set<number>(ctx.params.repositories.split(',').map((item: string) => +item).filter((item: any) => item)) // _.uniq() => Set\n  let result = []\n  for (let id of repositoryIds) {\n    let repository = await Repository.findByPk(id, {\n      attributes: { exclude: [] },\n      include: [\n        QueryInclude.Creator,\n        QueryInclude.Owner,\n        QueryInclude.Locker,\n        QueryInclude.Members,\n        QueryInclude.Organization,\n        QueryInclude.Collaborators,\n      ],\n    } as any)\n    if (!repository) continue\n    if (repository.collaborators) {\n      repository.collaborators.map(item => {\n        repositoryIds.add(item.id)\n      })\n    }\n    repository.interfaces = await Interface.findAll<Interface>({\n      attributes: { exclude: [] },\n      where: {\n        repositoryId: repository.id,\n      },\n      include: [\n        QueryInclude.Properties,\n      ],\n    } as any)\n    repository.interfaces.forEach(itf => {\n      itf.request = Tree.ArrayToTreeToTemplate(itf.properties.filter(item => item.scope === 'request'))\n      itf.response = Tree.ArrayToTreeToTemplate(itf.properties.filter(item => item.scope === 'response'))\n    })\n    // 修复 协议总是 http\n    // https://lark.alipay.com/login-session/unity-login/xp92ap\n    let protocol = ctx.headers['x-client-scheme'] || ctx.protocol\n    result.push(generatePlugin(protocol, ctx.host, repository))\n  }\n\n  ctx.type = 'application/x-javascript; charset=utf-8'\n  ctx.body = result.join('\\n')\n})\n\n// /app/mock/:repository/:method/:url\n// X DONE 2.2 支持 GET POST PUT DELETE 请求\n// DONE 2.2 忽略请求地址中的前缀斜杠\n// DONE 2.3 支持所有类型的请求，这样从浏览器中发送跨越请求时不需要修改 method\nrouter.all('/app/mock/:repositoryId(\\\\d+)/:url(.+)', async (ctx) => {\n  await MockService.mock(ctx, { forceVerify: true })\n})\n\nrouter.all('/app/mock-noverify/:repositoryId(\\\\d+)/:url(.+)', async ctx => {\n  await MockService.mock(ctx, { forceVerify: false })\n})\n\n// DONE 2.2 支持获取请求参数的模板、数据、Schema\nrouter.get('/app/mock/template/:interfaceId', async (ctx) => {\n  let app: any = ctx.app\n  app.counter.mock++\n  let { interfaceId } = ctx.params\n  let { scope = 'response' } = ctx.query\n  let properties = await Property.findAll({\n    attributes,\n    where: { interfaceId, scope },\n  })\n  // pt(properties.map(item => item.toJSON()))\n  let template = Tree.ArrayToTreeToTemplate(properties)\n  ctx.type = 'json'\n  ctx.body = Tree.stringifyWithFunctonAndRegExp(template)\n  // ctx.body = template\n  // ctx.body = JSON.stringify(template, null, 2)\n})\n\nrouter.all('/app/mock/data/:interfaceId', async (ctx) => {\n  let app: any = ctx.app\n  app.counter.mock++\n  let { interfaceId } = ctx.params\n  let { scope = 'response' } = ctx.query\n  let properties: any = await Property.findAll({\n    attributes,\n    where: { interfaceId, scope },\n  })\n  properties = properties.map((item: any) => item.toJSON())\n  // pt(properties)\n\n  // DONE 2.2 支持引用请求参数\n  let requestProperties: any = await Property.findAll({\n    attributes,\n    where: { interfaceId, scope: 'request' },\n  })\n  requestProperties = requestProperties.map((item: any) => item.toJSON())\n  let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties)\n  Object.assign(requestData, ctx.query)\n\n  let data = Tree.ArrayToTreeToTemplateToData(properties, requestData)\n  ctx.type = 'json'\n  if (data._root_) {\n    data = data._root_\n  }\n  ctx.body = JSON.stringify(data, undefined, 2)\n})\n\nrouter.get('/app/mock/schema/:interfaceId', async (ctx) => {\n  let app: any = ctx.app\n  app.counter.mock++\n  let { interfaceId } = ctx.params\n  let { scope = 'response' } = ctx.query\n  let properties: any = await Property.findAll({\n    attributes,\n    where: { interfaceId, scope },\n  })\n  pt(properties.map((item: any) => item.toJSON()))\n  properties = properties.map((item: any) => item.toJSON())\n  let schema = Tree.ArrayToTreeToTemplateToJSONSchema(properties)\n  ctx.type = 'json'\n  ctx.body = Tree.stringifyWithFunctonAndRegExp(schema)\n})\n\nrouter.get('/app/mock/tree/:interfaceId', async (ctx) => {\n  let app: any = ctx.app\n  app.counter.mock++\n  let { interfaceId } = ctx.params\n  let { scope = 'response' } = ctx.query\n  let properties: any = await Property.findAll({\n    attributes,\n    where: { interfaceId, scope },\n  })\n  pt(properties.map((item: any) => item.toJSON()))\n  properties = properties.map((item: any) => item.toJSON())\n  let tree = Tree.ArrayToTree(properties)\n  ctx.type = 'json'\n  ctx.body = Tree.stringifyWithFunctonAndRegExp(tree)\n})\n"
  },
  {
    "path": "src/routes/organization.ts",
    "content": "import router from './router'\nimport { Organization, User, Logger, Repository, Module, Interface, Property } from '../models'\nimport { QueryInclude } from '../models'\nimport * as _ from 'lodash'\nimport Pagination from './utils/pagination'\nimport OrganizationService from '../service/organization'\nimport { Op, FindOptions }  from 'sequelize'\nimport { isLoggedIn } from './base'\nimport { AccessUtils, ACCESS_TYPE } from './utils/access'\nimport { COMMON_ERROR_RES } from './utils/const'\n\nrouter.get('/app/get', async (ctx, next) => {\n  let data: any = {}\n  let query = ctx.query\n  let hooks: any = {\n    organization: Organization,\n  }\n  for (let name in hooks) {\n    if (!query[name]) continue\n    data[name] = await hooks[name].findByPk(query[name], {\n      attributes: { exclude: [] }\n    })\n  }\n  ctx.body = {\n    data: Object.assign({}, ctx.body && ctx.body.data, data),\n  }\n\n  return next()\n})\n\nrouter.get('/organization/count', async (ctx) => {\n  ctx.body = {\n    data: await Organization.count(),\n  }\n})\n\nrouter.get('/organization/list', async (ctx) => {\n  const curUserId = ctx.session.id\n  const { name } = ctx.query\n  const total = await OrganizationService.getAllOrganizationIdListNum(curUserId)\n  const pagination = new Pagination(total, ctx.query.cursor || 1, ctx.query.limit || 100)\n  const organizationIds = await OrganizationService.getAllOrganizationIdList(curUserId, pagination, name as string)\n  const options: FindOptions = {\n    where: {\n      id: {\n        [Op.in]: organizationIds,\n      },\n    },\n    include: [\n      QueryInclude.Creator,\n      QueryInclude.Owner,\n      QueryInclude.Members\n    ],\n    order: [['updatedAt', 'desc']]\n  }\n  const organizations = await Organization.findAll(options)\n  ctx.body = {\n    data: organizations,\n    pagination,\n  }\n})\nrouter.get('/organization/owned', isLoggedIn, async (ctx) => {\n  let where = {}\n  let { name } = ctx.query\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { name: { [Op.like]: `%${name}%` } },\n        { id: name } // name => id\n      ]\n    })\n  }\n\n  let auth = await User.findByPk(ctx.session.id)\n  let options: any = {\n    where,\n    attributes: { exclude: [] },\n    include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members],\n    order: [['updatedAt', 'DESC']],\n  }\n  let owned = await auth.$get('ownedOrganizations', options)\n  ctx.body = {\n    data: owned,\n    pagination: undefined,\n  }\n})\nrouter.get('/organization/joined', isLoggedIn, async (ctx) => {\n  let where = {}\n  let { name } = ctx.query\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { name: { [Op.like]: `%${name}%` } },\n        { id: name } // name => id\n      ]\n    })\n  }\n\n  let auth = await User.findByPk(ctx.session.id)\n  let options: object = {\n    where,\n    attributes: { exclude: [] },\n    include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members],\n    order: [['updatedAt', 'DESC']],\n  }\n  let joined = await auth.$get('joinedOrganizations', options)\n  // await auth.getOwnedOrganizations()\n  // await auth.getJoinedOrganizations()\n  ctx.body = {\n    data: joined,\n    pagination: undefined,\n  }\n})\nrouter.get('/organization/get', async (ctx) => {\n  const organizationId = +ctx.query.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_GET, ctx.session.id, organizationId)) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  const organization = await Organization.findByPk(+ctx.query.id, {\n    attributes: { exclude: [] },\n    include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members],\n  } as any)\n  ctx.body = {\n    data: organization,\n  }\n})\nrouter.post('/organization/create', isLoggedIn, async (ctx) => {\n  let creatorId = ctx.session.id\n  let body = Object.assign({}, ctx.request.body, { creatorId, ownerId: creatorId })\n  let created = await Organization.create(body)\n  if (body.memberIds) {\n    let members = await User.findAll({ where: { id: body.memberIds } })\n    await created.$set('members', members)\n  }\n  let filled = await Organization.findByPk(created.id, {\n    attributes: { exclude: [] },\n    include: [QueryInclude.Creator, QueryInclude.Owner, QueryInclude.Members],\n  } as any)\n  ctx.body = {\n    data: filled,\n  }\n})\nrouter.post('/organization/update', isLoggedIn, async (ctx, next) => {\n  let body = Object.assign({}, ctx.request.body)\n  const organizationId = +body.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  delete body.creatorId\n  // DONE 2.2 支持转移团队\n  // delete body.ownerId\n  let updated = await Organization.update(body, { where: { id: body.id } })\n  if (body.memberIds) {\n    let reloaded = await Organization.findByPk(body.id)\n    let members = await User.findAll({ where: { id: body.memberIds } })\n    ctx.prevAssociations = await reloaded.$get('members')\n    await reloaded.$set('members', members)\n    ctx.nextAssociations = await reloaded.$get('members')\n  }\n  ctx.body = {\n    data: updated[0],\n  }\n  return next()\n}, async (ctx) => {\n  let { id } = ctx.request.body\n  // 团队改\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'update',\n    organizationId: id,\n  })\n  // 加入 & 退出\n  if (!ctx.prevAssociations || !ctx.nextAssociations) return\n  let prevIds = ctx.prevAssociations.map((item: any) => item.id)\n  let nextIds = ctx.nextAssociations.map((item: any) => item.id)\n  let joined = _.difference(nextIds, prevIds)\n  let exited = _.difference(prevIds, nextIds)\n  let creatorId = ctx.session.id\n  for (let userId of joined) {\n    await Logger.create({ creatorId, userId, type: 'join', organizationId: id })\n  }\n  for (let userId of exited) {\n    await Logger.create({ creatorId, userId, type: 'exit', organizationId: id })\n  }\n})\nrouter.post('/organization/transfer',  isLoggedIn, async (ctx) => {\n  let { id, ownerId } = ctx.request.body\n  const organizationId = +id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let body = { ownerId }\n  let result = await Organization.update(body, { where: { id } })\n  ctx.body = {\n    data: result[0],\n  }\n})\nrouter.get('/organization/remove', isLoggedIn, async (ctx, next) => {\n  let { id } = ctx.query\n  const organizationId = +id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) {\n    ctx.body = COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let result = await Organization.destroy({ where: { id } })\n  let repositories = await Repository.findAll({\n    where: { organizationId: id },\n  })\n  if (repositories.length) {\n    let ids = repositories.map(item => item.id)\n    await Repository.destroy({ where: { id: ids } })\n    await Module.destroy({ where: { repositoryId: ids } })\n    await Interface.destroy({ where: { repositoryId: ids } })\n    await Property.destroy({ where: { repositoryId: ids } })\n  }\n  ctx.body = {\n    data: result,\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  let { id } = ctx.query\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'delete',\n    organizationId: id,\n  })\n})"
  },
  {
    "path": "src/routes/repository.ts",
    "content": "// TODO 2.1 大数据测试，含有大量模块、接口、属性的仓库\nimport router from './router'\nimport * as _ from 'underscore'\nimport Pagination from './utils/pagination'\nimport { User, Organization, Repository, Module, Interface, Property, QueryInclude, Logger, DefaultVal } from '../models'\nimport Tree from './utils/tree'\nimport { AccessUtils, ACCESS_TYPE } from './utils/access'\nimport * as Consts from './utils/const'\nimport RedisService, { CACHE_KEY } from '../service/redis'\nimport RepositoryService from '../service/repository'\nimport MigrateService from '../service/migrate'\nimport OrganizationService from '../service/organization'\nimport { Op } from 'sequelize'\nimport { isLoggedIn } from './base'\n\nimport { initRepository, initModule } from './utils/helper'\nimport nanoid = require('nanoid')\nimport { LOG_SEPERATOR, LOG_SUB_SEPERATOR } from '../models/bo/historyLog'\nimport { ENTITY_TYPE } from './utils/const'\nimport { IPager } from '../types'\nimport * as JSON5 from 'json5'\n\nrouter.get('/app/get', async (ctx, next) => {\n  let data: any = {}\n  let query = ctx.query\n  let hooks: any = {\n    repository: Repository,\n    module: Module,\n    interface: Interface,\n    property: Property,\n    user: User,\n  }\n  for (let name in hooks) {\n    if (!query[name]) continue\n    data[name] = await hooks[name].findByPk(query[name])\n  }\n  ctx.body = {\n    data: Object.assign({}, ctx.body && ctx.body.data, data),\n  }\n\n  return next()\n})\n\nrouter.get('/repository/count', async (ctx) => {\n  ctx.body = {\n    data: await Repository.count(),\n  }\n})\n\nrouter.get('/repository/list', async (ctx) => {\n  let where = {}\n  let { name, user, organization } = ctx.query\n\n  if (+organization > 0) {\n    const access = await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_GET, ctx.session.id, +organization)\n\n    if (access === false) {\n      ctx.body = {\n        isOk: false,\n        errMsg: Consts.COMMON_MSGS.ACCESS_DENY\n      }\n      return\n    }\n  }\n\n  // tslint:disable-next-line:no-null-keyword\n  if (user) Object.assign(where, { ownerId: user, organizationId: null })\n  if (organization) Object.assign(where, { organizationId: organization })\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { name: { [Op.like]: `%${name}%` } },\n        { id: name } // name => id\n      ]\n    })\n  }\n  let total = await Repository.count({\n    where,\n    include: [\n      QueryInclude.Creator,\n      QueryInclude.Owner,\n      QueryInclude.Locker\n    ]\n  })\n  let limit = Math.min(+ctx.query.limit ?? 10, 100)\n  let pagination = new Pagination(total, ctx.query.cursor || 1, limit)\n  let repositories = await Repository.findAll({\n    where,\n    attributes: { exclude: [] },\n    include: [\n      QueryInclude.Creator,\n      QueryInclude.Owner,\n      QueryInclude.Locker,\n      QueryInclude.Members,\n      QueryInclude.Organization,\n      QueryInclude.Collaborators,\n    ],\n    offset: pagination.start,\n    limit: pagination.limit,\n    order: [['updatedAt', 'DESC']]\n  })\n  let repoData = await Promise.all(repositories.map(async (repo) => {\n    const canUserEdit = await AccessUtils.canUserAccess(\n      ACCESS_TYPE.REPOSITORY_SET,\n      ctx.session.id,\n      repo.id,\n    )\n    return {\n      ...repo.toJSON(),\n      canUserEdit\n    }\n  }))\n  ctx.body = {\n    isOk: true,\n    data: repoData,\n    pagination: pagination,\n  }\n})\n\nrouter.get('/repository/owned', isLoggedIn, async (ctx) => {\n  let where = {}\n  let { name } = ctx.query\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { name: { [Op.like]: `%${name}%` } },\n        { id: name } // name => id\n      ]\n    })\n  }\n\n  let auth: User = await User.findByPk(ctx.query.user || ctx.session.id)\n\n  let repositories = await auth.$get('ownedRepositories', {\n    where,\n    include: [\n      QueryInclude.Creator,\n      QueryInclude.Owner,\n      QueryInclude.Locker,\n      QueryInclude.Members,\n      QueryInclude.Organization,\n      QueryInclude.Collaborators,\n    ],\n    order: [['updatedAt', 'DESC']]\n  })\n  let repoData = repositories.map(repo => {\n    return {\n      ...repo.toJSON(),\n      canUserEdit: true\n    }\n  })\n  ctx.body = {\n    data: repoData,\n    pagination: undefined,\n  }\n})\n\nrouter.get('/repository/joined', isLoggedIn, async (ctx) => {\n  let where: any = {}\n  let { name } = ctx.query\n  if (name) {\n    Object.assign(where, {\n      [Op.or]: [\n        { name: { [Op.like]: `%${name}%` } },\n        { id: name } // name => id\n      ]\n    })\n  }\n\n  let auth = await User.findByPk(ctx.query.user || ctx.session.id)\n  let repositories = await auth.$get('joinedRepositories', {\n    where,\n    attributes: { exclude: [] },\n    include: [\n      QueryInclude.Creator,\n      QueryInclude.Owner,\n      QueryInclude.Locker,\n      QueryInclude.Members,\n      QueryInclude.Organization,\n      QueryInclude.Collaborators,\n    ],\n    order: [['updatedAt', 'DESC']]\n  })\n  let repoData = repositories.map(repo => {\n    return {\n      ...repo.toJSON(),\n      canUserEdit: true,\n    }\n  })\n  ctx.body = {\n    data: repoData,\n    pagination: undefined\n  }\n})\n\nrouter.get('/repository/get', async (ctx) => {\n  const access = await AccessUtils.canUserAccess(\n    ACCESS_TYPE.REPOSITORY_GET,\n    ctx.session.id,\n    +ctx.query.id,\n    ctx.query.token as string\n  )\n  if (access === false) {\n    ctx.body = {\n      isOk: false,\n      errMsg: Consts.COMMON_MSGS.ACCESS_DENY\n    }\n    return\n  }\n  const excludeProperty = ctx.query.excludeProperty || false\n  const canUserEdit = await AccessUtils.canUserAccess(\n    ACCESS_TYPE.REPOSITORY_SET,\n    ctx.session.id,\n    +ctx.query.id,\n    ctx.query.token as string,\n  )\n  let repository: Partial<Repository> & {\n    canUserEdit: boolean\n  }\n  // 分开查询减少查询时间\n  let [repositoryOmitModules, repositoryModules] = await Promise.all([\n    Repository.findByPk(+ctx.query.id, {\n      attributes: { exclude: [] },\n      include: [\n        QueryInclude.Creator,\n        QueryInclude.Owner,\n        QueryInclude.Locker,\n        QueryInclude.Members,\n        QueryInclude.Organization,\n        QueryInclude.Collaborators,\n      ],\n    }),\n    Repository.findByPk(+ctx.query.id, {\n      attributes: { exclude: [] },\n      include: [\n        excludeProperty\n          ? QueryInclude.RepositoryHierarchyExcludeProperty\n          : QueryInclude.RepositoryHierarchy,\n      ],\n      order: [\n        [{ model: Module, as: 'modules' }, 'priority', 'asc'],\n        [\n          { model: Module, as: 'modules' },\n          { model: Interface, as: 'interfaces' },\n          'priority',\n          'asc',\n        ],\n      ],\n    }),\n  ])\n  repository = {\n    ...repositoryOmitModules.toJSON(),\n    ...repositoryModules.toJSON(),\n    canUserEdit\n  }\n\n  ctx.body = {\n    data: repository,\n  }\n})\n\nrouter.post('/repository/create', isLoggedIn, async (ctx, next) => {\n  let creatorId = ctx.session.id\n  let body = Object.assign({}, ctx.request.body, {\n    creatorId,\n    ownerId: creatorId,\n    token: nanoid(32)\n  })\n  let created = await Repository.create(body)\n  if (body.memberIds) {\n    let members = await User.findAll({ where: { id: body.memberIds } })\n    await created.$set('members', members)\n  }\n  if (body.collaboratorIds) {\n    let collaborators = await Repository.findAll({ where: { id: body.collaboratorIds } })\n    await created.$set('collaborators', collaborators)\n  }\n  await initRepository(created)\n  ctx.body = {\n    data: await Repository.findByPk(created.id, {\n      attributes: { exclude: [] },\n      include: [\n        QueryInclude.Creator,\n        QueryInclude.Owner,\n        QueryInclude.Locker,\n        QueryInclude.Members,\n        QueryInclude.Organization,\n        QueryInclude.RepositoryHierarchy,\n        QueryInclude.Collaborators,\n      ],\n    } as any),\n  }\n  return next()\n}, async (ctx) => {\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'create',\n    repositoryId: ctx.body.data.id,\n  })\n})\n\nrouter.post('/repository/update', isLoggedIn, async (ctx, next) => {\n  const body = Object.assign({}, ctx.request.body)\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, body.id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let repo = await Repository.findByPk(body.id)\n\n  // 更改团队需要校验是否有当前团队和目标团队的权限\n  if (body.organizationId != repo.organizationId) {\n\n    if (body.organizationId && !(await OrganizationService.canUserAccessOrganization(ctx.session.id, body.organizationId))) {\n      ctx.body = '没有当前团队的权限'\n      return\n    }\n\n    if (repo.organizationId && !(await OrganizationService.canUserAccessOrganization(ctx.session.id, repo.organizationId))) {\n      ctx.body = '没有目标团队的权限'\n      return\n    }\n  }\n\n  delete body.creatorId\n\n  let result = await Repository.update(body, { where: { id: body.id } })\n  if (body.memberIds) {\n    let reloaded = await Repository.findByPk(body.id, {\n      include: [{\n        model: User,\n        as: 'members',\n      }],\n    })\n    let members = await User.findAll({\n      where: {\n        id: {\n          [Op.in]: body.memberIds,\n        },\n      },\n    })\n    ctx.prevAssociations = reloaded.members\n    reloaded.$set('members', members)\n    await reloaded.save()\n    ctx.nextAssociations = reloaded.members\n  }\n  if (body.collaboratorIds) {\n    let reloaded = await Repository.findByPk(body.id)\n    let collaborators = await Repository.findAll({\n      where: {\n        id: {\n          [Op.in]: body.collaboratorIds,\n        },\n      },\n    })\n    reloaded.$set('collaborators', collaborators)\n    await reloaded.save()\n  }\n  ctx.body = {\n    data: result[0],\n  }\n  return next()\n}, async (ctx) => {\n  let { id } = ctx.request.body\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'update',\n    repositoryId: id,\n  })\n  // 加入 & 退出\n  if (!ctx.prevAssociations || !ctx.nextAssociations) return\n  let prevIds = ctx.prevAssociations.map((item: any) => item.id)\n  let nextIds = ctx.nextAssociations.map((item: any) => item.id)\n  let joined = _.difference(nextIds, prevIds)\n  let exited = _.difference(prevIds, nextIds)\n  let creatorId = ctx.session.id\n  for (let userId of joined) {\n    await Logger.create({ creatorId, userId, type: 'join', repositoryId: id })\n  }\n  for (let userId of exited) {\n    await Logger.create({ creatorId, userId, type: 'exit', repositoryId: id })\n  }\n})\n\nrouter.post('/repository/transfer', isLoggedIn, async (ctx) => {\n  let { id, ownerId, organizationId } = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, organizationId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let body: any = {}\n  if (ownerId) body.ownerId = ownerId // 转移给其他用户\n  if (organizationId) {\n    body.organizationId = organizationId // 转移给其他团队，同时转移给该团队拥有者\n    body.ownerId = (await Organization.findByPk(organizationId)).ownerId\n  }\n  let result = await Repository.update(body, { where: { id } })\n  ctx.body = {\n    data: result[0],\n  }\n})\n\nrouter.get('/repository/remove', isLoggedIn, async (ctx, next) => {\n  const id = +ctx.query.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let result = await Repository.destroy({ where: { id } })\n  await Module.destroy({ where: { repositoryId: id } })\n  await Interface.destroy({ where: { repositoryId: id } })\n  await Property.destroy({ where: { repositoryId: id } })\n  ctx.body = {\n    data: result,\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  let { id } = ctx.query\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'delete',\n    repositoryId: id,\n  })\n})\n\n// TOEO 锁定/解锁仓库 待测试\nrouter.post('/repository/lock', isLoggedIn, async (ctx) => {\n  const id = +ctx.request.body.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let user = ctx.session.id\n  if (!user) {\n    ctx.body = { data: 0 }\n    return\n  }\n  let result = await Repository.update({ lockerId: user }, {\n    where: { id },\n  })\n  ctx.body = { data: result[0] }\n})\n\nrouter.post('/repository/unlock', async (ctx) => {\n  if (!ctx.session.id) {\n    ctx.body = { data: 0 }\n    return\n  }\n  let { id } = ctx.request.body\n  // tslint:disable-next-line:no-null-keyword\n  let result = await Repository.update({ lockerId: null }, {\n    where: { id },\n  })\n  ctx.body = { data: result[0] }\n})\n\n// 模块\nrouter.get('/module/count', async (ctx) => {\n  ctx.body = {\n    data: await Module.count(),\n  }\n})\n\nrouter.get('/module/list', async (ctx) => {\n  let where: any = {}\n  let { repositoryId, name } = ctx.query\n  if (repositoryId) where.repositoryId = repositoryId\n  if (name) where.name = { [Op.like]: `%${name}%` }\n  ctx.body = {\n    data: await Module.findAll({\n      attributes: { exclude: [] },\n      where,\n    }),\n  }\n})\n\nrouter.get('/module/get', async (ctx) => {\n  ctx.body = {\n    data: await Module.findByPk(+ctx.query.id, {\n      attributes: { exclude: [] }\n    })\n  }\n})\n\nrouter.post('/module/create', isLoggedIn, async (ctx, next) => {\n  let creatorId = ctx.session.id\n  let body = Object.assign(ctx.request.body, { creatorId })\n  body.priority = Date.now()\n  let created = await Module.create(body)\n  await initModule(created)\n  ctx.body = {\n    data: await Module.findByPk(created.id)\n  }\n  return next()\n}, async (ctx) => {\n  let mod = ctx.body.data\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'create',\n    repositoryId: mod.repositoryId,\n    moduleId: mod.id,\n  })\n})\n\nrouter.post('/module/update', isLoggedIn, async (ctx, next) => {\n  const { id, name, description } = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let mod = await Module.findByPk(id)\n  await mod.update({ name, description })\n  ctx.request.body.repositoryId = mod.repositoryId\n  ctx.body = {\n    data: {\n      id,\n      name,\n      description\n    },\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  let mod = ctx.request.body\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'update',\n    repositoryId: mod.repositoryId,\n    moduleId: mod.id,\n  })\n})\n\nrouter.post('/module/move', isLoggedIn, async ctx => {\n  const { modId, op } = ctx.request.body\n  const repositoryId = ctx.request.body.repositoryId\n\n  if (!(await RepositoryService.canUserMoveModule(ctx.session.id, modId, repositoryId))) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  await RepositoryService.moveModule(op, modId, repositoryId)\n\n  ctx.body = {\n    data: {\n      isOk: true,\n    },\n  }\n})\n\nrouter.get('/module/remove', isLoggedIn, async (ctx, next) => {\n  let { id } = ctx.query\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let result = await Module.destroy({ where: { id } })\n  await Interface.destroy({ where: { moduleId: id } })\n  await Property.destroy({ where: { moduleId: id } })\n  ctx.body = {\n    data: result,\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  const id = +ctx.query.id\n  let mod = await Module.findByPk(id, { paranoid: false })\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'delete',\n    repositoryId: mod.repositoryId,\n    moduleId: mod.id,\n  })\n})\n\nrouter.post('/module/sort', isLoggedIn, async (ctx) => {\n  let { ids } = ctx.request.body\n  let counter = 1\n  for (let index = 0; index < ids.length; index++) {\n    await Module.update({ priority: counter++ }, {\n      where: { id: ids[index] }\n    })\n  }\n  if (ids && ids.length) {\n    const mod = await Module.findByPk(ids[0])\n    await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, mod.repositoryId)\n  }\n  ctx.body = {\n    data: ids.length,\n  }\n})\n\nrouter.get('/interface/count', async (ctx) => {\n  ctx.body = {\n    data: await Interface.count(),\n  }\n})\n\nrouter.get('/interface/list', async (ctx) => {\n  let where: any = {}\n  let { repositoryId, moduleId, name } = ctx.query\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_GET, ctx.session.id, +repositoryId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  if (repositoryId) where.repositoryId = repositoryId\n  if (moduleId) where.moduleId = moduleId\n  if (name) where.name = { [Op.like]: `%${name}%` }\n  ctx.body = {\n    data: await Interface.findAll({\n      attributes: { exclude: [] },\n      where,\n    }),\n  }\n})\n\nrouter.get('/repository/defaultVal/get/:id', async (ctx) => {\n  const repositoryId: number = +ctx.params.id\n  ctx.body = {\n    data: await DefaultVal.findAll({ where: { repositoryId } })\n  }\n})\n\nrouter.post('/repository/defaultVal/update/:id', async (ctx) => {\n  const repositoryId: number = +ctx.params.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, repositoryId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  const list = ctx.request.body.list.map(x => { const { id, ...y } = x; return y })\n  if (!(repositoryId > 0) || !list) {\n    ctx.body = Consts.COMMON_ERROR_RES.ERROR_PARAMS\n    return\n  }\n  await DefaultVal.destroy({\n    where: { repositoryId }\n  })\n  for (const item of list) {\n    await DefaultVal.create({\n      ...item,\n      repositoryId,\n    })\n  }\n\n  ctx.body = {\n    isOk: true,\n  }\n})\n\nrouter.get('/interface/get', async (ctx) => {\n  const id = +ctx.query.id\n\n  if (id === undefined || !id) {\n    ctx.body = {\n      isOk: false,\n      errMsg: '请输入参数id'\n    }\n    return\n  }\n\n  let itf = await Interface.findByPk(id, {\n    include: [QueryInclude.Locker],\n    attributes: { exclude: [] },\n  })\n\n  if (!itf) {\n    ctx.body = {\n      isOk: false,\n      errMsg: `没有找到 id 为 ${id} 的接口`\n    }\n    return\n  }\n\n  if (\n    !(await AccessUtils.canUserAccess(\n      ACCESS_TYPE.REPOSITORY_GET,\n      ctx.session.id,\n      itf.repositoryId\n    ))\n  ) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  const itfJSON: { [k: string]: any } = itf.toJSON()\n\n  let properties: any[] = await Property.findAll({\n    attributes: { exclude: [] },\n    where: { interfaceId: itf.id },\n  })\n\n  properties = properties.map((item: any) => item.toJSON())\n  itfJSON['properties'] = properties\n\n  let scopes = ['request', 'response']\n  for (let i = 0; i < scopes.length; i++) {\n    let scopeProperties = properties\n      .filter(p => p.scope === scopes[i])\n      .map((item: any) => ({ ...item }))\n    itfJSON[scopes[i] + 'Properties'] = Tree.ArrayToTree(scopeProperties).children\n  }\n\n  ctx.type = 'json'\n  ctx.body = Tree.stringifyWithFunctonAndRegExp({ data: itfJSON })\n})\n\nrouter.post('/interface/create', isLoggedIn, async (ctx, next) => {\n  let creatorId = ctx.session.id\n  let body = Object.assign(ctx.request.body, { creatorId })\n  body.priority = Date.now()\n  let created = await Interface.create(body)\n  // await initInterface(created)\n  ctx.body = {\n    data: {\n      itf: await Interface.findByPk(created.id),\n    }\n  }\n  return next()\n}, async (ctx) => {\n  let itf = ctx.body.data\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'create',\n    repositoryId: itf.repositoryId,\n    moduleId: itf.moduleId,\n    interfaceId: itf.id,\n  })\n})\n\nrouter.post('/interface/update', isLoggedIn, async (ctx, next) => {\n  let summary = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +summary.id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  const itf = await Interface.findByPk(summary.id)\n  const itfChangeLog: string[] = []\n  itf.name !== summary.name && itfChangeLog.push(`接口名 \\`${itf.name}\\` => \\`${summary.name}\\``)\n  itf.url !== summary.url && itfChangeLog.push(`URL \\`${itf.url || '空URL'}\\` => \\`${summary.url}\\``)\n  itf.method !== summary.method && itfChangeLog.push(`METHOD \\`${itf.method}\\` => \\`${summary.method}\\``)\n  itfChangeLog.length && await RepositoryService.addHistoryLog({\n    entityId: itf.id,\n    entityType: Consts.ENTITY_TYPE.INTERFACE,\n    changeLog: `接口${itf.name}(${itf.url || '空URL'}) 变更${itfChangeLog.join(LOG_SEPERATOR)}`,\n    userId: ctx.session.id,\n  })\n  await Interface.update(summary, {\n    where: { id: summary.id }\n  })\n  ctx.body = {\n    data: {\n      itf: await Interface.findByPk(summary.id),\n    }\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  let itf = ctx.request.body\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'update',\n    repositoryId: itf.repositoryId,\n    moduleId: itf.moduleId,\n    interfaceId: itf.id,\n  })\n})\n\nrouter.post('/interface/move', isLoggedIn, async ctx => {\n  const { modId, itfId, op } = ctx.request.body\n  const itf = await Interface.findByPk(itfId)\n  const repositoryId = ctx.request.body.repositoryId || itf.repositoryId\n  if (!(await RepositoryService.canUserMoveInterface(ctx.session.id, itfId, repositoryId, modId))) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  await RepositoryService.moveInterface(op, itfId, repositoryId, modId)\n\n  ctx.body = {\n    data: {\n      isOk: true,\n    },\n  }\n})\n\nrouter.get('/interface/remove', async (ctx, next) => {\n  let id = +ctx.query.id\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  const itf = await Interface.findByPk(id)\n  const properties = await Property.findAll({ where: { interfaceId: id } })\n  await RepositoryService.addHistoryLog({\n    entityId: itf.repositoryId,\n    entityType: Consts.ENTITY_TYPE.REPOSITORY,\n    changeLog: `接口 ${itf.name} (${itf.url}) 被删除，数据已备份。`,\n    userId: ctx.session.id,\n    relatedJSONData: JSON.stringify({ \"itf\": itf, \"properties\": properties })\n  })\n  let result = await Interface.destroy({ where: { id } })\n  await Property.destroy({ where: { interfaceId: id } })\n  ctx.body = {\n    data: result,\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  const id = +ctx.query.id\n  let itf = await Interface.findByPk(id, { paranoid: false })\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'delete',\n    repositoryId: itf.repositoryId,\n    moduleId: itf.moduleId,\n    interfaceId: itf.id,\n  })\n})\n\nrouter.get('/__test__', async (ctx) => {\n  const itf = await Interface.findByPk(5331)\n  itf.name = itf.name + '+'\n  await itf.save()\n  ctx.body = {\n    data: itf.name\n  }\n})\n\nrouter.post('/interface/lock', async (ctx, next) => {\n  if (!ctx.session.id) {\n    ctx.body = Consts.COMMON_ERROR_RES.NOT_LOGIN\n    return\n  }\n\n  let { id } = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let itf = await Interface.findByPk(id, {\n    attributes: ['lockerId'],\n    include: [\n      QueryInclude.Locker,\n    ]\n  })\n  if (itf.lockerId) { // DONE 2.3 BUG 接口可能被不同的人重复锁定。如果已经被锁定，则忽略。\n    ctx.body = {\n      data: itf.locker,\n    }\n    return\n  }\n\n  await Interface.update({ lockerId: ctx.session.id }, { where: { id } })\n  itf = await Interface.findByPk(id, {\n    attributes: ['lockerId'],\n    include: [\n      QueryInclude.Locker,\n    ]\n  })\n  ctx.body = {\n    data: itf.locker,\n  }\n  return next()\n})\n\nrouter.post('/interface/unlock', async (ctx) => {\n  if (!ctx.session.id) {\n    ctx.body = Consts.COMMON_ERROR_RES.NOT_LOGIN\n    return\n  }\n\n  let { id } = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let itf = await Interface.findByPk(id, { attributes: ['lockerId'] })\n  if (itf.lockerId !== ctx.session.id) { // DONE 2.3 BUG 接口可能被其他人解锁。如果不是同一个用户，则忽略。\n    ctx.body = {\n      isOk: false,\n      errMsg: '您不是锁定该接口的用户，无法对其解除锁定状态。请刷新页面。',\n    }\n    return\n  }\n  await Interface.update({\n    // tslint:disable-next-line:no-null-keyword\n    lockerId: null,\n  }, {\n    where: { id }\n  })\n\n  ctx.body = {\n    data: {\n      isOk: true,\n    }\n  }\n})\n\nrouter.post('/interface/sort', async (ctx) => {\n  let { ids } = ctx.request.body\n  let counter = 1\n  for (let index = 0; index < ids.length; index++) {\n    await Interface.update({ priority: counter++ }, {\n      where: { id: ids[index] }\n    })\n  }\n  ctx.body = {\n    data: ids.length,\n  }\n})\n\nrouter.get('/property/count', async (ctx) => {\n  ctx.body = {\n    data: 0\n  }\n})\n\nrouter.get('/property/list', async (ctx) => {\n  let where: any = {}\n  let { repositoryId, moduleId, interfaceId, name } = ctx.query\n  if (repositoryId) where.repositoryId = repositoryId\n  if (moduleId) where.moduleId = moduleId\n  if (interfaceId) where.interfaceId = interfaceId\n  if (name) where.name = { [Op.like]: `%${name}%` }\n  ctx.body = {\n    data: await Property.findAll({ where }),\n  }\n})\n\nrouter.get('/property/get', async (ctx) => {\n  const id = +ctx.query.id\n  ctx.body = {\n    data: await Property.findByPk(id, {\n      attributes: { exclude: [] }\n    })\n  }\n})\n\nrouter.post('/property/create', isLoggedIn, async (ctx) => {\n  let creatorId = ctx.session.id\n  let body = Object.assign(ctx.request.body, { creatorId })\n  let created = await Property.create(body)\n  ctx.body = {\n    data: await Property.findByPk(created.id, {\n      attributes: { exclude: [] }\n    })\n  }\n})\n\nrouter.post('/property/update', isLoggedIn, async (ctx) => {\n  let properties = ctx.request.body // JSON.parse(ctx.request.body)\n  properties = Array.isArray(properties) ? properties : [properties]\n  let result = 0\n  for (let item of properties) {\n    let property = _.pick(item, Object.keys(Property.rawAttributes))\n    let affected = await Property.update(property, {\n      where: { id: property.id },\n    })\n    result += affected[0]\n  }\n  ctx.body = {\n    data: result,\n  }\n})\n\nrouter.post('/properties/update', isLoggedIn, async (ctx, next) => {\n  const itfId = +ctx.query.itf\n  let needBackup = false\n  let changeCount = 0\n  let { properties, summary } = ctx.request.body as { properties: Property[], summary: Interface }\n  properties = Array.isArray(properties) ? properties : [properties]\n\n  let itf = await Interface.findByPk(itfId)\n\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_SET, ctx.session.id, itfId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  if (summary.bodyOption) {\n    itf.bodyOption = summary.bodyOption\n    await itf.save()\n  }\n\n\n  const itfPropertiesChangeLog: string[] = []\n\n  // 删除不在更新列表中的属性\n  // DONE 2.2 清除幽灵属性：子属性的父属性不存在（原因：前端删除父属性后，没有一并删除后代属性，依然传给了后端）\n  // SELECT * FROM properties WHERE parentId!=-1 AND parentId NOT IN (SELECT id FROM properties)\n  /* 查找和删除脚本\n    SELECT * FROM properties\n      WHERE\n        deletedAt is NULL AND\n        parentId != - 1 AND\n        parentId NOT IN (\n          SELECT * FROM (\n            SELECT id FROM properties WHERE deletedAt IS NULL\n          ) as p\n        )\n  */\n\n  const pLog = (p: Property, title: string) => `\\`${title}\\`${p.scope === 'request' ? '请求' : '响应'}参数\\`${p.name}\\`${p.description ? '(' + p.description + ')' : ''}`\n\n  const existingProperties = properties.filter((item: any) => !item.memory)\n  const existingPropertyIds = existingProperties.map(x => x.id)\n\n  const originalProperties = await Property.findAll({ where: { interfaceId: itfId } })\n\n  const backupJSON = JSON.stringify({ \"itf\": itf, \"properties\": originalProperties })\n\n  const deletedProperties = originalProperties.filter(x => existingPropertyIds.indexOf(x.id) === -1)\n\n  const deletedPropertyLog: string[] = []\n  for (const deletedProperty of deletedProperties) {\n    deletedPropertyLog.push(pLog(deletedProperty, '删除了'))\n  }\n  changeCount += deletedProperties.length\n  deletedPropertyLog.length && itfPropertiesChangeLog.push(deletedPropertyLog.join(LOG_SUB_SEPERATOR))\n\n  let result = await Property.destroy({\n    where: {\n      id: { [Op.notIn]: existingProperties.map((item: any) => item.id) },\n      interfaceId: itfId\n    }\n  })\n\n  const updatedPropertyLog: string[] = []\n  // 更新已存在的属性\n  for (let item of existingProperties) {\n    const changed: string[] = []\n    const o = originalProperties.filter(x => x.id === item.id)[0]\n    if (o) {\n      if (o.name !== item.name) {\n        changed.push(`变量名${o.name} => ${item.name}`)\n      }\n      // mock rules 不记入日志\n      if (o.type !== item.type) {\n        changed.push(`类型${o.type} => ${item.type}`)\n      }\n      changed.length && updatedPropertyLog.push(`${pLog(item, '更新了')} ${changed.join(' ')}`)\n      changeCount += changed.length\n    }\n    let affected = await Property.update(item, {\n      where: { id: item.id },\n    })\n    result += affected[0]\n  }\n  updatedPropertyLog.length && itfPropertiesChangeLog.push(updatedPropertyLog.join(LOG_SUB_SEPERATOR))\n  // 插入新增加的属性\n  let newProperties = properties.filter((item: any) => item.memory)\n  let memoryIdsMap: any = {}\n  const addedPropertyLog: string[] = []\n  for (let item of newProperties) {\n    let created = await Property.create(Object.assign({}, item, {\n      id: undefined,\n      parentId: -1,\n      priority: item.priority || Date.now()\n    }))\n    addedPropertyLog.push(pLog(item, '新增了'))\n    memoryIdsMap[item.id] = created.id\n    item.id = created.id\n    result += 1\n  }\n  changeCount += newProperties.length\n  addedPropertyLog.length && itfPropertiesChangeLog.push(addedPropertyLog.join(LOG_SUB_SEPERATOR))\n  // 同步 parentId\n  for (let item of newProperties) {\n    let parentId = memoryIdsMap[item.parentId] || item.parentId\n    await Property.update({ parentId }, {\n      where: { id: item.id },\n    })\n  }\n  itf = await Interface.findByPk(itfId, {\n    include: (QueryInclude.RepositoryHierarchy as any).include[0].include,\n  })\n\n  if (changeCount >= 5) {\n    needBackup = true\n  }\n\n  if (itfPropertiesChangeLog.length) {\n    await RepositoryService.addHistoryLog({\n      entityId: itf.id,\n      entityType: Consts.ENTITY_TYPE.INTERFACE,\n      changeLog: `接口 ${itf.name}(${itf.url}) 参数变更： ${itfPropertiesChangeLog.join(LOG_SEPERATOR)}${needBackup ? ', 改动较大已备份数据。' : ''}`,\n      userId: ctx.session.id,\n      ...needBackup ? { relatedJSONData: backupJSON } : {},\n    })\n  }\n\n  ctx.body = {\n    data: {\n      result,\n      properties: itf.properties,\n    }\n  }\n  return next()\n}, async (ctx) => {\n  if (ctx.body.data === 0) return\n  let itf = await Interface.findByPk(ctx.query.itf as string, {\n    attributes: { exclude: [] }\n  })\n  await Logger.create({\n    userId: ctx.session.id,\n    type: 'update',\n    repositoryId: itf.repositoryId,\n    moduleId: itf.moduleId,\n    interfaceId: itf.id,\n  })\n})\n\nrouter.get('/property/remove', isLoggedIn, async (ctx) => {\n  let { id } = ctx.query\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.PROPERTY_SET, ctx.session.id, +id)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  ctx.body = {\n    data: await Property.destroy({\n      where: { id },\n    }),\n  }\n})\n\nrouter.post('/repository/import', isLoggedIn, async (ctx) => {\n  const { docUrl, orgId, version, projectData } = ctx.request.body\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.ORGANIZATION_SET, ctx.session.id, orgId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  let success = false\n  let message = ''\n  try {\n    if (+version === 3) {\n      await MigrateService.importRepoFromJSON(JSON5.parse(projectData).data, ctx.session.id, true, orgId)\n      success = true\n    } else {\n      success = await MigrateService.importRepoFromRAP1DocUrl(orgId, ctx.session.id, docUrl, +version, projectData)\n    }\n  } catch (ex) {\n    success = false\n    message = ex.message\n  }\n  ctx.body = {\n    isOk: success,\n    message: success ? '导入成功' : `导入失败：${message}`,\n  }\n})\n\nrouter.post('/repository/importswagger', isLoggedIn, async (ctx) => {\n  const { orgId, repositoryId, swagger, version = 1, mode = 'manual' } = ctx.request.body\n  // 权限判断\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, repositoryId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  const result = await MigrateService.importRepoFromSwaggerDocUrl(orgId, ctx.session.id, swagger, version, mode, repositoryId)\n\n  ctx.body = {\n    isOk: result.code,\n    message: result.code === 'success' ? '导入成功' : '导入失败',\n    repository: {\n      id: 1,\n    }\n  }\n})\n\nrouter.post('/repository/importRAP2Backup', isLoggedIn, async (ctx) => {\n  const { repositoryId, swagger, modId } = ctx.request.body\n  // 权限判断\n  if (!await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, repositoryId)) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n\n  try {\n    await MigrateService.importInterfaceFromJSON(swagger, ctx.session.id, repositoryId, modId)\n    ctx.body = {\n      isOk: 'success',\n      message: '导入成功',\n      repository: {\n        id: 1,\n      }\n    }\n  } catch (ex) {\n    ctx.body = {\n      isOk: 'failure',\n      message: `导入失败: ${ex.message}`,\n    }\n  }\n})\n\nrouter.post('/repository/importJSON', isLoggedIn, async ctx => {\n  const { data } = ctx.request.body\n\n  if (!(await AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, ctx.session.id, data.id))) {\n    ctx.body = Consts.COMMON_ERROR_RES.ACCESS_DENY\n    return\n  }\n  try {\n    await MigrateService.importRepoFromJSON(data, ctx.session.id)\n    ctx.body = {\n      isOk: true,\n      repository: {\n        id: data.id,\n      },\n    }\n  } catch (error) {\n    ctx.body = {\n      isOk: false,\n      message: '服务器错误，导入失败'\n    }\n    throw (error)\n  }\n\n\n})\n\nrouter.get('/:type/history/:itfId', isLoggedIn, async ctx => {\n  const pager: IPager = {\n    limit: +ctx.query.limit || 10,\n    offset: +ctx.query.offset || 0,\n  }\n  let type: ENTITY_TYPE\n  if (ctx.params.type === 'interface') {\n    type = ENTITY_TYPE.INTERFACE\n  } else if (ctx.params.type === 'repository') {\n    type = ENTITY_TYPE.REPOSITORY\n  } else {\n    ctx.body = {\n      isOk: false,\n      errMsg: 'error path',\n    }\n    return\n  }\n  ctx.body = {\n    isOk: true,\n    data: await RepositoryService.getHistoryLog(+ctx.params.itfId, type, pager)\n  }\n})\n\nrouter.get('/interface/history/JSONData/:id', isLoggedIn, async ctx => {\n  const historyLogId = +ctx.params.id\n  ctx.set('Content-disposition', `attachment; filename=history_log_detail_data_${historyLogId}`)\n  ctx.set('Content-type', 'text/html; charset=UTF-8')\n  ctx.body = await RepositoryService.getHistoryLogJSONData(historyLogId)\n})\n\nrouter.get('/interface/backup/JSONData/:id', isLoggedIn, async ctx => {\n  const itfId = +ctx.params.id\n  ctx.set('Content-disposition', `attachment; filename=interface_backup_${itfId}`)\n  ctx.set('Content-type', 'text/html; charset=UTF-8')\n  ctx.body = await RepositoryService.getInterfaceJSONData(itfId)\n})\n"
  },
  {
    "path": "src/routes/router.ts",
    "content": "import * as Router from 'koa-router'\nimport { DefaultState, DefaultContext } from \"koa\"\nimport config from '../config'\n\nlet router = new Router<DefaultState, DefaultContext>({prefix: config.serve.path})\n\n// index\nrouter.get('/', (ctx) => {\n  ctx.body = 'Hello RAP!'\n})\n\n// env\nrouter.get('/env', (ctx) => {\n  ctx.body = process.env.NODE_ENV\n})\n\n// fix preload\nrouter.get('/check.node', (ctx) => {\n  ctx.body = 'success'\n})\nrouter.get('/status.taobao', (ctx) => {\n  ctx.body = 'success'\n})\nrouter.get('/test/test.status', (ctx) => {\n  ctx.body = 'success'\n})\n\n// proxy\nrouter.get('/proxy', async(ctx) => {\n  let { target } = ctx.query\n  console.log(`      <=> ${target}`)\n  let json = await fetch(target as string).then(res => res.json())\n  ctx.type = 'json'\n  ctx.body = json\n})\n\nexport default router"
  },
  {
    "path": "src/routes/utils/access.ts",
    "content": "import OrganizationService from '../../service/organization'\nimport RepositoryService from '../../service/repository'\nimport { Module, Interface, Property } from '../../models'\n\nexport enum ACCESS_TYPE {\n  ORGANIZATION_GET,\n  ORGANIZATION_SET,\n  REPOSITORY_GET,\n  REPOSITORY_SET,\n  MODULE_GET,\n  MODULE_SET,\n  INTERFACE_GET,\n  INTERFACE_SET,\n  PROPERTY_GET,\n  PROPERTY_SET,\n  USER,\n  ADMIN,\n}\nconst inTestMode = process.env.TEST_MODE === 'true'\n\nexport class AccessUtils {\n  public static async canUserAccess(\n    accessType: ACCESS_TYPE,\n    curUserId: number,\n    entityId: number,\n    token?: string,\n  ): Promise<boolean> {\n    // 测试模式无权限\n    if (inTestMode) {\n      return true\n    }\n\n    // 无 session 且无 toeken 时拒绝访问\n    if (!curUserId && !token) {\n      return false\n    }\n\n    if (\n      accessType === ACCESS_TYPE.ORGANIZATION_GET ||\n      accessType === ACCESS_TYPE.ORGANIZATION_SET\n    ) {\n      return OrganizationService.canUserAccessOrganization(curUserId, entityId)\n    } else if (\n      accessType === ACCESS_TYPE.REPOSITORY_GET ||\n      accessType === ACCESS_TYPE.REPOSITORY_SET\n    ) {\n      return RepositoryService.canUserAccessRepository(curUserId, entityId, token)\n    } else if (accessType === ACCESS_TYPE.MODULE_GET || accessType === ACCESS_TYPE.MODULE_SET) {\n      const mod = await Module.findByPk(entityId)\n      return RepositoryService.canUserAccessRepository(curUserId, mod.repositoryId, token)\n    } else if (\n      accessType === ACCESS_TYPE.INTERFACE_GET ||\n      accessType === ACCESS_TYPE.INTERFACE_SET\n    ) {\n      const itf = await Interface.findByPk(entityId)\n      return RepositoryService.canUserAccessRepository(curUserId, itf.repositoryId, token)\n    } else if (accessType === ACCESS_TYPE.PROPERTY_GET || accessType === ACCESS_TYPE.PROPERTY_SET) {\n      const p = await Property.findByPk(entityId)\n      return RepositoryService.canUserAccessRepository(curUserId, p.repositoryId, token)\n    }\n    return false\n  }\n\n  public static isAdmin(curUserId: number) {\n    if (inTestMode) {\n      return true\n    }\n    return curUserId === 1\n  }\n}\n"
  },
  {
    "path": "src/routes/utils/const.ts",
    "content": "export enum COMMON_MSGS {\n  ACCESS_DENY = '对不起，您没有访问该数据的权限。 Sorry, you have no access to visit this data.',\n}\n\nexport const COMMON_ERROR_RES = {\n  ERROR_PARAMS: { isOk: false, errMsg: '参数错误' },\n  ACCESS_DENY: { isOk: false, errMsg: '您没有访问权限' },\n  NOT_LOGIN: { isOk: false, errMsg: '您未登陆，或登陆状态过期。请登陆后重试' },\n}\n\nexport enum DATE_CONST {\n  SECOND = 1000,\n  MINUTE = 1000 * 60,\n  HOUR = 1000 * 60 * 60,\n  DAY = 1000 * 60 * 60 * 24,\n  MONTH = 1000 * 60 * 60 * 24 * 30,\n  YEAR = 1000 * 60 * 60 * 24 * 365,\n}\n\n\nexport enum ENTITY_TYPE {\n  REPOSITORY = 0,\n  INTERFACE = 1,\n  PARAMETER = 2,\n}\n\nexport enum THEME_TEMPLATE_KEY {\n  INDIGO = 'INDIGO', // DEFAULT\n  RED = 'RED',\n  BLACK = 'BLACK',\n  BLUE = 'BLUE',\n  GREEN = 'GREEN',\n  PINK = 'PINK',\n  ORANGE = 'ORANGE',\n  PURPLE = 'PURPLE',\n  CYAN = 'CYAN',\n}\n\nexport enum BODY_OPTION {\n  FORM_DATA = 'FORM_DATA',\n  FORM_URLENCODED = 'FORM_URLENCODED',\n  RAW = 'RAW',\n  BINARY = 'BINARY',\n}"
  },
  {
    "path": "src/routes/utils/helper.ts",
    "content": "import { Module, Interface, Property } from '../../models'\nimport { Repository } from '../../models'\n\nconst genExampleModule = (extra: any) => Object.assign({\n  name: '示例模块',\n  description: '示例模块',\n  creatorId: undefined,\n  repositoryId: undefined,\n}, extra)\nconst genExampleInterface = (extra: any) => Object.assign({\n  name: '示例接口',\n  url: `/example/${Date.now()}`,\n  method: 'GET',\n  description: '示例接口描述',\n  creatorId: undefined,\n  lockerId: undefined,\n  moduleId: undefined,\n  repositoryId: undefined,\n}, extra)\nconst genExampleProperty = (extra: any) => Object.assign({\n  scope: undefined,\n  name: 'foo',\n  type: 'String',\n  rule: '',\n  value: '@ctitle',\n  description: ({ request: '请求属性示例', response: '响应属性示例' } as any)[extra.scope],\n  parentId: -1,\n  creatorId: undefined,\n  interfaceId: undefined,\n  moduleId: undefined,\n  repositoryId: undefined,\n}, extra)\n\n// 初始化仓库\nconst initRepository = async (repository: Repository) => {\n  let mod = await Module.create(genExampleModule({\n    creatorId: repository.creatorId,\n    repositoryId: repository.id,\n  }))\n  await initModule(mod)\n}\n// 初始化模块\nconst initModule = async (mod: Module) => {\n  let itf = await Interface.create(genExampleInterface({\n    creatorId: mod.creatorId,\n    moduleId: mod.id,\n    repositoryId: mod.repositoryId,\n  }))\n  await initInterface(itf)\n}\n// 初始化接口\nconst initInterface = async (itf: Interface) => {\n  let { creatorId, repositoryId, moduleId } = itf\n  let interfaceId = itf.id\n  await Property.create(genExampleProperty({\n    scope: 'request',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  // TODO 2.1 完整的 Mock 示例：无法模拟所有 Mock 规则\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'string',\n    type: 'String',\n    rule: '1-10',\n    value: '★',\n    description: '字符串属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'number',\n    type: 'Number',\n    rule: '1-100',\n    value: '1',\n    description: '数字属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'boolean',\n    type: 'Boolean',\n    rule: '1-2',\n    value: 'true',\n    description: '布尔属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'regexp',\n    type: 'RegExp',\n    rule: '',\n    value: '/[a-z][A-Z][0-9]/',\n    description: '正则属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'function',\n    type: 'Function',\n    rule: '',\n    value: '() => Math.random()',\n    description: '函数属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  let array = await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'array',\n    type: 'Array',\n    rule: '1-10',\n    value: '',\n    description: '数组属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'foo',\n    type: 'Number',\n    rule: '+1',\n    value: 1,\n    description: '数组元素示例',\n    parentId: array.id,\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'bar',\n    type: 'String',\n    rule: '1-10',\n    value: '★',\n    description: '数组元素示例',\n    parentId: array.id,\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'items',\n    type: 'Array',\n    rule: '',\n    value: `[1, true, 'hello', /\\\\w{10}/]`,\n    description: '自定义数组元素示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  let object = await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'object',\n    type: 'Object',\n    rule: '',\n    value: '',\n    description: '对象属性示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'foo',\n    type: 'Number',\n    rule: '+1',\n    value: 1,\n    description: '对象属性示例',\n    parentId: object.id,\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'bar',\n    type: 'String',\n    rule: '1-10',\n    value: '★',\n    description: '对象属性示例',\n    parentId: object.id,\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n  await Property.create(genExampleProperty({\n    scope: 'response',\n    name: 'placeholder',\n    type: 'String',\n    rule: '',\n    value: '@title',\n    description: '占位符示例',\n    creatorId,\n    repositoryId,\n    moduleId,\n    interfaceId,\n  }))\n}\n\nexport {\n  genExampleModule,\n  genExampleInterface,\n  initRepository,\n  initModule,\n  initInterface,\n}\n"
  },
  {
    "path": "src/routes/utils/pagination.ts",
    "content": "/*\n    Pagination\n\n    Pure paging implementation reference.\n    纯粹的分页参考实现。\n\n    属性\n        data        数据\n        total       总条数\n        cursor      当前页数，第几页，从 1 开始计算\n        limit       分页大小\n        pages       总页数\n        start       当前页的起始下标\n        end         当前页的结束下标\n        hasPrev     是否有前一页\n        hasNext     是否有下一页\n        hasFirst    是否有第一页\n        hasLast     是否有最后一页\n        prev        前一页\n        next        后一页\n        first       第一页\n        last        最后一页\n        focus       当前页的当前焦点下标\n    方法\n        calc()              计算分页状态，当属性值发生变化时，方法 calc() 被调用。\n        moveTo(cursor)      移动到指定页\n        moveToPrev()        移动到前一页\n        moveToNext()        移动到下一页\n        moveToFirst()       移动到第一页\n        moveToLast()        移动到最后一页\n        fetch(arr)          获取当前页的数据，或者用当前状态获取参数 arr 的子集\n        setData(data)       更新数据集合\n        setTotal(total)     更新总条数\n        setCursor(cursor)   更新当前页数\n        setFocus(focus)     设置当前焦点\n        setLimit(limit)     设置分页大小\n        get(focus)          获取一条数据\n        toString()          友好打印\n        toHTML(url)         生成分页栏\n\n*/\n\n/*\n    new Pagination( data, cursor, limit )\n    new Pagination( total, cursor, limit )\n*/\n\nexport default class Pagination {\n  public data: any\n  public total: any\n  public cursor: number\n  public limit: number\n  public focus: number\n  public pages: any\n  public start: any\n  public end: any\n  public hasPrev: any\n  public hasNext: any\n  public hasFirst: any\n  public hasLast: any\n  public prev: any\n  public next: any\n  public first: any\n  public last: any\n\n  constructor(data: any, cursor: any, limit: any) {\n    this.data = (typeof data === 'number' || typeof data === 'string') ? undefined : data\n    this.total = this.data ? this.data.length : parseInt(data, 10)\n    this.cursor = parseInt(cursor, 10)\n    this.limit = parseInt(limit, 10)\n    this.calc()\n  }\n\n  public calc() {\n    if (this.total && parseInt(this.total, 10) > 0) {\n      this.limit = this.limit < 1 ? 1 : this.limit\n\n      this.pages = (this.total % this.limit === 0) ? this.total / this.limit : this.total / this.limit + 1\n      this.pages = parseInt(this.pages, 10)\n      this.cursor = (this.cursor > this.pages) ? this.pages : this.cursor\n      this.cursor = (this.cursor < 1) ? this.pages > 0 ? 1 : 0 : this.cursor\n\n      this.start = (this.cursor - 1) * this.limit\n      this.start = (this.start < 0) ? 0 : this.start // 从 0 开始计数\n      this.end = (this.start + this.limit > this.total) ? this.total : this.start + this.limit\n      this.end = (this.total < this.limit) ? this.total : this.end\n\n      this.hasPrev = (this.cursor > 1)\n      this.hasNext = (this.cursor < this.pages)\n      this.hasFirst = this.hasPrev\n      this.hasLast = this.hasNext\n\n      this.prev = this.hasPrev ? this.cursor - 1 : 0\n      this.next = this.hasNext ? this.cursor + 1 : 0\n      this.first = this.hasFirst ? 1 : 0\n      this.last = this.hasLast ? this.pages : 0\n\n      this.focus = this.focus ? this.focus : 0\n      this.focus = this.focus % this.limit + this.start\n      this.focus = this.focus > this.end - 1 ? this.end - 1 : this.focus\n    } else {\n      this.pages = this.cursor = this.start = this.end = 0\n      this.hasPrev = this.hasNext = this.hasFirst = this.hasLast = false\n      this.prev = this.next = this.first = this.last = 0\n      this.focus = 0\n    }\n\n    return this\n  }\n\n  public moveTo(cursor: any) {\n    this.cursor = parseInt(cursor, 10)\n    return this.calc()\n  }\n\n  public moveToPrev() {\n    return this.moveTo(this.cursor - 1)\n  }\n\n  public moveToNext() {\n    return this.moveTo(this.cursor + 1)\n  }\n\n  public moveToFirst() {\n    return this.moveTo(1)\n  }\n\n  public moveToLast() {\n    return this.moveTo(this.pages)\n  }\n\n  public fetch(arr: any) {\n    return (arr || this.data).slice(this.start, this.end)\n  }\n\n  public setData(data: any) {\n    this.data = data\n    this.total = data.length\n    return this.calc()\n  }\n\n  public setTotal(total: any) {\n    this.total = parseInt(total, 10)\n    return this.calc()\n  }\n\n  public setCursor(cursor: any) {\n    this.cursor = parseInt(cursor, 10)\n    return this.calc()\n  }\n\n  public setFocus(focus: any) {\n    this.focus = parseInt(focus, 10)\n    if (this.focus < 0) this.focus += this.total\n    if (this.focus >= this.total) this.focus -= this.total\n    this.cursor = parseInt(String(this.focus / this.limit), 10) + 1\n    return this.calc()\n  }\n\n  public setLimit(limit: any) {\n    this.limit = parseInt(limit, 10)\n    return this.calc()\n  }\n\n  public get(focus: any) {\n    if (focus !== undefined) return this.data[focus % this.data.length]\n    else return this.data[this.focus]\n  }\n\n  public toString() {\n    return JSON.stringify(this, undefined, 4)\n  }\n\n  public to = this.moveTo\n  public toPrev = this.moveToPrev\n  public toNext = this.moveToNext\n  public toFirst = this.moveToFirst\n  public toLast = this.moveToLast\n}"
  },
  {
    "path": "src/routes/utils/tree.ts",
    "content": "import { Property } from '../../models'\nimport * as _ from 'underscore'\nconst { VM } = require('vm2')\nimport * as Mock from 'mockjs'\nconst { RE_KEY } = require('mockjs/src/mock/constant')\n\nexport default class Tree {\n  public static ArrayToTree(list: Property[]) {\n    let result: any = {\n      name: 'root',\n      children: [],\n      depth: 0,\n    }\n\n    let mapped: any = {}\n    list.forEach(item => {\n      mapped[item.id] = item\n    })\n\n    function _parseChildren(parentId: any, children: any, depth: any) {\n      for (let id in mapped) {\n        let item = mapped[id]\n        if (typeof parentId === 'function' ? parentId(item.parentId) : item.parentId === parentId) {\n          children.push(item)\n          item.depth = depth + 1\n          item.children = _parseChildren(item.id, [], item.depth)\n        }\n      }\n      return children\n    }\n\n    _parseChildren(\n      (parentId: number) => {\n        // 忽略 parentId 为 0 的根属性（历史遗留），现为 -1\n        if (parentId === -1) return true\n        return false\n      },\n      result.children,\n      result.depth,\n    )\n\n    return result\n  }\n\n  // TODO 2.x 和前端重复了\n  public static TreeToTemplate(tree: any) {\n    const vm = new VM({\n      sandbox: {},\n      timeout: 3000\n    })\n    function parse(item: any, result: any) {\n      let rule = item.rule ? '|' + item.rule : ''\n      let value = item.value\n      if (\n        item.value &&\n        item.value.indexOf('[') === 0 &&\n        item.value.substring(item.value.length - 1) === ']' &&\n        !!rule\n      ) {\n        try {\n          result[item.name + rule] = vm.run(`(${item.value})`)\n        } catch (e) {\n          result[item.name + rule] = item.value\n        }\n      } else {\n        switch (item.type) {\n          case 'String':\n            result[item.name + rule] = item.value\n            break\n          case 'Number':\n            if (value === '') value = 1\n            let parsed = parseFloat(value)\n            if (!isNaN(parsed)) value = parsed\n            result[item.name + rule] = value\n            break\n          case 'Boolean':\n            if (value === 'true') value = true\n            if (value === 'false') value = false\n            if (value === '0') value = false\n            value = !!value\n            result[item.name + rule] = value\n            break\n          case 'Function':\n          case 'RegExp':\n            try {\n              result[item.name + rule] = vm.run('(' + item.value + ')')\n            } catch (e) {\n              console.warn(\n                `TreeToTemplate ${e.message}: ${item.type} { ${item.name}${rule}: ${item.value} }`,\n              ) // TODO 2.2 怎么消除异常值？\n              result[item.name + rule] = item.value\n            }\n            break\n          case 'Object':\n            if (item.value) {\n              try {\n                result[item.name + rule] = vm.run(`(${item.value})`)\n              } catch (e) {\n                result[item.name + rule] = item.value\n              }\n            } else {\n              result[item.name + rule] = {}\n              item.children.forEach((child: any) => {\n                parse(child, result[item.name + rule])\n              })\n            }\n            break\n          case 'Array':\n            if (item.value) {\n              try {\n                result[item.name + rule] = vm.run(`(${item.value})`)\n              } catch (e) {\n                result[item.name + rule] = item.value\n              }\n            } else {\n              result[item.name + rule] = item.children.length ? [{}] : []\n              item.children.forEach((child: any) => {\n                parse(child, result[item.name + rule][0])\n              })\n            }\n            break\n          case 'Null':\n            // tslint:disable-next-line: no-null-keyword\n            result[item.name + rule] = null\n            break\n        }\n      }\n    }\n    let result = {}\n    tree.children.forEach((child: any) => {\n      parse(child, result)\n    })\n    return result\n  }\n\n  public static TemplateToData(template: any) {\n    // 数据模板 template 中可能含有攻击代码，例如死循环，所以在沙箱中生成最终数据\n    // https://nodejs.org/dist/latest-v7.x/docs/api/vm.html\n    const vm = new VM({\n      sandbox: { mock: Mock.mock, template, },\n      timeout: 3000\n    })\n    try {\n      let data: any = vm.run('mock(template)')\n      let keys = Object.keys(data)\n      if (keys.length === 1 && keys[0] === '__root__') data = data.__root__\n      return data\n    } catch (err) {\n      console.error(err)\n      return {}\n    }\n  }\n\n  public static ArrayToTreeToTemplate(list: Property[]) {\n    let tree = Tree.ArrayToTree(list)\n    let template = Tree.TreeToTemplate(tree)\n    return template\n  }\n\n  public static ArrayToTreeToTemplateToData(list: Property[], extra?: any) {\n    let tree = Tree.ArrayToTree(list)\n    let template: { [key: string]: any } = Tree.TreeToTemplate(tree)\n    let data\n    const propertyMap: { [key: string]: Property } = {}\n    for (const p of list) {\n      propertyMap[p.name] = p\n    }\n    if (extra) {\n      // DONE 2.2 支持引用请求参数\n      let keys = Object.keys(template).map(item => item.replace(RE_KEY, '$1'))\n      let extraKeys = _.difference(Object.keys(extra), keys)\n      let scopedData = Tree.TemplateToData(Object.assign({}, _.pick(extra, extraKeys), template))\n\n      const recursivelyFillData = (node: any) => {\n        for (const key in node) {\n          if (!node.hasOwnProperty(key)) continue\n          let data = node[key]\n          if (_.isObject(data)) {\n            recursivelyFillData(data)\n            continue\n          }\n          for (const eKey in extra) {\n            if (!extra.hasOwnProperty(eKey)) continue\n            const pattern = new RegExp(`\\\\$${eKey}\\\\$`, 'g')\n            if (data && pattern.test(data)) {\n              let result = data.replace(pattern, extra[eKey])\n              const p = propertyMap[key]\n              if (p) {\n                if (p.type === 'Number') {\n                  result = +result || 1\n                } else if (p.type === 'Boolean') {\n                  result = result === 'true' || !!+result\n                }\n              }\n              data = node[key] = result\n            }\n          }\n        }\n      }\n\n      recursivelyFillData(scopedData)\n      data = _.pick(scopedData, keys)\n    } else {\n      data = Tree.TemplateToData(template)\n    }\n\n    return data\n  }\n\n  public static ArrayToTreeToTemplateToJSONSchema(list: Property[]) {\n    let tree = Tree.ArrayToTree(list)\n    let template = Tree.TreeToTemplate(tree)\n    let schema = Mock.toJSONSchema(template)\n    return schema\n  }\n\n  // TODO 2.2 执行 JSON.stringify() 序列化时会丢失正则和函数。需要转为字符串或者函数。\n  // X Function.protytype.toJSON = Function.protytype.toString\n  // X RegExp.protytype.toJSON = RegExp.protytype.toString\n  public static stringifyWithFunctonAndRegExp(json: object) {\n    return JSON.stringify(\n      json,\n      (k, v) => {\n        k\n        if (typeof v === 'function') return v.toString()\n        if (v !== undefined && v !== null && v.exec) return v.toString()\n        else return v\n      },\n      2,\n    )\n  }\n\n  // 把用户的 mock json 转换成 json-schema 再转换成 properties\n  public static jsonToArray(\n    json: any,\n    {\n      userId,\n      repositoryId,\n      moduleId,\n      interfaceId,\n      scope,\n    }: {\n      userId: number\n      repositoryId: number\n      moduleId: number\n      interfaceId: number\n      scope: 'request' | 'response'\n    },\n  ) {\n    const isIncreamentNumberSequence = (numbers: any) =>\n      numbers.every(\n        (num: any) =>\n          typeof num === 'number' &&\n          ((num: any, i: number) => i === 0 || num - numbers[i - 1] === 1),\n      )\n    function isPrimitiveType(type: string) {\n      return ['number', 'null', 'undefined', 'boolean', 'string'].indexOf(type.toLowerCase()) > -1\n    }\n    function mixItemsProperties(items: any) {\n      // 合并 item properties 的 key，返回的 item 拥有导入 json 的所有 key\n      if (!items || !items.length) {\n        return {\n          properties: [],\n        }\n      } else if (items.length === 1) {\n        if (!items[0].properties) {\n          items[0].properties = []\n        }\n        return items[0]\n      } else {\n        const baseItem = items[0]\n        if (!baseItem.properties) {\n          baseItem.properties = []\n        }\n        const baseProperties = baseItem.properties\n        for (let i = 1; i < items.length; ++i) {\n          const item = items[i]\n          if (item.properties && item.properties.length) {\n            for (const p of item.properties) {\n              if (!baseProperties.find((e: any) => e.name === p.name)) {\n                baseProperties.push(p)\n              }\n            }\n          }\n        }\n        return baseItem\n      }\n    }\n    /** MockJS 的 toJSONSchema 的 bug 会导致有 length 属性的对象被识别成数组\n     *  众所周知 MockJS 已经不维护了，所以只能自己想想办法\n     *  先递归把 length 替换成其他的名称，生成 schema 后再换回来\n     */\n    const lengthAlias = '__mockjs_length_*#06#'\n\n    const replaceLength = (obj: any) => {\n      for (const k in obj) {\n        if (obj[k] && typeof obj[k] === 'object') {\n          replaceLength(obj[k])\n        } else {\n          // Do something with obj[k]\n          if (k === 'length') {\n            const v = obj[k]\n            delete obj[k]\n            obj[lengthAlias] = v\n          }\n        }\n      }\n    }\n    function handleJSONSchema(\n      schema: any,\n      parent = { id: -1 },\n      memoryProperties: any,\n      siblings?: any,\n    ) {\n      if (!schema) {\n        return\n      }\n      const hasSiblings = siblings instanceof Array && siblings.length > 0\n      // DONE 2.1 需要与 Mock 的 rule.type 规则统一，首字符小写，好烦！应该忽略大小写！\n      if (schema.name === lengthAlias) {\n        schema.name = 'length'\n      }\n      let type = schema.type[0].toUpperCase() + schema.type.slice(1)\n      let rule = ''\n      if (type === 'Array' && schema.items && schema.items.length > 1) {\n        rule = schema.items.length + ''\n      }\n      let value = /Array|Object/.test(type) ? '' : schema.template\n      if (schema.items && schema.items.length) {\n        const childType = schema.items[0].type\n        if (isPrimitiveType(childType)) {\n          value = JSON.stringify(schema.template)\n          rule = ''\n        }\n      } else if (hasSiblings && isPrimitiveType(type)) {\n        // 如果是简单数据可以在这里进行合并\n        const valueArr = siblings.map((s: any) => s && s.template)\n        if (_.uniq(valueArr).length > 1) {\n          // 只有在数组里有不同元素时再合并\n          if (isIncreamentNumberSequence(valueArr)) {\n            // 如果是递增数字序列特殊处理\n            value = valueArr[0]\n            rule = '+1'\n          } else {\n            // 比如 [{a:1},{a:2}]\n            // 我们可以用 type: Array rule: +1 value: [1,2] 进行还原\n            value = JSON.stringify(valueArr)\n            type = 'Array'\n            rule = '+1'\n          }\n        }\n      }\n\n      type Property = {\n        name: any\n        type: any\n        rule: string\n        value: any\n        descripton: string\n        creator: any\n        repositoryId: any\n        moduleId: any\n        interfaceId: any\n        scope: any\n        parentId: number\n        memory: boolean\n        id: any\n      }\n      const property: Property = Object.assign(\n        {\n          name: schema.name,\n          type,\n          rule,\n          value,\n          descripton: '',\n        },\n        {\n          creator: userId,\n          repositoryId: repositoryId,\n          moduleId: moduleId,\n          interfaceId,\n          scope,\n          parentId: parent.id,\n        },\n        {\n          memory: true,\n          id: _.uniqueId('memory-'),\n        },\n      )\n      memoryProperties.push(property)\n      if (schema.properties) {\n        schema.properties.forEach((item: any) => {\n          const childSiblings = hasSiblings\n            ? siblings.map(\n                (s: any) =>\n                  (s && s.properties && s.properties.find((p: any) => p && p.name === item.name)) || null,\n              )\n            : undefined\n          handleJSONSchema(item, property, memoryProperties, childSiblings)\n        })\n      }\n      mixItemsProperties(schema.items).properties.forEach((item: any) => {\n        const siblings = schema.items.map(\n          (o: any) => o.properties.find((p: any) => p.name === item.name) || null,\n        )\n        handleJSONSchema(item, property, memoryProperties, siblings)\n      })\n    }\n\n    if (JSON.stringify(json).indexOf('length') > -1) {\n      // 递归查找替换 length 是一个重操作，先进行一次字符串查找，发现存在 length 字符再进行\n      replaceLength(json)\n    }\n\n    if (json instanceof Array) {\n      json = { _root_: json }\n    }\n    const schema = Mock.toJSONSchema(json)\n    const memoryProperties: any = []\n    if (schema.properties) {\n      schema.properties.forEach((item: any) => handleJSONSchema(item, undefined, memoryProperties))\n    }\n\n    return memoryProperties\n  }\n}\n"
  },
  {
    "path": "src/routes/utils/url.ts",
    "content": "let pathToRegexp = require('path-to-regexp')\n\nexport default class UrlUtils {\n\n  public static getRelative = (url: string) => {\n    url = url.toLowerCase()\n    const prefixes = ['https://', 'http://']\n    for (let item of prefixes) {\n      if (url.indexOf(item) > -1) {\n        url = url.substring(item.length)\n        if (url.indexOf('/') > -1) {\n          url = url.substring(url.indexOf('/'))\n        } else {\n          url = '/'\n        }\n        break\n      }\n    }\n    if (url.indexOf('?') > -1) {\n      url = url.substring(0, url.indexOf('?'))\n    }\n    if (url[0] !== '/') url = '/' + url\n    return url\n  }\n\n  // 把 /pet/{id}/ 转换成 /pet/:id/\n  // https://regexr.com/537jp\n  public static convertBracePatternRestfulUrl(url: string) {\n    return url.replace(/\\/{([^}]+)}/g, '/:$1')\n  }\n\n  public static urlMatchesPattern = (url: string, pattern: string) => {\n    url = UrlUtils.getRelative(url)\n    pattern = UrlUtils.getRelative(pattern)\n    let re = pathToRegexp(pattern)\n    return re.test(url)\n  }\n\n  public static getUrlPattern = (pattern: string) => {\n    pattern = UrlUtils.getRelative(pattern)\n    pattern = UrlUtils.convertBracePatternRestfulUrl(pattern)\n    return pathToRegexp(pattern)\n  }\n\n}\n"
  },
  {
    "path": "src/scripts/app.ts",
    "content": "// const debug = true\nimport * as Koa from 'koa'\nimport * as session from 'koa-generic-session'\nimport * as redisStore from 'koa-redis'\nimport * as logger from 'koa-logger'\nimport * as serve from 'koa-static'\nimport * as cors from 'kcors'\nimport * as body from 'koa-body'\nimport router from '../routes'\nimport config from '../config'\nimport { startTask } from '../service/task'\n\nconst app = new Koa()\nlet appAny: any = app\nappAny.counter = { users: {}, mock: 0 }\n\napp.keys = config.keys\napp.use(\n  session({\n    // @ts-ignore\n    store: redisStore(config.redis)\n  })\n)\nif (process.env.NODE_ENV === 'development' && process.env.TEST_MODE !== 'true') app.use(logger())\napp.use(async (ctx, next) => {\n\n  ctx.set('Access-Control-Allow-Origin', '*')\n  ctx.set('Access-Control-Allow-Methods', 'POST, GET, PUT, DELETE, OPTIONS')\n  ctx.set('Access-Control-Allow-Credentials', 'true')\n  await next()\n  if (ctx.path === '/favicon.ico') return\n  ctx.session.views = (ctx.session.views || 0) + 1\n  let app: any = ctx.app\n  if (ctx.session.fullname) app.counter.users[ctx.session.fullname] = true\n})\napp.use(cors({\n  credentials: true,\n}))\napp.use(async (ctx, next) => {\n  await next()\n  if (typeof ctx.body === 'object' && ctx.body?.data !== undefined) {\n    ctx.type = 'json'\n    ctx.body = JSON.stringify(ctx.body, undefined, 2)\n  }\n})\napp.use(async (ctx, next) => {\n  await next()\n  if (ctx.request.query.callback) {\n    let body = typeof ctx.body === 'object' ? JSON.stringify(ctx.body, undefined, 2) : ctx.body\n    ctx.body = ctx.request.query.callback + '(' + body + ')'\n    ctx.type = 'application/x-javascript'\n  }\n})\n\napp.use(serve('public'))\napp.use(serve('test'))\napp.use(\n  body({\n    multipart: true,\n    formLimit: '10mb',\n    textLimit: '10mb',\n    jsonLimit: '10mb',\n  }),\n)\napp.use(router.routes())\n\nstartTask()\n\nexport default app"
  },
  {
    "path": "src/scripts/dev.ts",
    "content": "import config from '../config'\nimport app from './app'\n\nconst start = () => {\n  let execSync = require('child_process').execSync\n  let port = config.serve.port\n  let url = `http://localhost:${port}` // /api.html\n  let open = false\n  console.log('----------------------------------------')\n  app.listen(port, () => {\n    console.log(`rap2-delos is running as ${url}`)\n    if (!open) return\n    try {\n      execSync(`osascript openChrome.applescript ${url}`, { cwd: __dirname, stdio: 'ignore' })\n    } catch (e) {\n      execSync(`open ${url}`)\n    }\n  })\n}\n\nstart()\nexport {}\n"
  },
  {
    "path": "src/scripts/init/bo.ts",
    "content": "import { mock } from 'mockjs'\n\nconst scopes = ['request', 'response']\nconst methods = ['GET', 'POST', 'PUT', 'DELETE']\nconst types = ['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp', 'Null']\nconst values = ['@INT', '@FLOAT', '@TITLE', '@NAME']\n\nlet USER_ID = 100000000\nlet ORGANIZATION_ID = 1\nlet REPOSITORY_ID = 1\nlet MODULE_ID = 1\nlet INTERFACE_ID = 1\nlet PROPERTY_ID = 1\n\nexport const  BO_ADMIN =  { id: USER_ID++, fullname: 'admin', email: 'admin@rap2.com', password: 'admin' }\n\nexport const BO_MOZHI = { id: USER_ID++, fullname: '墨智', email: 'mozhi@rap2.com', password: 'mozhi' }\n\nexport const BO_USER_COUNT = 10\n\nexport const BO_USER_FN = () => mock({\n  id: USER_ID++,\n  fullname: '@cname',\n  email: '@email',\n  password: '@word(6)',\n})\n\nexport const BO_ORGANIZATION_COUNT = 3\n\nexport const BO_ORGANIZATION_FN = (source: any) => {\n  return Object.assign(\n    mock({\n      id: ORGANIZATION_ID++,\n      name: '组织@ctitle(5)',\n      description: '@cparagraph',\n      logo: '@url',\n      creatorId: undefined,\n      owner: undefined,\n      members: '',\n    }),\n    source,\n  )\n}\nexport const BO_REPOSITORY_COUNT = 3\n\nexport const BO_REPOSITORY_FN = (source: any) => {\n  return Object.assign(\n    mock({\n      id: REPOSITORY_ID++,\n      name: '仓库@ctitle',\n      description: '@cparagraph',\n      logo: '@url',\n    }),\n    source,\n  )\n}\n\nexport const BO_MODULE_COUNT = 3\nexport const BO_MODULE_FN = (source: any) => {\n  return Object.assign(\n    mock({\n      id: MODULE_ID++,\n      name: '模块@ctitle(4)',\n      description: '@cparagraph',\n      repositoryId: undefined,\n      creatorId: undefined,\n    }),\n    source,\n  )\n}\nexport const BO_INTERFACE_COUNT = 3\nexport const BO_INTERFACE_FN = (source: any) => {\n  return Object.assign(\n    mock({\n      id: INTERFACE_ID++,\n      name: '接口@ctitle(4)',\n      url: '/@word(5)/@word(5)/@word(5).json',\n      'method|1': methods,\n      description: '@cparagraph',\n      creatorId: undefined,\n      lockerId: undefined,\n      repositoryId: undefined,\n      moduleId: undefined,\n    }),\n    source,\n  )\n}\nexport const BO_PROPERTY_COUNT = 6\nexport const BO_PROPERTY_FN = (source: any) => {\n  return Object.assign(\n    mock({\n      id: PROPERTY_ID++,\n      'scope|1': scopes,\n      name: '@word(6)',\n      'type|1': types,\n      'value|1': values,\n      description: '@csentence',\n      creatorId: undefined,\n      repositoryId: undefined,\n      moduleId: undefined,\n      interfaceId: undefined,\n    }),\n    source,\n  )\n}"
  },
  {
    "path": "src/scripts/init/delos.ts",
    "content": "import sequelize from '../../models/sequelize'\nimport { User, Organization, Repository, Module, Interface, Property } from '../../models/index'\nimport { BO_ADMIN, BO_MOZHI } from './bo'\nimport { BO_USER_FN, BO_ORGANIZATION_FN, BO_REPOSITORY_FN, BO_MODULE_FN, BO_INTERFACE_FN, BO_PROPERTY_FN } from './bo'\nimport { BO_USER_COUNT, BO_ORGANIZATION_COUNT, BO_REPOSITORY_COUNT, BO_MODULE_COUNT, BO_INTERFACE_COUNT, BO_PROPERTY_COUNT } from './bo'\n\nconst EMPTY_WHERE = { where: {} }\n\nexport async function init () {\n  await sequelize.drop()\n  await sequelize.sync({\n    force: true,\n    logging: console.log,\n  })\n  await User.destroy(EMPTY_WHERE)\n  await Organization.destroy(EMPTY_WHERE)\n  await Repository.destroy(EMPTY_WHERE)\n  await Module.destroy(EMPTY_WHERE)\n  await Interface.destroy(EMPTY_WHERE)\n  await Property.destroy(EMPTY_WHERE)\n\n\n  // 用户\n  await User.create(BO_ADMIN)\n  await User.create(BO_MOZHI)\n  for (let i = 0; i < BO_USER_COUNT; i++) {\n    await User.create(BO_USER_FN())\n  }\n\n  let users = await User.findAll()\n\n  // 用户 admin 仓库\n  for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) {\n    let repository = await Repository.create(\n      BO_REPOSITORY_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id }),\n    )\n    await repository.$set('members', users.filter(user => user.id !== BO_ADMIN.id))\n    await initRepository(repository)\n  }\n\n  // 用户 mozhi 的仓库\n  for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) {\n    let repository = await Repository.create(\n      BO_REPOSITORY_FN({ creatorId: BO_MOZHI.id, ownerId: BO_MOZHI.id }),\n    )\n    await repository.$set('members', (\n      users.filter(user => user.id !== BO_MOZHI.id)\n    ))\n    await initRepository(repository)\n  }\n\n  // 团队\n  for (let BO_ORGANIZATION_INDEX = 0; BO_ORGANIZATION_INDEX < BO_ORGANIZATION_COUNT; BO_ORGANIZATION_INDEX++) {\n    let organization = await Organization.create(\n      BO_ORGANIZATION_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id }),\n    )\n    await organization.$set('members', (\n      users.filter(user => user.id !== BO_ADMIN.id)\n    ))\n    // 团队的仓库\n    for (let BO_REPOSITORY_INDEX = 0; BO_REPOSITORY_INDEX < BO_REPOSITORY_COUNT; BO_REPOSITORY_INDEX++) {\n      let repository = await Repository.create(\n        BO_REPOSITORY_FN({ creatorId: BO_ADMIN.id, ownerId: BO_ADMIN.id, organizationId: organization.id }),\n      )\n      await repository.$set('members', users.filter(user => user.id !== BO_ADMIN.id))\n      await initRepository(repository)\n    }\n  }\n}\n\nasync function initRepository (repository: any) {\n  // 模块\n  for (let BO_MODULE_INDEX = 0; BO_MODULE_INDEX < BO_MODULE_COUNT; BO_MODULE_INDEX++) {\n    let mod = await Module.create(\n      BO_MODULE_FN({ creatorId: repository.creatorId, repositoryId: repository.id }),\n    )\n    await repository.addModule(mod)\n    // 接口\n    for (let BO_INTERFACE_INDEX = 0; BO_INTERFACE_INDEX < BO_INTERFACE_COUNT; BO_INTERFACE_INDEX++) {\n      let itf = await Interface.create(\n        BO_INTERFACE_FN({ creatorId: mod.creatorId, repositoryId: repository.id, moduleId: mod.id }),\n      )\n      await mod.$add('interfaces', itf)\n      // 属性\n      for (let BO_PROPERTY_INDEX = 0; BO_PROPERTY_INDEX < BO_PROPERTY_COUNT; BO_PROPERTY_INDEX++) {\n        let prop = await Property.create(\n          BO_PROPERTY_FN({ creatorId: itf.creatorId, repositoryId: repository.id, moduleId: mod.id, interfaceId: itf.id }),\n        )\n        await itf.$add('properties', prop)\n      }\n    }\n  }\n}\n\nexport async function after () {\n  let exclude = ['password', 'createdAt', 'updatedAt', 'deletedAt']\n  let repositories = await Repository.findAll({\n    attributes: { exclude: [] },\n    include: [\n      { model: User, as: 'creator', attributes: { exclude }, required: true },\n      { model: User, as: 'owner', attributes: { exclude }, required: true },\n      { model: Organization, as: 'organization', attributes: { exclude }, required: false },\n      { model: User, as: 'locker', attributes: { exclude }, required: false },\n      { model: User, as: 'members', attributes: { exclude }, through: { attributes: [] }, required: true },\n      { model: Module,\n        as: 'modules',\n        attributes: { exclude },\n        // through: { attributes: [] },\n        include: [\n          {\n            model: Interface,\n            as: 'interfaces',\n            attributes: { exclude },\n            // through: { attributes: [] },\n            include: [\n              {\n                model: Property,\n                as: 'properties',\n                attributes: { exclude },\n                // through: { attributes: [] },\n                required: true,\n              },\n            ],\n            required: true,\n          },\n        ],\n        required: true,\n      },\n    ],\n    offset: 0,\n    limit: 100,\n  })\n  // console.log(JSON.stringify(repositories, null, 2))\n  console.log(repositories.map(item => item.toJSON()))\n\n  let admin = await User.findByPk(BO_ADMIN.id)\n  // for (let k in admin) console.log(k)\n  let owned: any = await admin.$get('ownedOrganizations')\n  console.log(owned.map((item: any) => item.toJSON()))\n\n  let mozhi = await User.findByPk(BO_MOZHI.id)\n  for (let k in mozhi) console.log(k)\n  let joined: any = await mozhi.$get('joinedOrganizations')\n  console.log(joined.map((item: any) => item.toJSON()))\n}"
  },
  {
    "path": "src/scripts/init/index.ts",
    "content": "import { init, after } from './delos'\n/**\n * initialize database\n */\nexport async function main () {\n  await init()\n  console.log('after init')\n  await after()\n  console.log('after after')\n}\n\nmain().then(() => {\n  console.log('Run create-db finished successfully.')\n  process.exit(0)\n})"
  },
  {
    "path": "src/scripts/initSchema.ts",
    "content": "import sequelize from \"../models/sequelize\"\n\nsequelize\n  .sync({\n    force: true,\n  })\n  .then(() => {\n    console.log(\"成功初始化 DB Schema\")\n    process.exit(0)\n  })\n  .catch(e => {\n    console.log(\"初始化 DB Schema 中遇到了错误\")\n    console.log(e)\n    process.exit(0)\n  })\n"
  },
  {
    "path": "src/scripts/openChrome.applescript",
    "content": "(*\nCopyright (c) 2015-present, Facebook, Inc.\nAll rights reserved.\n\nThis source code is licensed under the BSD-style license found in the\n-- LICENSE file in the root directory of this source tree. An additional grant\nof patent rights can be found in the PATENTS file in the same directory.\n*)\n\nproperty targetTab: null\nproperty targetTabIndex: -1\nproperty targetWindow: null\n\non run argv\n  set theURL to item 1 of argv\n\n  tell application \"Chrome\"\n\n    if (count every window) = 0 then\n      make new window\n    end if\n\n    -- 1: Looking for tab running debugger\n    -- then, Reload debugging tab if found\n    -- then return\n    set found to my lookupTabWithUrl(theURL)\n    if found then\n      set targetWindow's active tab index to targetTabIndex\n      tell targetTab to reload\n      tell targetWindow to activate\n      set index of targetWindow to 1\n      return\n    end if\n\n    -- 2: Looking for Empty tab\n    -- In case debugging tab was not found\n    -- We try to find an empty tab instead\n    set found to my lookupTabWithUrl(\"chrome://newtab/\")\n    if found then\n      set targetWindow's active tab index to targetTabIndex\n      set URL of targetTab to theURL\n      tell targetWindow to activate\n      return\n    end if\n\n    -- 3: Create new tab\n    -- both debugging and empty tab were not found\n    -- make a new tab with url\n    tell window 1\n      activate\n      make new tab with properties {URL:theURL}\n    end tell\n  end tell\nend run\n\n-- Function:\n-- Lookup tab with given url\n-- if found, store tab, index, and window in properties\n-- (properties were declared on top of file)\non lookupTabWithUrl(lookupUrl)\n  tell application \"Chrome\"\n    -- Find a tab with the given url\n    set found to false\n    set theTabIndex to -1\n    repeat with theWindow in every window\n      set theTabIndex to 0\n      repeat with theTab in every tab of theWindow\n        set theTabIndex to theTabIndex + 1\n        if (theTab's URL as string) contains lookupUrl then\n          -- assign tab, tab index, and window to properties\n          set targetTab to theTab\n          set targetTabIndex to theTabIndex\n          set targetWindow to theWindow\n          set found to true\n          exit repeat\n        end if\n      end repeat\n\n      if found then\n        exit repeat\n      end if\n    end repeat\n  end tell\n  return found\nend lookupTabWithUrl\n"
  },
  {
    "path": "src/scripts/rap2_delos.sql",
    "content": "-- MySQL dump 10.13  Distrib 5.7.12, for osx10.9 (x86_64)\n--\n-- Host: localhost    Database: RAP2_DELOS_APP_LOCAL\n-- ------------------------------------------------------\n-- Server version\t5.7.12\n\n/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;\n/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;\n/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;\n/*!40101 SET NAMES utf8 */;\n/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;\n/*!40103 SET TIME_ZONE='+00:00' */;\n/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;\n/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;\n/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;\n/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;\n\n--\n-- Table structure for table `interfaces`\n--\n\nDROP TABLE IF EXISTS `interfaces`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `interfaces` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `name` varchar(256) NOT NULL COMMENT '-',\n  `url` varchar(256) NOT NULL COMMENT '-',\n  `method` varchar(32) NOT NULL COMMENT '-',\n  `description` text COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `lockerId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_moduleId` (`moduleId`),\n  KEY `idx_creatorId` (`creatorId`),\n  KEY `idx_lockerId` (`lockerId`),\n  KEY `idx_repositoryId` (`repositoryId`),\n  CONSTRAINT `interfaces_ibfk_1` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `interfaces_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `interfaces_ibfk_3` FOREIGN KEY (`lockerId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `interfaces_ibfk_4` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='接口';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `loggers`\n--\n\nDROP TABLE IF EXISTS `loggers`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `loggers` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `type` varchar(32) NOT NULL COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `userId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `organizationId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `interfaceId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_userId` (`userId`),\n  KEY `idx_repositoryId` (`repositoryId`),\n  KEY `idx_organizationId` (`organizationId`),\n  KEY `idx_moduleId` (`moduleId`),\n  KEY `idx_interfaceId` (`interfaceId`),\n  CONSTRAINT `loggers_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `loggers_ibfk_2` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `loggers_ibfk_3` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `loggers_ibfk_4` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `loggers_ibfk_5` FOREIGN KEY (`interfaceId`) REFERENCES `interfaces` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='操作日志';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `modules`\n--\n\nDROP TABLE IF EXISTS `modules`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `modules` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `name` varchar(256) NOT NULL COMMENT '-',\n  `description` text COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_repositoryId` (`repositoryId`),\n  KEY `idx_creatorId` (`creatorId`),\n  CONSTRAINT `modules_ibfk_1` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `modules_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='模块';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `notifications`\n--\n\nDROP TABLE IF EXISTS `notifications`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `notifications` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `fromId` bigint(11) DEFAULT NULL COMMENT '-',\n  `toId` bigint(11) NOT NULL COMMENT '-',\n  `type` varchar(128) NOT NULL COMMENT '-',\n  `param1` varchar(128) DEFAULT NULL COMMENT '-',\n  `param2` varchar(128) DEFAULT NULL COMMENT '-',\n  `param3` varchar(128) DEFAULT NULL COMMENT '-',\n  `readed` tinyint(1) NOT NULL COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`)\n) COMMENT='消息';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `organizations`\n--\n\nDROP TABLE IF EXISTS `organizations`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `organizations` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `name` varchar(256) NOT NULL COMMENT '-',\n  `description` text COMMENT '-',\n  `logo` varchar(256) DEFAULT NULL COMMENT '-',\n  `visibility` tinyint(1) NOT NULL DEFAULT '1' COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `ownerId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_creatorId` (`creatorId`),\n  CONSTRAINT `organizations_ibfk_1` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='团队';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `repositories_collaborators`\n--\n\nDROP TABLE IF EXISTS `repositories_collaborators`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `repositories_collaborators` (\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned NOT NULL COMMENT '-',\n  `collaboratorId` bigint(11) unsigned NOT NULL COMMENT '-',\n  PRIMARY KEY (`repositoryId`,`collaboratorId`),\n  KEY `idx_collaboratorId` (`collaboratorId`),\n  CONSTRAINT `repositories_collaborators_ibfk_1` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n  CONSTRAINT `repositories_collaborators_ibfk_2` FOREIGN KEY (`collaboratorId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) COMMENT='协同仓库';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `organizations_members`\n--\n\nDROP TABLE IF EXISTS `organizations_members`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `organizations_members` (\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `userId` bigint(11) unsigned NOT NULL COMMENT '-',\n  `organizationId` bigint(11) unsigned NOT NULL COMMENT '-',\n  PRIMARY KEY (`userId`,`organizationId`),\n  KEY `idx_organizationId` (`organizationId`),\n  CONSTRAINT `organizations_members_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n  CONSTRAINT `organizations_members_ibfk_2` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) COMMENT='用户';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `properties`\n--\n\nDROP TABLE IF EXISTS `properties`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `properties` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `scope` varchar(32) NOT NULL DEFAULT 'response' COMMENT '-',\n  `name` varchar(256) NOT NULL COMMENT '-',\n  `type` varchar(32) NOT NULL COMMENT '-',\n  `rule` varchar(128) DEFAULT NULL COMMENT '-',\n  `value` text COMMENT '-',\n  `description` text COMMENT '-',\n  `parentId` bigint(11) NOT NULL DEFAULT '-1' COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `interfaceId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `moduleId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_interfaceId` (`interfaceId`),\n  KEY `idx_creatorId` (`creatorId`),\n  KEY `idx_moduleId` (`moduleId`),\n  KEY `idx_repositoryId` (`repositoryId`),\n  CONSTRAINT `properties_ibfk_1` FOREIGN KEY (`interfaceId`) REFERENCES `interfaces` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `properties_ibfk_2` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `properties_ibfk_3` FOREIGN KEY (`moduleId`) REFERENCES `modules` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `properties_ibfk_4` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='属性';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `repositories`\n--\n\nDROP TABLE IF EXISTS `repositories`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `repositories` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `name` varchar(256) NOT NULL COMMENT '-',\n  `description` text COMMENT '-',\n  `logo` varchar(256) DEFAULT NULL COMMENT '-',\n  `visibility` tinyint(1) NOT NULL DEFAULT '1' COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  `ownerId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `organizationId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `creatorId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  `lockerId` bigint(11) unsigned DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  KEY `idx_ownerId` (`ownerId`),\n  KEY `idx_organizationId` (`organizationId`),\n  KEY `idx_creatorId` (`creatorId`),\n  CONSTRAINT `repositories_ibfk_1` FOREIGN KEY (`ownerId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `repositories_ibfk_2` FOREIGN KEY (`organizationId`) REFERENCES `organizations` (`id`) ON DELETE SET NULL ON UPDATE CASCADE,\n  CONSTRAINT `repositories_ibfk_3` FOREIGN KEY (`creatorId`) REFERENCES `users` (`id`) ON DELETE SET NULL ON UPDATE CASCADE\n) COMMENT='仓库';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `repositories_members`\n--\n\nDROP TABLE IF EXISTS `repositories_members`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `repositories_members` (\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `userId` bigint(11) unsigned NOT NULL COMMENT '-',\n  `repositoryId` bigint(11) unsigned NOT NULL COMMENT '-',\n  PRIMARY KEY (`userId`,`repositoryId`),\n  KEY `idx_repositoryId` (`repositoryId`),\n  CONSTRAINT `repositories_members_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n  CONSTRAINT `repositories_members_ibfk_2` FOREIGN KEY (`repositoryId`) REFERENCES `repositories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE\n) COMMENT='用户';\n/*!40101 SET character_set_client = @saved_cs_client */;\n\n--\n-- Table structure for table `users`\n--\n\nDROP TABLE IF EXISTS `users`;\n/*!40101 SET @saved_cs_client     = @@character_set_client */;\n/*!40101 SET character_set_client = utf8 */;\nCREATE TABLE `users` (\n  `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '唯一标识',\n  `fullname` varchar(32) NOT NULL COMMENT '-',\n  `password` varchar(32) DEFAULT NULL COMMENT '-',\n  `email` varchar(128) NOT NULL COMMENT '-',\n  `createdAt` datetime NOT NULL COMMENT '-',\n  `updatedAt` datetime NOT NULL COMMENT '-',\n  `deletedAt` datetime DEFAULT NULL COMMENT '-',\n  PRIMARY KEY (`id`),\n  UNIQUE KEY `uk_email` (`email`),\n  UNIQUE KEY `uk_users_email_unique` (`email`)\n) COMMENT='用户';\n/*!40101 SET character_set_client = @saved_cs_client */;\n/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;\n/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;\n/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;\n/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;\n/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;\n/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;\n/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;\n/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;\n\n-- Dump completed on 2017-05-19 23:47:54\n"
  },
  {
    "path": "src/scripts/setToken.ts",
    "content": "// 补数据 token\nimport nanoid = require(\"nanoid\")\nimport sequelize from \"../models/sequelize\"\nimport { Repository } from \"../models\"\nconst chalk = require(\"chalk\");\n\n(async () => {\n  sequelize.model(Repository)\n\n  const cnt = await Repository.count({\n    where: {\n      // tslint:disable-next-line: no-null-keyword\n      token: null\n    }\n  })\n\n  const limit = 500\n  const iteration = 1\n\n  console.log(`共有${cnt}条数据`)\n\n  while (true) {\n    const rows = await Repository.findAll({\n      where: {\n        // tslint:disable-next-line: no-null-keyword\n        token: null\n      },\n      limit\n    })\n    console.log(`正在处理第 ${iteration} 轮, length ${rows.length}`)\n\n    if (rows.length === 0) {\n      console.log('全部处理完成')\n      break\n    }\n\n    await Promise.all(\n      rows.map(async repo => {\n        if (!repo.token) {\n          repo.token = nanoid(32)\n          await repo.save()\n          console.log(\n            chalk.green(repo.name + \"添加了默认token：\" + repo.token)\n          )\n        }\n      })\n    )\n  }\n  process.exit(0)\n})()\n"
  },
  {
    "path": "src/scripts/updateSchema.ts",
    "content": "import sequelize from \"../models/sequelize\"\n\nsequelize\n  .sync({\n    alter: true\n  })\n  .then(() => {\n    console.log(\"成功升级 DB Schema\")\n    process.exit(0)\n  })\n  .catch(e => {\n    console.log(\"升级 DB Schema 中遇到了错误\")\n    console.log(e)\n    process.exit(0)\n  })\n"
  },
  {
    "path": "src/scripts/worker.ts",
    "content": "import config from '../config'\nimport app from './app'\nconst start = () => {\n  // https://github.com/node-modules/graceful\n  const graceful = require('graceful')\n  const now = () => new Date().toISOString().replace(/T/, ' ').replace(/Z/, '')\n  const { serve: { port } } = config\n  const server = app.listen(port, () => {\n    console.log(`[${now()}]   worker#${process.pid} rap2-dolores is running as ${port}`)\n  })\n\n  graceful({\n    servers: [server],\n    killTimeout: '10s',\n    error: function (err: Error, throwErrorCount: any) {\n      if (err.message) err.message += ` (uncaughtException throw ${throwErrorCount} times on pid:${process.pid})`\n      console.error(`[${now()}] worker#${process.pid}] ${err.message}`)\n    },\n  })\n}\n\nstart()\n\nexport {}\n"
  },
  {
    "path": "src/service/export/docx.ts",
    "content": "import MarkdownService from './markdown'\nimport pandoc from '../../helpers/pandoc'\n\nconst markdownToDocx = pandoc('markdown', 'docx', '--wrap', 'none')\n\nexport default class DocxService {\n  public static async export(repositoryId: number, origin: string): Promise<Buffer> {\n    const markdown = await MarkdownService.export(repositoryId, origin)\n    const docx = markdownToDocx(markdown)\n    return docx\n  }\n}\n"
  },
  {
    "path": "src/service/export/markdown.ts",
    "content": "import { Repository, Interface, Module, Property } from '../../models'\nimport dedent from '../../helpers/dedent'\nimport * as moment from 'moment'\nimport { asTree } from 'treeify'\n\nconst arrayToTree = (list: any[]): any => {\n  const getValue = (parent: any) => {\n    const children = list.filter((item: any) => item.parentId === parent.id)\n    if (!children.length) {\n      return `${parent.type} ${parent.required ? '(必选)' : ''} ${parent.description ? `(${parent.description})` : ''}`\n    }\n    const obj: { [k: string]: any } = {}\n    children.forEach((e: any) => {\n      if (e.type === 'Array' || e.type === 'Object') {\n        obj[e.name + `: ${e.type} ${e.description ? `(${e.description})` : ''}`] = getValue(e)\n      } else {\n        obj[e.name] = getValue(e)\n      }\n    })\n    return obj\n  }\n  return getValue({id: -1})\n}\n\nexport default class PostmanService {\n  public static async export(repositoryId: number, origin: string): Promise<string> {\n    const repo = await Repository.findByPk(repositoryId, {\n      include: [\n        {\n          model: Module,\n          as: 'modules',\n          include: [\n            {\n              model: Interface,\n              as: 'interfaces',\n              include: [\n                {\n                  model: Property,\n                  as: 'properties'\n                }\n              ]\n            }\n          ]\n        }\n      ]\n    })\n\n    const result = dedent`\n    ***本文档由 Rap2 (https://github.com/thx/rap2-delos) 生成***\n\n    ***本项目仓库：[${origin}/repository/editor?id=${repositoryId}](${origin}/repository/editor?id=${repositoryId}) ***\n\n    ***生成日期：${moment().format('YYYY-MM-DD HH:mm:ss')}***\n\n    # 仓库：${repo.name}\n    ${repo.modules\n      .map(\n        m => dedent`\n      ## 模块：${m.name}\n      ${m.interfaces\n        .map(\n          intf => dedent`\n        ### 接口：${intf.name}\n        * 地址：${intf.url}\n        * 类型：${intf.method}\n        * 状态码：${intf.status}\n        * 简介：${intf.description || '无'}\n        * Rap地址：[${origin}/repository/editor?id=${repositoryId}&mod=${m.id}&itf=${intf.id}](${origin}/repository/editor?id=${repositoryId}&mod=${m.id}&itf=${intf.id})\n        * 请求接口格式：\n\n        \\`\\`\\`\n        ${asTree(arrayToTree(intf.properties.filter(item => item.scope === 'request')), true, undefined)}\n        \\`\\`\\`\n\n        * 返回接口格式：\n\n        \\`\\`\\`\n        ${asTree(arrayToTree(intf.properties.filter(item => item.scope === 'response')), true, undefined)}\n        \\`\\`\\`\n      `\n        )\n        .join('\\n\\n\\n')}\n    `\n      )\n      .join('\\n\\n\\n')}\n    `\n    return result\n  }\n}\n"
  },
  {
    "path": "src/service/export/pdf.ts",
    "content": "// import MarkdownService from './markdown'\n// import markdownpdf = require('markdown-pdf')\n// // import { Readable, Stream } from 'stream'\n\n// export default class PDFService {\n//   public static async export(\n//     repositoryId: number,\n//     origin: string\n//   ): Promise<Buffer> {\n//     const markdown = await MarkdownService.export(repositoryId, origin)\n//     return new Promise((resolve, reject) => {\n//       markdownpdf()\n//         .from.string(markdown)\n//         // @ts-ignore\n//         .to.buffer(undefined, (err, data) => {\n//           if (err) {\n//             reject(err)\n//             return\n//           }\n//           resolve(data)\n//         })\n//     })\n//   }\n// }\n"
  },
  {
    "path": "src/service/export/postman.ts",
    "content": "import { PostmanCollection, Folder, Item } from \"../../types/postman\"\nimport { Repository, Interface, Module, Property } from \"../../models\"\nimport * as url from 'url'\nimport { POS_TYPE } from \"../../models/bo/property\"\nimport UrlUtils from \"../../routes/utils/url\"\n\nconst SCHEMA_V_2_1_0 = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'\n\nexport default class PostmanService {\n  public static async export(repositoryId: number): Promise<PostmanCollection> {\n    const repo = await Repository.findByPk(repositoryId, {\n      include: [{\n        model: Module,\n        as: 'modules',\n        include: [{\n          model: Interface,\n          as: 'interfaces',\n          include: [{\n            model: Property,\n            as: 'properties',\n          }]\n        }]\n      }]\n    })\n    const result: PostmanCollection = {\n      info: {\n        name: `RAP2 Pack ${repo.name}`,\n        schema: SCHEMA_V_2_1_0,\n      },\n      item: []\n    }\n\n    for (const mod of repo.modules) {\n      const modItem: Folder = {\n        name: mod.name,\n        item: [],\n      }\n\n      for (const itf of mod.interfaces) {\n        const interfaceId = itf.id\n        const requestParams = await Property.findAll({\n          where: { interfaceId, scope: 'request' }\n        })\n        const responseParams = await Property.findAll({\n          where: { interfaceId, scope: 'response' }\n        })\n        const eventScript = await Property.findAll({\n          where: { interfaceId, scope: 'script' }\n        })\n\n        const relativeUrl = UrlUtils.getRelative(itf.url)\n        const parseResult = url.parse(itf.url)\n        const itfItem: Item = {\n          name: itf.name,\n          request: {\n            method: itf.method as any,\n            header: getHeader(requestParams),\n            body: getBody(requestParams),\n            url: {\n              raw: `{{url}}${relativeUrl}`,\n              host: '{{url}}',\n              port: parseResult.port || null,\n              hash: parseResult.hash,\n              path: [parseResult.path],\n              query: getQuery(requestParams),\n            },\n            description: itf.description,\n          },\n          response: responseParams.map(x => ({ key: x.name, value: x.value })),\n          event: getEvent(eventScript)\n        }\n        modItem.item.push(itfItem)\n      }\n      result.item.push(modItem)\n    }\n    return result\n  }\n}\n\nfunction getBody(pList: Property[]) {\n  return {\n    \"mode\": \"formdata\" as \"formdata\",\n    \"formdata\": pList.filter(x => x.pos === POS_TYPE.BODY)\n      .map(x => ({ key: x.name, value: x.value, description: x.description, type: \"text\" as \"text\" })),\n  }\n}\n\nfunction getQuery(pList: Property[]) {\n  return pList.filter(x => x.pos === null || x.pos === POS_TYPE.QUERY)\n    .map(x => ({ key: x.name, value: x.value, description: x.description }))\n}\n\nfunction getHeader(pList: Property[]) {\n  return pList.filter(x => x.pos === POS_TYPE.HEADER)\n    .map(x => ({ key: x.name, value: x.value, description: x.description }))\n}\n\nfunction getEvent(pList: Property[]) {\n  return pList.filter(x => (x.pos === POS_TYPE.PRE_REQUEST_SCRIPT || x.pos === POS_TYPE.TEST))\n    .map(x => ({\n      key: x.name,\n      script: { key: x.name, type: 'text/javascript', exec: x.value },\n      disabled: false,\n      listen: x.pos === POS_TYPE.PRE_REQUEST_SCRIPT ? 'prerequest' : 'test'\n    }))\n}"
  },
  {
    "path": "src/service/mail.ts",
    "content": "import * as nodemailer from 'nodemailer'\nimport config from '../config'\n\nexport default class MailService {\n  public static async sendMail(mailOptions: nodemailer.SendMailOptions) {\n    const transporter = nodemailer.createTransport(config.mail)\n\n    return new Promise((resolve, reject) => {\n      transporter.verify(function(error) {\n        if (error) {\n          reject(error)\n        } else {\n          transporter.sendMail(mailOptions, function(error, info) {\n            if (error) {\n              reject(error)\n            } else {\n              resolve(info.messageId)\n            }\n          })\n        }\n      })\n    })\n  }\n\n  public static send(to: string | string[], subject: string, html: string) {\n    const transporter = nodemailer.createTransport(config.mail)\n\n    const mailOptions = {\n      from: `\"RAP2 Notifier\" <${config.mailSender}>`,\n      to,\n      subject,\n      html,\n    }\n\n    return new Promise((resolve, reject) => {\n      transporter.verify(function(error) {\n        if (error) {\n          reject(error)\n        } else {\n          transporter.sendMail(mailOptions, function(error, info) {\n            if (error) {\n              reject(error)\n            } else {\n              resolve(info.messageId)\n            }\n          })\n        }\n      })\n    })\n  }\n\n  public static mailNoticeTemp = `<head>\n  <base target=\"_blank\" />\n  </head>\n  <body>\n  <div id=\"content\" bgcolor=\"#f0f0f0\" style=\"background-color:#f0f0f0;padding:10px\">\n    <table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n      <tr>\n        <td valign=\"top\">\n          <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n            bgcolor=\"#ffffff\" style=\"background-color:#ffffff; width:700px;\">\n            <tr>\n              <td style=\"vertical-align:top;\">\n                <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                  style=\"width:700px;\">\n                  <tr>\n                    <td valign=\"top\">\n                      <table class=\"full-width\" width=\"549\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                        <tr>\n                          <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                          <td width=\"32\" valign=\"middle\" style=\"padding-top:30px; padding-bottom:32px; width:32px;\"><img width=\"140\" height=\"34\" src=\"http://img.alicdn.com/tfs/TB1vtTllHH1gK0jSZFwXXc7aXXa-140-34.png\"></td>\n                          <td valign=\"middle\"></td>\n                          <td width=\"10\"></td>\n                        </tr>\n                      </table>\n                    </td>\n                  </tr>\n                </table>\n              </td>\n            </tr>\n            <tr>\n              <td valign=\"top\" style=\"padding-top:10px ;\">\n                <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                  bgcolor=\"#ffffff\" style=\"background-color:#ffffff; width:700px;\">\n                  <tr>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                    <td class=\"mobile-headline\"\n                      style=\"color:#000000; font-size:24px; line-height:26px;\">{=TITLE=}</td>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  </tr>\n                  <tr>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                    <td\n                      style=\"color:#555555; font-size:15px; line-height:20px; padding-top:30px;\">\n                      <ul style=\"margin-bottom:0px; margin-top:0px;\">\n                        {=CONTENT=}\n                      </ul>\n                    </td>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  </tr>\n                </table>\n                <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                  bgcolor=\"#F7F8F9\" style=\"background-color:#F7F8F9; width:700px;\">\n                  <tr>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                    <td\n                      style=\"color:#555555; font-size:15px; line-height:20px; padding-top:30px; padding-bottom:30px;\">\n                      <strong>此邮件为系统发送,请勿直接回复</strong>\n                      </td>\n                    <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  </tr>\n                </table>\n              </td>\n            </tr>\n            <tr>\n              <td valign=\"top\">\n                <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                  bgcolor=\"#3f51b5\" style=\"background-color:#3f51b5; width:700px;\">\n                  <tr>\n                    <td width=\"22\">&nbsp;</td>\n                    <td align=\"center\"\n                      style=\"color:#ffffff; font-size:11px; line-height:14px; padding-top:20px;text-align:center;\">\n                      <strong><a href=\"http://rap2.alibaba-inc.com/\"\n                      style=\"color:#ffffff; font-weight:bold; text-decoration:none;\">RAP2</a> | <a\n                          href=\"https://github.com/thx/rap2-delos\"\n                          style=\"color:#ffffff; font-weight:bold; text-decoration:none;\">GitHub</a></strong>\n                    </td>\n                    <td width=\"22\">&nbsp;</td>\n                  </tr>\n                  <tr>\n                    <td width=\"22\" colspan=\"3\">&nbsp;</td>\n                  </tr>\n                </table>\n              </td>\n            </tr>\n          </table>\n        </td>\n      </tr>\n    </table>\n  </div>\n  <style type=\"text/css\">\n    body {\n      font-size: 14px;\n      font-family: \"Roboto\", \"Helvetica\", \"Arial\", sans-serif;\n      line-height: 1.666;\n      padding: 0;\n      margin: 0;\n      overflow: auto;\n      white-space: normal;\n      word-wrap: break-word;\n      min-height: 100px\n    }\n  </style>\n  </body>`\n\npublic static mailFindpwdTemp = `<head>\n<base target=\"_blank\" />\n</head>\n<body>\n<div id=\"content\" bgcolor=\"#f0f0f0\" style=\"background-color:#f0f0f0;padding:10px\">\n  <table width=\"100%\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n    <tr>\n      <td valign=\"top\">\n        <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n          bgcolor=\"#ffffff\" style=\"background-color:#ffffff; width:700px;\">\n          <tr>\n            <td style=\"vertical-align:top;\">\n              <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                style=\"width:700px;\">\n                <tr>\n                  <td valign=\"top\">\n                    <table class=\"full-width\" width=\"549\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n                      <tr>\n                        <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                        <td width=\"32\" valign=\"middle\" style=\"padding-top:30px; padding-bottom:32px; width:32px;\"><img width=\"140\" height=\"34\" src=\"http://img.alicdn.com/tfs/TB1vtTllHH1gK0jSZFwXXc7aXXa-140-34.png\"></td>\n                        <td valign=\"middle\"></td>\n                        <td width=\"10\"></td>\n                      </tr>\n                    </table>\n                  </td>\n                </tr>\n              </table>\n            </td>\n          </tr>\n          <tr>\n            <td valign=\"top\" style=\"padding-top:10px ;\">\n              <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                bgcolor=\"#ffffff\" style=\"background-color:#ffffff; width:700px;\">\n                <tr>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  <td class=\"mobile-headline\"\n                    style=\"color:#000000; font-size:24px; line-height:26px;\">\n                    {=NAME=} 欢迎您，您正在进行密码重置！</td>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                </tr>\n                <tr>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  <td\n                    style=\"color:#555555; font-size:15px; line-height:20px; padding-top:30px;\">\n                    <ul style=\"margin-bottom:0px; margin-top:0px;\">\n                      <li style=\"padding-bottom:15px;\">请单击下面的链接重置密码，该链接在<strong>1小时内有效</strong></li>\n                      <li style=\"padding-bottom:15px;\"><a href=\"{=URL=}\"\n                          style=\"color:#1473E6; font-weight:bold; text-decoration:none;\">重设密码</a></li>\n                      <li style=\"padding-bottom:15px;\">如果您的邮箱不支持链接点击，请将下面的链接地址拷贝到您的浏览器地址栏中</li>\n                      <li style=\"padding-bottom:25px;\">{=URL=}</li>\n                    </ul>\n                  </td>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                </tr>\n              </table>\n              <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                bgcolor=\"#F7F8F9\" style=\"background-color:#F7F8F9; width:700px;\">\n                <tr>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                  <td\n                    style=\"color:#555555; font-size:15px; line-height:20px; padding-top:30px; padding-bottom:30px;\">\n                    <strong>为什么我会收到这封邮件？</strong>您在RAP2系统使用了密码找回功能，我们发送这封邮件，以协助您重设密码。\n                    <br><br>如果您没有使用此功能，请忽略此邮件\n                    <br><br>如果您不想收到我们的邮件<a href=\"http://rap2.taobao.org/account/unsubscribe\" style=\"color:#1473E6; font-weight:bold; text-decoration:none;\">点击退订</a>\n                    </td>\n                  <td class=\"mobile-spacer\" width=\"30\" style=\"width:30px;\">&nbsp;</td>\n                </tr>\n              </table>\n            </td>\n          </tr>\n          <tr>\n            <td valign=\"top\">\n              <table class=\"full-width\" align=\"center\" width=\"700\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"\n                bgcolor=\"#3f51b5\" style=\"background-color:#3f51b5; width:700px;\">\n                <tr>\n                  <td width=\"22\">&nbsp;</td>\n                  <td align=\"center\"\n                    style=\"color:#ffffff; font-size:11px; line-height:14px; padding-top:20px;text-align:center;\">\n                    <strong><a href=\"http://rap2.taobao.org/\"\n                        style=\"color:#ffffff; font-weight:bold; text-decoration:none;\">RAP2</a> | <a\n                        href=\"https://github.com/thx/rap2-delos\"\n                        style=\"color:#ffffff; font-weight:bold; text-decoration:none;\">GitHub</a></strong>\n                  </td>\n                  <td width=\"22\">&nbsp;</td>\n                </tr>\n                <tr>\n                  <td width=\"22\" colspan=\"3\">&nbsp;</td>\n                </tr>\n              </table>\n            </td>\n          </tr>\n        </table>\n      </td>\n    </tr>\n  </table>\n</div>\n<style type=\"text/css\">\n  body {\n    font-size: 14px;\n    font-family: \"Roboto\", \"Helvetica\", \"Arial\", sans-serif;\n    line-height: 1.666;\n    padding: 0;\n    margin: 0;\n    overflow: auto;\n    white-space: normal;\n    word-wrap: break-word;\n    min-height: 100px\n  }\n</style>\n</body>`\n}\n"
  },
  {
    "path": "src/service/migrate.ts",
    "content": "import { Repository, Module, Interface, Property, User, QueryInclude } from '../models'\nimport { SCOPES } from '../models/bo/property'\nimport Tree from '../routes/utils/tree'\nimport * as JSON5 from 'json5'\nimport * as querystring from 'querystring'\nimport * as rp from 'request-promise'\nimport { Op } from 'sequelize'\nimport RedisService, { CACHE_KEY } from './redis'\nimport MailService from './mail'\nimport * as md5 from 'md5'\nconst isMd5 = require('is-md5')\nimport * as _ from 'lodash'\n\nconst safeEval = require('notevil')\n\nconst SWAGGER_VERSION = {\n  1: '2.0',\n}\n\n/**\n * swagger json结构转化的数组转化为树形结构\n * @param list\n */\nconst arrayToTree = list => {\n  const parseChildren = (list, parent) => {\n    list.forEach(item => {\n      if (item.parent === parent.id) {\n        item.depth = parent.depth + 1\n        item.parentName = parent.name\n        item.children = item.children || []\n        parent.children.push(item)\n        parseChildren(list, item)\n      }\n    })\n    return parent\n  }\n  return parseChildren(list, {\n    id: 'root',\n    name: 'root',\n    children: [],\n    depth: -1,\n    parent: -1,\n  })\n}\n\n/**\n * swagger json结构转化的数组转化的树形结构转化为数组\n * @param tree\n */\nconst treeToArray = (tree: any) => {\n  const parseChildren = (parent: any, result: any) => {\n    if (!parent.children) {\n      return result\n    }\n    parent.children.forEach((item: any) => {\n      result.push(item)\n      parseChildren(item, result)\n      delete item.children\n    })\n    return result\n  }\n  return parseChildren(tree, [])\n}\n\n/**\n * 接口属性-数组结构转化为树形结构\n * @param list\n */\nconst arrayToTreeProperties = (list: any) => {\n  const parseChildren = (list: any, parent: any) => {\n    list.forEach((item: any) => {\n      if (item.parentId === parent.id) {\n        item.depth = parent.depth + 1\n        item.children = item.children || []\n        parent.children.push(item)\n        parseChildren(list, item)\n      }\n    })\n    return parent\n  }\n  return parseChildren(list, {\n    id: -1,\n    name: 'root',\n    children: [],\n    depth: -1,\n  })\n}\n\n/**\n * 参数请求类型枚举\n */\nconst REQUEST_TYPE_POS = {\n  path: 2,\n  query: 2,\n  header: 1,\n  formData: 3,\n  body: 3,\n}\n\nlet checkSwaggerResult = []\nlet changeTip = '' // 变更信息\n\n/**\n * Swagger JSON 参数递归处理成数组\n * @param parameters 参数列表数组\n * @param parent 父级id\n * @param parentName 父级属性name\n * @param depth parameters list 中每个属性的深度\n * @param result \bswagger转化为数组结果 -- 对swagger参数处理结果\n * @param definitions swagger $ref definitions， 额外传递过来的swagger的definitions数据, 非计算核心算法\n * @param scope 参数类型 -- 【暂不用】用于参数校验后提示\n * @param apiInfo 接口信息 -- 【暂不用】用于参数校验后提示\n */\nconst parse = (parameters, parent, parentName, depth, result, definitions, scope, apiInfo) => {\n  for (let key = 0, len = parameters.length; key < len; key++) {\n    const param = parameters[key]\n\n    if (!param.$ref && !(param.items || {}).$ref) {\n      // 非对象或者数组的基础类型\n      result.push({\n        ...param,\n        parent,\n        parentName,\n        depth,\n        id: `${parent}-${key}`,\n      })\n    } else {\n      // 数组类型或者对象类型\n      let paramType = ''\n      if (param.items) {\n        paramType = 'array'\n      } else {\n        paramType = 'object'\n      }\n\n      result.push({\n        ...param,\n        parent,\n        parentName,\n        depth,\n        id: `${parent}-${key}`,\n        type: paramType,\n      })\n\n      let refName\n      if (!param.items) {\n        // 对象\n        refName = param.$ref.split('#/definitions/')[1]\n        delete result.find(item => item.id === `${parent}-${key}`)['$ref']\n      }\n      if (param.items) {\n        // 数组\n        refName = param.items.$ref.split('#/definitions/')[1]\n        delete result.find(item => item.id === `${parent}-${key}`).items\n      }\n\n      const ref = definitions[refName]\n      if (ref && ref.properties) {\n        const properties = ref.properties\n        const list = []\n\n        for (const key in properties) {\n          // swagger文档中对definition定义属性又引用自身的情况处理-死循环\n          if (properties[key].$ref) {\n            if (properties[key].$ref.split('#/definitions/')[1] === refName) {\n              // delete properties[key].$ref\n              list.push({\n                name: key,\n                parentName: param.name,\n                depth: depth + 1,\n                ...properties[key],\n                $ref: null,\n                type: 'object',\n                in: param.in,\n                required: (ref.required || []).indexOf(key) >= 0,\n                description: `【递归父级属性】${properties[key].description || ''}`,\n              })\n            } else {\n              list.push({\n                name: key,\n                parentName: param.name,\n                depth: depth + 1,\n                ...properties[key],\n                in: param.in,\n                required: (ref.required || []).indexOf(key) >= 0,\n              })\n            }\n          } else if ((properties[key].items || {}).$ref) {\n            if (properties[key].items.$ref.split('#/definitions/')[1] === refName) {\n              // delete properties[key].items.$ref\n              list.push({\n                name: key,\n                parentName: param.name,\n                depth: depth + 1,\n                ...properties[key],\n                type: 'array',\n                items: null,\n                $ref: null,\n                in: param.in,\n                required: (ref.required || []).indexOf(key) >= 0,\n                description: `【递归父级属性】${properties[key].description || ''}`,\n              })\n            } else {\n              list.push({\n                name: key,\n                parentName: param.name,\n                depth: depth + 1,\n                ...properties[key],\n                in: param.in,\n                required: (ref.required || []).indexOf(key) >= 0,\n              })\n            }\n          } else {\n            list.push({\n              name: key,\n              parentName: param.name,\n              depth: depth + 1,\n              ...properties[key],\n              in: param.in, // response 无所谓，不使用但是request 使用\n              required: (ref.required || []).indexOf(key) >= 0,\n            })\n          }\n        }\n        parse(list, `${parent}-${key}`, param.name, depth + 1, result, definitions, scope, apiInfo)\n      }\n    }\n  }\n}\n\nconst transformRapParams = p => {\n  let rule = '',\n    description = '',\n    value = p.default || ''\n\n  // 类型转化处理\n  let type = p.type || 'string'\n  if (type === 'integer') type = 'number'\n  type = type[0].toUpperCase() + type.slice(1)\n\n  // 规则属性说明处理\n  if (p.type === 'string' && p.minLength && p.maxLength) {\n    rule = `${p.minLength}-${p.maxLength}`\n    description = `${description}|长度限制: ${p.minLength}-${p.maxLength}`\n  } else if (p.type === 'string' && p.minLength && !p.maxLength) {\n    rule = `${p.minLength}`\n    description = `${description}|长度限制：最小值: ${p.minLength}`\n  } else if (p.type === 'string' && !p.minLength && p.maxLength) {\n    rule = `${p.required ? '1' : '0'}-${p.maxLength}`\n    description = `${description}|长度限制：最大值: ${p.maxLength}`\n  }\n  if (p.type === 'string' && p.enum && p.enum.length > 0) {\n    description = `${description}|枚举值: ${p.enum.join()}`\n  }\n  if ((p.type === 'integer' || p.type === 'number') && p.minimum && p.maxinum) {\n    rule = `${p.minimum}-${p.maxinum}`\n    description = `${description}|数据范围: ${p.minimum}-${p.maxinum}`\n  }\n  if ((p.type === 'integer' || p.type === 'number') && p.minimum && !p.maxinum) {\n    rule = `${p.minimum}`\n    description = `${description}|数据范围: 最小值：${p.minimum}`\n  }\n  if ((p.type === 'integer' || p.type === 'number') && !p.minimum && p.maxinum) {\n    rule = `${p.required ? '1' : '0'}-${p.maxinum}`\n    description = `${description}|数据范围: 最大值：${p.maxinum}`\n  }\n\n  // 默认值转化处理\n  value = p.default || ''\n  if (!p.default && p.type === 'string') value = '@ctitle'\n  if (!p.default && (p.type === 'number' || p.type === 'integer')) value = '@integer(0, 100000)'\n  if (p.type === 'boolean') {\n    value = p.default === true || p.default === false ? p.default.toString() : 'false'\n  }\n  if (p.enum && (p.enum || []).length > 0) value = p.enum[0]\n  if (p.type === 'string' && p.format === 'date-time') value = '@datetime'\n  if (p.type === 'string' && p.format === 'date') value = '@date'\n\n  if (p.type === 'array' && p.default) {\n    value = typeof p.default === 'object' ? JSON.stringify(p.default) : p.default.toString()\n  }\n  if (/^function/.test(value)) type = 'Function' // @mock=function(){} => Function\n  if (/^\\$order/.test(value)) {\n    // $order => Array|+1\n    type = 'Array'\n    rule = '+1'\n    let orderArgs = /\\$order\\((.+)\\)/.exec(value)\n    if (orderArgs) value = `[${orderArgs[1]}]`\n  }\n\n  if (['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp', 'Null'].indexOf(type) === -1) {\n    /** File暂时不支持，用Null代替 */\n    type = 'Null'\n  }\n\n  return {\n    type,\n    rule,\n    description: description.length > 0 ? description.substring(1) : '',\n    value,\n  }\n}\n\nconst propertiesUpdateService = async (properties, itfId) => {\n  properties = Array.isArray(properties) ? properties : [properties]\n  let itf = await Interface.findByPk(itfId)\n\n  let existingProperties = properties.filter((item: any) => !item.memory)\n  let result = await Property.destroy({\n    where: {\n      id: { [Op.notIn]: existingProperties.map((item: any) => item.id) },\n      interfaceId: itfId,\n    },\n  })\n\n  // 更新已存在的属性\n  for (let item of existingProperties) {\n    let affected = await Property.update(item, {\n      where: { id: item.id },\n    })\n    result += affected[0]\n  }\n  // 插入新增加的属性\n  let newProperties = properties.filter((item: any) => item.memory)\n  let memoryIdsMap: any = {}\n  for (let item of newProperties) {\n    let created = await Property.create(\n      Object.assign({}, item, {\n        id: undefined,\n        parentId: -1,\n        priority: item.priority || Date.now(),\n      }),\n    )\n    memoryIdsMap[item.id] = created.id\n    item.id = created.id\n    result += 1\n  }\n  // 同步 parentId\n  for (let item of newProperties) {\n    let parentId = memoryIdsMap[item.parentId] || item.parentId\n    await Property.update(\n      { parentId },\n      {\n        where: { id: item.id },\n      },\n    )\n  }\n  itf = await Interface.findByPk(itfId, {\n    include: (QueryInclude.RepositoryHierarchy as any).include[0].include,\n  })\n  return {\n    data: {\n      result,\n      properties: itf.properties,\n    },\n  }\n}\n\nconst sendMailTemplate = changeTip => {\n  let html = MailService.mailNoticeTemp\n    .replace('{=TITLE=}', '您相关的接口存在如下变更：(请注意代码是否要调整)')\n    .replace(\n      '{=CONTENT=}',\n      (changeTip.split('<br/>') || [])\n        .map(one => {\n          return one ? `<li style=\"margin-bottom: 20px;\">${one}</li>` : ''\n        })\n        .join(''),\n    )\n  return html\n}\nexport default class MigrateService {\n  public static async importRepoFromRAP1ProjectData(\n    orgId: number,\n    curUserId: number,\n    projectData: any,\n  ): Promise<boolean> {\n    if (!projectData || !projectData.id || !projectData.name) return false\n    let pCounter = 1\n    let mCounter = 1\n    let iCounter = 1\n    const repo = await Repository.create({\n      name: projectData.name,\n      description: projectData.introduction,\n      visibility: true,\n      ownerId: curUserId,\n      creatorId: curUserId,\n      organizationId: orgId,\n    })\n    for (const module of projectData.moduleList) {\n      const mod = await Module.create({\n        name: module.name,\n        description: module.introduction,\n        priority: mCounter++,\n        creatorId: curUserId,\n        repositoryId: repo.id,\n      })\n      for (const page of module.pageList) {\n        for (const action of page.actionList) {\n          const itf = await Interface.create({\n            moduleId: mod.id,\n            name: `${page.name}-${action.name}`,\n            description: action.description,\n            url: action.requestUrl || '',\n            priority: iCounter++,\n            creatorId: curUserId,\n            repositoryId: repo.id,\n            method: getMethodFromRAP1RequestType(+action.requestType),\n          })\n          for (const p of action.requestParameterList) {\n            await processParam(p, SCOPES.REQUEST)\n          }\n          for (const p of action.responseParameterList) {\n            await processParam(p, SCOPES.RESPONSE)\n          }\n          async function processParam(p: OldParameter, scope: SCOPES, parentId?: number) {\n            const RE_REMARK_MOCK = /@mock=(.+)$/\n            const ramarkMatchMock = RE_REMARK_MOCK.exec(p.remark)\n            const remarkWithoutMock = p.remark.replace(RE_REMARK_MOCK, '')\n            const name = p.identifier.split('|')[0]\n            let rule = p.identifier.split('|')[1] || ''\n            let type = (p.dataType || 'string').split('<')[0] // array<number|string|object|boolean> => Array\n            type = type[0].toUpperCase() + type.slice(1) // foo => Foo\n            let value = (ramarkMatchMock && ramarkMatchMock[1]) || ''\n            if (/^function/.test(value)) type = 'Function' // @mock=function(){} => Function\n            if (/^\\$order/.test(value)) {\n              // $order => Array|+1\n              type = 'Array'\n              rule = '+1'\n              let orderArgs = /\\$order\\((.+)\\)/.exec(value)\n              if (orderArgs) value = `[${orderArgs[1]}]`\n            }\n            let description = []\n            if (p.name) description.push(p.name)\n            if (p.remark && remarkWithoutMock) description.push(remarkWithoutMock)\n            const pCreated = await Property.create({\n              scope,\n              name,\n              rule,\n              value,\n              type,\n              description: `${p.remark}${p.name ? ', ' + p.name : ''}`,\n              priority: pCounter++,\n              interfaceId: itf.id,\n              creatorId: curUserId,\n              moduleId: mod.id,\n              repositoryId: repo.id,\n              parentId: parentId || -1,\n            })\n            for (const subParam of p.parameterList) {\n              processParam(subParam, scope, pCreated.id)\n            }\n          }\n        }\n      }\n    }\n    return true\n  }\n  public static checkAndFix(): void {\n    // console.log('checkAndFix')\n    // this.checkPasswordMd5().then()\n  }\n\n  static async checkPasswordMd5() {\n    console.log('  checkPasswordMd5')\n    const users = await User.findAll()\n    if (users.length === 0 || isMd5(users[0].password)) {\n      console.log('  users empty or md5 check passed')\n      return\n    }\n    for (const user of users) {\n      if (!isMd5(user.password)) {\n        user.password = md5(md5(user.password))\n        await user.save()\n        console.log(`handle user ${user.id}`)\n      }\n    }\n  }\n\n  /** RAP1 property */\n  public static async importRepoFromRAP1DocUrl(\n    orgId: number,\n    curUserId: number,\n    docUrl: string,\n    version: number,\n    projectDataJSON: string\n  ): Promise<boolean> {\n    let result: any = null\n    if (version === 1) {\n      const { projectId } = querystring.parse(docUrl.substring(docUrl.indexOf('?') + 1))\n      let domain = docUrl\n      if (domain.indexOf('http') === -1) {\n        domain = 'http://' + domain\n      }\n      domain = domain.substring(0, domain.indexOf('/', domain.indexOf('.')))\n      const response = await rp(`${domain}/api/queryRAPModel.do?projectId=${projectId}`, {\n        json: false,\n      })\n      result = JSON.parse(response)\n\n      // result =  unescape(result.modelJSON)\n      result = result.modelJSON\n      result = safeEval('(' + result + ')')\n    } else if (version === 2) {\n      result = safeEval('(' + projectDataJSON + ')')\n    }\n    return await this.importRepoFromRAP1ProjectData(orgId, curUserId, result)\n  }\n\n  /** 请求参对象->数组->标准树形对象 @param swagger @param parameters */\n  public static async swaggerToModelRequest(\n    swagger: SwaggerData,\n    parameters: Array<any>,\n    method: string,\n    apiInfo: any,\n  ): Promise<any> {\n    let { definitions = {} } = swagger\n    const result = []\n    definitions = JSON.parse(JSON.stringify(definitions)) // 防止接口之间数据处理相互影响\n\n    if (Array.isArray(parameters) && method === 'get' || method === 'GET') {\n      parse(\n        parameters.filter(item => item.in !== 'body') || [],\n        'root',\n        'root',\n        0,\n        result,\n        definitions,\n        'request',\n        apiInfo,\n      )\n    } else if (method === 'post' || method === 'POST') {\n      let list = [] // 外层处理参数数据结果\n      const bodyObj = parameters.find(item => item.in === 'body') // body unique\n\n      if (!bodyObj) list = [...parameters]\n      else {\n        const { schema } = bodyObj\n        if (!schema.$ref) {\n          // 没有按照接口规范返回数据结构,默认都是对象\n          list = parameters.filter(item => item.in === 'query' || item.in === 'header')\n        } else {\n          const refName = schema.$ref.split('#/definitions/')[1]\n          const ref = definitions[refName]\n\n          if (!ref)\n            list = [...parameters.filter(item => item.in === 'query' || item.in === 'header')]\n          else {\n            const properties = ref.properties || {}\n            const bodyParameters = []\n\n            for (const key in properties) {\n              bodyParameters.push({\n                name: key,\n                ...properties[key],\n                in: 'body',\n                required: (ref.required || []).indexOf(key) >= 0,\n              })\n            }\n            list = [\n              ...bodyParameters,\n              ...parameters.filter(item => item.in === 'query' || item.in === 'header'),\n            ]\n          }\n        }\n      }\n      parse(list, 'root', 'root', 0, result, definitions, 'request', apiInfo)\n    }\n\n    const tree = arrayToTree(JSON.parse(JSON.stringify(result)))\n    return tree\n  }\n\n  /**\n   * 返回参数对象->数组->标准树形对象\n   * 如果swagger responses参数没有的情况下异常处理\n   * 如果swagger responses对象200不存在情况下异常处理\n   * @param swagger\n   * @param response\n   */\n  public static async swaggerToModelRespnse(\n    swagger: SwaggerData,\n    response: object,\n    apiInfo: any,\n  ): Promise<any> {\n    let { definitions = {} } = swagger\n    definitions = JSON.parse(JSON.stringify(definitions)) // 防止接口之间数据处理相互影响\n\n    const successObj = response['200']\n    if (!successObj) return []\n\n    const { schema } = successObj\n    if (!schema?.$ref) {\n      // 没有按照接口规范返回数据结构,默认都是对象\n      return []\n    }\n\n    const parameters = []\n    const refName = schema.$ref.split('#/definitions/')[1]\n    const ref = definitions[refName]\n    if (ref && ref.properties) {\n      const properties = ref.properties\n\n      for (const key in properties) {\n        // 公共返回参数描述信息设置\n        let description = ''\n        if (!properties[key].description && key === 'errorCode') {\n          description = '错误码'\n        }\n        if (!properties[key].description && key === 'errorMessage') {\n          description = '错误描述'\n        }\n        if (!properties[key].description && key === 'success') {\n          description = '请求业务结果'\n        }\n\n        parameters.push({\n          name: key,\n          ...properties[key],\n          in: 'body',\n          required: key === 'success' ? true : (ref.required || []).indexOf(key) >= 0,\n          default: key === 'success' ? true : properties[key].default || false,\n          description: properties[key].description || description,\n        })\n      }\n    }\n\n    const result = []\n    parse(parameters, 'root', 'root', 0, result, definitions, 'response', apiInfo)\n    const tree = arrayToTree(JSON.parse(JSON.stringify(result)))\n    return tree\n  }\n\n  public static async importRepoFromSwaggerProjectData(\n    repositoryId: number,\n    curUserId: number,\n    swagger: SwaggerData,\n  ): Promise<boolean> {\n    checkSwaggerResult = []\n    if (!swagger.paths || !swagger.swagger) return false\n\n    let mCounter = 1 // 模块优先级顺序\n    let iCounter = 1 // 接口优先级顺序\n    let pCounter = 1 // 参数优先级顺序\n\n    /**\n     * 接口创建并批量创建属性，规则，默认值，说明等处理\n     * @param p\n     * @param scope\n     * @param interfaceId\n     * @param moduleId\n     * @param parentId\n     */\n    async function processParam(\n      p: SwaggerParameter,\n      scope: SCOPES,\n      interfaceId: number,\n      moduleId: number,\n      parentId?: number,\n    ) {\n      const { rule, value, type, description } = transformRapParams(p)\n      const joinDescription = `${p.description || ''}${\n        (p.description || '') && (description || '') ? '|' : ''\n        }${description || ''}`\n      const pCreated = await Property.create({\n        scope,\n        name: p.name,\n        rule,\n        value,\n        type,\n        required: p.required,\n        description: joinDescription,\n        priority: pCounter++,\n        interfaceId: interfaceId,\n        creatorId: curUserId,\n        moduleId: moduleId,\n        repositoryId: repositoryId,\n        parentId: parentId || -1,\n        pos: REQUEST_TYPE_POS[p.in],\n        memory: true,\n      })\n\n      for (const subParam of p.children) {\n        processParam(subParam, scope, interfaceId, moduleId, pCreated.id)\n      }\n    }\n\n    let { tags = [], paths = {}, host = '' } = swagger\n    let pathTag: SwaggerTag[] = []\n\n    // 获取所有的TAG: 处理ROOT TAG中没有的情况\n    for (const action in paths) {\n      const apiObj = paths[action][Object.keys(paths[action])[0]]\n      const index = pathTag.findIndex((it: SwaggerTag) => {\n        return apiObj.tags.length > 0 && it.name === apiObj.tags[0]\n      })\n      if (index < 0 && apiObj.tags.length > 0)\n        pathTag.push({\n          name: apiObj.tags[0],\n          description: tags.find(item => item.name === apiObj.tags[0]).description || '',\n        })\n    }\n    tags = pathTag\n\n    if (checkSwaggerResult.length > 0) return false\n\n    for (const tag of tags) {\n      if (checkSwaggerResult.length > 0) break\n\n      let repository: Partial<Repository>\n      let [repositoryModules] = await Promise.all([\n        Repository.findByPk(repositoryId, {\n          attributes: { exclude: [] },\n          include: [QueryInclude.RepositoryHierarchy],\n          order: [\n            [{ model: Module, as: 'modules' }, 'priority', 'asc'],\n            [\n              { model: Module, as: 'modules' },\n              { model: Interface, as: 'interfaces' },\n              'priority',\n              'asc',\n            ],\n          ],\n        }),\n      ])\n      repository = {\n        ...repositoryModules.toJSON(),\n      }\n\n      const findIndex = repository.modules.findIndex(item => {\n        return item.name === tag.name\n      }) // 判断是否存在模块\n      let mod = null\n      if (findIndex < 0) {\n        mod = await Module.create({\n          name: tag.name,\n          description: tag.description,\n          priority: mCounter++,\n          creatorId: curUserId,\n          repositoryId: repositoryId,\n        })\n      } else {\n        mod = repository.modules[findIndex]\n      }\n      for (const action in paths) {\n        const apiObj = paths[action][Object.keys(paths[action])[0]]\n        const method = Object.keys(paths[action])[0]\n        const actionTags0 = apiObj.tags[0]\n        const url = action\n        const summary = apiObj.summary\n\n        if (actionTags0 === tag.name) {\n          // 判断接口是否存在该模块中，如果不存在则创建接口，存在则更新接口信息\n          let [repositoryModules] = await Promise.all([\n            Repository.findByPk(repositoryId, {\n              attributes: { exclude: [] },\n              include: [QueryInclude.RepositoryHierarchy],\n              order: [\n                [{ model: Module, as: 'modules' }, 'priority', 'asc'],\n                [\n                  { model: Module, as: 'modules' },\n                  { model: Interface, as: 'interfaces' },\n                  'priority',\n                  'asc',\n                ],\n              ],\n            }),\n          ])\n          repository = {\n            ...repositoryModules.toJSON(),\n          }\n\n          const request = await this.swaggerToModelRequest(\n            swagger,\n            apiObj.parameters || [],\n            method,\n            { url, summary },\n          )\n          const response = await this.swaggerToModelRespnse(swagger, apiObj.responses || {}, {\n            url,\n            summary,\n          })\n          // 处理完每个接口请求参数后，如果-遇到第一个存在接口不符合规范就全部返回\n          if (checkSwaggerResult.length > 0) break\n\n          // 判断对应模块是否存在该接口\n          const index = repository.modules.findIndex(item => {\n            return (\n              item.id === mod.id &&\n              item.interfaces.findIndex(it => (it.url || '') === url) >= 0\n            ) // 已经存在接口\n          })\n\n          if (index < 0) {\n            // 创建接口\n            const itf = await Interface.create({\n              moduleId: mod.id,\n              name: `${apiObj.summary}`,\n              description: apiObj.description,\n              url: `${host ? `https://${host}` : ''}${url.replace('-test', '')}`,\n              priority: iCounter++,\n              creatorId: curUserId,\n              repositoryId: repositoryId,\n              method: method.toUpperCase(),\n            })\n\n            for (const p of request.children || []) {\n              await processParam(p, SCOPES.REQUEST, itf.id, mod.id)\n            }\n            for (const p of response.children || []) {\n              await processParam(p, SCOPES.RESPONSE, itf.id, mod.id)\n            }\n          } else {\n            const findApi = repository.modules[index].interfaces.find(\n              item => item.url.indexOf(url) >= 0,\n            )\n            // 更新接口\n            await Interface.update(\n              {\n                moduleId: mod.id,\n                name: `${apiObj.summary}`,\n                description: apiObj.description,\n                url: `${host ? `https://${host}` : ''}${url.replace('-test', '')}`,\n                repositoryId: repositoryId,\n                method: method.toUpperCase(),\n              },\n              { where: { id: findApi.id } },\n            )\n\n            // 获取已经存在的接口的属性信息，并处理深度和parentName\n            let A_ExistsPropertiesOld = JSON.parse(JSON.stringify(findApi.properties))\n            A_ExistsPropertiesOld = JSON.parse(\n              JSON.stringify(treeToArray(arrayToTreeProperties(A_ExistsPropertiesOld))),\n            )\n            let A_ExistsProperties = A_ExistsPropertiesOld.map(property => {\n              return {\n                ...property,\n                parentName:\n                  (A_ExistsPropertiesOld.find(item => item.id === property.parentId) || {}).name ||\n                  'root',\n              }\n            })\n\n            const B_SwaggerProperties_Request = treeToArray(request)\n            const B_SwaggerProperties_Response = treeToArray(response)\n            let PropertyId = 0,\n              PriorityId = 0\n\n            let maxDepth_Request = 0,\n              maxDepth_Response = 0,\n              maxDepth_A_ExistsProperties = 0\n            // 计算B的最大深度-- request\n            B_SwaggerProperties_Request.map(item => {\n              if (item.depth > maxDepth_Request) {\n                maxDepth_Request = item.depth\n              }\n              return item\n            })\n\n            // 计算B的最大深度-- response\n            B_SwaggerProperties_Response.map(item => {\n              if (item.depth > maxDepth_Response) {\n                maxDepth_Response = item.depth\n              }\n              return item\n            })\n\n            // 计算A的最大深度\n            A_ExistsProperties.map(item => {\n              if (item.depth > maxDepth_A_ExistsProperties) {\n                maxDepth_A_ExistsProperties = item.depth\n              }\n              return item\n            })\n\n            const properties = []\n\n            /**\n             * 批量更新接口属性名称，类型，规则，默认值等处理\n             * @param BFilterByDepth\n             * @param depth\n             * @param scope\n             */\n            const updateProperties = (BFilterByDepth, depth, scope) => {\n              for (const key in BFilterByDepth) {\n                const bValue = BFilterByDepth[key]\n                const index = A_ExistsProperties.findIndex(\n                  item =>\n                    item.name === bValue.name &&\n                    item.depth === bValue.depth &&\n                    item.parentName === bValue.parentName &&\n                    item.scope === scope,\n                )\n                const { type, description, rule, value } = transformRapParams(bValue)\n                const joinDescription = `${bValue.description || ''}${\n                  (bValue.description || '') && (description || '') ? '|' : ''\n                  }${description || ''}`\n\n                if (index >= 0) {\n                  // 属性存在 ---修改：类型；是否必填；属性说明；不修改规则和默认值(前端可能正在mock)\n                  // 如何判断有更新\n                  if (type !== A_ExistsProperties[index].type) {\n                    // 类型变更了\n                    changeTip = `${changeTip}<br/>接口名称：${apiObj.summary} [更新属性：${A_ExistsProperties[index].name}类型由“${A_ExistsProperties[index].type}”变更为“${type}]”`\n                  }\n                  if (!!bValue.required !== A_ExistsProperties[index].required) {\n                    // 是否必填变更\n                    changeTip = `${changeTip}<br/>接口名称：${apiObj.summary} [更新属性：${A_ExistsProperties[index].name}是否必填由“${A_ExistsProperties[index].required}”变更为“${bValue.required}]”`\n                  }\n\n                  if (\n                    joinDescription !== A_ExistsProperties[index].description &&\n                    bValue.name !== 'success' &&\n                    bValue.name !== 'errorCode' &&\n                    bValue.name !== 'errorMessage'\n                  ) {\n                    // 描述信息变更\n                    changeTip = `${changeTip}<br/>接口名称：${apiObj.summary} [更新属性：${\n                      A_ExistsProperties[index].name\n                      }属性简介由“${A_ExistsProperties[index].description ||\n                      '无'}”变更为“${joinDescription}”]`\n                  }\n\n                  properties.push({\n                    ...A_ExistsProperties[index],\n                    rule:\n                      !A_ExistsProperties[index].rule && !A_ExistsProperties[index].value\n                        ? rule\n                        : A_ExistsProperties[index].rule,\n                    value:\n                      !A_ExistsProperties[index].rule && !A_ExistsProperties[index].value\n                        ? value\n                        : A_ExistsProperties[index].value,\n                    type,\n                    required: !!bValue.required, // 是否必填更改\n                    description: `${joinDescription}`,\n                  })\n                } else {\n                  changeTip = `${changeTip}<br/>接口名称：${apiObj.summary} [属性添加：${\n                    bValue.name\n                    }；类型：${type} ；简介: ${bValue.description || ''}${\n                    bValue.description || '' ? '|' : ''\n                    }${description || ''} ]`\n                  // 属性不存在\n                  if (depth === 0) {\n                    properties.push({\n                      id: `memory-${++PropertyId}`,\n                      scope,\n                      type,\n                      pos: REQUEST_TYPE_POS[bValue.in],\n                      name: bValue.name,\n                      rule,\n                      value,\n                      description: `${joinDescription}`,\n                      parentId: -1,\n                      priority: `${++PriorityId}`,\n                      interfaceId: findApi.id,\n                      moduleId: mod.id,\n                      repositoryId,\n                      memory: true,\n                      depth: bValue.depth,\n                      changeType: 'add',\n                    })\n                  } else {\n                    // 找到父级属性信息\n                    const parent = properties.find(\n                      it =>\n                        it.depth === bValue.depth - 1 &&\n                        it.name === bValue.parentName &&\n                        it.scope === scope,\n                    )\n                    properties.push({\n                      id: `memory-${++PropertyId}`,\n                      scope,\n                      type,\n                      pos: REQUEST_TYPE_POS[bValue.in],\n                      name: bValue.name,\n                      rule,\n                      value,\n                      description: `${joinDescription}`,\n                      parentId: parent.id,\n                      priority: `${++PriorityId}`,\n                      interfaceId: findApi.id,\n                      moduleId: mod.id,\n                      repositoryId,\n                      memory: true,\n                      depth: bValue.depth,\n                      changeType: 'add',\n                    })\n                  }\n                }\n              }\n            }\n\n            /** 删除属性计算 */\n            const deleteProperties = (AFilterByDepth, scope) => {\n              for (const key in AFilterByDepth) {\n                const aValue = AFilterByDepth[key]\n                let index = -1\n                if (scope === 'request') {\n                  index = B_SwaggerProperties_Request.findIndex(\n                    item =>\n                      item.name === aValue.name &&\n                      item.depth === aValue.depth &&\n                      item.parentName === aValue.parentName,\n                  )\n                } else if (scope === 'response') {\n                  index = B_SwaggerProperties_Response.findIndex(\n                    item =>\n                      item.name === aValue.name &&\n                      item.depth === aValue.depth &&\n                      item.parentName === aValue.parentName,\n                  )\n                }\n\n                const { type, description } = transformRapParams(aValue)\n                if (index < 0) {\n                  // A 存在，B不存在\n                  changeTip = `${changeTip} <br/> 接口名称：${apiObj.summary} [属性删除：${\n                    aValue.name\n                    }；类型：${type} ；简介: ${aValue.description || ''} ${\n                    description ? `${aValue.description ? '|' : ''}${description}` : ''\n                    } ]`\n                }\n              }\n            }\n            for (let depth = 0; depth <= maxDepth_A_ExistsProperties; depth++) {\n              const AFilterByDepth = A_ExistsProperties.filter(\n                item => item.depth === depth && item.scope === 'request',\n              )\n              deleteProperties(AFilterByDepth, 'request')\n            }\n            for (let depth = 0; depth <= maxDepth_A_ExistsProperties; depth++) {\n              const AFilterByDepth = A_ExistsProperties.filter(\n                item => item.depth === depth && item.scope === 'response',\n              )\n              deleteProperties(AFilterByDepth, 'response')\n            }\n\n            for (let depth = 0; depth <= maxDepth_Request; depth++) {\n              const BFilterByDepth = B_SwaggerProperties_Request.filter(\n                item => item.depth === depth,\n              )\n              updateProperties(BFilterByDepth, depth, 'request')\n            }\n\n            for (let depth = 0; depth <= maxDepth_Response; depth++) {\n              const BFilterByDepth = B_SwaggerProperties_Response.filter(\n                item => item.depth === depth,\n              )\n              updateProperties(BFilterByDepth, depth, 'response')\n            }\n            await propertiesUpdateService(properties, findApi.id)\n          }\n        }\n      }\n    }\n\n    if (checkSwaggerResult.length > 0) return false\n    return true\n  }\n\n  /** Swagger property */\n  public static async importRepoFromSwaggerDocUrl(\n    orgId: number,\n    curUserId: number,\n    swagger: SwaggerData,\n    version: number,\n    mode: string,\n    repositoryId: number,\n  ): Promise<any> {\n    try {\n      if (!swagger) return { result: false, code: 'swagger' }\n      const { host = '', info = {} } = swagger\n\n      if (swagger.swagger === SWAGGER_VERSION[version]) {\n        let result\n        let mailRepositoryName = '',\n          mailRepositoryId = 0,\n          mailRepositoryMembers = []\n\n        if (mode === 'manual') {\n          const repos = await Repository.findByPk(repositoryId, {\n            attributes: { exclude: [] },\n            include: [\n              QueryInclude.Creator,\n              QueryInclude.Owner,\n              QueryInclude.Members,\n              QueryInclude.Organization,\n              QueryInclude.Collaborators,\n            ],\n          })\n          const { creatorId, members, collaborators, ownerId, name } = repos\n\n          const body = {\n            creatorId: creatorId,\n            organizationId: orgId,\n            memberIds: (members || []).map((item: any) => item.id),\n            collaboratorIds: (collaborators || []).map((item: any) => item.id),\n            ownerId,\n            visibility: true,\n            name,\n            id: repositoryId,\n            description: `[host=${host}]${info.title || ''}`,\n          }\n          result = await Repository.update(body, { where: { id: repositoryId } })\n\n          mailRepositoryName = name\n          mailRepositoryMembers = members\n          mailRepositoryId = repositoryId\n        } else if (mode === 'auto') {\n          // 团队下直接导入功能作废，此处不用执行\n          result = await Repository.create({\n            id: 0,\n            name: info.title || 'swagger导入仓库',\n            description: info.description || 'swagger导入仓库',\n            visibility: true,\n            ownerId: curUserId,\n            creatorId: curUserId,\n            organizationId: orgId,\n            members: [],\n            collaborators: [],\n            collaboratorIdstring: '',\n            memberIds: [],\n            collaboratorIds: [],\n          })\n        }\n\n        if (result[0] || result.id) {\n          const bol = await this.importRepoFromSwaggerProjectData(\n            mode === 'manual' ? repositoryId : result.id,\n            curUserId,\n            swagger,\n          )\n\n          if (!bol) {\n            return { result: checkSwaggerResult, code: 'checkSwagger' }\n          } else {\n            await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, result.id)\n            if (changeTip.length > 0) {\n              const to = mailRepositoryMembers.map(item => {\n                return `\"${item.fullname}\" ${item.email},`\n              })\n\n              MailService.send(\n                to,\n                `仓库：${mailRepositoryName}(${mailRepositoryId})接口更新同步`,\n                sendMailTemplate(changeTip),\n              )\n                .then(() => { })\n                .catch(() => { })\n\n              // 钉钉消息发送\n              // const dingMsg = {\n              //   msgtype: 'action_card',\n              //   action_card: {\n              //     title: `仓库：${mailRepositoryName}(${mailRepositoryId})接口更新同步`,\n              //     markdown: \"支持markdown格式的正文内容\",\n              //     single_title: \"查看仓库更新\", // swagger 批量导入跳转至仓库， 如果后期只要接口更新就通知相关人的话，需要设置具体接口链接\n              //     single_url: `https://rap2.alibaba-inc.com/repository/editor?id=${repositoryId}`\n              //   }\n              // }\n\n              // DingPushService.dingPush(mailRepositoryMembers.map(item => item.empId).join(), dingMsg)\n              // .catch((err) => { console.log(err) })\n            }\n            changeTip = ''\n            return { result: bol, code: 'success' }\n          }\n        }\n      } else {\n        return { result: true, code: 'version' }\n      }\n    } catch (err) {\n      console.log(err)\n      return { result: false, code: 'error' }\n    }\n  }\n\n  public static async importInterfaceFromJSON(data: any, curUserId: number, repositoryId: number, modId: number) {\n\n    let itfData = data.itf ? data.itf : data\n    let properties = data.itf ? data.properties : itfData?.properties\n\n    const itf = await Interface.create({\n      moduleId: modId,\n      name: itfData.name,\n      description: itfData.description || '',\n      url: itfData.url,\n      priority: 1,\n      creatorId: curUserId,\n      repositoryId,\n      method: itfData.method,\n    })\n\n    if (!properties) {\n      properties = []\n    }\n\n    const idMaps: any = {}\n\n    await Promise.all(\n      properties.map(async (pData, index) => {\n        const property = await Property.create({\n          scope: pData.scope,\n          name: pData.name,\n          rule: pData.rule,\n          value: pData.value,\n          type: pData.type,\n          description: pData.description,\n          pos: pData.pos,\n          priority: 1 + index,\n          interfaceId: itf.id,\n          creatorId: curUserId,\n          moduleId: modId,\n          repositoryId,\n          parentId: -1,\n        })\n        idMaps[pData.id] = property.id\n      }),\n    )\n\n    await Promise.all(\n      properties.map(async pData => {\n        const newId = idMaps[pData.id]\n        const newParentId = idMaps[pData.parentId]\n        await Property.update(\n          {\n            parentId: newParentId,\n          },\n          {\n            where: {\n              id: newId,\n            },\n          },\n        )\n      }),\n    )\n    await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, repositoryId)\n  }\n  /** 可以直接让用户把自己本地的 data 数据导入到 RAP 中 */\n  public static async importRepoFromJSON(data: JsonData, curUserId: number, createRepo: boolean = false, orgId?: number) {\n    function parseJSON(str: string) {\n      try {\n        const data = JSON5.parse(str)\n        return _.isObject(data) ? data : {}\n      } catch (error) {\n        return {}\n      }\n    }\n\n    if (createRepo) {\n      if (orgId === undefined) {\n        throw new Error(\"orgId is essential while createRepo = true\")\n      }\n      const repo = await Repository.create({\n        name: data.name,\n        description: data.description,\n        visibility: true,\n        ownerId: curUserId,\n        creatorId: curUserId,\n        organizationId: orgId,\n      })\n      data.id = repo.id\n    }\n\n    const repositoryId = data.id\n    await Promise.all(\n      data.modules.map(async (modData, index) => {\n        const mod = await Module.create({\n          name: modData.name,\n          description: modData.description || '',\n          priority: index + 1,\n          creatorId: curUserId,\n          repositoryId,\n        })\n\n        await Promise.all(\n          modData.interfaces.map(async (iftData, index) => {\n            let properties = iftData.properties\n\n            const itf = await Interface.create({\n              moduleId: mod.id,\n              name: iftData.name,\n              description: iftData.description || '',\n              url: iftData.url,\n              priority: index + 1,\n              creatorId: curUserId,\n              repositoryId,\n              method: iftData.method,\n            })\n\n            if (!properties && (iftData.requestJSON || iftData.responseJSON)) {\n              const reqData = parseJSON(iftData.requestJSON)\n              const resData = parseJSON(iftData.responseJSON)\n              properties = [\n                ...Tree.jsonToArray(reqData, {\n                  interfaceId: itf.id,\n                  moduleId: mod.id,\n                  repositoryId,\n                  scope: 'request',\n                  userId: curUserId,\n                }),\n                ...Tree.jsonToArray(resData, {\n                  interfaceId: itf.id,\n                  moduleId: mod.id,\n                  repositoryId,\n                  scope: 'response',\n                  userId: curUserId,\n                }),\n              ]\n            }\n\n            if (!properties) {\n              properties = []\n            }\n\n            const idMaps: any = {}\n\n            await Promise.all(\n              properties.map(async (pData, index) => {\n                const property = await Property.create({\n                  scope: pData.scope,\n                  name: pData.name,\n                  rule: pData.rule,\n                  value: pData.value,\n                  type: pData.type,\n                  description: pData.description,\n                  pos: pData.pos,\n                  priority: index + 1,\n                  interfaceId: itf.id,\n                  creatorId: curUserId,\n                  moduleId: mod.id,\n                  repositoryId,\n                  parentId: -1,\n                })\n                idMaps[pData.id] = property.id\n              }),\n            )\n\n            await Promise.all(\n              properties.map(async pData => {\n                const newId = idMaps[pData.id]\n                const newParentId = idMaps[pData.parentId]\n                await Property.update(\n                  {\n                    parentId: newParentId,\n                  },\n                  {\n                    where: {\n                      id: newId,\n                    },\n                  },\n                )\n              }),\n            )\n          }),\n        )\n      }),\n    )\n\n    await RedisService.delCache(CACHE_KEY.REPOSITORY_GET, repositoryId)\n  }\n}\n\nfunction getMethodFromRAP1RequestType(type: number) {\n  switch (type) {\n    case 1:\n      return 'GET'\n    case 2:\n      return 'POST'\n    case 3:\n      return 'PUT'\n    case 4:\n      return 'DELETE'\n    default:\n      return 'GET'\n  }\n}\n\ninterface JsonData {\n  /**\n   * 要导入的目标 repo id 名\n   */\n  id: number\n  name?: string\n  description?: string\n  modules: {\n    name: string\n    description?: string\n    /**\n     * 排序优先级\n     * 从 1 开始，小的在前面\n     */\n    interfaces: {\n      name: string\n      url: string\n      /**\n       * GET POST\n       */\n      method: string\n      description?: string\n      /**\n       * 状态码\n       */\n      status: number\n      /**\n       * 标准属性数组\n       */\n      properties: Partial<Property>[]\n      /**\n       * 导入请求数据 json 字符串\n       */\n      requestJSON: string\n      /**\n       * 导入响应数据 json 字符串\n       */\n      responseJSON: string\n    }[]\n  }[]\n}\n\ninterface OldParameter {\n  id: number\n  name: string\n  mockData: string\n  identifier: string\n  remark: string\n  dataType: string\n  parameterList: OldParameter[]\n  parentName: string\n  depth: number\n}\n\ninterface SwaggerParameter {\n  name: string\n  in: string\n  description?: string\n  required: boolean\n  type: string\n  allowEmptyValue?: boolean\n  minLength?: number\n  maxLength?: number\n  format?: string\n  minimum?: number\n  maxinum?: number\n  default?: any\n  items?: SwaggerParameter[]\n  collectionFormat?: string\n  exclusiveMaximum?: number\n  exclusiveMinimum?: number\n  enum?: Array<any>\n  multipleOf?: number\n  uniqueItems?: boolean\n  pattern?: string\n  schema: any\n  children: SwaggerParameter[]\n  id: string\n  depth: number\n}\n\ninterface SwaggerTag {\n  name: string\n  description?: string\n}\n\ninterface SwaggerInfo {\n  description?: string\n  title?: string\n  version?: string\n}\n\ninterface SwaggerData {\n  swagger: string\n  host: string\n  tags: SwaggerTag[]\n  paths: object\n  definitions?: object\n  info?: SwaggerInfo\n}\n"
  },
  {
    "path": "src/service/mock.ts",
    "content": "import { Repository, Interface, Property, DefaultVal } from '../models'\nimport { Op } from 'sequelize'\nimport urlUtils from '../routes/utils/url'\nimport Tree from '../routes/utils/tree'\nimport * as urlPkg from 'url'\nimport * as querystring from 'querystring'\n\nconst REG_URL_METHOD = /^\\/?(get|post|delete|put)/i\nconst attributes: any = { exclude: [] }\n\nexport class MockService {\n  public static async mock(ctx: any, option: { forceVerify: boolean } = { forceVerify: false }) {\n    const { forceVerify } = option\n    let app: any = ctx.app\n    app.counter.mock++\n    let { repositoryId, url } = ctx.params\n    let method = ctx.request.method\n    repositoryId = +repositoryId\n    if (REG_URL_METHOD.test(url)) {\n      REG_URL_METHOD.lastIndex = -1\n      method = REG_URL_METHOD.exec(url)[1].toUpperCase()\n      REG_URL_METHOD.lastIndex = -1\n      url = url.replace(REG_URL_METHOD, '')\n    }\n\n    let urlWithoutPrefixSlash = /(\\/)?(.*)/.exec(url)[2]\n\n    let repository = await Repository.findByPk(repositoryId)\n    let collaborators: Repository[] = (await repository.$get('collaborators')) as Repository[]\n    let itf: Interface\n\n    let matchedItfList = await Interface.findAll({\n      attributes,\n      where: {\n        repositoryId: [repositoryId, ...collaborators.map(item => item.id)],\n        ...(forceVerify ? { method } : {}),\n        url: {\n          [Op.like]: `%${urlWithoutPrefixSlash}%`,\n        },\n      },\n    })\n\n    function getRelativeURLWithoutParams(url: string) {\n      if (url.indexOf('http://') > -1) {\n        url = url.substring('http://'.length)\n      }\n      if (url.indexOf('https://') > -1) {\n        url = url.substring('https://'.length)\n      }\n      if (url.indexOf('/') > -1) {\n        url = url.substring(url.indexOf('/') + 1)\n      }\n      if (url.indexOf('?') > -1) {\n        url = url.substring(0, url.indexOf('?'))\n      }\n      return url\n    }\n\n    // matching by path\n    if (matchedItfList.length > 1) {\n      matchedItfList = matchedItfList.filter(x => {\n        const urlDoc = getRelativeURLWithoutParams(x.url)\n        const urlRequest = urlWithoutPrefixSlash\n        return urlDoc === urlRequest\n      })\n    }\n\n    // matching by params\n    if (matchedItfList.length > 1) {\n      const params = {\n        ...ctx.request.query,\n        ...ctx.request.body,\n      }\n      const paramsKeysCnt = Object.keys(params).length\n      matchedItfList = matchedItfList.filter(x => {\n        const parsedUrl = urlPkg.parse(x.url)\n        const pairs = parsedUrl.query ? parsedUrl.query.split('&').map(x => x.split('=')) : []\n        // 接口没有定义参数时看请求是否有参数\n        if (pairs.length === 0) {\n          return paramsKeysCnt === 0\n        }\n        // 接口定义参数时看每一项的参数是否一致\n        for (const p of pairs) {\n          const key = p[0]\n          const val = p[1]\n          if (params[key] != val) {\n            return false\n          }\n        }\n        return true\n      })\n    }\n\n    // 多个协同仓库的结果优先返回当前仓库的\n    if (matchedItfList.length > 1) {\n      const currProjMatchedItfList = matchedItfList.filter(x => x.repositoryId === repositoryId)\n      // 如果直接存在当前仓库的就当做结果集，否则放弃\n      if (currProjMatchedItfList.length > 0) {\n        matchedItfList = currProjMatchedItfList\n      }\n    }\n\n    for (const item of matchedItfList) {\n      itf = item\n      let url = item.url\n      if (url.charAt(0) === '/') {\n        url = url.substring(1)\n      }\n      if (url === urlWithoutPrefixSlash) {\n        break\n      }\n    }\n\n    if (!itf) {\n      // try RESTFul API search...\n      let list = await Interface.findAll({\n        attributes: ['id', 'url', 'method'],\n        where: {\n          repositoryId: [repositoryId, ...collaborators.map(item => item.id)],\n          method,\n        },\n      })\n\n      let listMatched = []\n      let relativeUrl = urlUtils.getRelative(url)\n\n      for (let item of list) {\n        let regExp = urlUtils.getUrlPattern(item.url) // 获取地址匹配正则\n        if (regExp.test(relativeUrl)) {\n          // 检查地址是否匹配\n          let regMatchLength = regExp.exec(relativeUrl).length // 执行地址匹配\n          if (listMatched[regMatchLength]) {\n            // 检查匹配地址中，是否具有同group数量的数据\n            ctx.body = {\n              isOk: false,\n              errMsg: '匹配到多个同级别接口，请修改规则确保接口规则唯一性。',\n            }\n            return\n          }\n          listMatched[regMatchLength] = item // 写入数据\n        }\n      }\n\n      let loadDataId = 0\n      if (listMatched.length > 1) {\n        for (let matchedItem of listMatched) {\n          // 循环匹配内的数据\n          if (matchedItem) {\n            // 忽略为空的数据\n            loadDataId = matchedItem.id // 设置需查询的id\n            break\n          }\n        }\n      } else if (listMatched.length === 0) {\n        ctx.body = { isOk: false, errMsg: '未匹配到任何接口，请检查请求类型是否一致。' }\n        ctx.status = 404\n        return\n      } else {\n        loadDataId = listMatched[0].id\n      }\n\n      itf = itf = await Interface.findByPk(loadDataId)\n    }\n\n    let interfaceId = itf.id\n    let properties = await Property.findAll({\n      attributes,\n      where: { interfaceId, scope: 'response' },\n    })\n\n    // default values override\n    const defaultVals = await DefaultVal.findAll({ where: { repositoryId } })\n    const defaultValsMap: { [key: string]: DefaultVal } = {}\n    for (const dv of defaultVals) {\n      defaultValsMap[dv.name] = dv\n    }\n    for (const p of properties) {\n      const dv = defaultValsMap[p.name]\n      if (!p.value && !p.rule && dv) {\n        p.value = dv.value\n        p.rule = dv.rule\n      }\n    }\n\n    // check required\n    if (forceVerify && ~['GET', 'POST'].indexOf(method)) {\n      let requiredProperties = await Property.findAll({\n        attributes,\n        where: { interfaceId, scope: 'request', required: true },\n      })\n      let passed = true\n      let pFailed: Property | undefined\n      let params = { ...ctx.request.query, ...ctx.request.body }\n      // http request中head的参数未添加，会造成head中的参数必填勾选后即使header中有值也会检查不通过\n      params = Object.assign(params, ctx.request.headers)\n      for (const p of requiredProperties) {\n        if (typeof params[p.name] === 'undefined') {\n          passed = false\n          pFailed = p\n          break\n        }\n      }\n      if (!passed) {\n        ctx.set(\n          'X-RAP-WARNING',\n          `Required parameter ${pFailed.name} has not be passed in.`,\n        )\n      }\n    }\n\n    properties = properties.map((item: any) => item.toJSON())\n\n    // 支持引用请求参数\n    let requestProperties: any = await Property.findAll({\n      attributes,\n      where: { interfaceId, scope: 'request' },\n    })\n    requestProperties = requestProperties.map((item: any) => item.toJSON())\n    let requestData = Tree.ArrayToTreeToTemplateToData(requestProperties)\n    Object.assign(requestData, { ...ctx.params, ...ctx.query, ...ctx.body })\n    let data = Tree.ArrayToTreeToTemplateToData(properties, requestData)\n    if (data.__root__) {\n      data = data.__root__\n    }\n    ctx.type = 'json'\n    ctx.status = itf.status\n    ctx.body = JSON.stringify(data, undefined, 2)\n    const Location = data.Location\n    if (Location && itf.status === 301) {\n      ctx.redirect(Location)\n      return\n    }\n    if (itf && itf.url.indexOf('[callback]=') > -1) {\n      const query = querystring.parse(itf.url.substring(itf.url.indexOf('?') + 1))\n      const cbName = query['[callback]']\n      const cbVal = ctx.request.query[`${cbName}`]\n      if (cbVal) {\n        let body = typeof ctx.body === 'object' ? JSON.stringify(ctx.body, undefined, 2) : ctx.body\n        ctx.type = 'application/x-javascript'\n        ctx.body = cbVal + '(' + body + ')'\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/service/organization.ts",
    "content": "import seq from '../models/sequelize'\nimport Pagination from '../routes/utils/pagination'\nimport Utils from './utils'\nexport default class OrganizationService {\n  public static canUserAccessOrganization(userId: number, organizationId: number): Promise<boolean> {\n    const sql = `\n      SELECT COUNT(id) AS num FROM (\n        SELECT o.id, o.name\n        FROM Organizations o\n        WHERE ownerId = ${userId}\n        UNION\n        SELECT o.id, o.name\n        FROM Organizations o\n        JOIN organizations_members om ON o.id = om.organizationId\n        WHERE om.userId = ${userId}\n      ) as result\n      WHERE id = ${organizationId}\n    `\n    return new Promise(resolve => {\n      seq.query(sql).spread((result: any) => {\n        resolve(+result[0].num > 0)\n      })\n    })\n  }\n\n  public static getAllOrganizationIdList(curUserId: number, pager: Pagination, query?: string): Promise<number[]> {\n    if (query) {\n      query = Utils.escapeSQL(query)\n    }\n    const sql = `\n      SELECT id FROM (\n        SELECT o.id, o.name\n        FROM Organizations o\n        WHERE visibility = ${1} OR ownerId = ${curUserId}\n        UNION\n        SELECT o.id, o.name\n        FROM Organizations o\n        JOIN organizations_members om ON o.id = om.organizationId\n        WHERE om.userId = ${curUserId}\n      ) as result\n      ${query ? `WHERE id = '${query}' OR name LIKE '%${query}%'` : ''}\n      ORDER BY id desc\n      LIMIT ${pager.start}, ${pager.limit}\n    `\n    return new Promise(resolve => {\n      seq.query(sql).spread((result: { id: number }[]) => {\n        resolve(result.map(item => item.id))\n      })\n    })\n  }\n\n  public static getAllOrganizationIdListNum(curUserId: number): Promise<number> {\n    const sql = `\n      SELECT count(*) AS num FROM (\n        SELECT o.id, o.name\n        FROM Organizations o\n        WHERE visibility = ${1} OR ownerId = ${curUserId}\n        UNION\n        SELECT o.id, o.name\n        FROM Organizations o\n        JOIN organizations_members om ON o.id = om.organizationId\n        WHERE om.userId = ${curUserId}\n      ) as result\n      ORDER BY id desc\n    `\n    return new Promise(resolve => {\n      seq.query(sql).spread((result: { num: number }[]) => {\n        resolve(result[0].num)\n      })\n    })\n  }\n}"
  },
  {
    "path": "src/service/redis.ts",
    "content": "import * as redis from 'redis'\nimport config from '../config'\nimport * as ioredis from 'ioredis'\nimport { THEME_TEMPLATE_KEY } from '../routes/utils/const'\n\nexport enum CACHE_KEY {\n  REPOSITORY_GET = 'REPOSITORY_GET',\n  PWDRESETTOKEN_GET = 'PWDRESETTOKEN_GET',\n  REPOSITORY_GET_EXCLUDE_PROPERTY = 'REPOSITORY_GET_EXCLUDE_PROPERTY',\n  /** GLOBAL PERSONAL PREFERENCES */\n  THEME_ID = 'THEME_ID',\n  GUIDE_20200714 = 'GUIDE_20200714',\n}\n\nexport const DEFAULT_CACHE_VAL = {\n  [CACHE_KEY.THEME_ID]: THEME_TEMPLATE_KEY.INDIGO\n}\n\nexport default class RedisService {\n  private static client: redis.RedisClient = config.redis && config.redis.isRedisCluster ? new ioredis.Cluster(config.redis.nodes, {redisOptions: config.redis.redisOptions}) : redis.createClient(config.redis)\n\n  private static getCacheKey(key: CACHE_KEY, entityId?: number): string {\n    return `${key}:${entityId || ''}`\n  }\n\n  public static getCache(key: CACHE_KEY, entityId?: number): Promise<string | null> {\n    const cacheKey = this.getCacheKey(key, entityId)\n    return new Promise((resolve, reject) => {\n      RedisService.client.get(cacheKey, (error: Error, value: string | null) => {\n        if (error) {\n          return reject(error)\n        }\n        resolve(value)\n      })\n    })\n  }\n\n  public static setCache(key: CACHE_KEY, val: string, entityId?: number, expireTime?: number): Promise<boolean> {\n    const cacheKey = this.getCacheKey(key, entityId)\n    return new Promise((resolve, reject) => {\n      RedisService.client.set(cacheKey, val, 'EX', expireTime || 1 * 24 * 60 * 60, (err: Error) => {\n        if (err) {\n          return reject(false)\n        }\n        return resolve(true)\n      })\n    })\n  }\n\n  public static delCache(key: CACHE_KEY, entityId?: number): Promise<boolean> {\n    let cacheKey = this.getCacheKey(key, entityId)\n    return new Promise((resolve, reject) => {\n      RedisService.client.del(cacheKey, (error) => {\n        if (error) {\n          reject(error)\n        }\n        resolve(true)\n      })\n    })\n  }\n}"
  },
  {
    "path": "src/service/repository.ts",
    "content": "import { Repository, RepositoriesMembers, Interface, Property, Module, HistoryLog, User } from '../models'\nimport { MoveOp } from '../models/bo/interface'\nimport RedisService, { CACHE_KEY } from '../service/redis'\nimport { AccessUtils, ACCESS_TYPE } from '../routes/utils/access'\nimport OrganizationService from './organization'\nimport { ENTITY_TYPE } from '../routes/utils/const'\nimport { Op, col, fn } from 'sequelize'\nimport { IPager } from '../types'\n\nexport default class RepositoryService {\n  public static async canUserAccessRepository(\n    userId: number,\n    repositoryId: number,\n    token?: string,\n  ): Promise<boolean> {\n    const repo = await Repository.findByPk(repositoryId)\n    if (token && repo.token === token) return true\n    if (!repo) return false\n    if (repo.ownerId === userId) return true\n    const memberExistsNum = await RepositoriesMembers.count({\n      where: {\n        userId,\n        repositoryId,\n      },\n    })\n    if (memberExistsNum > 0) return true\n    return OrganizationService.canUserAccessOrganization(userId, repo.organizationId)\n  }\n\n  public static async canUserMoveInterface(\n    userId: number,\n    itfId: number,\n    destRepoId: number,\n    destModuleId: number,\n  ) {\n    return (\n      AccessUtils.canUserAccess(ACCESS_TYPE.INTERFACE_GET, userId, itfId) &&\n      AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, userId, destRepoId) &&\n      AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_SET, userId, destModuleId)\n    )\n  }\n\n  public static async canUserMoveModule(userId: number, modId: number, destRepoId: number) {\n    return (\n      AccessUtils.canUserAccess(ACCESS_TYPE.MODULE_GET, userId, modId) &&\n      AccessUtils.canUserAccess(ACCESS_TYPE.REPOSITORY_SET, userId, destRepoId)\n    )\n  }\n\n  public static async moveModule(op: MoveOp, modId: number, destRepoId: number, nameSuffix = '副本') {\n    const mod = await Module.findByPk(modId)\n    const fromRepoId = mod.repositoryId\n    if (op === MoveOp.MOVE) {\n      mod.repositoryId = destRepoId\n      await mod.save()\n      await Interface.update(\n        {\n          repositoryId: destRepoId,\n        },\n        {\n          where: {\n            moduleId: modId,\n          },\n        },\n      )\n      await Property.update(\n        {\n          repositoryId: destRepoId,\n        },\n        {\n          where: {\n            moduleId: modId,\n          },\n        },\n      )\n    } else if (op === MoveOp.COPY) {\n      const { id, name, ...otherProps } = mod.toJSON() as Module\n      const interfaces = await Interface.findAll({\n        where: {\n          moduleId: modId,\n        },\n      })\n      const newMod = await Module.create({\n        name: mod.name + nameSuffix,\n        ...otherProps,\n        repositoryId: destRepoId,\n      })\n      const promises = interfaces.map(itf =>\n        RepositoryService.moveInterface(MoveOp.COPY, itf.id, destRepoId, newMod.id, ''),\n      )\n      await Promise.all(promises)\n    }\n    await Promise.all([\n      RedisService.delCache(CACHE_KEY.REPOSITORY_GET, fromRepoId),\n      RedisService.delCache(CACHE_KEY.REPOSITORY_GET, destRepoId),\n    ])\n  }\n\n  public static async moveInterface(\n    op: MoveOp,\n    itfId: number,\n    destRepoId: number,\n    destModuleId: number,\n    nameSuffix = '副本'\n  ) {\n    const itf = await Interface.findByPk(itfId)\n    const fromRepoId = itf.repositoryId\n    if (op === MoveOp.MOVE) {\n      itf.moduleId = destModuleId\n      itf.repositoryId = destRepoId\n      await Property.update(\n        {\n          moduleId: destModuleId,\n          repositoryId: destRepoId,\n        },\n        {\n          where: {\n            interfaceId: itf.id,\n          },\n        },\n      )\n      await itf.save()\n    } else if (op === MoveOp.COPY) {\n      const { id, name, ...otherProps } = itf.toJSON() as Interface\n      const newItf = await Interface.create({\n        name: name + nameSuffix,\n        ...otherProps,\n        repositoryId: destRepoId,\n        moduleId: destModuleId,\n      })\n\n      const properties = await Property.findAll({\n        where: {\n          interfaceId: itf.id,\n        },\n        order: [['parentId', 'asc']],\n      })\n      // 解决parentId丢失的问题\n      let idMap: any = {}\n      for (const property of properties) {\n        const { id, parentId, ...props } = property.toJSON() as Property\n        const newParentId = idMap[parentId + ''] ? idMap[parentId + ''] : -1\n        const newProperty = await Property.create({\n          ...props,\n          interfaceId: newItf.id,\n          parentId: newParentId,\n          repositoryId: destRepoId,\n          moduleId: destModuleId,\n        })\n        idMap[id + ''] = newProperty.id\n      }\n    }\n    await Promise.all([\n      RedisService.delCache(CACHE_KEY.REPOSITORY_GET, fromRepoId),\n      RedisService.delCache(CACHE_KEY.REPOSITORY_GET, destRepoId),\n    ])\n  }\n\n  public static async addHistoryLog(log: Partial<HistoryLog>) {\n    await HistoryLog.create(log)\n  }\n\n  public static async getHistoryLog(entityId: number, entityType: ENTITY_TYPE.INTERFACE | ENTITY_TYPE.REPOSITORY, pager: IPager) {\n    const { offset, limit } = pager\n    const baseCon = { entityType: entityType, entityId: entityId }\n    const isRepo = entityType === ENTITY_TYPE.REPOSITORY\n    let relatedInterfaceIds: number[] = []\n    if (isRepo) {\n      const interfaces = await Interface.findAll({ attributes: ['id'], where: { repositoryId: entityId } })\n      relatedInterfaceIds = interfaces.map(x => x.id)\n    }\n    return (await HistoryLog.findAndCountAll({\n      attributes: ['id', 'changeLog', 'entityId', 'entityType', 'userId', 'createdAt', [fn('isnull', col('relatedJSONData')), 'jsonDataIsNull']],\n      where: {\n        ...relatedInterfaceIds.length === 0 ? baseCon : {\n          [Op.or]: [baseCon, {\n            entityType: ENTITY_TYPE.INTERFACE,\n            entityId: { [Op.in]: relatedInterfaceIds },\n          }]\n        },\n      },\n      include: [{\n        attributes: ['id', 'fullname'],\n        model: User,\n        as: 'user',\n      }],\n      order: [['id', 'desc']],\n      offset,\n      limit,\n    }))\n  }\n\n  public static async getHistoryLogJSONData(id: number) {\n    return (await HistoryLog.findByPk(id))?.relatedJSONData\n  }\n\n  public static async getInterfaceJSONData(id: number) {\n    const itf = await Interface.findByPk(id)\n    const properties = await Property.findAll({ where: { interfaceId: id } })\n    return JSON.stringify({ \"itf\": itf, \"properties\": properties })\n  }\n}\n"
  },
  {
    "path": "src/service/task.ts",
    "content": "import * as schedule from 'node-schedule'\nimport { Interface } from '../models'\nimport { Op } from 'sequelize'\nimport { DATE_CONST } from '../routes/utils/const'\n\nexport async function startTask() {\n\n  console.log(`Starting task: locker check`)\n\n  /**\n   * 每5分钟检查lock超时\n   */\n  schedule.scheduleJob('*/5 * * * *', async () => {\n    // tslint:disable-next-line: no-null-keyword\n    const [num] = await Interface.update({ lockerId: null }, {\n      where: {\n        lockerId: {\n          [Op.gt]: 0,\n        },\n        updatedAt: {\n          [Op.lt]: new Date(Date.now() - DATE_CONST.DAY),\n        },\n      },\n    })\n\n    num > 0 && console.log(`cleared ${num} locks`)\n  })\n}"
  },
  {
    "path": "src/service/utils.ts",
    "content": "export default class Utils {\n  public static escapeSQL(str: string) {\n    if (typeof str === 'string') {\n      str = str.replace(/[\\t\\r\\n]|(--[^\\r\\n]*)|(\\/\\*[\\w\\W]*?(?=\\*)\\*\\/)/gi, '')\n      str = str.replace(/['\",\\.]/ig, '')\n      return str\n    }\n    return ''\n  }\n}"
  },
  {
    "path": "src/types/custom-typings.d.ts",
    "content": "declare module 'ioredis'"
  },
  {
    "path": "src/types/index.d.ts",
    "content": "/// <reference path=\"custom-typings.d.ts\" />\nimport { PoolOptions } from \"sequelize\"\nimport { ISequelizeConfig } from \"sequelize-typescript\"\nimport { RedisOptions } from \"koa-redis\"\nimport { PoolOptions } from \"sequelize\"\n\ndeclare interface RedisAndClusterOptions extends RedisOptions {\n  isRedisCluster?: boolean\n  nodes?: object[]\n  redisOptions?: any\n}\n\n\ndeclare interface IConfigOptions {\n  version: string\n  serve: {\n    port: number\n    path: string // Context Path\n  },\n  keys: string[]\n  session: {\n    key: string\n  },\n  keycenter?: string | boolean,\n  db: ISequelizeConfig,\n  redis: any,\n  mail: SMTPTransport,\n  mailSender: string,\n}\n\ndeclare interface IPager {\n  offset: number\n  limit: number\n  order?: TOrder\n  orderBy?: string\n  query?: string\n}"
  },
  {
    "path": "src/types/postman.d.ts",
    "content": "/**\n * This file was automatically generated by json-schema-to-typescript.\n * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,\n * and run json-schema-to-typescript to regenerate this file.\n */\n\n/**\n * A collection's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this collection among a bunch of other collections, as such outlining its usage or content.\n */\nexport type NameOfTheCollection = string\n/**\n * A Description can be a raw text, or be an object, which holds the description along with its format.\n */\nexport type DefinitionsDescription = Description | string | null\n/**\n * Postman allows you to version your collections as they grow, and this field holds the version number. While optional, it is recommended that you use this field to its fullest extent!\n */\nexport type CollectionVersion =\n  | {\n      /**\n       * Increment this number if you make changes to the collection that changes its behaviour. E.g: Removing or adding new test scripts. (partly or completely).\n       */\n      major: number\n      /**\n       * You should increment this number if you make changes that will not break anything that uses the collection. E.g: removing a folder.\n       */\n      minor: number\n      /**\n       * Ideally, minor changes to a collection should result in the increment of this number.\n       */\n      patch: number\n      /**\n       * A human friendly identifier to make sense of the version numbers. E.g: 'beta-3'\n       */\n      identifier?: string\n      meta?: any\n      [k: string]: any\n    }\n  | string\nexport type Items1 = Item | Folder\n/**\n * Using variables in your Postman requests eliminates the need to duplicate requests, which can save a lot of time. Variables can be defined, and referenced to from any part of a request.\n */\nexport type Variable =\n  | {\n      [k: string]: any\n    }\n  | {\n      [k: string]: any\n    }\n  | {\n      [k: string]: any\n    }\n/**\n * Collection variables allow you to define a set of variables, that are a *part of the collection*, as opposed to environments, which are separate entities.\n * *Note: Collection variables must not contain any sensitive information.*\n */\nexport type VariableList = Variable[]\n/**\n * If object, contains the complete broken-down URL for this request. If string, contains the literal request URL.\n */\nexport type Url =\n  | {\n      /**\n       * The string representation of the request URL, including the protocol, host, path, hash, query parameter(s) and path variable(s).\n       */\n      raw?: string\n      /**\n       * The protocol associated with the request, E.g: 'http'\n       */\n      protocol?: string\n      host?: Host\n      path?:\n        | string\n        | (\n            | string\n            | {\n                type?: string\n                value?: string\n                [k: string]: any\n              })[]\n      /**\n       * The port number present in this URL. An empty value implies 80/443 depending on whether the protocol field contains http/https.\n       */\n      port?: string\n      /**\n       * An array of QueryParams, which is basically the query string part of the URL, parsed into separate variables\n       */\n      query?: QueryParam[]\n      /**\n       * Contains the URL fragment (if any). Usually this is not transmitted over the network, but it could be useful to store this in some cases.\n       */\n      hash?: string\n      /**\n       * Postman supports path variables with the syntax `/path/:variableName/to/somewhere`. These variables are stored in this field.\n       */\n      variable?: Variable[]\n      [k: string]: any\n    }\n  | string\n/**\n * The host for the URL, E.g: api.yourdomain.com. Can be stored as a string or as an array of strings.\n */\nexport type Host = string | string[]\n/**\n * Postman allows you to configure scripts to run when specific events occur. These scripts are stored here, and can be referenced in the collection by their ID.\n */\nexport type EventList = Event[]\n/**\n * A request represents an HTTP request. If a string, the string is assumed to be the request URL and the method is assumed to be 'GET'.\n */\nexport type Request1 = Request | string\n/**\n * The attributes for [AWS Auth](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html).\n */\nexport type AwsSignatureV4 = Auth1[]\n/**\n * The attributes for [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication).\n */\nexport type BasicAuthentication = Auth1[]\n/**\n * The helper attributes for [Bearer Token Authentication](https://tools.ietf.org/html/rfc6750)\n */\nexport type BearerTokenAuthentication = Auth1[]\n/**\n * The attributes for [Digest Authentication](https://en.wikipedia.org/wiki/Digest_access_authentication).\n */\nexport type DigestAuthentication = Auth1[]\n/**\n * The attributes for [Hawk Authentication](https://github.com/hueniverse/hawk)\n */\nexport type HawkAuthentication = Auth1[]\n/**\n * The attributes for [NTLM Authentication](https://msdn.microsoft.com/en-us/library/cc237488.aspx)\n */\nexport type NtlmAuthentication = Auth1[]\n/**\n * The attributes for [OAuth2](https://oauth.net/1/)\n */\nexport type OAuth1 = Auth1[]\n/**\n * Helper attributes for [OAuth2](https://oauth.net/2/)\n */\nexport type OAuth2 = Auth1[]\n/**\n * A representation for a list of headers\n */\nexport type HeaderList = Header[]\nexport type FormParameter =\n  | {\n      key: string\n      value?: string\n      /**\n       * When set to true, prevents this form data entity from being sent.\n       */\n      disabled?: boolean\n      type?: \"text\"\n      description?: DefinitionsDescription\n      [k: string]: any\n    }\n  | {\n      key: string\n      src?: string\n      /**\n       * When set to true, prevents this form data entity from being sent.\n       */\n      disabled?: boolean\n      type?: \"file\"\n      description?: DefinitionsDescription\n      [k: string]: any\n    }\n/**\n * The time taken by the request to complete. If a number, the unit is milliseconds. If the response is manually created, this can be set to `null`.\n */\nexport type ResponseTime = null | string | number\nexport type Headers = Header2 | string\nexport type Header1 = string\n/**\n * No HTTP request is complete without its headers, and the same is true for a Postman request. This field is an array containing all the headers.\n */\nexport type Header2 = (Header | Header1)[]\nexport type Responses = Response[]\nexport type Items = Item | Folder\n\nexport interface PostmanCollection {\n  info: Information\n  /**\n   * Items are the basic unit for a Postman collection. You can think of them as corresponding to a single API endpoint. Each Item has one request and may have multiple API responses associated with it.\n   */\n  item: Items1[]\n  event?: EventList\n  variable?: VariableList\n  auth?: null | Auth\n  [k: string]: any\n}\n/**\n * Detailed description of the info block\n */\nexport interface Information {\n  name: NameOfTheCollection\n  /**\n   * Every collection is identified by the unique value of this field. The value of this field is usually easiest to generate using a UID generator function. If you already have a collection, it is recommended that you maintain the same id since changing the id usually implies that is a different collection than it was originally.\n   *  *Note: This field exists for compatibility reasons with Collection Format V1.*\n   */\n  _postman_id?: string\n  description?: DefinitionsDescription\n  version?: CollectionVersion\n  /**\n   * This should ideally hold a link to the Postman schema that is used to validate this collection. E.g: https://schema.getpostman.com/collection/v1\n   */\n  schema: string\n  [k: string]: any\n}\nexport interface Description {\n  /**\n   * The content of the description goes here, as a raw string.\n   */\n  content?: string\n  /**\n   * Holds the mime type of the raw description content. E.g: 'text/markdown' or 'text/html'.\n   * The type is used to correctly render the description when generating documentation, or in the Postman app.\n   */\n  type?: string\n  /**\n   * Description can have versions associated with it, which should be put in this property.\n   */\n  version?: {\n    [k: string]: any\n  }\n  [k: string]: any\n}\n/**\n * Items are entities which contain an actual HTTP request, and sample responses attached to it.\n */\nexport interface Item {\n  /**\n   * A unique ID that is used to identify collections internally\n   */\n  id?: string\n  /**\n   * A human readable identifier for the current item.\n   */\n  name?: string\n  description?: DefinitionsDescription\n  variable?: VariableList\n  event?: EventList\n  request: Request1\n  response?: Responses\n  [k: string]: any\n}\n/**\n * Defines a script associated with an associated event name\n */\nexport interface Event {\n  /**\n   * A unique identifier for the enclosing event.\n   */\n  id?: string\n  /**\n   * Can be set to `test` or `prerequest` for test scripts or pre-request scripts respectively.\n   */\n  listen: string\n  script?: Script\n  /**\n   * Indicates whether the event is disabled. If absent, the event is assumed to be enabled.\n   */\n  disabled?: boolean\n  [k: string]: any\n}\n/**\n * A script is a snippet of Javascript code that can be used to to perform setup or teardown operations on a particular response.\n */\nexport interface Script {\n  /**\n   * A unique, user defined identifier that can  be used to refer to this script from requests.\n   */\n  id?: string\n  /**\n   * Type of the script. E.g: 'text/javascript'\n   */\n  type?: string\n  exec?: string[] | string\n  src?: Url\n  /**\n   * Script name\n   */\n  name?: string\n  [k: string]: any\n}\nexport interface QueryParam {\n  key?: string | null\n  value?: string | null\n  /**\n   * If set to true, the current query parameter will not be sent with the request.\n   */\n  disabled?: boolean\n  description?: DefinitionsDescription\n  [k: string]: any\n}\nexport interface Request {\n  url?: Url\n  auth?: null | Auth\n  proxy?: ProxyConfig\n  certificate?: Certificate\n  /**\n   * The HTTP method associated with this request.\n   */\n  method?:\n    | \"GET\"\n    | \"PUT\"\n    | \"POST\"\n    | \"PATCH\"\n    | \"DELETE\"\n    | \"COPY\"\n    | \"HEAD\"\n    | \"OPTIONS\"\n    | \"LINK\"\n    | \"UNLINK\"\n    | \"PURGE\"\n    | \"LOCK\"\n    | \"UNLOCK\"\n    | \"PROPFIND\"\n    | \"VIEW\"\n  description?: DefinitionsDescription\n  header?: HeaderList | string\n  /**\n   * This field contains the data usually contained in the request body.\n   */\n  body?: {\n    /**\n     * Postman stores the type of data associated with this request in this field.\n     */\n    mode?: \"raw\" | \"urlencoded\" | \"formdata\" | \"file\"\n    raw?: string\n    urlencoded?: UrlEncodedParameter[]\n    formdata?: FormParameter[]\n    file?: {\n      /**\n       * Contains the name of the file to upload. _Not the path_.\n       */\n      src?: string\n      content?: string\n      [k: string]: any\n    }\n    [k: string]: any\n  }\n  [k: string]: any\n}\n/**\n * Represents authentication helpers provided by Postman\n */\nexport interface Auth {\n  type: \"awsv4\" | \"basic\" | \"bearer\" | \"digest\" | \"hawk\" | \"noauth\" | \"oauth1\" | \"oauth2\" | \"ntlm\"\n  noauth?: any\n  awsv4?: AwsSignatureV4\n  basic?: BasicAuthentication\n  bearer?: BearerTokenAuthentication\n  digest?: DigestAuthentication\n  hawk?: HawkAuthentication\n  ntlm?: NtlmAuthentication\n  oauth1?: OAuth1\n  oauth2?: OAuth2\n  [k: string]: any\n}\n/**\n * Represents an attribute for any authorization method provided by Postman. For example `username` and `password` are set as auth attributes for Basic Authentication method.\n */\nexport interface Auth1 {\n  key: string\n  value?: any\n  type?: string\n  [k: string]: any\n}\n/**\n * Using the Proxy, you can configure your custom proxy into the postman for particular url match\n */\nexport interface ProxyConfig {\n  /**\n   * The Url match for which the proxy config is defined\n   */\n  match?: string\n  /**\n   * The proxy server host\n   */\n  host?: string\n  /**\n   * The proxy server port\n   */\n  port?: number\n  /**\n   * The tunneling details for the proxy config\n   */\n  tunnel?: boolean\n  /**\n   * When set to true, ignores this proxy configuration entity\n   */\n  disabled?: boolean\n  [k: string]: any\n}\n/**\n * A representation of an ssl certificate\n */\nexport interface Certificate {\n  /**\n   * A name for the certificate for user reference\n   */\n  name?: string\n  /**\n   * A list of Url match pattern strings, to identify Urls this certificate can be used for.\n   */\n  matches?: any[]\n  /**\n   * An object containing path to file containing private key, on the file system\n   */\n  key?: {\n    /**\n     * The path to file containing key for certificate, on the file system\n     */\n    src?: {\n      [k: string]: any\n    }\n    [k: string]: any\n  }\n  /**\n   * An object containing path to file certificate, on the file system\n   */\n  cert?: {\n    /**\n     * The path to file containing key for certificate, on the file system\n     */\n    src?: {\n      [k: string]: any\n    }\n    [k: string]: any\n  }\n  /**\n   * The passphrase for the certificate\n   */\n  passphrase?: string\n  [k: string]: any\n}\n/**\n * Represents a single HTTP Header\n */\nexport interface Header {\n  /**\n   * This holds the LHS of the HTTP Header, e.g ``Content-Type`` or ``X-Custom-Header``\n   */\n  key: string\n  /**\n   * The value (or the RHS) of the Header is stored in this field.\n   */\n  value: string\n  /**\n   * If set to true, the current header will not be sent with requests.\n   */\n  disabled?: boolean\n  description?: DefinitionsDescription\n  [k: string]: any\n}\nexport interface UrlEncodedParameter {\n  key: string\n  value?: string\n  disabled?: boolean\n  description?: DefinitionsDescription\n  [k: string]: any\n}\n/**\n * A response represents an HTTP response.\n */\nexport interface Response {\n  /**\n   * A unique, user defined identifier that can  be used to refer to this response from requests.\n   */\n  id?: string\n  originalRequest?: Request1\n  responseTime?: ResponseTime\n  header?: Headers\n  cookie?: Cookie[]\n  /**\n   * The raw text of the response.\n   */\n  body?: string\n  /**\n   * The response status, e.g: '200 OK'\n   */\n  status?: string\n  /**\n   * The numerical response code, example: 200, 201, 404, etc.\n   */\n  code?: number\n  [k: string]: any\n}\n/**\n * A Cookie, that follows the [Google Chrome format](https://developer.chrome.com/extensions/cookies)\n */\nexport interface Cookie {\n  /**\n   * The domain for which this cookie is valid.\n   */\n  domain: string\n  /**\n   * When the cookie expires.\n   */\n  expires?: string | number\n  maxAge?: string\n  /**\n   * True if the cookie is a host-only cookie. (i.e. a request's URL domain must exactly match the domain of the cookie).\n   */\n  hostOnly?: boolean\n  /**\n   * Indicates if this cookie is HTTP Only. (if True, the cookie is inaccessible to client-side scripts)\n   */\n  httpOnly?: boolean\n  /**\n   * This is the name of the Cookie.\n   */\n  name?: string\n  /**\n   * The path associated with the Cookie.\n   */\n  path: string\n  /**\n   * Indicates if the 'secure' flag is set on the Cookie, meaning that it is transmitted over secure connections only. (typically HTTPS)\n   */\n  secure?: boolean\n  /**\n   * True if the cookie is a session cookie.\n   */\n  session?: boolean\n  /**\n   * The value of the Cookie.\n   */\n  value?: string\n  /**\n   * Custom attributes for a cookie go here, such as the [Priority Field](https://code.google.com/p/chromium/issues/detail?id=232693)\n   */\n  extensions?: any[]\n  [k: string]: any\n}\n/**\n * One of the primary goals of Postman is to organize the development of APIs. To this end, it is necessary to be able to group requests together. This can be achived using 'Folders'. A folder just is an ordered set of requests.\n */\nexport interface Folder {\n  /**\n   * A folder's friendly name is defined by this field. You would want to set this field to a value that would allow you to easily identify this folder.\n   */\n  name?: string\n  description?: DefinitionsDescription\n  variable?: VariableList\n  /**\n   * Items are entities which contain an actual HTTP request, and sample responses attached to it. Folders may contain many items.\n   */\n  item: Items[]\n  event?: EventList\n  auth?: null | Auth\n  [k: string]: any\n}\n"
  },
  {
    "path": "test/helper.js",
    "content": "/* global before, after */\nconst Random = require('mockjs').Random\nmodule.exports = {\n  mockUsers: () => [{}, {}, {}, {}, {}].map(item => (\n    {\n      fullname: Random.cname(),\n      email: Random.email(),\n      password: Random.word(6)\n    }\n  )),\n  mockRepository: () => (\n    {\n      name: '测试用例_临时_' + Random.ctitle(6) + Math.random(),\n      description: Random.cparagraph(),\n      logo: Random.url()\n    }\n  ),\n  prepare: (request, should, users, repository) => {\n    users.forEach((item, index) => {\n      before(done => {\n        request.post('/account/register').send(item).expect(200)\n          .end((err, res) => {\n            should.not.exist(err)\n            Object.assign(item, res.body.data)\n            done()\n          })\n      })\n    })\n    before(done => {\n      request.post('/account/login')\n        .send({ email: users[0].email, password: users[0].password })\n        .expect('Content-Type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          should.not.exist(err)\n          let { data } = res.body\n          data.should.be.a('object').have.all.keys({ id: users[0].id, fullname: users[0].fullname, email: users[0].email })\n          done()\n        })\n    })\n    if (repository) {\n      before(done => {\n        request.post('/repository/create')\n        .send(\n          Object.assign(repository, {\n            organizationId: undefined,\n            memberIds: users.slice(2).map(item => item.id)\n          })\n        )\n        .expect('Content-Type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          should.not.exist(err)\n          Object.assign(repository, res.body.data)\n          done()\n        })\n      })\n      after(done => {\n        request.get('/repository/remove')\n          .query({ id: repository.id })\n          .expect('Content-Type', /json/)\n          .expect(200)\n          .end((err, res) => {\n            should.not.exist(err)\n            res.body.data.should.eq(1)\n            done()\n          })\n      })\n    }\n    after(done => {\n      request.get('/account/logout')\n        .expect('Content-Type', /json/)\n        .expect(200)\n        .end((err, res) => {\n          should.not.exist(err)\n          done()\n        })\n    })\n    users.forEach((item, index) => {\n      after(done => {\n        request.get('/account/remove').query({ id: users[index].id }).expect(200)\n          .end((err, res) => {\n            should.not.exist(err)\n            res.body.data.should.eq(1)\n            done()\n          })\n      })\n    })\n  },\n  keys: {\n    pagination: ['cursor', 'limit', 'total']\n  },\n  excludes: {\n    user: ['password', 'create_date', 'update_date', 'delete_date', 'reserve'],\n    organization: [],\n    repository: ['create_date', 'update_date', 'delete_date', 'reserve']\n  }\n}\n"
  },
  {
    "path": "test/index.js",
    "content": "// clear console\nprocess.stdout.write(process.platform === 'win32' ? '\\x1Bc' : '\\x1B[2J\\x1B[3J\\x1B[H')\n"
  },
  {
    "path": "test/test.account.js",
    "content": "/* global describe, it */\nconst app = require('../dist/scripts/app').default\nconst request = require('supertest').agent(app.listen())\nconst should = require('chai').should()\nconst Random = require('mockjs').Random\n\ndescribe('Account', () => {\n  let user = { fullname: Random.cname(), email: Random.email(), password: Random.word(6) }\n  let validUser = (user) => {\n    user.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n  }\n  let validUserForSearch = (user) => {\n    user.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n  }\n  let validPagination = (pagination) => {\n    pagination.should.be.a('object').contain.all.keys(['cursor', 'limit', 'total'])\n  }\n  it('/account/register', (done) => {\n    request.post('/account/register')\n      .send(user)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        console.log(err)\n        should.not.exist(err)\n        validUser(res.body.data)\n        user.id = res.body.data.id\n        done()\n      })\n  })\n  it('/account/count', (done) => {\n    request.get('/account/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number').above(0)\n        done()\n      })\n  })\n  it('/account/list', (done) => {\n    request.get('/account/list')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data, pagination } = res.body\n        data.should.be.a('array').have.length.above(0)\n        data.forEach(item => {\n          validUserForSearch(item)\n        })\n        validPagination(pagination)\n        done()\n      })\n  })\n  it('/account/login', done => {\n    request.post('/account/login')\n      .send({ email: user.email, password: user.password })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validUser(res.body.data)\n        done()\n      })\n  })\n  it('/account/info', done => {\n    request.get('/account/info')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validUser(res.body.data)\n        done()\n      })\n  })\n  it('/account/logout', done => {\n    request.get('/account/logout')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.be.a('object').have.all.keys({ id: user.id })\n        done()\n      })\n  })\n  it('/account/info', done => {\n    request.get('/account/info')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        should.not.exist(res.body.data)\n        done()\n      })\n  })\n  it('/account/remove', (done) => {\n    request.get('/account/remove')\n      .query({ id: user.id })\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.counter.js",
    "content": "/* global describe, it */\nconst app = require('../dist/scripts/app').default\nconst request = require('supertest').agent(app.listen())\nconst should = require('chai').should()\nconst { mockUsers, prepare } = require('./helper')\n\ndescribe('Counter', () => {\n  let users = mockUsers()\n  prepare(request, should, users)\n\n  it('/app/counter', done => {\n    request.get('/app/counter')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { version, users, mock } = res.body.data\n        version.should.be.a('string').not.eq('')\n        users.should.be.a('number').above(0)\n        mock.should.be.a('number')\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.interface.js",
    "content": "/* global describe, it, before */\nlet app = require('../dist/scripts/app').default\nlet request = require('supertest').agent(app.listen())\nlet should = require('chai').should()\nlet expect = require('chai').expect\nlet Random = require('mockjs').Random\nconst { Interface } = require('../dist/models')\nconst { mockUsers, mockRepository, prepare } = require('./helper')\n\n\ndescribe('Interface', () => {\n  let createdItfId = 1\n  let users = mockUsers()\n  let repository = mockRepository()\n  prepare(request, should, users, repository)\n\n  let itf = {}\n  before(done => {\n    itf = {\n      name: '测试用例_临时_' + Random.ctitle(6) + Math.random(),\n      url: Random.url(),\n      method: Random.pick(['GET', 'POST', 'PUT', 'DELETE']),\n      description: Random.cparagraph(),\n      lockerId: null,\n      repositoryId: repository.id,\n      moduleId: repository.modules[0].id\n    }\n    done()\n  })\n  let validInterface = (itf, extras = []) => {\n    itf.creatorId.should.be.a('number')\n    itf.repositoryId.should.be.a('number')\n    itf.moduleId.should.be.a('number')\n  }\n\n  it('/interface/create', done => {\n    request.post('/interface/create')\n      .send(itf)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        itf = res.body.data.itf\n        createdItfId = itf.id\n        validInterface(itf)\n        done()\n      })\n  })\n  it('/interface/count', done => {\n    request.get('/interface/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number').above(0)\n        done()\n      })\n  })\n  it('/interface/list', done => {\n    request.get('/interface/list')\n      .query({ moduleId: repository.modules[0].id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data } = res.body\n        data.should.be.a('array').have.length.within(1, 2)\n        data.forEach(item => {\n          validInterface(item)\n        })\n        done()\n      })\n  })\n  it('/interface/get', done => {\n    request.get('/interface/get')\n      .query({ id: createdItfId })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validInterface(res.body.data, ['requestProperties', 'responseProperties'])\n        done()\n      })\n  })\n  it('/interface/update', done => {\n    request.post('/interface/update')\n      .send({ id: itf.id, name: Random.ctitle(6) })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.itf.id.should.not.be.null\n        done()\n      })\n  })\n  it('/interface/lock', done => {\n    request.post('/interface/lock')\n      .send({ id: itf.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.id.should.not.be.null\n        done()\n      })\n  })\n  it('/interface/unlock', done => {\n    request.post('/interface/unlock')\n      .send({ id: itf.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        expect(res.body.data.isOk).to.be.true\n        done()\n      })\n  })\n  it('/interface/remove', done => {\n    request.get('/interface/remove')\n      .query({ id: itf.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.js",
    "content": "/* global describe, it */\nlet app = require('../dist/scripts/app').default\nlet request = require('supertest').agent(app.listen())\nlet should = require('chai').should()\n\ndescribe('App', () => {\n  it('/', (done) => {\n    request.get('/')\n      .expect('Content-Type', /html/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  it('/check.node', (done) => {\n    request.get('/check.node')\n      .expect('Content-Type', /text/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.text.should.eq('success')\n        done()\n      })\n  })\n  it('/status.taobao', (done) => {\n    request.get('/status.taobao')\n      .expect('Content-Type', /text/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.text.should.eq('success')\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.mock.js",
    "content": "/* global describe, it, before */\nlet app = require('../dist/scripts/app').default\nlet request = require('supertest').agent(app.listen())\nlet should = require('chai').should()\nconst { mockUsers, mockRepository, prepare } = require('./helper')\n\ndescribe('Mock', () => {\n  let users = mockUsers()\n  let repository = mockRepository()\n  prepare(request, should, users, repository)\n\n  let interfaces\n  before(done => {\n    request.get('/interface/list')\n      .query({ repositoryId: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        interfaces = res.body.data\n        done()\n      })\n  })\n  it('/app/plugin/:repository', done => {\n    request.get(`/app/plugin/${repository.id}`)\n      .expect('Content-Type', /javascript/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  it('/app/plugin/:repository,:repository', done => {\n    request.get(`/app/plugin/${repository.id},${repository.id}`)\n      .expect('Content-Type', /javascript/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  it('/app/mock/:repository/:method/:url', done => {\n    request.get(`/app/mock/${interfaces[0].repositoryId}/${interfaces[0].method}/${interfaces[0].url}`)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  it('/app/mock/template/:interfaceId', done => {\n    request.get(`/app/mock/template/${interfaces[0].id}`)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  it('/app/mock/data/:interfaceId', done => {\n    request.get(`/app/mock/data/${interfaces[0].id}`)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        done()\n      })\n  })\n  // it('/app/get', done => {\n  //   request.get('/app/get')\n  //     .query({ user: 100000000, organization: 1, repository: 1, module: 1, interface: 1, property: 1 })\n  //     .expect('Content-Type', /json/)\n  //     .expect(200)\n  //     .end((err, res) => {\n  //       console.log(res.body.data)\n  //       should.not.exist(err)\n  //       let { user, organization, repository, property } = res.body.data\n  //       let mod = res.body.data.module\n  //       let itf = res.body.data.interface\n  //       user.should.be.a('object')\n  //       organization.should.be.a('object')\n  //       repository.should.be.a('object')\n  //       mod.should.be.a('object')\n  //       itf.should.be.a('object')\n  //       property.should.be.a('object')\n  //       done()\n  //     })\n  // })\n})\n"
  },
  {
    "path": "test/test.module.js",
    "content": "/* global describe, it, before */\nlet app = require('../dist/scripts/app').default\nlet request = require('supertest').agent(app.listen())\nlet should = require('chai').should()\nlet Random = require('mockjs').Random\nconst { Module } = require('../dist/models')\nconst { mockUsers, mockRepository, prepare } = require('./helper')\n\ndescribe('Module', () => {\n  let users = mockUsers()\n  let repository = mockRepository()\n  prepare(request, should, users, repository)\n\n  let mod = {}\n  before(done => {\n    mod = {\n      name: Random.ctitle(6),\n      description: Random.cparagraph(),\n      repositoryId: repository.id\n    }\n    done()\n  })\n  let validModule = (mod) => {\n    mod.should.be.a('object').have.all.keys(\n      Object.keys(Module.rawAttributes)\n    )\n    mod.creatorId.should.be.a('number')\n    mod.repositoryId.should.be.a('number')\n  }\n\n  it('/module/create', done => {\n    request.post('/module/create')\n      .send(mod)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validModule(res.body.data)\n        mod = res.body.data\n        done()\n      })\n  })\n  it('/module/count', done => {\n    request.get('/module/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number').above(0)\n        done()\n      })\n  })\n  it('/module/list', done => {\n    request.get('/module/list')\n      .query({ repositoryId: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data } = res.body\n        data.should.be.a('array').have.length.within(1, 2)\n        data.forEach(item => {\n          validModule(item)\n        })\n        done()\n      })\n  })\n  it('/module/get', done => {\n    request.get('/module/get')\n      .query({ id: mod.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validModule(res.body.data)\n        done()\n      })\n  })\n  it('/module/update', done => {\n    request.post('/module/update')\n      .send(Object.assign({}, mod, { name: Random.ctitle(6) + Math.random() }))\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.name.should.be.a('string')\n        res.body.data.description.should.be.a('string')\n        done()\n      })\n  })\n  it('/module/remove', done => {\n    request.get('/module/remove')\n      .query({ id: mod.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.organization.js",
    "content": "/* global describe, it, before */\nconst app = require('../dist/scripts/app').default\nconst request = require('supertest').agent(app.listen())\nconst should = require('chai').should()\nconst Random = require('mockjs').Random\nconst { Organization } = require('../dist/models')\nconst { mockUsers, prepare, keys } = require('./helper')\n\ndescribe('Organization', () => {\n  let users = mockUsers()\n  prepare(request, should, users)\n\n  let organization\n  before(done => {\n    organization = {\n      name: Random.ctitle(6) + Math.random(),\n      description: Random.cparagraph(),\n      logo: Random.url(),\n      memberIds: users.slice(2).map(item => item.id),\n      visibility: 1,\n    }\n    done()\n  })\n  let validOrganization = (organization) => {\n    organization.should.be.a('object').have.all.keys(\n      [...Object.keys(Organization.rawAttributes), 'creator', 'owner', 'members']\n    )\n    let { creator, owner, members } = organization\n    creator.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    members.should.be.a('array').have.length.within(3, 3)\n    members.forEach((user, index) => {\n      owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    })\n  }\n  let validPagination = (pagination) => {\n    pagination.should.be.a('object').contain.all.keys(keys.pagination)\n  }\n  it('/organization/create', done => {\n    request.post('/organization/create')\n      .send(organization)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validOrganization(res.body.data)\n        organization = res.body.data\n        done()\n      })\n  })\n  it('/organization/count', done => {\n    request.get('/organization/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number').above(0)\n        done()\n      })\n  })\n  it('/organization/list', done => {\n    request.get('/organization/list')\n      .query({ name: organization.id, cursor: 1, limit: 1 })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data, pagination } = res.body\n        data.should.be.a('array').have.lengthOf(1)\n        data.forEach(item => {\n          validOrganization(item)\n        })\n        validPagination(pagination)\n        done()\n      })\n  })\n  it('/organization/owned', done => {\n    request.get('/organization/owned')\n      .query({ name: organization.name })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data, pagination } = res.body\n        data.should.be.a('array').have.length.within(1, 1)\n        data.forEach(item => {\n          validOrganization(item)\n        })\n        should.not.exist(pagination)\n        done()\n      })\n  })\n  it('/organization/get', done => {\n    request.get('/organization/get')\n      .query({ id: organization.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validOrganization(res.body.data)\n        done()\n      })\n  })\n  it('/organization/update', done => {\n    request.post('/organization/update')\n      .send(Object.assign({}, organization, { name: Random.ctitle(6) + Math.random() }))\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/organization/transfer', done => {\n    request.post('/organization/transfer')\n      .send({ id: organization.id, ownerId: users[1].id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/organization/remove', done => {\n    request.get('/organization/remove')\n      .query({ id: organization.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.property.js",
    "content": "/* global describe, it, before */\nlet app = require('../dist/scripts/app').default\nlet request = require('supertest').agent(app.listen())\nlet should = require('chai').should()\nlet Random = require('mockjs').Random\nconst { Property } = require('../dist/models')\nconst { mockUsers, mockRepository, prepare } = require('./helper')\n\ndescribe('Property', () => {\n  let users = mockUsers()\n  let repository = mockRepository()\n  prepare(request, should, users, repository)\n\n  let mod = {}\n  let itf = {}\n  let property = {}\n  before(done => {\n    mod = repository.modules[0]\n    itf = mod.interfaces[0]\n    property = {\n      scope: Random.pick(['request', 'response']),\n      name: Random.word(6),\n      type: Random.pick(['String', 'Number', 'Boolean', 'Object', 'Array', 'Function', 'RegExp']),\n      rule: '',\n      value: Random.pick(['@INT', '@FLOAT', '@TITLE', '@NAME']),\n      description: Random.cparagraph(),\n      parentId: -1,\n      repositoryId: repository.id,\n      moduleId: mod.id,\n      interfaceId: itf.id\n    }\n    done()\n  })\n  let validProperty = (property) => {\n    property.should.be.a('object').have.all.keys(\n      Object.keys(Property.rawAttributes)\n    )\n    property.creatorId.should.be.a('number')\n    property.repositoryId.should.be.a('number')\n    property.moduleId.should.be.a('number')\n  }\n  it('/property/create', done => {\n    request.post('/property/create')\n      .send(property)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validProperty(res.body.data)\n        property = res.body.data\n        done()\n      })\n  })\n\n  it('/property/count', done => {\n    request.get('/property/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number')\n        done()\n      })\n  })\n  it('/property/list', done => {\n    request.get('/property/list')\n      .query({ interfaceId: itf.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data } = res.body\n        data.should.be.a('array').have.length.within(1, 15)\n        data.forEach(item => {\n          validProperty(item)\n        })\n        done()\n      })\n  })\n  it('/property/get', done => {\n    request.get('/property/get')\n      .query({ id: property.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validProperty(res.body.data)\n        done()\n      })\n  })\n  it('/property/update', done => {\n    request.post('/property/update')\n      .send({ id: property.id, name: Random.word(6) })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/property/remove', done => {\n    request.get('/property/remove')\n      .query({ id: property.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "test/test.repository.js",
    "content": "/* global describe, it, before */\nconst app = require('../dist/scripts/app').default\nconst request = require('supertest').agent(app.listen())\nconst should = require('chai').should()\nconst Random = require('mockjs').Random\nconst { Repository } = require('../dist/models')\nconst { mockUsers, prepare, keys } = require('./helper')\n\ndescribe('Repository', () => {\n  let users = mockUsers()\n  prepare(request, should, users)\n\n  let repository = {}\n  before(done => {\n    repository = {\n      name: `测试用例_临时仓库_${Random.ctitle(6)}_${Date.now()}`,\n      description: Random.cparagraph(),\n      logo: Random.url(),\n      organizationId: undefined,\n      memberIds: users.slice(2).map(item => item.id)\n    }\n    done()\n  })\n  let validRepository = (repository, deep) => {\n    // repository.should.be.a('object').have.all.keys(\n    //   [...Object.keys(Repository.rawAttributes), 'creator', 'owner', 'members', 'locker', 'organization', 'collaborators']\n    //     .concat(deep ? ['modules'] : [])\n    // )\n    let { creator, owner, members } = repository\n    creator.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    members.should.be.a('array').have.length.within(3, 3)\n    members.forEach((user, index) => {\n      owner.should.be.a('object').have.all.keys(['id', 'fullname', 'email'])\n    })\n  }\n  let validPagination = (pagination) => {\n    pagination.should.be.a('object').contain.all.keys(keys.pagination)\n  }\n\n  it('/repository/create', done => {\n    request.post('/repository/create')\n      .send(repository)\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validRepository(res.body.data, true)\n        repository = res.body.data\n        done()\n      })\n  })\n  it('/repository/count', done => {\n    request.get('/repository/count')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.to.be.a('number').above(0)\n        done()\n      })\n  })\n  it('/repository/list', done => {\n    request.get('/repository/list')\n      .query({ name: repository.name, cursor: 1, limit: 1 })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        let { data, pagination } = res.body\n        data.should.be.a('array').have.length.within(1, 1)\n        data.forEach(item => {\n          validRepository(item)\n        })\n        validPagination(pagination)\n        done()\n      })\n  })\n  it('/repository/get', done => {\n    request.get('/repository/get')\n      .query({ id: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        validRepository(res.body.data, true)\n        done()\n      })\n  })\n  it('/repository/update', done => {\n    request.post('/repository/update')\n      .send(Object.assign({}, repository, { name: `测试用例_临时仓库_${Random.ctitle(6)}_${Date.now()}` }))\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/repository/lock', done => {\n    request.post('/repository/lock')\n      .send({ id: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/repository/unlock', done => {\n    request.post('/repository/unlock')\n      .send({ id: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/repository/transfer', done => {\n    request.post('/repository/transfer')\n      .send({ id: repository.id, ownerId: users[1].id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n  it('/repository/remove', done => {\n    request.get('/repository/remove')\n      .query({ id: repository.id })\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end((err, res) => {\n        should.not.exist(err)\n        res.body.data.should.eq(1)\n        done()\n      })\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"outDir\": \"./dist\",\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"target\": \"es2019\",\n    \"removeComments\": true,\n    \"sourceMap\": true,\n    \"watch\": false,\n    \"baseUrl\": \"./src\",\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitAny\": false,\n    \"skipLibCheck\": true,\n    \"noImplicitReturns\": true,\n    \"noImplicitThis\": true,\n    \"noImplicitUseStrict\": true,\n    \"suppressImplicitAnyIndexErrors\": true,\n    \"rootDir\": \"./src\",\n    \"paths\": {\n      \"*\": [\n        \"node_modules/*\",\n        \"src/types/*\"\n      ]\n    }\n  },\n  \"include\": [\n    \"src/**/*\"\n  ]\n}"
  },
  {
    "path": "tslint.json",
    "content": "{\n  \"tslint.enable\": true,\n  \"rules\": {\n    \"class-name\": true,\n    \"comment-format\": [\n      true,\n      \"check-space\"\n    ],\n    \"indent\": [\n      true,\n      \"spaces\",\n      2\n    ],\n    \"one-line\": [\n      true,\n      \"check-open-brace\",\n      \"check-whitespace\"\n    ],\n    \"no-var-keyword\": true,\n    \"quotemark\": [\n      false,\n      \"double\",\n      \"avoid-escape\"\n    ],\n    \"semicolon\": [\n      true,\n      \"never\",\n      \"ignore-bound-class-methods\"\n    ],\n    \"whitespace\": [\n      true,\n      \"check-branch\",\n      \"check-decl\",\n      \"check-operator\",\n      \"check-module\",\n      \"check-separator\",\n      \"check-type\"\n    ],\n    \"trailing-comma\": [\n      false,\n      {\n        \"multiline\": \"always\",\n        \"singleline\": \"never\"\n      }\n    ],\n    \"typedef-whitespace\": [\n      true,\n      {\n        \"call-signature\": \"nospace\",\n        \"index-signature\": \"nospace\",\n        \"parameter\": \"nospace\",\n        \"property-declaration\": \"nospace\",\n        \"variable-declaration\": \"nospace\"\n      },\n      {\n        \"call-signature\": \"onespace\",\n        \"index-signature\": \"onespace\",\n        \"parameter\": \"onespace\",\n        \"property-declaration\": \"onespace\",\n        \"variable-declaration\": \"onespace\"\n      }\n    ],\n    \"no-internal-module\": true,\n    \"no-trailing-whitespace\": true,\n    \"no-null-keyword\": false,\n    \"prefer-const\": false,\n    \"jsdoc-format\": true\n  }\n}"
  }
]