From f5d01c3180ee8bc780fb3e6526bdc27440904421 Mon Sep 17 00:00:00 2001 From: SuZhoue-Joe Date: Mon, 12 Jun 2023 12:00:48 +0800 Subject: [PATCH] feat: implement workspaces service Signed-off-by: SuZhoue-Joe --- src/core/server/internal_types.ts | 3 + src/core/server/server.ts | 14 ++ src/core/server/types.ts | 1 + src/core/server/workspaces/index.ts | 13 ++ src/core/server/workspaces/routes/index.ts | 146 ++++++++++++++++ .../server/workspaces/saved_objects/index.ts | 6 + .../workspaces/saved_objects/workspace.ts | 46 +++++ src/core/server/workspaces/types.ts | 71 ++++++++ .../workspaces_client_with_saved_object.ts | 157 ++++++++++++++++++ .../server/workspaces/workspaces_service.ts | 74 +++++++++ 10 files changed, 531 insertions(+) create mode 100644 src/core/server/workspaces/index.ts create mode 100644 src/core/server/workspaces/routes/index.ts create mode 100644 src/core/server/workspaces/saved_objects/index.ts create mode 100644 src/core/server/workspaces/saved_objects/workspace.ts create mode 100644 src/core/server/workspaces/types.ts create mode 100644 src/core/server/workspaces/workspaces_client_with_saved_object.ts create mode 100644 src/core/server/workspaces/workspaces_service.ts diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index 2b4df7da68bf..0eb0b684d4f4 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -48,6 +48,7 @@ import { InternalStatusServiceSetup } from './status'; import { AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { InternalLoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; +import { InternalWorkspacesServiceSetup, InternalWorkspacesServiceStart } from './workspaces'; /** @internal */ export interface InternalCoreSetup { @@ -64,6 +65,7 @@ export interface InternalCoreSetup { auditTrail: AuditTrailSetup; logging: InternalLoggingServiceSetup; metrics: InternalMetricsServiceSetup; + workspaces: InternalWorkspacesServiceSetup; } /** @@ -78,6 +80,7 @@ export interface InternalCoreStart { uiSettings: InternalUiSettingsServiceStart; auditTrail: AuditTrailStart; coreUsageData: CoreUsageDataStart; + workspaces: InternalWorkspacesServiceStart; } /** diff --git a/src/core/server/server.ts b/src/core/server/server.ts index d4c041725ac7..99c79351d861 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -62,6 +62,7 @@ import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; import { CoreRouteHandlerContext } from './core_route_handler_context'; +import { WorkspacesService } from './workspaces'; const coreId = Symbol('core'); const rootConfigPath = ''; @@ -86,6 +87,7 @@ export class Server { private readonly coreApp: CoreApp; private readonly auditTrail: AuditTrailService; private readonly coreUsageData: CoreUsageDataService; + private readonly workspaces: WorkspacesService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -118,6 +120,7 @@ export class Server { this.auditTrail = new AuditTrailService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); + this.workspaces = new WorkspacesService(core); } public async setup() { @@ -172,6 +175,11 @@ export class Server { const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const workspacesSetup = await this.workspaces.setup({ + http: httpSetup, + savedObject: savedObjectsSetup, + }); + const statusSetup = await this.status.setup({ opensearch: opensearchServiceSetup, pluginDependencies: pluginTree.asNames, @@ -212,6 +220,7 @@ export class Server { auditTrail: auditTrailSetup, logging: loggingSetup, metrics: metricsSetup, + workspaces: workspacesSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -253,6 +262,9 @@ export class Server { opensearch: opensearchStart, savedObjects: savedObjectsStart, }); + const workspacesStart = await this.workspaces.start({ + savedObjects: savedObjectsStart, + }); this.coreStart = { capabilities: capabilitiesStart, @@ -263,6 +275,7 @@ export class Server { uiSettings: uiSettingsStart, auditTrail: auditTrailStart, coreUsageData: coreUsageDataStart, + workspaces: workspacesStart, }; const pluginsStart = await this.plugins.start(this.coreStart); @@ -295,6 +308,7 @@ export class Server { await this.status.stop(); await this.logging.stop(); await this.auditTrail.stop(); + await this.workspaces.stop(); } private registerCoreContext(coreSetup: InternalCoreSetup) { diff --git a/src/core/server/types.ts b/src/core/server/types.ts index 90ccef575807..f6e54c201dae 100644 --- a/src/core/server/types.ts +++ b/src/core/server/types.ts @@ -35,3 +35,4 @@ export * from './ui_settings/types'; export * from './legacy/types'; export type { EnvironmentMode, PackageInfo } from '@osd/config'; export { Branding } from '../../core/types'; +export * from './workspaces/types'; diff --git a/src/core/server/workspaces/index.ts b/src/core/server/workspaces/index.ts new file mode 100644 index 000000000000..838f216bbd86 --- /dev/null +++ b/src/core/server/workspaces/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +export { + WorkspacesService, + InternalWorkspacesServiceSetup, + WorkspacesServiceStart, + WorkspacesServiceSetup, + InternalWorkspacesServiceStart, +} from './workspaces_service'; + +export { WorkspaceAttribute, WorkspaceFindOptions, WORKSPACES_API_BASE_URL } from './types'; diff --git a/src/core/server/workspaces/routes/index.ts b/src/core/server/workspaces/routes/index.ts new file mode 100644 index 000000000000..dbd7b20809e2 --- /dev/null +++ b/src/core/server/workspaces/routes/index.ts @@ -0,0 +1,146 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { schema } from '@osd/config-schema'; +import { InternalHttpServiceSetup } from '../../http'; +import { Logger } from '../../logging'; +import { IWorkspaceDBImpl, WORKSPACES_API_BASE_URL } from '../types'; + +export function registerRoutes({ + client, + logger, + http, +}: { + client: IWorkspaceDBImpl; + logger: Logger; + http: InternalHttpServiceSetup; +}) { + const router = http.createRouter(WORKSPACES_API_BASE_URL); + router.get( + { + path: '/_list', + validate: { + query: schema.object({ + per_page: schema.number({ min: 0, defaultValue: 20 }), + page: schema.number({ min: 0, defaultValue: 1 }), + sort_field: schema.maybe(schema.string()), + fields: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const result = await client.list( + { + context, + request: req, + logger, + }, + req.query + ); + return res.ok({ body: result }); + }) + ); + router.get( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + const result = await client.get( + { + context, + request: req, + logger, + }, + id + ); + return res.ok({ body: result }); + }) + ); + router.post( + { + path: '/{id?}', + validate: { + body: schema.object({ + attributes: schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + }), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { attributes } = req.body; + + const result = await client.create( + { + context, + request: req, + logger, + }, + attributes + ); + return res.ok({ body: result }); + }) + ); + router.put( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({ + attributes: schema.object({ + description: schema.maybe(schema.string()), + name: schema.string(), + }), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + const { attributes } = req.body; + + const result = await client.update( + { + context, + request: req, + logger, + }, + id, + attributes + ); + return res.ok({ body: result }); + }) + ); + router.delete( + { + path: '/{id}', + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + router.handleLegacyErrors(async (context, req, res) => { + const { id } = req.params; + + const result = await client.delete( + { + context, + request: req, + logger, + }, + id + ); + return res.ok({ body: result }); + }) + ); +} diff --git a/src/core/server/workspaces/saved_objects/index.ts b/src/core/server/workspaces/saved_objects/index.ts new file mode 100644 index 000000000000..51653c50681e --- /dev/null +++ b/src/core/server/workspaces/saved_objects/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { workspace } from './workspace'; diff --git a/src/core/server/workspaces/saved_objects/workspace.ts b/src/core/server/workspaces/saved_objects/workspace.ts new file mode 100644 index 000000000000..d211aaa3ea93 --- /dev/null +++ b/src/core/server/workspaces/saved_objects/workspace.ts @@ -0,0 +1,46 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsType } from 'opensearch-dashboards/server'; + +export const workspace: SavedObjectsType = { + name: 'workspace', + namespaceType: 'agnostic', + hidden: false, + management: { + icon: 'apps', // todo: pending ux #2034 + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/opensearch-dashboards/dataSources/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/management/opensearch-dashboards/dataSources/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'management.opensearchDashboards.dataSources', + }; + }, + }, + mappings: { + dynamic: false, + properties: { + title: { + type: 'text', + }, + description: { + type: 'text', + }, + /** + * In opensearch, string[] is also mapped to text + */ + features: { + type: 'text', + }, + }, + }, +}; diff --git a/src/core/server/workspaces/types.ts b/src/core/server/workspaces/types.ts new file mode 100644 index 000000000000..cc24797605e2 --- /dev/null +++ b/src/core/server/workspaces/types.ts @@ -0,0 +1,71 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { + Logger, + OpenSearchDashboardsRequest, + RequestHandlerContext, + SavedObjectsFindResponse, +} from '..'; +import { WorkspacesSetupDeps } from './workspaces_service'; + +export interface WorkspaceAttribute { + id: string; + name: string; + description?: string; + features?: string[]; +} + +export interface WorkspaceFindOptions { + page?: number; + per_page?: number; + search?: string; + search_fields?: string[]; + sort_field?: string; + sort_order?: string; +} + +export interface IRequestDetail { + request: OpenSearchDashboardsRequest; + context: RequestHandlerContext; + logger: Logger; +} + +export interface IWorkspaceDBImpl { + setup(dep: WorkspacesSetupDeps): Promise>; + create( + requestDetail: IRequestDetail, + payload: Omit + ): Promise>; + list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): Promise< + IResponse< + { + workspaces: WorkspaceAttribute[]; + } & Pick + > + >; + get(requestDetail: IRequestDetail, id: string): Promise>; + update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise>; + delete(requestDetail: IRequestDetail, id: string): Promise>; + destroy(): Promise>; +} + +export type IResponse = + | { + result: T; + success: true; + } + | { + success: false; + error?: string; + }; + +export const WORKSPACES_API_BASE_URL = '/api/workspaces'; diff --git a/src/core/server/workspaces/workspaces_client_with_saved_object.ts b/src/core/server/workspaces/workspaces_client_with_saved_object.ts new file mode 100644 index 000000000000..058d7477efd4 --- /dev/null +++ b/src/core/server/workspaces/workspaces_client_with_saved_object.ts @@ -0,0 +1,157 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { SavedObject, SavedObjectError, SavedObjectsClientContract } from '../types'; +import { + IWorkspaceDBImpl, + WorkspaceAttribute, + WorkspaceFindOptions, + IResponse, + IRequestDetail, +} from './types'; +import { WorkspacesSetupDeps } from './workspaces_service'; +import { workspace } from './saved_objects'; + +export const WORKSPACES_TYPE_FOR_SAVED_OBJECT = 'workspace'; + +export class WorkspacesClientWithSavedObject implements IWorkspaceDBImpl { + private setupDep: WorkspacesSetupDeps; + constructor(dep: WorkspacesSetupDeps) { + this.setupDep = dep; + } + private getSavedObjectClientsFromRequestDetail( + requestDetail: IRequestDetail + ): SavedObjectsClientContract { + return requestDetail.context.core.savedObjects.client; + } + private getFlatternedResultWithSavedObject( + savedObject: SavedObject + ): WorkspaceAttribute { + return { + ...savedObject.attributes, + id: savedObject.id, + }; + } + private formatError(error: SavedObjectError | Error | any): string { + return error.message || error.error || 'Error'; + } + public async setup(dep: WorkspacesSetupDeps): Promise> { + this.setupDep.savedObject.registerType(workspace); + return { + success: true, + result: true, + }; + } + public async create( + requestDetail: IRequestDetail, + payload: Omit + ): ReturnType { + try { + const result = await this.getSavedObjectClientsFromRequestDetail(requestDetail).create< + Omit + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, payload); + return { + success: true, + result: { + id: result.id, + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async list( + requestDetail: IRequestDetail, + options: WorkspaceFindOptions + ): ReturnType { + try { + const { + saved_objects: savedObjects, + ...others + } = await this.getSavedObjectClientsFromRequestDetail(requestDetail).find( + { + ...options, + type: WORKSPACES_TYPE_FOR_SAVED_OBJECT, + } + ); + return { + success: true, + result: { + ...others, + workspaces: savedObjects.map((item) => this.getFlatternedResultWithSavedObject(item)), + }, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async get( + requestDetail: IRequestDetail, + id: string + ): Promise> { + try { + const result = await this.getSavedObjectClientsFromRequestDetail(requestDetail).get< + WorkspaceAttribute + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, id); + return { + success: true, + result: this.getFlatternedResultWithSavedObject(result), + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async update( + requestDetail: IRequestDetail, + id: string, + payload: Omit + ): Promise> { + try { + await this.getSavedObjectClientsFromRequestDetail(requestDetail).update< + Omit + >(WORKSPACES_TYPE_FOR_SAVED_OBJECT, id, payload); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async delete(requestDetail: IRequestDetail, id: string): Promise> { + try { + await this.getSavedObjectClientsFromRequestDetail(requestDetail).delete( + WORKSPACES_TYPE_FOR_SAVED_OBJECT, + id + ); + return { + success: true, + result: true, + }; + } catch (e: unknown) { + return { + success: false, + error: this.formatError(e), + }; + } + } + public async destroy(): Promise> { + return { + success: true, + result: true, + }; + } +} diff --git a/src/core/server/workspaces/workspaces_service.ts b/src/core/server/workspaces/workspaces_service.ts new file mode 100644 index 000000000000..15c150b7378a --- /dev/null +++ b/src/core/server/workspaces/workspaces_service.ts @@ -0,0 +1,74 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { InternalHttpServiceSetup } from '../http'; +import { Logger } from '../logging'; +import { registerRoutes } from './routes'; +import { + InternalSavedObjectsServiceSetup, + InternalSavedObjectsServiceStart, +} from '../saved_objects'; +import { IWorkspaceDBImpl } from './types'; +import { WorkspacesClientWithSavedObject } from './workspaces_client_with_saved_object'; + +export interface WorkspacesServiceSetup { + setWorkspacesClient: (client: IWorkspaceDBImpl) => void; +} + +export interface WorkspacesServiceStart { + client: IWorkspaceDBImpl; +} + +export interface WorkspacesSetupDeps { + http: InternalHttpServiceSetup; + savedObject: InternalSavedObjectsServiceSetup; +} + +export type InternalWorkspacesServiceSetup = WorkspacesServiceSetup; +export type InternalWorkspacesServiceStart = WorkspacesServiceStart; + +/** @internal */ +export interface WorkspacesStartDeps { + savedObjects: InternalSavedObjectsServiceStart; +} + +export class WorkspacesService + implements CoreService { + private logger: Logger; + private client?: IWorkspaceDBImpl; + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('workspaces-service'); + } + + public async setup(setupDeps: WorkspacesSetupDeps): Promise { + this.logger.debug('Setting up Workspaces service'); + + this.client = this.client || new WorkspacesClientWithSavedObject(setupDeps); + await this.client.setup(setupDeps); + + registerRoutes({ + http: setupDeps.http, + logger: this.logger, + client: this.client as IWorkspaceDBImpl, + }); + + return { + setWorkspacesClient: (client: IWorkspaceDBImpl) => { + this.client = client; + }, + }; + } + + public async start(deps: WorkspacesStartDeps): Promise { + this.logger.debug('Starting SavedObjects service'); + + return { + client: this.client as IWorkspaceDBImpl, + }; + } + + public async stop() {} +}