diff --git a/src/plugins/saved_objects/README.md b/src/plugins/saved_objects/README.md index 2f7d98dbb36b..f323b4a94609 100644 --- a/src/plugins/saved_objects/README.md +++ b/src/plugins/saved_objects/README.md @@ -175,3 +175,597 @@ The migraton version will be saved as a `migrationVersion` attribute in the save ``` For a more detailed explanation on the migration, refer to [`saved objects management`](src/core/server/saved_objects/migrations/README.md). + +## Server APIs + +### Get saved objects API + +Retrieve a single saved object by its ID. + +* Path and HTTP methods + +```json +GET :/api/saved_objects// +``` + +* Path parameters + +The following table lists the available path parameters. All path parameters are required. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `` | String | YES | The ID of the saved object. | + +* Example request + +```json +GET api/saved_objects/index-pattern/619cc200-ecd0-11ee-95b1-e7363f9e289d +``` + +* Example response + +```json +{ + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d", + "type": "index-pattern", + "namespaces": [ + "default" + ], + "updated_at": "2024-03-28T06:57:03.008Z", + "version": "WzksMl0=", + "attributes": { + "title": "test*", + "fields": "[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + } +} +``` + +### Bulk get saved objects API + +Retrieve mutiple saved objects. + +* Path and HTTP methods + +```json +POST :/api/saved_objects/_bulk_get +``` + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `id` | String | YES | The ID of the saved object. | +| `fields` | Array | NO | The fields of the saved obejct need to be returned in the response. | + +* Example request + +```json +POST api/saved_objects/_bulk_get +[ + { + "type": "index-pattern", + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d" + }, + { + "type": "config", + "id": "3.0.0" + } +] +``` + +* Example response + +```json +{ + "saved_objects": [ + { + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d", + "type": "index-pattern", + "namespaces": [ + "default" + ], + "updated_at": "2024-03-28T06:57:03.008Z", + "version": "WzksMl0=", + "attributes": { + "title": "test*", + "fields": "[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + } + }, + { + "id": "3.0.0", + "type": "config", + "namespaces": [ + "default" + ], + "updated_at": "2024-03-19T06:11:41.608Z", + "version": "WzAsMV0=", + "attributes": { + "buildNum": 9007199254740991 + }, + "references": [ + + ], + "migrationVersion": { + "config": "7.9.0" + } + } + ] +} +``` + +### Find saved objects API + +Retrieve a paginated set of saved objects by mulitple conditions. + +* Path and HTTP methods + +```json +GET :/api/saved_objects/_find +``` + +* Query parameters + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `per_page` | Number | NO | The number of saved objects to return in each page. | +| `page` | Number | NO | The page of saved objects to return. | +| `search` | String | NO | A `simple_query_string` query DSL that used to filter the saved objects. | +| `default_search_operator` | String | NO | The default operator to use for the `simple_query_string` query. | +| `search_fields` | Array | NO | The fields to perform the `simple_query_string` parsed query against. | +| `fields` | Array | NO | The fields of the saved obejct need to be returned in the response. | +| `sort_field` | String | NO | The field used for sorting the response. | +| `has_reference` | Object | NO | Filters to objects that have a relationship with the type and ID combination. | +| `filter` | String | NO | The query string used to filter the attribute of the saved object. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Example request + +```json +GET api/saved_objects/_find?type=index-pattern&search_fields=title +``` + +* Example response + +```json +{ + "page": 1, + "per_page": 20, + "total": 2, + "saved_objects": [ + { + "type": "index-pattern", + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d", + "attributes": { + "title": "test*", + "fields": "[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "updated_at": "2024-03-28T06:57:03.008Z", + "version": "WzksMl0=", + "namespaces": [ + "default" + ], + "score": 0 + }, + { + "type": "index-pattern", + "id": "2ffee5da-55b3-49b4-b9e1-c3af5d1adbd3", + "attributes": { + "title": "test*", + "fields": "[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "updated_at": "2024-03-28T07:10:13.513Z", + "version": "WzEwLDJd", + "workspaces": [ + "9gt4lB" + ], + "namespaces": [ + "default" + ], + "score": 0 + } + ] +} +``` + +### Create saved objects API + +Create saved objects. + +* Path and HTTP methods + +```json +POST :/api/saved_objects// +``` + +* Path parameters + +The following table lists the available path parameters. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `` | String | NO |The ID of the saved object. | + +* Query parameters + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `overwrite` | Boolean | NO | If `true`, overwrite the saved object with the same ID, defaults to `false`. | + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `attributes` | Object | YES | The attributes of the saved object. | +| `references` | Array | NO | The attributes of the referenced objects. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Example request + +```json +POST api/saved_objects/index-pattern/test-pattern +{ + "attributes": { + "title": "test-pattern-*" + } +} +``` + +* Example response + +```json +{ + "type": "index-pattern", + "id": "test-pattern", + "attributes": { + "title": "test-pattern-*" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "updated_at": "2024-03-29T05:55:09.270Z", + "version": "WzExLDJd", + "namespaces": [ + "default" + ] +} +``` + +### Bulk create saved objects API + +Bulk create saved objects. + +* Path and HTTP methods + +```json +POST :/api/saved_objects/_bulk_create +``` + +* Query parameters + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `overwrite` | Boolean | NO | If `true`, overwrite the saved object with the same ID, defaults to `false`. | + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `id` | String | NO |The ID of the saved object. | +| `attributes` | Object | YES | The attributes of the saved object. | +| `references` | Array | NO | The attributes of the referenced objects. | +| `version` | String | NO | The version of the saved object. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Example request + +```json +POST api/saved_objects/_bulk_create +[ + { + "type": "index-pattern", + "id": "test-pattern1", + "attributes": { + "title": "test-pattern1-*" + } + } +] +``` + +* Example response + +```json +{ + "saved_objects": [ + { + "type": "index-pattern", + "id": "test-pattern1", + "attributes": { + "title": "test-pattern1-*" + }, + "references": [ + + ], + "migrationVersion": { + "index-pattern": "7.6.0" + }, + "updated_at": "2024-03-29T06:01:59.453Z", + "version": "WzEyLDJd", + "namespaces": [ + "default" + ] + } + ] +} +``` +### Upate saved objects API + +Update saved objects. + +* Path and HTTP methods + +```json +PUT :/api/saved_objects// +``` + +* Path parameters + +The following table lists the available path parameters. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `` | String | NO |The ID of the saved object. | + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `attributes` | Object | YES | The attributes of the saved object. | +| `references` | Array | NO | The attributes of the referenced objects. | + +* Example request + +```json +PUT api/saved_objects/index-pattern/test-pattern +{ + "attributes": { + "title": "test-pattern-update-*" + } +} +``` + +* Example response + +```json +{ + "id": "test-pattern", + "type": "index-pattern", + "updated_at": "2024-03-29T06:04:32.743Z", + "version": "WzEzLDJd", + "namespaces": [ + "default" + ], + "attributes": { + "title": "test-pattern-update-*" + } +} +``` +### Delete saved objects API + +Delete saved objects. + +* Path and HTTP methods + +```json +DELETE :/api/saved_objects// +``` + +* Path parameters + +The following table lists the available path parameters. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `` | String | NO | The ID of the saved object. | + +* Example request + +```json +DELETE api/saved_objects/index-pattern/test-pattern +``` + +* Example response + +```json +{} +``` +### Export saved object API + +Export saved objects. + +* Path and HTTP methods + +```json +POST :/api/saved_objects/_export +``` + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String|Array | NO | The types of the saved object to be included in the export. | +| `objects` | Array | NO | A list of saved objects to export. | +| `includeReferencesDeep` | Boolean | NO | Includes all of the referenced objects in the export. | +| `excludeExportDetails` | Boolean | NO | Exclude the export summary in the export. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Example request + +```json +POST api/saved_objects/_export +{ + "type": "index-pattern" +} +``` + +* Example response + +```json +{"attributes":{"fields":"[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]","title":"test*"},"id":"2ffee5da-55b3-49b4-b9e1-c3af5d1adbd3","migrationVersion":{"index-pattern":"7.6.0"},"references":[],"type":"index-pattern","updated_at":"2024-03-28T07:10:13.513Z","version":"WzEwLDJd","workspaces":["9gt4lB"]} +{"attributes":{"fields":"[{\"count\":0,\"name\":\"_id\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_index\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_score\",\"type\":\"number\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_source\",\"type\":\"_source\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"count\":0,\"name\":\"_type\",\"type\":\"string\",\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false}]","title":"test*"},"id":"619cc200-ecd0-11ee-95b1-e7363f9e289d","migrationVersion":{"index-pattern":"7.6.0"},"references":[],"type":"index-pattern","updated_at":"2024-03-28T06:57:03.008Z","version":"WzksMl0="} +{"attributes":{"title":"test-pattern1-*"},"id":"test-pattern1","migrationVersion":{"index-pattern":"7.6.0"},"references":[],"type":"index-pattern","updated_at":"2024-03-29T06:01:59.453Z","version":"WzEyLDJd"} +{"exportedCount":3,"missingRefCount":0,"missingReferences":[]} +``` + +### Import saved object API + +Import saved objects from the file generated by the export API. + +* Path and HTTP methods + +```json +POST :/api/saved_objects/_import +``` + +* Query parameters + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `createNewCopies` | Boolean | NO | Creates copies of the saved objects, genereate new IDs for the imported saved obejcts and resets the reference. | +| `overwrite` | Boolean | NO | Overwrites the saved objects when they already exist. | +| `dataSourceId` | String | NO | The ID of the data source. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Request body + +The request body must include a multipart/form-data. + +* Example request + +```json +POST api/saved_objects/_import?createNewCopies=true --form file=@export.ndjson +``` + +* Example response + +```json +{ + "successCount": 3, + "success": true, + "successResults": [ + { + "type": "index-pattern", + "id": "2ffee5da-55b3-49b4-b9e1-c3af5d1adbd3", + "meta": { + "title": "test*", + "icon": "indexPatternApp" + }, + "destinationId": "f0b08067-d6ab-4153-ba7d-0304506430d6" + }, + { + "type": "index-pattern", + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d", + "meta": { + "title": "test*", + "icon": "indexPatternApp" + }, + "destinationId": "ffd3719c-2314-4022-befc-7d3007225952" + }, + { + "type": "index-pattern", + "id": "test-pattern1", + "meta": { + "title": "test-pattern1-*", + "icon": "indexPatternApp" + }, + "destinationId": "e87e7f2d-8498-4e44-8d25-f7d41f3b3844" + } + ] +} +``` + +### Resolve import saved objects errors API + +Resolve the errors if the import API returns errors, this API can be used to retry importing some saved obejcts, overwrite specific saved objects, or change the references to different saved objects. + +* Path and HTTP methods + +```json +POST :/api/saved_objects/_resolve_import_errors +``` + +* Query parameters + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `createNewCopies` | Boolean | NO | Creates copies of the saved objects, genereate new IDs for the imported saved obejcts and resets the reference. | +| `dataSourceId` | String | NO | The ID of the data source. | +| `workspaces` | String\|Array | NO | The ID of the workspace which the saved objects exist in. | + +* Request body + +The request body must include a multipart/form-data. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `file` | ndjson file | YES | The same file given to the import API. | +| `retries` | Array | YES | The retry operations. | + +The attrbutes of the object in the `objects` parameter are as follows: +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `id` | String | YES |The ID of the saved object. | +| `overwrite` | Boolean | NO | If `true`, overwrite the saved object with the same ID, defaults to `false`. | +| `destinationId` | String | NO | The destination ID that the imported object should have, if different from the current ID. | +| `replaceReferences` | Array | NO | A list of `type`, `from`, and `to` to be used to change the saved object's references. | +| `ignoreMissingReferences` | Boolean | NO | If `true`, ignores missing reference errors, defaults to `false`. | + +* Example request + +```json +POST api/saved_objects/_import?createNewCopies=true --form file=@export.ndjson --form retries='[{"type":"index-pattern","id":"my-pattern","overwrite":true}]' + +``` + +* Example response + +```json +{ + "successCount": 0, + "success": true +} +``` diff --git a/src/plugins/workspace/README.md b/src/plugins/workspace/README.md new file mode 100644 index 000000000000..7e3fff562d82 --- /dev/null +++ b/src/plugins/workspace/README.md @@ -0,0 +1,319 @@ +# Workspace + +## Server APIs + +### List workspaces API + +List workspaces. + +* Path and HTTP methods + +```json +POST :/api/workspaces/_list +``` + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `search` | String | NO | A `simple_query_string` query DSL used to search the workspaces. | +| `searchFields` | Array | NO | The fields to perform the `simple_query_string` parsed query against. | +| `sortField` | String | NO | The fields used for sorting the response. | +| `sortOrder` | String | NO | The order used for sorting the response. | +| `perPage` | String | NO | The number of workspaces to return in each page. | +| `page` | String | NO | The page of workspaces to return. | +| `permissionModes` | Array | NO | The permission mode list. | + +* Example request + +```json +POST api/workspaces/_list +``` + +* Example response + +```json +{ + "success": true, + "result": { + "page": 1, + "per_page": 20, + "total": 3, + "workspaces": [ + { + "name": "test1", + "features": [ + "workspace_update", + "workspace_overview", + "dashboards", + "visualize", + "opensearchDashboardsOverview", + "indexPatterns", + "discover", + "objects", + "objects_searches", + "objects_query", + "dev_tools" + ], + "id": "hWNZls" + }, + { + "name": "test2", + "features": [ + "workspace_update", + "workspace_overview", + "dashboards", + "visualize", + "opensearchDashboardsOverview", + "indexPatterns", + "discover", + "objects", + "objects_searches", + "objects_query" + ], + "id": "SnkOPt" + }, + { + "name": "Global workspace", + "features": [ + "*", + "!@management" + ], + "reserved": true, + "id": "public" + } + ] + } +} +``` + + +### Get workspace API + +Retrieve a single workspace. + +* Path and HTTP methods + +```json +GET :/api/workspaces/ +``` + +* Path parameters + +The following table lists the available path parameters. All path parameters are required. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The ID of the workspace. | + +* Example request + +```json +GET api/workspaces/SnkOPt +``` + +* Example response + +```json +{ + "success": true, + "result": { + "name": "test2", + "features": [ + "workspace_update", + "workspace_overview", + "dashboards", + "visualize", + "opensearchDashboardsOverview", + "indexPatterns", + "discover", + "objects", + "objects_searches", + "objects_query" + ], + "id": "SnkOPt" + } +} +``` + +### Create workspace API + +Create a workspace. + +* Path and HTTP methods + +```json +POST :/api/workspaces +``` + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `attributes` | Object | YES | The attributes of the workspace. | +| `permissions` | Object | NO | The permission info of the workspace. | + + +* Example request + +```json +POST api/workspaces +{ + "attributes": { + "name": "test4", + "description": "test4" + } +} +``` + +* Example response + +```json +{ + "success": true, + "result": { + "id": "eHVoCJ" + } +} +``` + +### Update workspace API + +Update the attributes and permissions of a workspace. + +* Path and HTTP methods + +```json +PUT :/api/workspaces/ +``` + +* Path parameters + +The following table lists the available path parameters. All path parameters are required. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The ID of the workspace. | + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `attributes` | Object | YES | The attributes of the workspace. | +| `permissions` | Object | NO | The permission info of the workspace. | + + +* Example request + +```json +PUT api/workspaces/eHVoCJ +{ + "attributes": { + "name": "test4", + "description": "test update" + } +} +``` + +* Example response + +```json +{ + "success": true, + "result": true +} +``` + +### Delete workspace API + +Delete a workspace. + +* Path and HTTP methods + +```json +DELETE :/api/workspaces/ +``` + +* Path parameters + +The following table lists the available path parameters. All path parameters are required. + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `` | String | YES | The ID of the workspace. | + + +* Example request + +```json +DELETE api/workspaces/eHVoCJ +``` + +* Example response + +```json +{ + "success": true, + "result": true +} +``` + +### Duplicate saved objects API + +Duplicate saved objects among workspaces. + +* Path and HTTP methods + +```json +POST :/api/workspaces/_duplicate_saved_objects +``` + +* Request body + +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `objects` | Array | YES | A list of saved objects to copy. | +| `targetWorkspace` | String | YES | The ID of the workspace to copy to. | +| `includeReferencesDeep` | Boolean | NO | Copy all of the referenced objects of the specified objects to the target workspace . Defaults to `true`.| + +The attrbutes of the object in the `objects` parameter are as follows: +| Parameter | Data type | Required | Description | +| :--- | :--- | :--- | :--- | +| `type` | String | YES | The type of the saved object, such as `index-pattern`, `config` and `dashboard`. | +| `id` | String | YES | The ID of the saved object. | + +* Example request + +```json +POST api/workspaces/_duplicate_saved_objects +{ + "objects": [ + { + "type": "index-pattern", + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d" + } + ], + "targetWorkspace": "9gt4lB" +} +``` + +* Example response + +```json +{ + "successCount": 1, + "success": true, + "successResults": [ + { + "type": "index-pattern", + "id": "619cc200-ecd0-11ee-95b1-e7363f9e289d", + "meta": { + "title": "test*", + "icon": "indexPatternApp" + }, + "destinationId": "f4b724fd-9647-4bbf-bf59-610b43a62c75" + } + ] +} +``` + diff --git a/src/plugins/workspace/server/integration_tests/duplicate.test.ts b/src/plugins/workspace/server/integration_tests/duplicate.test.ts new file mode 100644 index 000000000000..e994586c631c --- /dev/null +++ b/src/plugins/workspace/server/integration_tests/duplicate.test.ts @@ -0,0 +1,326 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as exportMock from '../../../../core/server'; +import supertest from 'supertest'; +import { SavedObjectsErrorHelpers } from '../../../../core/server'; +import { UnwrapPromise } from '@osd/utility-types'; +import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks'; +import { setupServer } from '../../../../core/server/test_utils'; +import { registerDuplicateRoute } from '../routes/duplicate'; +import { createListStream } from '../../../../core/server/utils/streams'; +import Boom from '@hapi/boom'; + +jest.mock('../../../../core/server/saved_objects/export', () => ({ + exportSavedObjectsToStream: jest.fn(), +})); + +type SetupServerReturn = UnwrapPromise>; + +const allowedTypes = ['index-pattern', 'visualization', 'dashboard']; +const URL = '/api/workspaces/_duplicate_saved_objects'; +const exportSavedObjectsToStream = exportMock.exportSavedObjectsToStream as jest.Mock; +const logger = loggingSystemMock.create(); +const clientMock = { + init: jest.fn(), + enterWorkspace: jest.fn(), + getCurrentWorkspaceId: jest.fn(), + getCurrentWorkspace: jest.fn(), + create: jest.fn(), + delete: jest.fn(), + list: jest.fn(), + get: jest.fn(), + update: jest.fn(), + stop: jest.fn(), + setup: jest.fn(), + destroy: jest.fn(), + setSavedObjects: jest.fn(), +}; + +export const createExportableType = (name: string): exportMock.SavedObjectsType => { + return { + name, + hidden: false, + namespaceType: 'single', + mappings: { + properties: {}, + }, + management: { + importableAndExportable: true, + }, + }; +}; + +describe(`duplicate saved objects among workspaces`, () => { + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let handlerContext: SetupServerReturn['handlerContext']; + let savedObjectsClient: ReturnType; + + const emptyResponse = { saved_objects: [], total: 0, per_page: 0, page: 0 }; + const mockIndexPattern = { + type: 'index-pattern', + id: 'my-pattern', + attributes: { title: 'my-pattern-*' }, + references: [], + }; + const mockVisualization = { + type: 'visualization', + id: 'my-visualization', + attributes: { title: 'Test visualization' }, + references: [ + { + name: 'ref_0', + type: 'index-pattern', + id: 'my-pattern', + }, + ], + }; + const mockDashboard = { + type: 'dashboard', + id: 'my-dashboard', + attributes: { title: 'Look at my dashboard' }, + references: [], + }; + + beforeEach(async () => { + ({ server, httpSetup, handlerContext } = await setupServer()); + handlerContext.savedObjects.typeRegistry.getImportableAndExportableTypes.mockReturnValue( + allowedTypes.map(createExportableType) + ); + handlerContext.savedObjects.typeRegistry.getType.mockImplementation( + (type: string) => + // other attributes aren't needed for the purposes of injecting metadata + ({ management: { icon: `${type}-icon` } } as any) + ); + + savedObjectsClient = handlerContext.savedObjects.client; + savedObjectsClient.find.mockResolvedValue(emptyResponse); + savedObjectsClient.checkConflicts.mockResolvedValue({ errors: [] }); + + const router = httpSetup.createRouter(''); + + registerDuplicateRoute(router, logger.get(), clientMock, 10000); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('duplicate failed if the requested saved objects are not valid', async () => { + const savedObjects = [mockIndexPattern, mockDashboard]; + clientMock.get.mockResolvedValueOnce({ success: true }); + exportSavedObjectsToStream.mockImplementation(() => { + const err = Boom.badRequest(); + err.output.payload.attributes = { + objects: savedObjects, + }; + throw err; + }); + + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + targetWorkspace: 'test_workspace', + }) + .expect(400); + + expect(result.body.error).toEqual('Bad Request'); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); // no objects were created + }); + + it('requires objects', async () => { + const result = await supertest(httpSetup.server.listener).post(URL).send({}).expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"[request body.objects]: expected value of type [array] but got [undefined]"` + ); + }); + + it('requires target workspace', async () => { + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"[request body.targetWorkspace]: expected value of type [string] but got [undefined]"` + ); + }); + + it('target workspace does not exist', async () => { + clientMock.get.mockResolvedValueOnce({ success: false }); + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + targetWorkspace: 'non-existen-workspace', + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"Get target workspace non-existen-workspace error: undefined"` + ); + }); + + it('duplicate unsupported objects', async () => { + clientMock.get.mockResolvedValueOnce({ success: true }); + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'unknown', + id: 'my-pattern', + }, + ], + includeReferencesDeep: true, + targetWorkspace: 'test_workspace', + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"Trying to duplicate object(s) with unsupported types: unknown:my-pattern"` + ); + }); + + it('duplicate index pattern and dashboard into a workspace successfully', async () => { + const targetWorkspace = 'target_workspace_id'; + const savedObjects = [mockIndexPattern, mockDashboard]; + clientMock.get.mockResolvedValueOnce({ success: true }); + exportSavedObjectsToStream.mockResolvedValueOnce(createListStream(savedObjects)); + savedObjectsClient.bulkCreate.mockResolvedValueOnce({ + saved_objects: savedObjects.map((obj) => ({ ...obj, workspaces: [targetWorkspace] })), + }); + + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + targetWorkspace, + }) + .expect(200); + expect(result.body).toEqual({ + success: true, + successCount: 2, + successResults: [ + { + type: mockIndexPattern.type, + id: mockIndexPattern.id, + meta: { title: mockIndexPattern.attributes.title, icon: 'index-pattern-icon' }, + }, + { + type: mockDashboard.type, + id: mockDashboard.id, + meta: { title: mockDashboard.attributes.title, icon: 'dashboard-icon' }, + }, + ], + }); + expect(savedObjectsClient.bulkCreate).toHaveBeenCalledTimes(1); + }); + + it('duplicate a saved object failed if its references are missing', async () => { + const targetWorkspace = 'target_workspace_id'; + const savedObjects = [mockVisualization]; + const exportDetail = { + exportedCount: 2, + missingRefCount: 1, + missingReferences: [{ type: 'index-pattern', id: 'my-pattern' }], + }; + clientMock.get.mockResolvedValueOnce({ success: true }); + exportSavedObjectsToStream.mockResolvedValueOnce( + createListStream(...savedObjects, exportDetail) + ); + + const error = SavedObjectsErrorHelpers.createGenericNotFoundError( + 'index-pattern', + 'my-pattern-*' + ).output.payload; + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [{ ...mockIndexPattern, error }], + }); + + const result = await supertest(httpSetup.server.listener) + .post(URL) + .send({ + objects: [ + { + type: 'visualization', + id: 'my-visualization', + }, + ], + includeReferencesDeep: true, + targetWorkspace, + }) + .expect(200); + expect(result.body).toEqual({ + success: false, + successCount: 0, + errors: [ + { + id: 'my-visualization', + type: 'visualization', + title: 'Test visualization', + meta: { title: 'Test visualization', icon: 'visualization-icon' }, + error: { + type: 'missing_references', + references: [{ type: 'index-pattern', id: 'my-pattern' }], + }, + }, + ], + }); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); + expect(savedObjectsClient.bulkGet).toHaveBeenCalledWith( + [{ fields: ['id'], id: 'my-pattern', type: 'index-pattern' }], + expect.any(Object) // options + ); + expect(savedObjectsClient.bulkCreate).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/workspace/server/integration_tests/routes.test.ts b/src/plugins/workspace/server/integration_tests/routes.test.ts index 4ef7aeb13d5e..66b7032a003a 100644 --- a/src/plugins/workspace/server/integration_tests/routes.test.ts +++ b/src/plugins/workspace/server/integration_tests/routes.test.ts @@ -264,6 +264,152 @@ describe('workspace service api integration test', () => { expect(listResult.body.result.total).toEqual(1); }); }); + + describe('Duplicate saved objects APIs', () => { + const mockIndexPattern = { + type: 'index-pattern', + id: 'my-pattern', + attributes: { title: 'my-pattern-*' }, + references: [], + }; + const mockDashboard = { + type: 'dashboard', + id: 'my-dashboard', + attributes: { title: 'Look at my dashboard' }, + references: [], + }; + + afterAll(async () => { + const listResult = await osdTestServer.request + .post(root, `/api/workspaces/_list`) + .send({ + page: 1, + }) + .expect(200); + const savedObjectsRepository = osd.coreStart.savedObjects.createInternalRepository([ + WORKSPACE_TYPE, + ]); + await Promise.all( + listResult.body.result.workspaces.map((item: WorkspaceAttribute) => + // this will delete reserved workspace + savedObjectsRepository.delete(WORKSPACE_TYPE, item.id) + ) + ); + }); + + it('requires objects', async () => { + const result = await osdTestServer.request + .post(root, `/api/workspaces/_duplicate_saved_objects`) + .send({}) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"[request body.objects]: expected value of type [array] but got [undefined]"` + ); + }); + + it('requires target workspace', async () => { + const result = await osdTestServer.request + .post(root, `/api/workspaces/_duplicate_saved_objects`) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"[request body.targetWorkspace]: expected value of type [string] but got [undefined]"` + ); + }); + + it('duplicate unsupported objects', async () => { + const result = await osdTestServer.request + .post(root, `/api/workspaces/_duplicate_saved_objects`) + .send({ + objects: [ + { + type: 'unknown', + id: 'my-pattern', + }, + ], + includeReferencesDeep: true, + targetWorkspace: 'test_workspace', + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"Trying to duplicate object(s) with unsupported types: unknown:my-pattern"` + ); + }); + + it('target workspace does not exist', async () => { + const result = await osdTestServer.request + .post(root, `/api/workspaces/_duplicate_saved_objects`) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + ], + includeReferencesDeep: true, + targetWorkspace: 'test_workspace', + }) + .expect(400); + + expect(result.body.message).toMatchInlineSnapshot( + `"Get target workspace test_workspace error: Saved object [workspace/test_workspace] not found"` + ); + }); + + it('duplicate index pattern and dashboard into a workspace successfully', async () => { + const createWorkspaceResult: any = await osdTestServer.request + .post(root, `/api/workspaces`) + .send({ + attributes: omitId(testWorkspace), + }) + .expect(200); + + expect(createWorkspaceResult.body.success).toEqual(true); + expect(typeof createWorkspaceResult.body.result.id).toBe('string'); + + const createSavedObjectsResult = await osdTestServer.request + .post(root, '/api/saved_objects/_bulk_create') + .send([mockIndexPattern, mockDashboard]) + .expect(200); + expect(createSavedObjectsResult.body.saved_objects.length).toBe(2); + + const targetWorkspace = createWorkspaceResult.body.result.id; + const result = await osdTestServer.request + .post(root, `/api/workspaces/_duplicate_saved_objects`) + .send({ + objects: [ + { + type: 'index-pattern', + id: 'my-pattern', + }, + { + type: 'dashboard', + id: 'my-dashboard', + }, + ], + includeReferencesDeep: true, + targetWorkspace, + }) + .expect(200); + expect(result.body.success).toEqual(true); + expect(result.body.successCount).toEqual(2); + }); + }); }); describe('workspace service api integration test when savedObjects.permission.enabled equal true', () => { diff --git a/src/plugins/workspace/server/plugin.ts b/src/plugins/workspace/server/plugin.ts index df1ece8ef469..d86a22296788 100644 --- a/src/plugins/workspace/server/plugin.ts +++ b/src/plugins/workspace/server/plugin.ts @@ -78,6 +78,7 @@ export class WorkspacePlugin implements Plugin { + router.post( + { + path: `${WORKSPACES_API_BASE_URL}/_duplicate_saved_objects`, + validate: { + body: schema.object({ + objects: schema.arrayOf( + schema.object({ + type: schema.string(), + id: schema.string(), + }) + ), + includeReferencesDeep: schema.boolean({ defaultValue: true }), + targetWorkspace: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const savedObjectsClient = context.core.savedObjects.client; + const { objects, includeReferencesDeep, targetWorkspace } = req.body; + + // need to access the registry for type validation, can't use the schema for this + const supportedTypes = context.core.savedObjects.typeRegistry + .getImportableAndExportableTypes() + .map((t) => t.name); + + const invalidObjects = objects.filter((obj) => !supportedTypes.includes(obj.type)); + if (invalidObjects.length) { + return res.badRequest({ + body: { + message: `Trying to duplicate object(s) with unsupported types: ${invalidObjects + .map((obj) => `${obj.type}:${obj.id}`) + .join(', ')}`, + }, + }); + } + + // check whether the target workspace exists or not + const getTargetWorkspaceResult = await client.get( + { + context, + request: req, + logger, + }, + targetWorkspace + ); + if (!getTargetWorkspaceResult.success) { + return res.badRequest({ + body: { + message: `Get target workspace ${targetWorkspace} error: ${getTargetWorkspaceResult.error}`, + }, + }); + } + + // fetch all the details of the specified saved objects + const objectsListStream = await exportSavedObjectsToStream({ + savedObjectsClient, + objects, + exportSizeLimit: maxImportExportSize, + includeReferencesDeep, + excludeExportDetails: true, + }); + + // import the saved objects into the target workspace + const result = await importSavedObjectsFromStream({ + savedObjectsClient: context.core.savedObjects.client, + typeRegistry: context.core.savedObjects.typeRegistry, + readStream: objectsListStream, + objectLimit: maxImportExportSize, + overwrite: false, + createNewCopies: true, + workspaces: [targetWorkspace], + }); + + return res.ok({ body: result }); + }) + ); +}; diff --git a/src/plugins/workspace/server/routes/index.ts b/src/plugins/workspace/server/routes/index.ts index 3e08fc298ea9..b49bb2893575 100644 --- a/src/plugins/workspace/server/routes/index.ts +++ b/src/plugins/workspace/server/routes/index.ts @@ -8,8 +8,9 @@ import { CoreSetup, Logger, PrincipalType, ACL } from '../../../../core/server'; import { WorkspacePermissionMode } from '../../common/constants'; import { IWorkspaceClientImpl, WorkspaceAttributeWithPermission } from '../types'; import { SavedObjectsPermissionControlContract } from '../permission_control/client'; +import { registerDuplicateRoute } from './duplicate'; -const WORKSPACES_API_BASE_URL = '/api/workspaces'; +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; const workspacePermissionMode = schema.oneOf([ schema.literal(WorkspacePermissionMode.Read), @@ -42,12 +43,14 @@ export function registerRoutes({ client, logger, http, + maxImportExportSize, permissionControlClient, isPermissionControlEnabled, }: { client: IWorkspaceClientImpl; logger: Logger; http: CoreSetup['http']; + maxImportExportSize: number; permissionControlClient?: SavedObjectsPermissionControlContract; isPermissionControlEnabled: boolean; }) { @@ -207,4 +210,7 @@ export function registerRoutes({ return res.ok({ body: result }); }) ); + + // duplicate saved objects among workspaces + registerDuplicateRoute(router, logger, client, maxImportExportSize); }