{"action":"created","comment":{"url":"https://api.github.com/repos/elastic/kibana/pulls/comments/215616950","pull_request_review_id":152919026,"id":215616950,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIxNTYxNjk1MA==","diff_hunk":"@@ -0,0 +1,63 @@\n+# Saved Object Validations\n+\n+The saved object client supports validation of documents during create / bulkCreate operations.\n+\n+This allows us tighter control over what documents get written to the saved object index, and helps us keep the index in a healthy state.\n+\n+## Creating validations\n+\n+Plugin authors can write their own validations by adding a `validations` property to their uiExports. A validation is nothing more than a dictionary of `{[prop: string]: validationFunction}` where:\n+\n+* `prop` - a root-property on a saved object document\n+* `validationFunction` - a function that takes a document and throws an error if it does not meet expectations.\n+\n+## Example\n+\n+```js\n+// In myFanciPlugin...\n+uiExports: {\n+  validations: {\n+    myProperty(doc) {\n+      if (doc.attributes.someField === undefined) {\n+        throw new Error(`Document ${doc.id} did not define \"someField\"`);\n+      }\n+    },\n+\n+    someOtherProp(doc) {\n+      if (doc.attributes.counter < 0) {\n+        throw new Error(`Document ${doc.id} cannot have a negative counter.`);\n+      }\n+    },\n+  },\n+},\n+```\n+\n+In this example, `myFanciPlugin` defines validations for two properties: `myProperty` and `someOtherProp`.\n+\n+This means that no other plugin can define validations for myProperty or someOtherProp.","path":"src/server/saved_objects/validation/readme.md","position":37,"original_position":37,"commit_id":"1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d","original_commit_id":"1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d","user":{"login":"chrisdavies","id":833377,"node_id":"MDQ6VXNlcjgzMzM3Nw==","avatar_url":"https://avatars0.githubusercontent.com/u/833377?v=4","gravatar_id":"","url":"https://api.github.com/users/chrisdavies","html_url":"https://github.com/chrisdavies","followers_url":"https://api.github.com/users/chrisdavies/followers","following_url":"https://api.github.com/users/chrisdavies/following{/other_user}","gists_url":"https://api.github.com/users/chrisdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/chrisdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrisdavies/subscriptions","organizations_url":"https://api.github.com/users/chrisdavies/orgs","repos_url":"https://api.github.com/users/chrisdavies/repos","events_url":"https://api.github.com/users/chrisdavies/events{/privacy}","received_events_url":"https://api.github.com/users/chrisdavies/received_events","type":"User","site_admin":false},"body":"It's handled here: https://github.com/elastic/kibana/blob/1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d/src/ui/ui_exports/ui_export_types/saved_object.js\r\n\r\nSo, the `uniqueKeys` test should be the thing that covers uniqueness. I'm not entirely sure how else I'd test this, other than to write tests around this `saved_object.js` file ^^^. Originally, I was enforcing the uniqueness in my code, and that was tested, but that was redundant with the `uniqueKeys` uiExports building logic. ¯\\_(ツ)_/¯\r\n\r\nHappy to do whatever you think is best here!","created_at":"2018-09-06T13:05:37Z","updated_at":"2018-09-06T13:05:37Z","html_url":"https://github.com/elastic/kibana/pull/20243#discussion_r215616950","pull_request_url":"https://api.github.com/repos/elastic/kibana/pulls/20243","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/elastic/kibana/pulls/comments/215616950"},"html":{"href":"https://github.com/elastic/kibana/pull/20243#discussion_r215616950"},"pull_request":{"href":"https://api.github.com/repos/elastic/kibana/pulls/20243"}},"in_reply_to_id":215432347},"pull_request":{"url":"https://api.github.com/repos/elastic/kibana/pulls/20243","id":197536039,"node_id":"MDExOlB1bGxSZXF1ZXN0MTk3NTM2MDM5","html_url":"https://github.com/elastic/kibana/pull/20243","diff_url":"https://github.com/elastic/kibana/pull/20243.diff","patch_url":"https://github.com/elastic/kibana/pull/20243.patch","issue_url":"https://api.github.com/repos/elastic/kibana/issues/20243","number":20243,"state":"open","locked":false,"title":"Migrations/core","user":{"login":"chrisdavies","id":833377,"node_id":"MDQ6VXNlcjgzMzM3Nw==","avatar_url":"https://avatars0.githubusercontent.com/u/833377?v=4","gravatar_id":"","url":"https://api.github.com/users/chrisdavies","html_url":"https://github.com/chrisdavies","followers_url":"https://api.github.com/users/chrisdavies/followers","following_url":"https://api.github.com/users/chrisdavies/following{/other_user}","gists_url":"https://api.github.com/users/chrisdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/chrisdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrisdavies/subscriptions","organizations_url":"https://api.github.com/users/chrisdavies/orgs","repos_url":"https://api.github.com/users/chrisdavies/repos","events_url":"https://api.github.com/users/chrisdavies/events{/privacy}","received_events_url":"https://api.github.com/users/chrisdavies/received_events","type":"User","site_admin":false},"body":"This PR introduces Kibana index migrations, per issue [15100](https://github.com/elastic/kibana/issues/15100).\r\n\r\nImplementation details can be found in the [readme](https://github.com/chrisdavies/kibana/tree/migrations/core/src/server/saved_objects/migrations).\r\n\r\nBriefly, migrations are the mechanism for upgrading Kibana saved object documents and mappings from one version of Kibana to another.\r\n\r\nCurrently, the bulk of the migration logic is in place, and is expected to be relatively stable (thus ready for an initial review). But there are still tests to be written, particularly integration tests.\r\n\r\n## Testing\r\n\r\nSome scenarios to consider:\r\n\r\n- Run Kibana with no migrations defined?\r\n- Define some migrations and boot Kibana without any .kibana index?\r\n- Define some migrations and boot Kibana on an existing index?\r\n- Add subsequent migrations, do they all apply?\r\n- Write a migration that throws an exception. Does the system do what you'd expect?\r\n- Run multiple Kibana instances that attempt to run migrations.\r\n- Disable a plugin that has migrations defined\r\n  - Can you import documents associated with that plugin?\r\n  - Can you re-enable the plugin?\r\n  - Do things work as expected?\r\n- Can you write a migration that changes a document's type from, say `type: 'foo'` to `type: 'bar'`?\r\n\r\nTo test this, the best thing to do is to write your own test migrations, probably adding them to an existing plugin.\r\n\r\nI've done this in the following way in `src/core_plugins/kibana`:\r\n\r\nModify `mappings.json` to have a new, test document type:\r\n\r\n```js\r\n\"foo\": {\r\n  \"properties\": {\r\n    \"meaning\": { \"type\": \"integer\" }\r\n  }\r\n},\r\n```\r\n\r\nStart Kibana in REPL mode and create some docs:\r\n\r\n`yarn run --repl`\r\n\r\n```js\r\nvar callCluster = kbnServer.server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;\r\n\r\ncallCluster('index', { index: '.kibana', type: 'doc', id: 'foo:a', body: { type: 'foo', foo: { meaning: 1 } } });\r\ncallCluster('index', { index: '.kibana', type: 'doc', id: 'foo:b', body: { type: 'foo', foo: { meaning: 2 } } });\r\ncallCluster('index', { index: '.kibana', type: 'doc', id: 'foo:c', body: { type: 'foo', foo: { meaning: 3 } } });\r\n```\r\n\r\nTo `index.js`, import lodash (for convenience):\r\n\r\n```js\r\nimport _ from 'lodash';\r\n```\r\n\r\nModify the `uiExports` object to have some migrations:\r\n\r\n```js\r\nmigrations: {\r\n  foo: {\r\n    '1.0.0': doc => _.set(doc, 'attributes.meaning', doc.attributes.meaning + 1),\r\n    '1.0.1': doc => _.set(doc, 'attributes.meaning', doc.attributes.meaning * 2),\r\n  },\r\n},\r\n```\r\n\r\nSave and wait for Kibana to reload. It should run migrations, and you should be able to query and see that the docs were updated:\r\n\r\n```js\r\nvar callCluster = kbnServer.server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;\r\n\r\ncallCluster('search', { index: '.kibana', type: 'doc', body: { query: { term: { type: 'foo' } } } });\r\n```\r\n\r\nAs you tinker, you'll likely get into a state where migrations fail, in which case, there will be an invalid index lingering around that will prevent migrations from running again. So, this little CURL is your friend (replacing the index name as appropriate):\r\n\r\n```\r\ncurl -X DELETE \"http://localhost:9200/.kibana_5\"\r\n```\r\n\r\nOr if you have Kibana running in REPL mode:\r\n\r\n```js\r\nvar callCluster = kbnServer.server.plugins.elasticsearch.getCluster('admin').callWithInternalUser;\r\ncallCluster('indices.delete', { index: '.kibana_5' });\r\n```\r\n\r\nRight now, Kibana won't attempt to re-run migrations until you've restarted it. It would be fairly simple to have the migration polling mechanism detect that the index was removed, and automatically attempt to re-run migrations. The question is, should this be a manual thing (e.g. restart) or should it be automatic?\r\n","created_at":"2018-06-26T20:06:30Z","updated_at":"2018-09-06T13:05:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"1458d053027e54ca9c7a23fec0784b0abf6be580","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/elastic/kibana/pulls/20243/commits","review_comments_url":"https://api.github.com/repos/elastic/kibana/pulls/20243/comments","review_comment_url":"https://api.github.com/repos/elastic/kibana/pulls/comments{/number}","comments_url":"https://api.github.com/repos/elastic/kibana/issues/20243/comments","statuses_url":"https://api.github.com/repos/elastic/kibana/statuses/1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d","head":{"label":"chrisdavies:migrations/core","ref":"migrations/core","sha":"1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d","user":{"login":"chrisdavies","id":833377,"node_id":"MDQ6VXNlcjgzMzM3Nw==","avatar_url":"https://avatars0.githubusercontent.com/u/833377?v=4","gravatar_id":"","url":"https://api.github.com/users/chrisdavies","html_url":"https://github.com/chrisdavies","followers_url":"https://api.github.com/users/chrisdavies/followers","following_url":"https://api.github.com/users/chrisdavies/following{/other_user}","gists_url":"https://api.github.com/users/chrisdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/chrisdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrisdavies/subscriptions","organizations_url":"https://api.github.com/users/chrisdavies/orgs","repos_url":"https://api.github.com/users/chrisdavies/repos","events_url":"https://api.github.com/users/chrisdavies/events{/privacy}","received_events_url":"https://api.github.com/users/chrisdavies/received_events","type":"User","site_admin":false},"repo":{"id":123947515,"node_id":"MDEwOlJlcG9zaXRvcnkxMjM5NDc1MTU=","name":"kibana","full_name":"chrisdavies/kibana","owner":{"login":"chrisdavies","id":833377,"node_id":"MDQ6VXNlcjgzMzM3Nw==","avatar_url":"https://avatars0.githubusercontent.com/u/833377?v=4","gravatar_id":"","url":"https://api.github.com/users/chrisdavies","html_url":"https://github.com/chrisdavies","followers_url":"https://api.github.com/users/chrisdavies/followers","following_url":"https://api.github.com/users/chrisdavies/following{/other_user}","gists_url":"https://api.github.com/users/chrisdavies/gists{/gist_id}","starred_url":"https://api.github.com/users/chrisdavies/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/chrisdavies/subscriptions","organizations_url":"https://api.github.com/users/chrisdavies/orgs","repos_url":"https://api.github.com/users/chrisdavies/repos","events_url":"https://api.github.com/users/chrisdavies/events{/privacy}","received_events_url":"https://api.github.com/users/chrisdavies/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/chrisdavies/kibana","description":":bar_chart: Kibana analytics and search dashboard for Elasticsearch","fork":true,"url":"https://api.github.com/repos/chrisdavies/kibana","forks_url":"https://api.github.com/repos/chrisdavies/kibana/forks","keys_url":"https://api.github.com/repos/chrisdavies/kibana/keys{/key_id}","collaborators_url":"https://api.github.com/repos/chrisdavies/kibana/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/chrisdavies/kibana/teams","hooks_url":"https://api.github.com/repos/chrisdavies/kibana/hooks","issue_events_url":"https://api.github.com/repos/chrisdavies/kibana/issues/events{/number}","events_url":"https://api.github.com/repos/chrisdavies/kibana/events","assignees_url":"https://api.github.com/repos/chrisdavies/kibana/assignees{/user}","branches_url":"https://api.github.com/repos/chrisdavies/kibana/branches{/branch}","tags_url":"https://api.github.com/repos/chrisdavies/kibana/tags","blobs_url":"https://api.github.com/repos/chrisdavies/kibana/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/chrisdavies/kibana/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/chrisdavies/kibana/git/refs{/sha}","trees_url":"https://api.github.com/repos/chrisdavies/kibana/git/trees{/sha}","statuses_url":"https://api.github.com/repos/chrisdavies/kibana/statuses/{sha}","languages_url":"https://api.github.com/repos/chrisdavies/kibana/languages","stargazers_url":"https://api.github.com/repos/chrisdavies/kibana/stargazers","contributors_url":"https://api.github.com/repos/chrisdavies/kibana/contributors","subscribers_url":"https://api.github.com/repos/chrisdavies/kibana/subscribers","subscription_url":"https://api.github.com/repos/chrisdavies/kibana/subscription","commits_url":"https://api.github.com/repos/chrisdavies/kibana/commits{/sha}","git_commits_url":"https://api.github.com/repos/chrisdavies/kibana/git/commits{/sha}","comments_url":"https://api.github.com/repos/chrisdavies/kibana/comments{/number}","issue_comment_url":"https://api.github.com/repos/chrisdavies/kibana/issues/comments{/number}","contents_url":"https://api.github.com/repos/chrisdavies/kibana/contents/{+path}","compare_url":"https://api.github.com/repos/chrisdavies/kibana/compare/{base}...{head}","merges_url":"https://api.github.com/repos/chrisdavies/kibana/merges","archive_url":"https://api.github.com/repos/chrisdavies/kibana/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/chrisdavies/kibana/downloads","issues_url":"https://api.github.com/repos/chrisdavies/kibana/issues{/number}","pulls_url":"https://api.github.com/repos/chrisdavies/kibana/pulls{/number}","milestones_url":"https://api.github.com/repos/chrisdavies/kibana/milestones{/number}","notifications_url":"https://api.github.com/repos/chrisdavies/kibana/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/chrisdavies/kibana/labels{/name}","releases_url":"https://api.github.com/repos/chrisdavies/kibana/releases{/id}","deployments_url":"https://api.github.com/repos/chrisdavies/kibana/deployments","created_at":"2018-03-05T16:32:39Z","updated_at":"2018-08-20T14:19:26Z","pushed_at":"2018-09-06T12:38:59Z","git_url":"git://github.com/chrisdavies/kibana.git","ssh_url":"git@github.com:chrisdavies/kibana.git","clone_url":"https://github.com/chrisdavies/kibana.git","svn_url":"https://github.com/chrisdavies/kibana","homepage":"https://www.elastic.co/products/kibana","size":447713,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":0,"open_issues":1,"watchers":0,"default_branch":"master"}},"base":{"label":"elastic:master","ref":"master","sha":"23ed2135bf2917642651248610ced7b7c724f581","user":{"login":"elastic","id":6764390,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY3NjQzOTA=","avatar_url":"https://avatars0.githubusercontent.com/u/6764390?v=4","gravatar_id":"","url":"https://api.github.com/users/elastic","html_url":"https://github.com/elastic","followers_url":"https://api.github.com/users/elastic/followers","following_url":"https://api.github.com/users/elastic/following{/other_user}","gists_url":"https://api.github.com/users/elastic/gists{/gist_id}","starred_url":"https://api.github.com/users/elastic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/elastic/subscriptions","organizations_url":"https://api.github.com/users/elastic/orgs","repos_url":"https://api.github.com/users/elastic/repos","events_url":"https://api.github.com/users/elastic/events{/privacy}","received_events_url":"https://api.github.com/users/elastic/received_events","type":"Organization","site_admin":false},"repo":{"id":7833168,"node_id":"MDEwOlJlcG9zaXRvcnk3ODMzMTY4","name":"kibana","full_name":"elastic/kibana","owner":{"login":"elastic","id":6764390,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY3NjQzOTA=","avatar_url":"https://avatars0.githubusercontent.com/u/6764390?v=4","gravatar_id":"","url":"https://api.github.com/users/elastic","html_url":"https://github.com/elastic","followers_url":"https://api.github.com/users/elastic/followers","following_url":"https://api.github.com/users/elastic/following{/other_user}","gists_url":"https://api.github.com/users/elastic/gists{/gist_id}","starred_url":"https://api.github.com/users/elastic/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/elastic/subscriptions","organizations_url":"https://api.github.com/users/elastic/orgs","repos_url":"https://api.github.com/users/elastic/repos","events_url":"https://api.github.com/users/elastic/events{/privacy}","received_events_url":"https://api.github.com/users/elastic/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/elastic/kibana","description":":bar_chart: Kibana analytics and search dashboard for Elasticsearch","fork":false,"url":"https://api.github.com/repos/elastic/kibana","forks_url":"https://api.github.com/repos/elastic/kibana/forks","keys_url":"https://api.github.com/repos/elastic/kibana/keys{/key_id}","collaborators_url":"https://api.github.com/repos/elastic/kibana/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/elastic/kibana/teams","hooks_url":"https://api.github.com/repos/elastic/kibana/hooks","issue_events_url":"https://api.github.com/repos/elastic/kibana/issues/events{/number}","events_url":"https://api.github.com/repos/elastic/kibana/events","assignees_url":"https://api.github.com/repos/elastic/kibana/assignees{/user}","branches_url":"https://api.github.com/repos/elastic/kibana/branches{/branch}","tags_url":"https://api.github.com/repos/elastic/kibana/tags","blobs_url":"https://api.github.com/repos/elastic/kibana/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/elastic/kibana/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/elastic/kibana/git/refs{/sha}","trees_url":"https://api.github.com/repos/elastic/kibana/git/trees{/sha}","statuses_url":"https://api.github.com/repos/elastic/kibana/statuses/{sha}","languages_url":"https://api.github.com/repos/elastic/kibana/languages","stargazers_url":"https://api.github.com/repos/elastic/kibana/stargazers","contributors_url":"https://api.github.com/repos/elastic/kibana/contributors","subscribers_url":"https://api.github.com/repos/elastic/kibana/subscribers","subscription_url":"https://api.github.com/repos/elastic/kibana/subscription","commits_url":"https://api.github.com/repos/elastic/kibana/commits{/sha}","git_commits_url":"https://api.github.com/repos/elastic/kibana/git/commits{/sha}","comments_url":"https://api.github.com/repos/elastic/kibana/comments{/number}","issue_comment_url":"https://api.github.com/repos/elastic/kibana/issues/comments{/number}","contents_url":"https://api.github.com/repos/elastic/kibana/contents/{+path}","compare_url":"https://api.github.com/repos/elastic/kibana/compare/{base}...{head}","merges_url":"https://api.github.com/repos/elastic/kibana/merges","archive_url":"https://api.github.com/repos/elastic/kibana/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/elastic/kibana/downloads","issues_url":"https://api.github.com/repos/elastic/kibana/issues{/number}","pulls_url":"https://api.github.com/repos/elastic/kibana/pulls{/number}","milestones_url":"https://api.github.com/repos/elastic/kibana/milestones{/number}","notifications_url":"https://api.github.com/repos/elastic/kibana/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/elastic/kibana/labels{/name}","releases_url":"https://api.github.com/repos/elastic/kibana/releases{/id}","deployments_url":"https://api.github.com/repos/elastic/kibana/deployments","created_at":"2013-01-26T04:00:59Z","updated_at":"2018-09-06T11:00:28Z","pushed_at":"2018-09-06T13:01:02Z","git_url":"git://github.com/elastic/kibana.git","ssh_url":"git@github.com:elastic/kibana.git","clone_url":"https://github.com/elastic/kibana.git","svn_url":"https://github.com/elastic/kibana","homepage":"https://www.elastic.co/products/kibana","size":451765,"stargazers_count":10031,"watchers_count":10031,"language":"JavaScript","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3894,"mirror_url":null,"archived":false,"open_issues_count":3391,"license":{"key":"other","name":"Other","spdx_id":null,"url":null,"node_id":"MDc6TGljZW5zZTA="},"forks":3894,"open_issues":3391,"watchers":10031,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/elastic/kibana/pulls/20243"},"html":{"href":"https://github.com/elastic/kibana/pull/20243"},"issue":{"href":"https://api.github.com/repos/elastic/kibana/issues/20243"},"comments":{"href":"https://api.github.com/repos/elastic/kibana/issues/20243/comments"},"review_comments":{"href":"https://api.github.com/repos/elastic/kibana/pulls/20243/comments"},"review_comment":{"href":"https://api.github.com/repos/elastic/kibana/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/elastic/kibana/pulls/20243/commits"},"statuses":{"href":"https://api.github.com/repos/elastic/kibana/statuses/1cd609f37f7d85d9a5ec5c6a7c45e286da1dc02d"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"chrisdavies"}}