generate-routes.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. const _ = require('lodash')
  2. const writeFileSync = require('fs').writeFileSync
  3. const NEW_ROUTES = require('@octokit/routes')
  4. const CURRENT_ROUTES = require('../lib/routes')
  5. function sortRoutesByKeys (routes) {
  6. Object.keys(routes).forEach(scope => {
  7. routes[scope] = sortByKeys(routes[scope])
  8. Object.keys(routes[scope]).forEach(method => {
  9. routes[scope][method] = sortByKeys(routes[scope][method])
  10. // routes-for-api-docs keeps parameters as array, while lib/routes.json uses an object
  11. if (Array.isArray(routes[scope][method].params)) {
  12. return
  13. }
  14. routes[scope][method].params = sortByKeys(routes[scope][method].params)
  15. Object.keys(routes[scope][method].params).forEach(paramName => {
  16. routes[scope][method].params[paramName] = sortByKeys(routes[scope][method].params[paramName])
  17. })
  18. })
  19. })
  20. return sortByKeys(routes)
  21. }
  22. function sortByKeys (object) {
  23. return _(object).toPairs().sortBy(0).fromPairs().value()
  24. }
  25. function matchesRoute (currentEndpoint, newEndpoint) {
  26. if (newEndpoint.method !== currentEndpoint.method || newEndpoint.path !== currentEndpoint.url) {
  27. return
  28. }
  29. // implement workaround for cases where different methods share same route
  30. // by comparing an additional parameter
  31. // 1. https://developer.github.com/v3/pulls/#create-a-pull-request
  32. // (POST /repos/:owner/:repo/pulls)
  33. // a. "Create a pull request" -> pulls.create
  34. // b. "Create a Pull Request from an existing Issue by passing an Issue number" -> pulls.createFromIssue
  35. // 2. https://developer.github.com/v3/pulls/comments/#create-a-comment
  36. // (POST /repos/:owner/:repo/pulls/:number/comments)
  37. // a. pulls.createComment
  38. // b. pulls.createCommentReply
  39. // 3. https://developer.github.com/v3/repos/contents/#create-a-file & https://developer.github.com/v3/repos/contents/#update-a-file
  40. // (PUT /repos/:owner/:repo/contents/:path)
  41. // a. repos.createFile
  42. // a. repos.updateFile
  43. const route = `${newEndpoint.method} ${newEndpoint.path}`
  44. const additionalParameter = {
  45. 'POST /repos/:owner/:repo/pulls': 'issues',
  46. 'POST /repos/:owner/:repo/pulls/:number/comments': 'in_reply_to',
  47. 'PUT /repos/:owner/:repo/contents/:path': 'sha'
  48. }[route]
  49. if (!additionalParameter) {
  50. return true
  51. }
  52. const newEndpointHasAdditionalParam = !!newEndpoint.params.find(param => param.name === additionalParameter)
  53. const currentEndpointHasAdditionalParam = !!currentEndpoint.params[additionalParameter]
  54. return newEndpointHasAdditionalParam === currentEndpointHasAdditionalParam
  55. }
  56. const MISC_SCOPES = [
  57. 'codesOfConduct',
  58. // 'emojis', https://github.com/octokit/routes/issues/50
  59. 'gitignore',
  60. 'licenses',
  61. 'markdown',
  62. 'rateLimit'
  63. ]
  64. NEW_ROUTES['misc'] = [].concat(...MISC_SCOPES.map(scope => NEW_ROUTES[scope]))
  65. NEW_ROUTES['orgs'] = NEW_ROUTES['orgs'].concat(NEW_ROUTES['teams'])
  66. // move around some methods ¯\_(ツ)_/¯
  67. const ORG_USER_PATHS = [
  68. '/user/orgs',
  69. '/user/memberships/orgs',
  70. '/user/memberships/orgs/:org',
  71. '/user/teams'
  72. ]
  73. const REPOS_USER_PATHS = [
  74. '/user/repository_invitations',
  75. '/user/repository_invitations/:invitation_id'
  76. ]
  77. const APPS_USER_PATHS = [
  78. '/user/installations',
  79. '/user/installations/:installation_id/repositories',
  80. '/user/installations/:installation_id/repositories/:repository_id',
  81. '/user/marketplace_purchases',
  82. '/user/marketplace_purchases/stubbed'
  83. ]
  84. NEW_ROUTES['users'].push(...NEW_ROUTES['orgs'].filter(endpoint => ORG_USER_PATHS.includes(endpoint.path)))
  85. NEW_ROUTES['users'].push(...NEW_ROUTES['repos'].filter(endpoint => REPOS_USER_PATHS.includes(endpoint.path)))
  86. NEW_ROUTES['users'].push(...NEW_ROUTES['apps'].filter(endpoint => APPS_USER_PATHS.includes(endpoint.path)))
  87. // map scopes from @octokit/routes to what we currently have in lib/routes.json
  88. const mapScopes = {
  89. activity: 'activity',
  90. apps: 'apps',
  91. checks: 'checks',
  92. codesOfConduct: false,
  93. gists: 'gists',
  94. git: 'gitdata',
  95. gitignore: false,
  96. issues: 'issues',
  97. licenses: false,
  98. markdown: false,
  99. migrations: 'migrations',
  100. misc: 'misc',
  101. oauthAuthorizations: 'authorization',
  102. orgs: 'orgs',
  103. projects: 'projects',
  104. pulls: 'pullRequests',
  105. rateLimit: false,
  106. reactions: 'reactions',
  107. repos: 'repos',
  108. scim: false,
  109. search: 'search',
  110. teams: false,
  111. users: 'users'
  112. }
  113. const newRoutes = {}
  114. const newDocRoutes = {}
  115. Object.keys(NEW_ROUTES).forEach(scope => {
  116. const currentScopeName = mapScopes[scope]
  117. if (!currentScopeName) {
  118. return
  119. }
  120. NEW_ROUTES[currentScopeName] = NEW_ROUTES[scope]
  121. newRoutes[currentScopeName] = {}
  122. newDocRoutes[currentScopeName] = {}
  123. })
  124. // mutate the new routes to what we have today
  125. Object.keys(CURRENT_ROUTES).sort().forEach(scope => {
  126. // enterprise is not part of @octokit/routes, we’ll leave it as-is.
  127. if (scope === 'enterprise') {
  128. return
  129. }
  130. // leave the deprecated integrations methods as they are for now
  131. if (scope === 'integrations') {
  132. return
  133. }
  134. Object.keys(CURRENT_ROUTES[scope]).map(methodName => {
  135. const currentEndpoint = CURRENT_ROUTES[scope][methodName]
  136. if (currentEndpoint.method === 'GET' && currentEndpoint.url === '/repos/:owner/:repo/git/refs') {
  137. console.log('Ignoring custom override for GET /repos/:owner/:repo/git/refs (https://github.com/octokit/routes/commit/b7a9800)')
  138. newRoutes[scope][methodName] = currentEndpoint
  139. return
  140. }
  141. if (currentEndpoint.url === '/repos/:owner/:repo/git/refs/tags') {
  142. console.log('Ignoring endpoint for getTags()')
  143. newRoutes[scope][methodName] = currentEndpoint
  144. return
  145. }
  146. if (currentEndpoint.deprecated) {
  147. console.log(`No endpoint found for deprecated ${currentEndpoint.method} ${currentEndpoint.url}, leaving route as is.`)
  148. newRoutes[scope][methodName] = currentEndpoint
  149. return
  150. }
  151. if (currentEndpoint.url === '/repositories/:id') {
  152. console.log('Ignoring endpoint for repos.getById()')
  153. newRoutes[scope][methodName] = currentEndpoint
  154. return
  155. }
  156. if (currentEndpoint.url === '/user/:id') {
  157. console.log('Ignoring endpoint for users.getById()')
  158. newRoutes[scope][methodName] = currentEndpoint
  159. return
  160. }
  161. // https://github.com/octokit/routes/issues/50
  162. if (scope === 'misc' && (methodName === 'getMeta' || methodName === 'getEmojis')) {
  163. newRoutes[scope][methodName] = currentEndpoint
  164. return
  165. }
  166. if ([
  167. '/users/:username/suspended',
  168. '/users/:username/site_admin'
  169. ].includes(currentEndpoint.url)) {
  170. console.log('Ignoring endpoints belonging to enterprise admin')
  171. return
  172. }
  173. const newEndpoint = NEW_ROUTES[mapScopes[scope] || scope].find(matchesRoute.bind(null, currentEndpoint))
  174. if (!newEndpoint) {
  175. throw new Error(`No endpoint found for ${currentEndpoint.method} ${currentEndpoint.url} (scope: ${scope}, ${JSON.stringify(currentEndpoint, null, 2)})`)
  176. }
  177. // reduce from params array to params object
  178. const currentParams = currentEndpoint.params
  179. const newParams = newEndpoint.params.reduce((map, param) => {
  180. map[param.name] = _.clone(param)
  181. delete map[param.name].name
  182. return map
  183. }, {})
  184. currentEndpoint.url = newEndpoint.path
  185. currentEndpoint.params = newParams
  186. // we no longer need description, we can generate docs from @octokit/routes
  187. delete currentEndpoint.description
  188. Object.keys(currentEndpoint.params).forEach(name => {
  189. delete currentEndpoint.params[name].description
  190. delete currentEndpoint.params[name].default
  191. if (currentEndpoint.params[name].required === false) {
  192. delete currentEndpoint.params[name].required
  193. }
  194. })
  195. // leave params with .alias or .mapTo property so we don’t break current code
  196. Object.keys(currentParams).forEach(name => {
  197. if (currentParams[name].alias || currentParams[name].mapTo) {
  198. currentEndpoint.params[name] = currentParams[name]
  199. }
  200. })
  201. // DEPRECATED: workaround to leave "validation" property. We won’t be able
  202. // to get that from @octokit/routes, but we leave it in for now so to not
  203. // break current behavior
  204. Object.keys(currentParams).forEach(name => {
  205. if (currentParams[name].validation) {
  206. currentEndpoint.params[name].validation = currentParams[name].validation
  207. }
  208. })
  209. // map header parameters to `headers.<parameter name>`
  210. Object.keys(newParams).forEach(name => {
  211. const location = newParams[name].location
  212. delete newParams[name].location
  213. if (location !== 'headers') {
  214. return
  215. }
  216. currentEndpoint.params[`headers.${name.toLowerCase()}`] = currentEndpoint.params[name]
  217. delete currentEndpoint.params[name]
  218. })
  219. // Workaround for https://github.com/octokit/routes/issues/121
  220. Object.keys(currentParams).forEach(name => {
  221. if (!currentEndpoint.params[name].enum) {
  222. return
  223. }
  224. const placeholderValue = currentEndpoint.params[name].enum.find(value => /<\w+_id>/.test(value))
  225. if (!placeholderValue) {
  226. return
  227. }
  228. currentEndpoint.params[name].validation = `^(${currentEndpoint.params[name].enum.join('|')})$`.replace(/<\w+_id>/, '\\d+')
  229. delete currentEndpoint.params[name].enum
  230. })
  231. newRoutes[scope][methodName] = currentEndpoint
  232. newDocRoutes[scope][methodName] = newEndpoint
  233. delete newDocRoutes[scope][methodName].idName
  234. })
  235. })
  236. // don’t break the "enterprise" scope, it’s not part of @octokit/routes at this point
  237. newRoutes.enterprise = CURRENT_ROUTES.enterprise
  238. newRoutes.users.promote = CURRENT_ROUTES.users.promote
  239. newRoutes.users.demote = CURRENT_ROUTES.users.demote
  240. newRoutes.users.suspend = CURRENT_ROUTES.users.suspend
  241. newRoutes.users.unsuspend = CURRENT_ROUTES.users.unsuspend
  242. // don’t break the deprecated "integrations" scope
  243. newRoutes.integrations = CURRENT_ROUTES.integrations
  244. writeFileSync('lib/routes.json', JSON.stringify(sortRoutesByKeys(newRoutes), null, 2) + '\n')
  245. writeFileSync('scripts/routes-for-api-docs.json', JSON.stringify(sortRoutesByKeys(newDocRoutes), null, 2))