[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:
parent
48e39755ee
commit
485eaf773d
|
@ -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<T></code> | |
|
||||
| [references](./kibana-plugin-server.savedobjectsupdateresponse.references.md) | <code>SavedObjectReference[] | undefined</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SavedObjectsUpdateResponse](./kibana-plugin-server.savedobjectsupdateresponse.md) > [references](./kibana-plugin-server.savedobjectsupdateresponse.references.md)
|
||||
|
||||
## SavedObjectsUpdateResponse.references property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
references: SavedObjectReference[] | undefined;
|
||||
```
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -99,7 +99,7 @@ describe('PUT /api/saved_objects/{type}/{id?}', () => {
|
|||
'index-pattern',
|
||||
'logstash-*',
|
||||
{ title: 'Testing' },
|
||||
{ version: 'foo', references: [] }
|
||||
{ version: 'foo' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue