Skip to content

Commit 937c5f9

Browse files
authored
[Dashboard API][Links] Refactor LinksStorage class to use CM transforms (#221985)
## Summary Part of #219947 Refactors `LinksStorage` in the same vein as `DashboardStorage`, referring to local `itemToSavedObject`/`savedObjectToItem` transforms.
1 parent f631570 commit 937c5f9

File tree

8 files changed

+438
-18
lines changed

8 files changed

+438
-18
lines changed

src/platform/plugins/private/links/common/content_management/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type {
1919
LinksItem,
2020
LinksCrudTypes,
2121
LinksAttributes,
22+
LinksSearchOut,
2223
} from './latest';
2324

2425
export {

src/platform/plugins/private/links/common/content_management/v1/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export type {
1717
LinkType,
1818
} from './types';
1919
export type LinksItem = LinksCrudTypes['Item'];
20+
export type LinksSearchOut = LinksCrudTypes['SearchOut'];
2021
export {
2122
EXTERNAL_LINK_TYPE,
2223
DASHBOARD_LINK_TYPE,

src/platform/plugins/private/links/server/content_management/links_storage.ts

Lines changed: 329 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,344 @@
88
*/
99

1010
import type { Logger } from '@kbn/logging';
11-
import { SOContentStorage } from '@kbn/content-management-utils';
12-
import { CONTENT_ID } from '../../common';
13-
import type { LinksCrudTypes } from '../../common/content_management';
11+
import { StorageContext } from '@kbn/content-management-plugin/server';
12+
import { SavedObject, SavedObjectsFindOptions } from '@kbn/core-saved-objects-api-server';
13+
import Boom from '@hapi/boom';
14+
import { CreateResult, DeleteResult, SearchQuery } from '@kbn/content-management-plugin/common';
15+
import { CONTENT_ID as LINKS_SAVED_OBJECT_TYPE } from '../../common';
16+
import type { LinksAttributes, LinksItem, LinksSearchOut } from '../../common/content_management';
1417
import { cmServicesDefinition } from './schema/cm_services';
18+
import {
19+
LinksCreateOptions,
20+
LinksCreateOut,
21+
LinksGetOut,
22+
LinksSavedObjectAttributes,
23+
savedObjectToItem,
24+
itemToSavedObject,
25+
LinksUpdateOptions,
26+
LinksUpdateOut,
27+
LinksSearchOptions,
28+
} from './schema/latest';
1529

16-
export class LinksStorage extends SOContentStorage<LinksCrudTypes> {
30+
const savedObjectClientFromRequest = async (ctx: StorageContext) => {
31+
if (!ctx.requestHandlerContext) {
32+
throw new Error('Storage context.requestHandlerContext missing.');
33+
}
34+
35+
const { savedObjects } = await ctx.requestHandlerContext.core;
36+
return savedObjects.client;
37+
};
38+
39+
const searchArgsToSOFindOptions = (
40+
query: SearchQuery,
41+
options: LinksSearchOptions
42+
): SavedObjectsFindOptions => {
43+
return {
44+
type: LINKS_SAVED_OBJECT_TYPE,
45+
searchFields: options?.onlyTitle ? ['title'] : ['title^3', 'description'],
46+
search: query.text,
47+
perPage: query.limit,
48+
page: query.cursor ? +query.cursor : undefined,
49+
defaultSearchOperator: 'AND',
50+
};
51+
};
52+
53+
export class LinksStorage {
1754
constructor({
1855
logger,
1956
throwOnResultValidationError,
2057
}: {
2158
logger: Logger;
2259
throwOnResultValidationError: boolean;
2360
}) {
24-
super({
25-
savedObjectType: CONTENT_ID,
26-
cmServicesDefinition,
27-
enableMSearch: true,
28-
allowedSavedObjectAttributes: ['id', 'title', 'description', 'links', 'layout'],
29-
logger,
30-
throwOnResultValidationError,
61+
this.logger = logger;
62+
this.throwOnResultValidationError = throwOnResultValidationError ?? false;
63+
}
64+
65+
private logger: Logger;
66+
private throwOnResultValidationError: boolean;
67+
68+
async get(ctx: StorageContext, id: string): Promise<LinksGetOut> {
69+
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
70+
const soClient = await savedObjectClientFromRequest(ctx);
71+
72+
// Save data in DB
73+
const {
74+
saved_object: savedObject,
75+
alias_purpose: aliasPurpose,
76+
alias_target_id: aliasTargetId,
77+
outcome,
78+
} = await soClient.resolve<LinksSavedObjectAttributes>(LINKS_SAVED_OBJECT_TYPE, id);
79+
80+
const item = savedObjectToItem(savedObject, false);
81+
const response = { item, meta: { aliasPurpose, aliasTargetId, outcome } };
82+
83+
const validationError = transforms.get.out.result.validate(response);
84+
if (validationError) {
85+
if (this.throwOnResultValidationError) {
86+
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
87+
} else {
88+
this.logger.warn(`Invalid response. ${validationError.message}`);
89+
}
90+
}
91+
92+
// Validate response and DOWN transform to the request version
93+
const { value, error: resultError } = transforms.get.out.result.down<LinksGetOut, LinksGetOut>(
94+
response,
95+
undefined, // do not override version
96+
{ validate: false } // validation is done above
97+
);
98+
99+
if (resultError) {
100+
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
101+
}
102+
103+
return value;
104+
}
105+
106+
async bulkGet(): Promise<never> {
107+
// Not implemented
108+
throw new Error(`[bulkGet] has not been implemented. See LinksStorage class.`);
109+
}
110+
111+
async create(
112+
ctx: StorageContext,
113+
data: LinksAttributes,
114+
options: LinksCreateOptions
115+
): Promise<LinksCreateOut> {
116+
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
117+
const soClient = await savedObjectClientFromRequest(ctx);
118+
119+
// Validate input (data & options) & UP transform them to the latest version
120+
const { value: dataToLatest, error: dataError } = transforms.create.in.data.up<
121+
LinksAttributes,
122+
LinksAttributes
123+
>(data);
124+
if (dataError) {
125+
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
126+
}
127+
128+
const { value: optionsToLatest, error: optionsError } = transforms.create.in.options.up<
129+
LinksCreateOptions,
130+
LinksCreateOptions
131+
>(options);
132+
if (optionsError) {
133+
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
134+
}
135+
136+
const { attributes: soAttributes, references: soReferences } = await itemToSavedObject({
137+
attributes: dataToLatest,
138+
references: options.references,
139+
});
140+
141+
// Save data in DB
142+
const savedObject = await soClient.create<LinksSavedObjectAttributes>(
143+
LINKS_SAVED_OBJECT_TYPE,
144+
soAttributes,
145+
{ ...optionsToLatest, references: soReferences }
146+
);
147+
148+
const item = savedObjectToItem(savedObject, false);
149+
150+
const validationError = transforms.create.out.result.validate({ item });
151+
if (validationError) {
152+
if (this.throwOnResultValidationError) {
153+
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
154+
} else {
155+
this.logger.warn(`Invalid response. ${validationError.message}`);
156+
}
157+
}
158+
159+
// Validate DB response and DOWN transform to the request version
160+
const { value, error: resultError } = transforms.create.out.result.down<
161+
CreateResult<LinksItem>
162+
>(
163+
{ item },
164+
undefined, // do not override version
165+
{ validate: false } // validation is done above
166+
);
167+
168+
if (resultError) {
169+
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
170+
}
171+
172+
return value;
173+
}
174+
175+
async update(
176+
ctx: StorageContext,
177+
id: string,
178+
data: LinksAttributes,
179+
options: LinksUpdateOptions
180+
): Promise<LinksUpdateOut> {
181+
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
182+
const soClient = await savedObjectClientFromRequest(ctx);
183+
184+
// Validate input (data & options) & UP transform them to the latest version
185+
const { value: dataToLatest, error: dataError } = transforms.update.in.data.up<
186+
LinksAttributes,
187+
LinksAttributes
188+
>(data);
189+
if (dataError) {
190+
throw Boom.badRequest(`Invalid data. ${dataError.message}`);
191+
}
192+
193+
const { value: optionsToLatest, error: optionsError } = transforms.update.in.options.up<
194+
LinksUpdateOptions,
195+
LinksUpdateOptions
196+
>(options);
197+
if (optionsError) {
198+
throw Boom.badRequest(`Invalid options. ${optionsError.message}`);
199+
}
200+
201+
const { attributes: soAttributes, references: soReferences } = await itemToSavedObject({
202+
attributes: dataToLatest,
203+
references: options.references,
31204
});
205+
206+
// Save data in DB
207+
const partialSavedObject = await soClient.update<LinksSavedObjectAttributes>(
208+
LINKS_SAVED_OBJECT_TYPE,
209+
id,
210+
soAttributes,
211+
{ ...optionsToLatest, references: soReferences }
212+
);
213+
214+
const item = savedObjectToItem(partialSavedObject, true);
215+
216+
const validationError = transforms.update.out.result.validate({ item });
217+
if (validationError) {
218+
if (this.throwOnResultValidationError) {
219+
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
220+
} else {
221+
this.logger.warn(`Invalid response. ${validationError.message}`);
222+
}
223+
}
224+
225+
// Validate DB response and DOWN transform to the request version
226+
const { value, error: resultError } = transforms.update.out.result.down<
227+
LinksUpdateOut,
228+
LinksUpdateOut
229+
>(
230+
{ item },
231+
undefined, // do not override version
232+
{ validate: false } // validation is done above
233+
);
234+
235+
if (resultError) {
236+
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
237+
}
238+
239+
return value;
32240
}
241+
242+
async delete(
243+
ctx: StorageContext,
244+
id: string,
245+
// force is necessary to delete saved objects that exist in multiple namespaces
246+
options?: { force: boolean }
247+
): Promise<DeleteResult> {
248+
const soClient = await savedObjectClientFromRequest(ctx);
249+
await soClient.delete(LINKS_SAVED_OBJECT_TYPE, id, { force: options?.force ?? false });
250+
return { success: true };
251+
}
252+
253+
async search(
254+
ctx: StorageContext,
255+
query: SearchQuery,
256+
options: LinksSearchOptions
257+
): Promise<LinksSearchOut> {
258+
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
259+
const soClient = await savedObjectClientFromRequest(ctx);
260+
261+
// Validate and UP transform the options
262+
const { value: optionsToLatest, error: optionsError } = transforms.search.in.options.up<
263+
LinksSearchOptions,
264+
LinksSearchOptions
265+
>(options);
266+
if (optionsError) {
267+
throw Boom.badRequest(`Invalid payload. ${optionsError.message}`);
268+
}
269+
270+
const soQuery = searchArgsToSOFindOptions(query, optionsToLatest);
271+
// Execute the query in the DB
272+
const soResponse = await soClient.find<LinksSavedObjectAttributes>(soQuery);
273+
const hits = await Promise.all(
274+
soResponse.saved_objects
275+
.map(async (so) => {
276+
const item = savedObjectToItem(so, false);
277+
return item;
278+
})
279+
// Ignore any saved objects that failed to convert to items.
280+
.filter((item) => item !== null)
281+
);
282+
const response = {
283+
hits,
284+
pagination: {
285+
total: soResponse.total,
286+
},
287+
};
288+
289+
const validationError = transforms.search.out.result.validate(response);
290+
if (validationError) {
291+
if (this.throwOnResultValidationError) {
292+
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
293+
} else {
294+
this.logger.warn(`Invalid response. ${validationError.message}`);
295+
}
296+
}
297+
298+
// Validate the response and DOWN transform to the request version
299+
const { value, error: resultError } = transforms.search.out.result.down<
300+
LinksSearchOut,
301+
LinksSearchOut
302+
>(
303+
response,
304+
undefined, // do not override version
305+
{ validate: false } // validation is done above
306+
);
307+
308+
if (resultError) {
309+
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
310+
}
311+
312+
return value;
313+
}
314+
315+
mSearch = {
316+
savedObjectType: LINKS_SAVED_OBJECT_TYPE,
317+
toItemResult: (
318+
ctx: StorageContext,
319+
savedObject: SavedObject<LinksSavedObjectAttributes>
320+
): LinksItem => {
321+
const transforms = ctx.utils.getTransforms(cmServicesDefinition);
322+
323+
const contentItem = savedObjectToItem(savedObject, false);
324+
325+
const validationError = transforms.mSearch.out.result.validate(contentItem);
326+
if (validationError) {
327+
if (this.throwOnResultValidationError) {
328+
throw Boom.badRequest(`Invalid response. ${validationError.message}`);
329+
} else {
330+
this.logger.warn(`Invalid response. ${validationError.message}`);
331+
}
332+
}
333+
334+
// Validate DB response and DOWN transform to the request version
335+
const { value, error: resultError } = transforms.mSearch.out.result.down<
336+
LinksItem,
337+
LinksItem
338+
>(
339+
contentItem,
340+
undefined, // do not override version
341+
{ validate: false } // validation is done above
342+
);
343+
344+
if (resultError) {
345+
throw Boom.badRequest(`Invalid response. ${resultError.message}`);
346+
}
347+
348+
return value;
349+
},
350+
};
33351
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export * from './v1';

0 commit comments

Comments
 (0)