Răsfoiți Sursa

Merge pull request #10063 from weseek/imprv/apiv1-openapi-spec

imprv: OpenAPI Spec for GROWI API v1
Yuki Takei 10 luni în urmă
părinte
comite
062e50f442

+ 0 - 20
apps/app/src/server/models/openapi/v1-response.js

@@ -1,20 +0,0 @@
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *      V1ResponseOK:
- *        description: API is succeeded
- *        type: boolean
- *      V1Response:
- *        description: Response v1
- *        type: object
- *        properties:
- *          ok:
- *            $ref: '#/components/schemas/V1ResponseOK'
- *    responses:
- *      403:
- *        description: 'Forbidden'
- *      500:
- *        description: 'Internal Server Error'
- */

+ 131 - 0
apps/app/src/server/models/openapi/v1-response.ts

@@ -0,0 +1,131 @@
+/**
+ * @swagger
+ *
+ *  components:
+ *    schemas:
+ *
+ *      # Common API Response Schemas (modern pattern)
+ *      ApiResponseBase:
+ *        type: object
+ *        required:
+ *          - ok
+ *        properties:
+ *          ok:
+ *            type: boolean
+ *            description: Indicates if the request was successful
+ *
+ *      ApiResponseSuccess:
+ *        description: Successful API response
+ *        allOf:
+ *          - $ref: '#/components/schemas/ApiResponseBase'
+ *          - type: object
+ *            properties:
+ *              ok:
+ *                type: boolean
+ *                enum: [true]
+ *                example: true
+ *                description: Success indicator (always true for successful responses)
+ *
+ *      ApiResponseError:
+ *        description: Error API response
+ *        allOf:
+ *          - $ref: '#/components/schemas/ApiResponseBase'
+ *          - type: object
+ *            properties:
+ *              ok:
+ *                type: boolean
+ *                enum: [false]
+ *                example: false
+ *                description: Success indicator (always false for error responses)
+ *              error:
+ *                oneOf:
+ *                  - type: string
+ *                    description: Simple error message
+ *                    example: "Invalid parameter"
+ *                  - type: object
+ *                    description: Detailed error object
+ *                    example: { "code": "VALIDATION_ERROR", "message": "Field validation failed" }
+ *                description: Error message or error object containing details about the failure
+ *
+ *    responses:
+ *      # Common error responses
+ *      BadRequest:
+ *        description: Bad request
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              missingParameter:
+ *                summary: Missing required parameter
+ *                value:
+ *                  ok: false
+ *                  error: "Invalid parameter"
+ *              validationError:
+ *                summary: Validation error
+ *                value:
+ *                  ok: false
+ *                  error: "Validation failed"
+ *
+ *      Forbidden:
+ *        description: Forbidden - insufficient permissions
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            example:
+ *              ok: false
+ *              error: "Access denied"
+ *
+ *      NotFound:
+ *        description: Resource not found
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              resourceNotFound:
+ *                summary: Resource not found
+ *                value:
+ *                  ok: false
+ *                  error: "Resource not found"
+ *              notFoundOrForbidden:
+ *                summary: Resource not found or forbidden
+ *                value:
+ *                  ok: false
+ *                  error: "notfound_or_forbidden"
+ *
+ *      Conflict:
+ *        description: Conflict
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            examples:
+ *              resourceConflict:
+ *                summary: Resource conflict
+ *                value:
+ *                  ok: false
+ *                  error: "Resource conflict"
+ *              outdated:
+ *                summary: Resource was updated by someone else
+ *                value:
+ *                  ok: false
+ *                  error: "outdated"
+ *              alreadyExists:
+ *                summary: Resource already exists
+ *                value:
+ *                  ok: false
+ *                  error: "already_exists"
+ *
+ *      InternalServerError:
+ *        description: Internal server error
+ *        content:
+ *          application/json:
+ *            schema:
+ *              $ref: '#/components/schemas/ApiResponseError'
+ *            example:
+ *              ok: false
+ *              error: "Internal server error"
+ *
+ */

+ 4 - 4
apps/app/src/server/routes/apiv3/attachment.js

