diff --git a/src/plugins/kibana/server/lib/schemas/create_api_document_schema.js b/src/plugins/kibana/server/lib/schemas/create_api_document_schema.js new file mode 100644 index 000000000000..1f2e797f5620 --- /dev/null +++ b/src/plugins/kibana/server/lib/schemas/create_api_document_schema.js @@ -0,0 +1,12 @@ +const Joi = require('joi'); + +module.exports = function (primary, included) { + const doc = {data: primary}; + if (included) { + doc.included = included; + } + + return Joi.object(doc); +}; + + diff --git a/src/plugins/kibana/server/lib/schemas/create_resource_object.js b/src/plugins/kibana/server/lib/schemas/create_resource_object.js new file mode 100644 index 000000000000..93492b8b1a57 --- /dev/null +++ b/src/plugins/kibana/server/lib/schemas/create_resource_object.js @@ -0,0 +1,15 @@ +const Joi = require('joi'); + +module.exports = function (attributes, relationships) { + const resource = { + type: Joi.string().required(), + id: Joi.string().required(), + attributes: attributes + }; + if (relationships) { + resource.relationships = relationships; + } + + return Joi.object(resource); +}; + diff --git a/src/plugins/kibana/server/lib/schemas/index_pattern_schema.js b/src/plugins/kibana/server/lib/schemas/index_pattern_schema.js index aea9f9b5cfa4..42f0f45cb6b0 100644 --- a/src/plugins/kibana/server/lib/schemas/index_pattern_schema.js +++ b/src/plugins/kibana/server/lib/schemas/index_pattern_schema.js @@ -1,31 +1,60 @@ -var Joi = require('joi'); +const Joi = require('joi'); +const createApiDocumentSchema = require('./create_api_document_schema'); +const createResourceObject = require('./create_resource_object'); +const relationshipObjectSchema = require('./relationship_object_schema'); + module.exports = { - post: Joi.object({ - title: Joi.string().required(), - time_field_name: Joi.string(), - interval_name: Joi.string(), - fields: Joi.array().items(Joi.object({ - name: Joi.string().required(), - count: Joi.number().integer(), - scripted: Joi.boolean(), - mapping: Joi.object({ - type: Joi.string().required() - }).unknown() - })), - field_format_map: Joi.object() - }), + post: createApiDocumentSchema( + createResourceObject( + Joi.object({ + title: Joi.string().required(), + time_field_name: Joi.string(), + interval_name: Joi.string(), + fields: Joi.array().items( + Joi.object({ + name: Joi.string().required(), + count: Joi.number().integer(), + scripted: Joi.boolean() + }) + ), + field_format_map: Joi.object() + }), + Joi.object({ + template: relationshipObjectSchema + }) + ), + Joi.array().items( + createResourceObject( + Joi.object({ + template: Joi.string().required(), + order: Joi.number().integer(), + mappings: Joi.object() + }) + ) + ) + ), - put: Joi.object({ - title: Joi.string(), - time_field_name: Joi.string(), - interval_name: Joi.string(), - fields: Joi.array().items(Joi.object({ - name: Joi.string().required(), - count: Joi.number().integer(), - scripted: Joi.boolean(), - mapping: Joi.any().forbidden() - })), - field_format_map: Joi.object() - }) + // No attributes are required for an update + // Templates can't be updated in an index_pattern PUT + put: createApiDocumentSchema( + createResourceObject( + Joi.object({ + title: Joi.string(), + time_field_name: Joi.string(), + interval_name: Joi.string(), + fields: Joi.array().items( + Joi.object({ + name: Joi.string().required(), + count: Joi.number().integer(), + scripted: Joi.boolean() + }) + ), + field_format_map: Joi.object() + }), + Joi.object({ + template: relationshipObjectSchema + }) + ) + ) }; diff --git a/src/plugins/kibana/server/lib/schemas/relationship_object_schema.js b/src/plugins/kibana/server/lib/schemas/relationship_object_schema.js new file mode 100644 index 000000000000..c51bb3bca323 --- /dev/null +++ b/src/plugins/kibana/server/lib/schemas/relationship_object_schema.js @@ -0,0 +1,9 @@ +const Joi = require('joi'); + +module.exports = Joi.object({ + data: Joi.object({ + type: Joi.string().required(), + id: Joi.string().required() + }) +}); + diff --git a/src/plugins/kibana/server/routes/api/index_patterns/register_post.js b/src/plugins/kibana/server/routes/api/index_patterns/register_post.js index cba49413f189..e6cf165dc3ad 100644 --- a/src/plugins/kibana/server/routes/api/index_patterns/register_post.js +++ b/src/plugins/kibana/server/routes/api/index_patterns/register_post.js @@ -19,17 +19,14 @@ module.exports = function registerPost(server) { } const callWithRequest = server.plugins.elasticsearch.callWithRequest; - const indexPattern = _.cloneDeep(req.payload); + const requestDocument = _.cloneDeep(req.payload); + const included = requestDocument.included; + const indexPatternId = requestDocument.data.id; + const indexPattern = requestDocument.data.attributes; const isWildcard = _.contains(indexPattern.title, '*'); - const mappings = _(req.payload.fields) - .indexBy('name') - .mapValues(value => value.mapping) - .omit(_.isUndefined) - .value(); + const templateResource = _.isEmpty(included) ? null : included[0]; - indexPattern.fields = JSON.stringify(_.map(indexPattern.fields, (field) => { - return _.omit(field, 'mapping'); - })); + indexPattern.fields = JSON.stringify(indexPattern.fields); const patternCreateParams = { index: '.kibana', @@ -40,7 +37,7 @@ module.exports = function registerPost(server) { callWithRequest(req, 'create', patternCreateParams) .then((patternResponse) => { - if (!isWildcard || _.isEmpty(mappings)) { + if (!isWildcard || _.isEmpty(included)) { return patternResponse; } @@ -51,17 +48,10 @@ module.exports = function registerPost(server) { } const templateParams = { - order: 0, + order: templateResource.attributes.order, create: true, - name: patternToTemplate(indexPattern.title), - body: { - template: indexPattern.title, - mappings: { - _default_: { - properties: mappings - } - } - } + name: templateResource.id, + body: _.omit(templateResource.attributes, 'order') }; return callWithRequest(req, 'indices.putTemplate', templateParams); diff --git a/test/unit/api/index_patterns/_post.js b/test/unit/api/index_patterns/_post.js index 6839d131187d..0d5f5072d09c 100644 --- a/test/unit/api/index_patterns/_post.js +++ b/test/unit/api/index_patterns/_post.js @@ -24,41 +24,29 @@ define(function (require) { .expect(400), request.post('/kibana/index_patterns') - .send(_.assign(createTestData().indexPatternWithMappings, {title: false})) + .send(_.set(createTestData().indexPatternWithTemplate, 'data.attributes.title', false)) .expect(400), request.post('/kibana/index_patterns') - .send(_.assign(createTestData().indexPatternWithMappings, {fields: {}})) + .send(_.set(createTestData().indexPatternWithTemplate, 'data.attributes.fields', {})) .expect(400), // Fields must have a name request.post('/kibana/index_patterns') - .send(_.assign(createTestData().indexPatternWithMappings, {fields: [{count: 0}]})) - .expect(400), - - // Mapping requires type - request.post('/kibana/index_patterns') - .send(_.assign(createTestData().indexPatternWithMappings, { - fields: [{ - 'name': 'geo.coordinates', - 'count': 0, - 'scripted': false, - 'mapping': {'index': 'not_analyzed', 'doc_values': false} - }] - })) + .send(_.set(createTestData().indexPatternWithTemplate, 'data.attributes.fields', [{count: 0}])) .expect(400) ]); }); bdd.it('should return 201 when a pattern is successfully created', function createPattern() { return request.post('/kibana/index_patterns') - .send(createTestData().indexPatternWithMappings) + .send(createTestData().indexPatternWithTemplate) .expect(201); }); bdd.it('should create an index template if mappings are provided', function createTemplate() { return request.post('/kibana/index_patterns') - .send(createTestData().indexPatternWithMappings) + .send(createTestData().indexPatternWithTemplate) .expect(201) .then(function () { return scenarioManager.client.indices.getTemplate({name: 'kibana-logstash-*'}); @@ -66,7 +54,7 @@ define(function (require) { }); bdd.it('should NOT create an index template if mappings are NOT provided', function noMappings() { - var pattern = createTestData().indexPatternWithMappings; + var pattern = createTestData().indexPatternWithTemplate; pattern.fields = _.map(pattern.fields, function (field) { return _.omit(field, 'mapping'); }); @@ -85,7 +73,7 @@ define(function (require) { }); bdd.it('should NOT create an index template if pattern does not contain a wildcard', function noWildcard() { - var pattern = createTestData().indexPatternWithMappings; + var pattern = createTestData().indexPatternWithTemplate; pattern.title = 'notawildcard'; return request.post('/kibana/index_patterns') @@ -103,11 +91,11 @@ define(function (require) { bdd.it('should return 409 conflict when a pattern with the given ID already exists', function patternConflict() { return request.post('/kibana/index_patterns') - .send(createTestData().indexPatternWithMappings) + .send(createTestData().indexPatternWithTemplate) .expect(201) .then(function () { return request.post('/kibana/index_patterns') - .send(createTestData().indexPatternWithMappings) + .send(createTestData().indexPatternWithTemplate) .expect(409); }); }); @@ -120,14 +108,14 @@ define(function (require) { } }).then(function () { return request.post('/kibana/index_patterns') - .send(createTestData().indexPatternWithMappings) + .send(createTestData().indexPatternWithTemplate) .expect(409); }); }); bdd.it('should return 409 conflict when mappings are provided with a pattern that matches existing indices', function existingIndicesConflict() { - var pattern = createTestData().indexPatternWithMappings; + var pattern = createTestData().indexPatternWithTemplate; pattern.title = '.kib*'; return request.post('/kibana/index_patterns') @@ -137,7 +125,7 @@ define(function (require) { bdd.it('should return 201 created successfully if a pattern matches existing indices but has NO mappings', function existingIndicesNoMappings() { - var pattern = createTestData().indexPatternWithMappings; + var pattern = createTestData().indexPatternWithTemplate; pattern.fields = _.map(pattern.fields, function (field) { return _.omit(field, 'mapping'); }); @@ -150,7 +138,7 @@ define(function (require) { bdd.it('should enforce snake_case in the request body', function () { return request.post('/kibana/index_patterns') - .send(_.mapKeys(createTestData().indexPatternWithMappings, function (value, key) { + .send(_.mapKeys(createTestData().indexPatternWithTemplate, function (value, key) { return _.camelCase(key); })) .expect(400); diff --git a/test/unit/api/index_patterns/data.js b/test/unit/api/index_patterns/data.js index 54c3d39ac648..aa855ba6ddde 100644 --- a/test/unit/api/index_patterns/data.js +++ b/test/unit/api/index_patterns/data.js @@ -1,33 +1,58 @@ module.exports = function createTestData() { return { - indexPatternWithMappings: { - 'title': 'logstash-*', - 'time_field_name': '@timestamp', - 'fields': [{ - 'name': 'geo.coordinates', - 'count': 0, - 'scripted': false, - 'mapping': {'type': 'geo_point', 'index': 'not_analyzed', 'doc_values': false} - }, { - 'name': 'ip', - 'count': 2, - 'scripted': false, - 'mapping': {'type': 'ip', 'index': 'not_analyzed', 'doc_values': true} - }, { - 'name': '@timestamp', - 'count': 0, - 'scripted': false, - 'mapping': {'type': 'date', 'index': 'not_analyzed', 'doc_values': true} - }, { - 'name': 'agent', - 'count': 0, - 'scripted': false, - 'mapping': {'type': 'string', 'index': 'analyzed', 'doc_values': false} - }, { - 'name': 'bytes', - 'count': 2, - 'scripted': false, - 'mapping': {'type': 'number', 'index': 'not_analyzed', 'doc_values': true} + indexPatternWithTemplate: { + 'data': { + 'type': 'index_patterns', + 'id': 'logstash-*', + 'attributes': { + 'title': 'logstash-*', + 'time_field_name': '@timestamp', + 'fields': [{ + 'name': 'geo.coordinates', + 'count': 0, + 'scripted': false + }, { + 'name': 'ip', + 'count': 2, + 'scripted': false + }, { + 'name': '@timestamp', + 'count': 0, + 'scripted': false + }, { + 'name': 'agent', + 'count': 0, + 'scripted': false + }, { + 'name': 'bytes', + 'count': 2, + 'scripted': false + }] + }, + 'relationships': { + 'template': { + 'data': {'type': 'index_templates', 'id': 'kibana-logstash-*'} + } + } + }, + 'included': [{ + 'type': 'index_templates', + 'id': 'kibana-logstash-*', + 'attributes': { + 'template': 'logstash-*', + 'order': 0, + 'mappings': { + '_default_': { + 'properties': { + 'geo.coordinates': {'type': 'geo_point', 'index': 'not_analyzed', 'doc_values': false}, + 'ip': {'type': 'ip', 'index': 'not_analyzed', 'doc_values': true}, + '@timestamp': {'type': 'date', 'index': 'not_analyzed', 'doc_values': true}, + 'agent': {'type': 'string', 'index': 'analyzed', 'doc_values': false}, + 'mapping': {'type': 'number', 'index': 'not_analyzed', 'doc_values': true} + } + } + } + } }] } };