[core] Do not overwrite saved object references if not specified (#47248)

* saved objects: allow partial update without references

For normal attributes, the update API for saved objects supports partial
updates, where it will only attempt to change those attributes you
specify. References should behave the same way otherwise they will be
replaced entirely if you call update without specifying the original
references.
This commit is contained in:
Court Ewing 2019-10-07 16:10:29 -04:00 committed by GitHub
parent 48e39755ee
commit 485eaf773d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 150 additions and 25 deletions

View file

@ -8,7 +8,7 @@
<b>Signature:</b>
```typescript
export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = any> extends Omit<SavedObject<T>, 'attributes'>
export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = any> extends Omit<SavedObject<T>, 'attributes' | 'references'>
```
## Properties
@ -16,4 +16,5 @@ export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = an
| Property | Type | Description |
| --- | --- | --- |
| [attributes](./kibana-plugin-server.savedobjectsupdateresponse.attributes.md) | <code>Partial&lt;T&gt;</code> | |
| [references](./kibana-plugin-server.savedobjectsupdateresponse.references.md) | <code>SavedObjectReference[] &#124; undefined</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-server](./kibana-plugin-server.md) &gt; [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) &gt; [references](./kibana-plugin-server.savedobjectsupdateresponse.references.md)
## SavedObjectsUpdateResponse.references property
<b>Signature:</b>
```typescript
references: SavedObjectReference[] | undefined;
```

View file

@ -1744,6 +1744,68 @@ describe('SavedObjectsRepository', () => {
);
});
it('does not pass references if omitted', async () => {
await savedObjectsRepository.update(
type,
id,
{ title: 'Testing' }
);
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).not.toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: {
doc: expect.objectContaining({
references: [],
})
}
})
);
});
it('passes references if they are provided', async () => {
await savedObjectsRepository.update(
type,
id,
{ title: 'Testing' },
{ references: ['foo'] }
);
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: {
doc: expect.objectContaining({
references: ['foo'],
})
}
})
);
});
it('passes empty references array if empty references array is provided', async () => {
await savedObjectsRepository.update(
type,
id,
{ title: 'Testing' },
{ references: [] }
);
expect(callAdminCluster).toHaveBeenCalledTimes(1);
expect(callAdminCluster).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: {
doc: expect.objectContaining({
references: [],
})
}
})
);
});
it(`prepends namespace to the id but doesn't add namespace to body when providing namespace for namespaced type`, async () => {
await savedObjectsRepository.update(
'index-pattern',

View file

@ -644,9 +644,19 @@ export class SavedObjectsRepository {
throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id);
}
const { version, namespace, references = [] } = options;
const { version, namespace, references } = options;
const time = this._getCurrentTime();
const doc = {
[type]: attributes,
updated_at: time,
references,
};
if (!Array.isArray(doc.references)) {
delete doc.references;
}
const response = await this._writeToCluster('update', {
id: this._serializer.generateRawId(namespace, type, id),
index: this.getIndexForType(type),
@ -654,11 +664,7 @@ export class SavedObjectsRepository {
refresh: 'wait_for',
ignore: [404],
body: {
doc: {
[type]: attributes,
updated_at: time,
references,
},
doc,
},
});

View file

@ -114,8 +114,9 @@ export interface SavedObjectsBulkResponse<T extends SavedObjectAttributes = any>
* @public
*/
export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = any>
extends Omit<SavedObject<T>, 'attributes'> {
extends Omit<SavedObject<T>, 'attributes' | 'references'> {
attributes: Partial<T>;
references: SavedObjectReference[] | undefined;
}
/**

View file

@ -1511,9 +1511,11 @@ export interface SavedObjectsUpdateOptions extends SavedObjectsBaseOptions {
// Warning: (ae-forgotten-export) The symbol "Omit" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = any> extends Omit<SavedObject<T>, 'attributes'> {
export interface SavedObjectsUpdateResponse<T extends SavedObjectAttributes = any> extends Omit<SavedObject<T>, 'attributes' | 'references'> {
// (undocumented)
attributes: Partial<T>;
// (undocumented)
references: SavedObjectReference[] | undefined;
}
// @public

View file

@ -99,7 +99,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
'index-pattern',
'logstash-*',
{ title: 'Testing' },
{ version: 'foo', references: [] }
{ version: 'foo' }
);
});
});

View file

@ -53,15 +53,13 @@ export const createUpdateRoute = (prereqs: Prerequisites) => {
payload: Joi.object({
attributes: Joi.object().required(),
version: Joi.string(),
references: Joi.array()
.items(
Joi.object().keys({
name: Joi.string().required(),
type: Joi.string().required(),
id: Joi.string().required(),
})
)
.default([]),
references: Joi.array().items(
Joi.object().keys({
name: Joi.string().required(),
type: Joi.string().required(),
id: Joi.string().required(),
})
),
}).required(),
},
handler(request: UpdateRequest) {

View file

@ -52,11 +52,55 @@ export default function ({ getService }) {
attributes: {
title: 'My second favorite vis'
},
references: [],
});
});
});
it('does not pass references if omitted', async () => {
const resp = await supertest
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.send({
attributes: {
title: 'foo'
}
})
.expect(200);
expect(resp.body).not.to.have.property('references');
});
it('passes references if they are provided', async () => {
const references = [{ id: 'foo', name: 'Foo', type: 'visualization' }];
const resp = await supertest
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.send({
attributes: {
title: 'foo'
},
references
})
.expect(200);
expect(resp.body).to.have.property('references');
expect(resp.body.references).to.eql(references);
});
it('passes empty references array if empty references array is provided', async () => {
const resp = await supertest
.put(`/api/saved_objects/visualization/dd7caf20-9efd-11e7-acb3-3dab96693fab`)
.send({
attributes: {
title: 'foo'
},
references: []
})
.expect(200);
expect(resp.body).to.have.property('references');
expect(resp.body.references).to.eql([]);
});
describe('unknown id', () => {
it('should return a generic 404', async () => {
await supertest

View file

@ -449,7 +449,7 @@ export class AlertsClient {
private getAlertFromRaw(
id: string,
rawAlert: Partial<RawAlert>,
references: SavedObjectReference[]
references: SavedObjectReference[] | undefined
) {
if (!rawAlert.actions) {
return {
@ -457,7 +457,7 @@ export class AlertsClient {
...rawAlert,
};
}
const actions = this.injectReferencesIntoActions(rawAlert.actions, references);
const actions = this.injectReferencesIntoActions(rawAlert.actions, references || []);
return {
id,
...rawAlert,

View file

@ -481,7 +481,9 @@ function taskInstanceToAttributes(doc: TaskInstance): SavedObjectAttributes {
};
}
function savedObjectToConcreteTaskInstance(savedObject: SavedObject): ConcreteTaskInstance {
function savedObjectToConcreteTaskInstance(
savedObject: Omit<SavedObject, 'references'>
): ConcreteTaskInstance {
return {
...savedObject.attributes,
id: savedObject.id,

View file

@ -87,7 +87,6 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
attributes: {
name: 'My second favorite',
},
references: [],
});
};
@ -112,7 +111,6 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
attributes: {
title: 'My second favorite vis',
},
references: [],
});
};