@@ -267,9 +267,9 @@ module.exports = (crowi) => {
    *                      description: uploadable
    *                      example: true
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   router.get('/limit', accessTokenParser, loginRequiredStrictly, validator.retrieveFileLimit, apiV3FormValidator, async(req, res) => {
     const { fileUploadService } = crowi;
@@ -333,9 +333,9 @@ module.exports = (crowi) => {
    *                    revision:
    *                      type: string
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   router.post('/', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, uploads.single('file'),
     validator.retrieveAddAttachment, apiV3FormValidator, addActivity,

+ 2 - 2
apps/app/src/server/routes/apiv3/page/index.ts

@@ -409,9 +409,9 @@ module.exports = (crowi) => {
    *                        revision:
    *                          $ref: '#/components/schemas/Revision'
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   router.put('/', updatePageHandlersFactory(crowi));
 

+ 2 - 2
apps/app/src/server/routes/apiv3/users.js

@@ -1212,9 +1212,9 @@ module.exports = (crowi) => {
    *                          $ref: '#/components/schemas/User'
    *                        description: user list
    *            403:
-   *              $ref: '#/components/responses/403'
+   *              $ref: '#/components/responses/Forbidden'
    *            500:
-   *              $ref: '#/components/responses/500'
+   *              $ref: '#/components/responses/InternalServerError'
    */
   router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
     const userIds = req.query.userIds ?? null;

+ 15 - 18
apps/app/src/server/routes/attachment/api.js

@@ -1,4 +1,3 @@
-
 import { SupportedAction } from '~/interfaces/activity';
 import { AttachmentType } from '~/server/interfaces/attachment';
 import loggerFactory from '~/utils/logger';
@@ -220,15 +219,17 @@ export const routesFactory = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    attachment:
-   *                      $ref: '#/components/schemas/AttachmentProfile'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        attachment:
+   *                          $ref: '#/components/schemas/AttachmentProfile'
+   *                          description: The uploaded profile image attachment
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /attachments.uploadProfileImage Add attachment for profile image
@@ -298,13 +299,11 @@ export const routesFactory = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /attachments.remove Remove attachments
@@ -363,13 +362,11 @@ export const routesFactory = (crowi) => {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /attachments.removeProfileImage Remove profile image attachments

+ 32 - 31
apps/app/src/server/routes/comment.js

@@ -1,4 +1,3 @@
-
 import { getIdStringForRef } from '@growi/core';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
@@ -101,17 +100,19 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    comments:
-   *                      type: array
-   *                      items:
-   *                        $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comments:
+   *                          type: array
+   *                          items:
+   *                            $ref: '#/components/schemas/Comment'
+   *                          description: List of comments for the page revision
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /comments.get Get comments of the page of the revision
@@ -207,15 +208,17 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comment:
+   *                          $ref: '#/components/schemas/Comment'
+   *                          description: The newly created comment
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /comments.add Post comment for the page
@@ -353,15 +356,17 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        comment:
+   *                          $ref: '#/components/schemas/Comment'
+   *                          description: The updated comment
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /comments.update Update comment dody
@@ -444,15 +449,11 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    comment:
-   *                      $ref: '#/components/schemas/Comment'
+   *                  $ref: '#/components/schemas/ApiResponseSuccess'
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /comments.remove Remove specified comment

+ 330 - 117
apps/app/src/server/routes/page.js

@@ -14,54 +14,6 @@ import UpdatePost from '../models/update-post';
  *    name: Pages
  */
 
-/**
- * @swagger
- *
- *  components:
- *    schemas:
- *
- *      UpdatePost:
- *        description: UpdatePost
- *        type: object
- *        properties:
- *          _id:
- *            type: string
- *            description: update post ID
- *            example: 5e0734e472560e001761fa68
- *          __v:
- *            type: number
- *            description: DB record version
- *            example: 0
- *          pathPattern:
- *            type: string
- *            description: path pattern
- *            example: /test
- *          patternPrefix:
- *            type: string
- *            description: patternPrefix prefix
- *            example: /
- *          patternPrefix2:
- *            type: string
- *            description: path
- *            example: test
- *          channel:
- *            type: string
- *            description: channel
- *            example: general
- *          provider:
- *            type: string
- *            description: provider
- *            enum:
- *              - slack
- *            example: slack
- *          creator:
- *            $ref: '#/components/schemas/User'
- *          createdAt:
- *            type: string
- *            description: date created at
- *            example: 2010-01-01T00:00:00.000Z
- */
-
 /* eslint-disable no-use-before-define */
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
 module.exports = function(crowi, app) {
@@ -134,37 +86,57 @@ module.exports = function(crowi, app) {
   const validator = {};
 
   actions.api = api;
-  actions.validator = validator;
-
-  /**
+  actions.validator = validator; /**
    * @swagger
    *
-   *    /pages.getPageTag:
-   *      get:
-   *        tags: [Pages]
-   *        operationId: getPageTag
-   *        summary: /pages.getPageTag
-   *        description: Get page tag
-   *        parameters:
-   *          - in: query
-   *            name: pageId
-   *            schema:
-   *              $ref: '#/components/schemas/ObjectId'
-   *        responses:
-   *          200:
-   *            description: Succeeded to get page tags.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
+   * components:
+   *   schemas:
+   *     PageTagsData:
+   *       type: object
+   *       properties:
+   *         tags:
+   *           type: array
+   *           items:
+   *             type: string
+   *           description: Array of tag names associated with the page
+   *           example: ["javascript", "tutorial", "backend"]
+   *
+   *   responses:
+   *     PageTagsSuccess:
+   *       description: Successfully retrieved page tags
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageTagsData'
+   *
+   * /pages.getPageTag:
+   *   get:
+   *     tags: [Pages]
+   *     operationId: getPageTag
+   *     summary: Get page tags
+   *     description: Retrieve all tags associated with a specific page
+   *     parameters:
+   *       - in: query
+   *         name: pageId
+   *         required: true
+   *         description: Unique identifier of the page
+   *         schema:
+   *           type: string
+   *           format: ObjectId
+   *           example: "507f1f77bcf86cd799439011"
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageTagsSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /pages.getPageTag get page tags
@@ -187,32 +159,58 @@ module.exports = function(crowi, app) {
   /**
    * @swagger
    *
-   *    /pages.updatePost:
-   *      get:
-   *        tags: [Pages]
-   *        operationId: getUpdatePostPage
-   *        summary: /pages.updatePost
-   *        description: Get UpdatePost setting list
-   *        parameters:
-   *          - in: query
-   *            name: path
-   *            schema:
-   *              $ref: '#/components/schemas/PagePath'
-   *        responses:
-   *          200:
-   *            description: Succeeded to get UpdatePost setting list.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    updatePost:
-   *                      $ref: '#/components/schemas/UpdatePost'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
+   * components:
+   *   schemas:
+   *     UpdatePostData:
+   *       type: object
+   *       properties:
+   *         updatePost:
+   *           type: array
+   *           items:
+   *             type: string
+   *           description: Array of channel names for notifications
+   *           example: ["general", "development", "notifications"]
+   *
+   *   responses:
+   *     UpdatePostSuccess:
+   *       description: Successfully retrieved UpdatePost settings
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/UpdatePostData'
+   *
+   * /pages.updatePost:
+   *   get:
+   *     tags: [Pages]
+   *     operationId: getUpdatePost
+   *     summary: Get UpdatePost settings
+   *     description: Retrieve UpdatePost notification settings for a specific path
+   *     parameters:
+   *       - in: query
+   *         name: path
+   *         required: true
+   *         description: Page path to get UpdatePost settings for
+   *         schema:
+   *           type: string
+   *           example: "/user/example"
+   *         examples:
+   *           userPage:
+   *             value: "/user/john"
+   *             description: User page path
+   *           projectPage:
+   *             value: "/project/myproject"
+   *             description: Project page path
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/UpdatePostSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /pages.updatePost
@@ -254,12 +252,100 @@ module.exports = function(crowi, app) {
   ];
 
   /**
-   * @api {post} /pages.remove Remove page
-   * @apiName RemovePage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageRemoveData:
+   *       type: object
+   *       properties:
+   *         path:
+   *           type: string
+   *           description: Path of the deleted page
+   *           example: "/user/example"
+   *         isRecursively:
+   *           type: boolean
+   *           description: Whether deletion was recursive
+   *           example: true
+   *         isCompletely:
+   *           type: boolean
+   *           description: Whether deletion was complete
+   *           example: false
+   *
+   *   responses:
+   *     PageRemoveSuccess:
+   *       description: Page successfully deleted
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageRemoveData'
    *
-   * @apiParam {String} page_id Page Id.
-   * @apiParam {String} revision_id
+   * /pages.remove:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: removePage
+   *     summary: Remove page
+   *     description: Delete a page either softly or completely, with optional recursive deletion
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - page_id
+   *             properties:
+   *               page_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Unique identifier of the page to delete
+   *                 example: "507f1f77bcf86cd799439011"
+   *               revision_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Revision ID for conflict detection
+   *                 example: "507f1f77bcf86cd799439012"
+   *               completely:
+   *                 type: boolean
+   *                 description: Whether to delete the page completely (true) or soft delete (false)
+   *                 default: false
+   *                 example: false
+   *               recursively:
+   *                 type: boolean
+   *                 description: Whether to delete child pages recursively
+   *                 default: false
+   *                 example: true
+   *           examples:
+   *             softDelete:
+   *               summary: Soft delete single page
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 revision_id: "507f1f77bcf86cd799439012"
+   *             recursiveDelete:
+   *               summary: Recursive soft delete
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 recursively: true
+   *             completeDelete:
+   *               summary: Complete deletion
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 completely: true
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageRemoveSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       409:
+   *         $ref: '#/components/responses/Conflict'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
   api.remove = async function(req, res) {
     const pageId = req.body.page_id;
@@ -365,11 +451,89 @@ module.exports = function(crowi, app) {
   ];
 
   /**
-   * @api {post} /pages.revertRemove Revert removed page
-   * @apiName RevertRemovePage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageRevertData:
+   *       type: object
+   *       properties:
+   *         page:
+   *           type: object
+   *           description: Restored page object
+   *           properties:
+   *             _id:
+   *               type: string
+   *               format: ObjectId
+   *               example: "507f1f77bcf86cd799439011"
+   *             path:
+   *               type: string
+   *               example: "/user/example"
+   *             title:
+   *               type: string
+   *               example: "Example Page"
+   *             status:
+   *               type: string
+   *               example: "published"
    *
-   * @apiParam {String} page_id Page Id.
+   *   responses:
+   *     PageRevertSuccess:
+   *       description: Page successfully restored
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageRevertData'
+   *
+   * /pages.revertRemove:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: revertRemovePage
+   *     summary: Revert removed page
+   *     description: Restore a previously deleted (soft-deleted) page
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - page_id
+   *             properties:
+   *               page_id:
+   *                 type: string
+   *                 format: ObjectId
+   *                 description: Unique identifier of the page to restore
+   *                 example: "507f1f77bcf86cd799439011"
+   *               recursively:
+   *                 type: boolean
+   *                 description: Whether to restore child pages recursively
+   *                 default: false
+   *                 example: true
+   *           examples:
+   *             singleRevert:
+   *               summary: Revert single page
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *             recursiveRevert:
+   *               summary: Revert page and children
+   *               value:
+   *                 page_id: "507f1f77bcf86cd799439011"
+   *                 recursively: true
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageRevertSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       404:
+   *         $ref: '#/components/responses/NotFound'
+   *       409:
+   *         $ref: '#/components/responses/Conflict'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
   api.revertRemove = async function(req, res, options) {
     const pageId = req.body.page_id;
@@ -406,12 +570,61 @@ module.exports = function(crowi, app) {
   };
 
   /**
-   * @api {post} /pages.unlink Remove the redirecting page
-   * @apiName UnlinkPage
-   * @apiGroup Page
+   * @swagger
+   *
+   * components:
+   *   schemas:
+   *     PageUnlinkData:
+   *       type: object
+   *       properties:
+   *         path:
+   *           type: string
+   *           description: Path for which redirects were removed
+   *           example: "/user/example"
+   *
+   *   responses:
+   *     PageUnlinkSuccess:
+   *       description: Successfully removed page redirects
+   *       content:
+   *         application/json:
+   *           schema:
+   *             allOf:
+   *               - $ref: '#/components/schemas/ApiResponseSuccess'
+   *               - $ref: '#/components/schemas/PageUnlinkData'
    *
-   * @apiParam {String} page_id Page Id.
-   * @apiParam {String} revision_id
+   * /pages.unlink:
+   *   post:
+   *     tags: [Pages]
+   *     operationId: unlinkPage
+   *     summary: Remove page redirects
+   *     description: Remove all redirect entries that point to the specified page path
+   *     requestBody:
+   *       required: true
+   *       content:
+   *         application/json:
+   *           schema:
+   *             type: object
+   *             required:
+   *               - path
+   *             properties:
+   *               path:
+   *                 type: string
+   *                 description: Target path to remove redirects for
+   *                 example: "/user/example"
+   *           examples:
+   *             unlinkPage:
+   *               summary: Remove redirects to a page
+   *               value:
+   *                 path: "/user/example"
+   *     responses:
+   *       200:
+   *         $ref: '#/components/responses/PageUnlinkSuccess'
+   *       400:
+   *         $ref: '#/components/responses/BadRequest'
+   *       403:
+   *         $ref: '#/components/responses/Forbidden'
+   *       500:
+   *         $ref: '#/components/responses/InternalServerError'
    */
   api.unlink = async function(req, res) {
     const path = req.body.path;

+ 18 - 16
apps/app/src/server/routes/search.ts

@@ -79,24 +79,26 @@ module.exports = function(crowi: Crowi, app) {
    *           content:
    *             application/json:
    *               schema:
-   *                 properties:
-   *                   ok:
-   *                     $ref: '#/components/schemas/V1ResponseOK'
-   *                   meta:
-   *                     $ref: '#/components/schemas/ElasticsearchResultMeta'
-   *                   totalCount:
-   *                     type: integer
-   *                     description: total count of pages
-   *                     example: 35
-   *                   data:
-   *                     type: array
-   *                     items:
-   *                       $ref: '#/components/schemas/Page'
-   *                     description: page list
+   *                 allOf:
+   *                   - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                   - type: object
+   *                     properties:
+   *                       meta:
+   *                         $ref: '#/components/schemas/ElasticsearchResultMeta'
+   *                         description: Elasticsearch metadata
+   *                       totalCount:
+   *                         type: integer
+   *                         description: total count of pages
+   *                         example: 35
+   *                       data:
+   *                         type: array
+   *                         items:
+   *                           $ref: '#/components/schemas/Page'
+   *                         description: page list
    *         403:
-   *           $ref: '#/components/responses/403'
+   *           $ref: '#/components/responses/Forbidden'
    *         500:
-   *           $ref: '#/components/responses/500'
+   *           $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /search search page

+ 29 - 23
apps/app/src/server/routes/tag.js

@@ -37,15 +37,17 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        tags:
+   *                          $ref: '#/components/schemas/Tags'
+   *                          description: List of matching tags
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /tags.search search tags
@@ -91,15 +93,17 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    tags:
-   *                      $ref: '#/components/schemas/Tags'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        tags:
+   *                          $ref: '#/components/schemas/Tags'
+   *                          description: Updated tags for the page
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {post} /tags.update update tags on view-mode (not edit-mode)
@@ -168,17 +172,19 @@ module.exports = function(crowi, app) {
    *            content:
    *              application/json:
    *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1ResponseOK'
-   *                    data:
-   *                      type: array
-   *                      items:
-   *                        $ref: '#/components/schemas/Tag'
+   *                  allOf:
+   *                    - $ref: '#/components/schemas/ApiResponseSuccess'
+   *                    - type: object
+   *                      properties:
+   *                        data:
+   *                          type: array
+   *                          items:
+   *                            $ref: '#/components/schemas/Tag'
+   *                          description: List of tags with count information
    *          403:
-   *            $ref: '#/components/responses/403'
+   *            $ref: '#/components/responses/Forbidden'
    *          500:
-   *            $ref: '#/components/responses/500'
+   *            $ref: '#/components/responses/InternalServerError'
    */
   /**
    * @api {get} /tags.list get tagnames and count pages relate each tag