mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[data views] swap_references api improvements (#163225)
## Summary Some simple dev UX improvements to the swap_references data views api - ``` POST /api/data_views/swap_references/_preview { "fromId" : "abcd-efg", "toId" : "xyz-123" } returns { result: [{ id: "123", type: "visualization" }], } ``` ``` POST /api/data_views/swap_references { "fromId" : "abcd-efg", "toId" : "xyz-123", "delete" : true // optional, removes data view which is no longer referenced } returns { result: [{ id: "123", type: "visualization" }], deleteStatus: { remainingRefs: 0, deletePerformed: true } ``` Additional params - ``` fromType: string - specify the saved object type. Default is `index-pattern` for data view forId: string | string[] - limit the affected saved objects to one or more by id forType: string - limit the affected saved objects by type ``` Improves upon https://github.com/elastic/kibana/pull/157665 Docs will be created in follow up PR
This commit is contained in:
parent
d59d778555
commit
cafaa9295e
3 changed files with 172 additions and 143 deletions
|
@ -46,7 +46,8 @@ const routes = [
|
|||
updateRoutes.registerUpdateDataViewRoute,
|
||||
updateRoutes.registerUpdateDataViewRouteLegacy,
|
||||
...Object.values(scriptedRoutes),
|
||||
swapReferencesRoute,
|
||||
swapReferencesRoute({ previewRoute: false }),
|
||||
swapReferencesRoute({ previewRoute: true }),
|
||||
];
|
||||
|
||||
export { routes };
|
||||
|
|
|
@ -27,8 +27,10 @@ interface GetDataViewArgs {
|
|||
|
||||
interface SwapRefResponse {
|
||||
result: Array<{ id: string; type: string }>;
|
||||
preview: boolean;
|
||||
deleteSuccess?: boolean;
|
||||
deleteStatus?: {
|
||||
remainingRefs: number;
|
||||
deletePerformed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const swapReference = async ({
|
||||
|
@ -43,135 +45,147 @@ export const swapReference = async ({
|
|||
|
||||
const idSchema = schema.string();
|
||||
|
||||
export const swapReferencesRoute = (
|
||||
router: IRouter,
|
||||
getStartServices: StartServicesAccessor<
|
||||
DataViewsServerPluginStartDependencies,
|
||||
DataViewsServerPluginStart
|
||||
>,
|
||||
usageCollection?: UsageCounter
|
||||
) => {
|
||||
router.versioned.post({ path: DATA_VIEW_SWAP_REFERENCES_PATH, access: 'public' }).addVersion(
|
||||
{
|
||||
version: INITIAL_REST_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
body: schema.object({
|
||||
from_id: idSchema,
|
||||
from_type: schema.maybe(schema.string()),
|
||||
to_id: idSchema,
|
||||
for_id: schema.maybe(schema.oneOf([idSchema, schema.arrayOf(idSchema)])),
|
||||
for_type: schema.maybe(schema.string()),
|
||||
preview: schema.maybe(schema.boolean()),
|
||||
delete: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
export const swapReferencesRoute =
|
||||
({ previewRoute }: { previewRoute: boolean }) =>
|
||||
(
|
||||
router: IRouter,
|
||||
getStartServices: StartServicesAccessor<
|
||||
DataViewsServerPluginStartDependencies,
|
||||
DataViewsServerPluginStart
|
||||
>,
|
||||
usageCollection?: UsageCounter
|
||||
) => {
|
||||
const path = previewRoute
|
||||
? `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview`
|
||||
: DATA_VIEW_SWAP_REFERENCES_PATH;
|
||||
router.versioned.post({ path, access: 'public' }).addVersion(
|
||||
{
|
||||
version: INITIAL_REST_VERSION,
|
||||
validate: {
|
||||
request: {
|
||||
body: schema.object({
|
||||
result: schema.arrayOf(schema.object({ id: idSchema, type: schema.string() })),
|
||||
preview: schema.boolean(),
|
||||
deleteSuccess: schema.maybe(schema.boolean()),
|
||||
fromId: idSchema,
|
||||
fromType: schema.maybe(schema.string()),
|
||||
toId: idSchema,
|
||||
forId: schema.maybe(schema.oneOf([idSchema, schema.arrayOf(idSchema)])),
|
||||
forType: schema.maybe(schema.string()),
|
||||
delete: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
body: schema.object({
|
||||
result: schema.arrayOf(schema.object({ id: idSchema, type: schema.string() })),
|
||||
deleteStatus: schema.maybe(
|
||||
schema.object({
|
||||
remainingRefs: schema.number(),
|
||||
deletePerformed: schema.boolean(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
handleErrors(async (ctx, req, res) => {
|
||||
const savedObjectsClient = (await ctx.core).savedObjects.client;
|
||||
const [core] = await getStartServices();
|
||||
const types = core.savedObjects.getTypeRegistry().getAllTypes();
|
||||
const type = req.body.from_type || DATA_VIEW_SAVED_OBJECT_TYPE;
|
||||
const preview = req.body.preview !== undefined ? req.body.preview : true;
|
||||
const searchId =
|
||||
!Array.isArray(req.body.for_id) && req.body.for_id !== undefined
|
||||
? [req.body.for_id]
|
||||
: req.body.for_id;
|
||||
router.handleLegacyErrors(
|
||||
handleErrors(async (ctx, req, res) => {
|
||||
const savedObjectsClient = (await ctx.core).savedObjects.client;
|
||||
const [core] = await getStartServices();
|
||||
const types = core.savedObjects.getTypeRegistry().getAllTypes();
|
||||
const type = req.body.fromType || DATA_VIEW_SAVED_OBJECT_TYPE;
|
||||
const searchId =
|
||||
!Array.isArray(req.body.forId) && req.body.forId !== undefined
|
||||
? [req.body.forId]
|
||||
: req.body.forId;
|
||||
|
||||
usageCollection?.incrementCounter({ counterName: 'swap_references' });
|
||||
usageCollection?.incrementCounter({ counterName: 'swap_references' });
|
||||
|
||||
// verify 'to' object actually exists
|
||||
try {
|
||||
await savedObjectsClient.get(type, req.body.to_id);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not find object with type ${type} and id ${req.body.to_id}`);
|
||||
}
|
||||
// verify 'to' object actually exists
|
||||
try {
|
||||
await savedObjectsClient.get(type, req.body.toId);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not find object with type ${type} and id ${req.body.toId}`);
|
||||
}
|
||||
|
||||
// assemble search params
|
||||
const findParams: SavedObjectsFindOptions = {
|
||||
type: types.map((t) => t.name),
|
||||
hasReference: { type, id: req.body.from_id },
|
||||
};
|
||||
// assemble search params
|
||||
const findParams: SavedObjectsFindOptions = {
|
||||
type: types.map((t) => t.name),
|
||||
hasReference: { type, id: req.body.fromId },
|
||||
};
|
||||
|
||||
if (req.body.for_type) {
|
||||
findParams.type = [req.body.for_type];
|
||||
}
|
||||
if (req.body.forType) {
|
||||
findParams.type = [req.body.forType];
|
||||
}
|
||||
|
||||
const { saved_objects: savedObjects } = await savedObjectsClient.find(findParams);
|
||||
const { saved_objects: savedObjects } = await savedObjectsClient.find(findParams);
|
||||
|
||||
const filteredSavedObjects = searchId
|
||||
? savedObjects.filter((so) => searchId?.includes(so.id))
|
||||
: savedObjects;
|
||||
const filteredSavedObjects = searchId
|
||||
? savedObjects.filter((so) => searchId?.includes(so.id))
|
||||
: savedObjects;
|
||||
|
||||
// create summary of affected objects
|
||||
const resultSummary = filteredSavedObjects.map((savedObject) => ({
|
||||
id: savedObject.id,
|
||||
type: savedObject.type,
|
||||
}));
|
||||
// create summary of affected objects
|
||||
const resultSummary = filteredSavedObjects.map((savedObject) => ({
|
||||
id: savedObject.id,
|
||||
type: savedObject.type,
|
||||
}));
|
||||
|
||||
const body: SwapRefResponse = {
|
||||
result: resultSummary,
|
||||
preview,
|
||||
};
|
||||
const body: SwapRefResponse = {
|
||||
result: resultSummary,
|
||||
};
|
||||
|
||||
// bail if preview
|
||||
if (previewRoute) {
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
// iterate over list and update references
|
||||
for (const savedObject of filteredSavedObjects) {
|
||||
const updatedRefs = savedObject.references.map((ref) => {
|
||||
if (ref.type === type && ref.id === req.body.fromId) {
|
||||
return { ...ref, id: req.body.toId };
|
||||
} else {
|
||||
return ref;
|
||||
}
|
||||
});
|
||||
|
||||
await savedObjectsClient.update(
|
||||
savedObject.type,
|
||||
savedObject.id,
|
||||
{},
|
||||
{
|
||||
references: updatedRefs,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (req.body.delete) {
|
||||
const verifyNoMoreRefs = await savedObjectsClient.find(findParams);
|
||||
if (verifyNoMoreRefs.total > 0) {
|
||||
body.deleteStatus = {
|
||||
remainingRefs: verifyNoMoreRefs.total,
|
||||
deletePerformed: false,
|
||||
};
|
||||
} else {
|
||||
await savedObjectsClient.delete(type, req.body.fromId, { refresh: 'wait_for' });
|
||||
body.deleteStatus = {
|
||||
remainingRefs: verifyNoMoreRefs.total,
|
||||
deletePerformed: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// bail if preview
|
||||
if (preview) {
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
// iterate over list and update references
|
||||
for (const savedObject of filteredSavedObjects) {
|
||||
const updatedRefs = savedObject.references.map((ref) => {
|
||||
if (ref.type === type && ref.id === req.body.from_id) {
|
||||
return { ...ref, id: req.body.to_id };
|
||||
} else {
|
||||
return ref;
|
||||
}
|
||||
});
|
||||
|
||||
await savedObjectsClient.update(
|
||||
savedObject.type,
|
||||
savedObject.id,
|
||||
{},
|
||||
{
|
||||
references: updatedRefs,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (req.body.delete) {
|
||||
const verifyNoMoreRefs = await savedObjectsClient.find(findParams);
|
||||
if (verifyNoMoreRefs.total > 0) {
|
||||
body.deleteSuccess = false;
|
||||
} else {
|
||||
await savedObjectsClient.delete(type, req.body.from_id);
|
||||
body.deleteSuccess = true;
|
||||
}
|
||||
}
|
||||
|
||||
return res.ok({
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body,
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const supertest = getService('supertest');
|
||||
const title = 'logs-*';
|
||||
const prevDataViewId = '91200a00-9efd-11e7-acb3-3dab96693fab';
|
||||
const PREVIEW_PATH = `${DATA_VIEW_SWAP_REFERENCES_PATH}/_preview`;
|
||||
let dataViewId = '';
|
||||
|
||||
describe('main', () => {
|
||||
|
@ -49,23 +50,23 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
|
||||
it('can preview', async () => {
|
||||
const res = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.post(PREVIEW_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
from_id: prevDataViewId,
|
||||
to_id: dataViewId,
|
||||
fromId: prevDataViewId,
|
||||
toId: dataViewId,
|
||||
});
|
||||
expect(res).to.have.property('status', 200);
|
||||
});
|
||||
|
||||
it('can preview specifying type', async () => {
|
||||
const res = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.post(PREVIEW_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
from_id: prevDataViewId,
|
||||
from_type: 'index-pattern',
|
||||
to_id: dataViewId,
|
||||
fromId: prevDataViewId,
|
||||
fromType: 'index-pattern',
|
||||
toId: dataViewId,
|
||||
});
|
||||
expect(res).to.have.property('status', 200);
|
||||
});
|
||||
|
@ -75,13 +76,11 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
from_id: prevDataViewId,
|
||||
to_id: dataViewId,
|
||||
preview: false,
|
||||
fromId: prevDataViewId,
|
||||
toId: dataViewId,
|
||||
});
|
||||
expect(res).to.have.property('status', 200);
|
||||
expect(res.body.result.length).to.equal(1);
|
||||
expect(res.body.preview).to.equal(false);
|
||||
expect(res.body.result[0].id).to.equal('dd7caf20-9efd-11e7-acb3-3dab96693fab');
|
||||
expect(res.body.result[0].type).to.equal('visualization');
|
||||
});
|
||||
|
@ -91,13 +90,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
from_id: prevDataViewId,
|
||||
to_id: dataViewId,
|
||||
preview: false,
|
||||
fromId: prevDataViewId,
|
||||
toId: dataViewId,
|
||||
delete: true,
|
||||
});
|
||||
expect(res).to.have.property('status', 200);
|
||||
expect(res.body.result.length).to.equal(1);
|
||||
expect(res.body.deleteStatus.remainingRefs).to.equal(0);
|
||||
expect(res.body.deleteStatus.deletePerformed).to.equal(true);
|
||||
|
||||
const res2 = await supertest
|
||||
.get(SPECIFIC_DATA_VIEW_PATH.replace('{id}', prevDataViewId))
|
||||
|
@ -118,13 +118,29 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
it("won't delete if reference remains", async () => {
|
||||
const res = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION)
|
||||
.send({
|
||||
fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
forId: ['960372e0-3224-11e8-a572-ffca06da1357'],
|
||||
delete: true,
|
||||
});
|
||||
expect(res).to.have.property('status', 200);
|
||||
expect(res.body.result.length).to.equal(1);
|
||||
expect(res.body.deleteStatus.remainingRefs).to.equal(1);
|
||||
expect(res.body.deleteStatus.deletePerformed).to.equal(false);
|
||||
});
|
||||
|
||||
it('can limit by id', async () => {
|
||||
// confirm this will find two items
|
||||
const res = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.post(PREVIEW_PATH)
|
||||
.send({
|
||||
from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
})
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
|
||||
expect(res).to.have.property('status', 200);
|
||||
|
@ -134,10 +150,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const res2 = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.send({
|
||||
from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
for_id: ['960372e0-3224-11e8-a572-ffca06da1357'],
|
||||
preview: false,
|
||||
fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
forId: ['960372e0-3224-11e8-a572-ffca06da1357'],
|
||||
})
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
|
||||
expect(res2).to.have.property('status', 200);
|
||||
|
@ -147,10 +162,10 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
it('can limit by type', async () => {
|
||||
// confirm this will find two items
|
||||
const res = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.post(PREVIEW_PATH)
|
||||
.send({
|
||||
from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
})
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
|
||||
expect(res).to.have.property('status', 200);
|
||||
|
@ -160,10 +175,9 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const res2 = await supertest
|
||||
.post(DATA_VIEW_SWAP_REFERENCES_PATH)
|
||||
.send({
|
||||
from_id: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
to_id: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
for_type: 'search',
|
||||
preview: false,
|
||||
fromId: '8963ca30-3224-11e8-a572-ffca06da1357',
|
||||
toId: '91200a00-9efd-11e7-acb3-3dab96693fab',
|
||||
forType: 'search',
|
||||
})
|
||||
.set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION);
|
||||
expect(res2).to.have.property('status', 200);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue