diff --git a/api/jest-e2e.config.js b/api/jest-e2e.config.js index 59518221c..da4478d94 100644 --- a/api/jest-e2e.config.js +++ b/api/jest-e2e.config.js @@ -5,6 +5,7 @@ module.exports = { ...defaultConfig, coverageDirectory: './coverage-e2e/', globalSetup: '/tests/e2e/jest.setup.ts', + setupFilesAfterEnv: ['/tests/e2e/jest.mock.ts'], testRegex: '(/tests/e2e/.*(test|spec|e2e))\\.(jsx?|tsx?)$', roots: ['/tests/e2e/'], testSequencer: './test.sequencer.js', diff --git a/api/package.json b/api/package.json index 289d72dba..4b60f7fad 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "@devtable/api", - "version": "8.61.2", + "version": "8.62.2", "description": "", "main": "index.js", "scripts": { diff --git a/api/src/api_models/dashboard.ts b/api/src/api_models/dashboard.ts index 4782f8505..0fc734821 100644 --- a/api/src/api_models/dashboard.ts +++ b/api/src/api_models/dashboard.ts @@ -1,5 +1,5 @@ -import { ApiModel, ApiModelProperty, SwaggerDefinitionConstant } from 'swagger-express-ts'; -import { IsObject, Length, IsString, IsOptional, ValidateNested, IsUUID, IsBoolean, IsIn } from 'class-validator'; +import { ApiModel, ApiModelProperty } from 'swagger-express-ts'; +import { Length, IsString, IsOptional, ValidateNested, IsUUID, IsBoolean, IsIn, ValidateIf } from 'class-validator'; import { Type } from 'class-transformer'; import { Authentication, FilterObject, PaginationRequest, PaginationResponse, SortRequest } from './base'; import { PermissionResource } from './dashboard_permission'; @@ -11,50 +11,41 @@ import { PermissionResource } from './dashboard_permission'; export class Dashboard { @ApiModelProperty({ description: 'Dashboard ID in uuid format', - required: false, }) id: string; @ApiModelProperty({ description: 'Name of the dashboard', - required: true, }) name: string; @ApiModelProperty({ - description: 'content of the dashboard stored in json object format', - required: true, - type: SwaggerDefinitionConstant.JSON, + description: 'dashboard content ID in uuid format', }) - content: object | null; + content_id: string | null; @ApiModelProperty({ description: 'whether the dashboard is removed or not', - required: false, }) is_removed: boolean; @ApiModelProperty({ description: 'whether the dashboard is preset or not', - required: false, }) is_preset: boolean; @ApiModelProperty({ description: 'Dashboard group', - required: false, }) group: string; @ApiModelProperty({ description: 'Create time', - required: false, }) create_time: Date; @ApiModelProperty({ description: 'Time of last update', - required: false, }) update_time: Date; @@ -214,14 +205,6 @@ export class DashboardCreateRequest { }) name: string; - @IsObject() - @ApiModelProperty({ - description: 'content of the dashboard stored in json object format', - required: true, - type: SwaggerDefinitionConstant.JSON, - }) - content: Record; - @IsString() @ApiModelProperty({ description: 'Dashboard group', @@ -262,13 +245,13 @@ export class DashboardUpdateRequest { name?: string; @IsOptional() - @IsObject() + @IsUUID() + @ValidateIf((_object, value) => value !== null) @ApiModelProperty({ - description: 'content of the dashboard stored in json object format', + description: 'dashboard content ID in uuid format', required: false, - type: SwaggerDefinitionConstant.JSON, }) - content?: Record; + content_id?: string | null; @IsOptional() @IsBoolean() diff --git a/api/src/api_models/dashboard_content.ts b/api/src/api_models/dashboard_content.ts new file mode 100644 index 000000000..12f37ad94 --- /dev/null +++ b/api/src/api_models/dashboard_content.ts @@ -0,0 +1,259 @@ +import { ApiModel, ApiModelProperty, SwaggerDefinitionConstant } from 'swagger-express-ts'; +import { IsObject, Length, IsString, IsOptional, ValidateNested, IsUUID, IsIn } from 'class-validator'; +import { Type } from 'class-transformer'; +import { Authentication, FilterObject, PaginationRequest, PaginationResponse, SortRequest } from './base'; + +@ApiModel({ + description: 'Dashboard content entity', + name: 'DashboardContent', +}) +export class DashboardContent { + @ApiModelProperty({ + description: 'Dashboard content ID in uuid format', + }) + id: string; + + @ApiModelProperty({ + description: 'Dashboard ID in uuid format', + }) + dashboard_id: string; + + @ApiModelProperty({ + description: 'Name of the dashboard content', + }) + name: string; + + @ApiModelProperty({ + description: 'content of the dashboard stored in json object format', + type: SwaggerDefinitionConstant.JSON, + }) + content: object | null; + + @ApiModelProperty({ + description: 'Create time', + }) + create_time: Date; + + @ApiModelProperty({ + description: 'Time of last update', + }) + update_time: Date; +} + +@ApiModel({ + description: 'Dashboard content filter object', + name: 'DashboardContentFilterObject', +}) +export class DashboardContentFilterObject { + @IsOptional() + @Type(() => FilterObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Filter based on name', + required: false, + model: 'FilterObject', + }) + name?: FilterObject; +} + +@ApiModel({ + description: 'Dashboard content sort object', + name: 'DashboardContentSortObject', +}) +export class DashboardContentSortObject implements SortRequest { + @IsIn(['name', 'create_time', 'update_time']) + @ApiModelProperty({ + description: 'Field for sorting', + required: true, + enum: ['name', 'create_time', 'update_time'], + }) + field: 'name' | 'create_time' | 'update_time'; + + @IsIn(['ASC', 'DESC']) + @ApiModelProperty({ + description: 'Sort order', + required: true, + enum: ['ASC', 'DESC'], + }) + order: 'ASC' | 'DESC'; + + constructor(data: any) { + Object.assign(this, data); + } +} + +@ApiModel({ + description: 'Dashboard content list request object', + name: 'DashboardContentListRequest', +}) +export class DashboardContentListRequest { + @IsUUID() + @ApiModelProperty({ + description: 'Dashboard ID in uuid format', + required: true, + }) + dashboard_id: string; + + @IsOptional() + @Type(() => DashboardContentFilterObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Dashboard content filter object', + required: false, + model: 'DashboardContentFilterObject', + }) + filter?: DashboardContentFilterObject; + + @Type(() => DashboardContentSortObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Dashboard content sort object', + required: true, + model: 'DashboardContentSortObject', + }) + sort: DashboardContentSortObject[] = [new DashboardContentSortObject({ field: 'create_time', order: 'ASC' })]; + + @Type(() => PaginationRequest) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Pagination object', + required: true, + model: 'PaginationRequest', + }) + pagination: PaginationRequest = new PaginationRequest({ page: 1, pagesize: 20 }); + + @IsOptional() + @Type(() => Authentication) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'authentication object for use with app_id / app_secret', + required: false, + model: 'Authentication', + }) + authentication?: Authentication; +} + +@ApiModel({ + description: 'dashboard content pagination response object', + name: 'DashboardContentPaginationResponse', +}) +export class DashboardContentPaginationResponse implements PaginationResponse { + @ApiModelProperty({ + description: 'Total number results', + }) + total: number; + + @ApiModelProperty({ + description: 'Current offset of results', + }) + offset: number; + + @ApiModelProperty({ + description: 'Dashboard content list', + model: 'DashboardContent', + }) + data: DashboardContent[]; +} + +@ApiModel({ + description: 'dashboard content create request object', + name: 'DashboardContentCreateRequest', +}) +export class DashboardContentCreateRequest { + @IsUUID() + @ApiModelProperty({ + description: 'Dashboard ID in uuid format', + required: true, + }) + dashboard_id: string; + + @IsString() + @Length(1, 250) + @ApiModelProperty({ + description: 'Name of the dashboard content', + required: true, + }) + name: string; + + @IsObject() + @ApiModelProperty({ + description: 'content stored in json object format', + required: true, + type: SwaggerDefinitionConstant.JSON, + }) + content: Record; + + @IsOptional() + @Type(() => Authentication) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'authentication object for use with app_id / app_secret', + required: false, + model: 'Authentication', + }) + authentication?: Authentication; +} + +@ApiModel({ + description: 'dashboard content update request object', + name: 'DashboardContentUpdateRequest', +}) +export class DashboardContentUpdateRequest { + @IsUUID() + @ApiModelProperty({ + description: 'Dashboard content ID in uuid format', + required: true, + }) + id: string; + + @IsOptional() + @IsString() + @Length(1, 250) + @ApiModelProperty({ + description: 'Name of the dashboard content', + required: false, + }) + name?: string; + + @IsOptional() + @IsObject() + @ApiModelProperty({ + description: 'content of the dashboard stored in json object format', + required: false, + type: SwaggerDefinitionConstant.JSON, + }) + content?: Record; + + @IsOptional() + @Type(() => Authentication) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'authentication object for use with app_id / app_secret', + required: false, + model: 'Authentication', + }) + authentication?: Authentication; +} + +@ApiModel({ + description: 'Dashboard content ID request', + name: 'DashboardContentIDRequest', +}) +export class DashboardContentIDRequest { + @IsUUID() + @ApiModelProperty({ + description: 'Dashboard content ID in uuid format', + required: true, + }) + id: string; + + @IsOptional() + @Type(() => Authentication) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'authentication object for use with app_id / app_secret', + required: false, + model: 'Authentication', + }) + authentication?: Authentication; +} diff --git a/api/src/api_models/dashboard_content_changelog.ts b/api/src/api_models/dashboard_content_changelog.ts new file mode 100644 index 000000000..19dd0f6ad --- /dev/null +++ b/api/src/api_models/dashboard_content_changelog.ts @@ -0,0 +1,144 @@ +import { Type } from 'class-transformer'; +import { IsIn, IsOptional, ValidateNested } from 'class-validator'; +import { ApiModel, ApiModelProperty } from 'swagger-express-ts'; +import { Authentication, FilterObject, PaginationRequest, PaginationResponse, SortRequest } from './base'; + +@ApiModel({ + description: 'DashboardContentChangelog entity', + name: 'DashboardContentChangelog', +}) +export class DashboardContentChangelog { + @ApiModelProperty({ + description: 'changelog ID in uuid format', + required: false, + }) + id: string; + + @ApiModelProperty({ + description: 'ID of the related dashboard content in uuid format', + required: true, + }) + dashboard_content_id: string; + + @ApiModelProperty({ + description: 'git diff of the changes', + required: true, + }) + diff: string; + + @ApiModelProperty({ + description: 'Create time', + required: false, + }) + create_time: Date; +} + +@ApiModel({ + description: 'DashboardContentChangelog filter object', + name: 'DashboardContentChangelogFilterObject', +}) +export class DashboardContentChangelogFilterObject { + @IsOptional() + @Type(() => FilterObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Filter based on dashboard_content_id. isFuzzy is ignored and always filters on exact match', + required: false, + model: 'FilterObject', + }) + dashboard_content_id?: FilterObject; +} + +@ApiModel({ + description: 'DashboardContentChangelog sort object', + name: 'DashboardContentChangelogSortObject', +}) +export class DashboardContentChangelogSortObject implements SortRequest { + @IsIn(['dashboard_content_id', 'create_time']) + @ApiModelProperty({ + description: 'Field for sorting', + required: true, + enum: ['dashboard_content_id', 'create_time'], + }) + field: 'dashboard_content_id' | 'create_time'; + + @IsIn(['ASC', 'DESC']) + @ApiModelProperty({ + description: 'Sort order', + required: true, + enum: ['ASC', 'DESC'], + }) + order: 'ASC' | 'DESC'; + + constructor(data: any) { + Object.assign(this, data); + } +} + +@ApiModel({ + description: 'DashboardContentChangelog list request object', + name: 'DashboardContentChangelogListRequest', +}) +export class DashboardContentChangelogListRequest { + @IsOptional() + @Type(() => DashboardContentChangelogFilterObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'DashboardContentChangelog filter object', + required: false, + model: 'DashboardContentChangelogFilterObject', + }) + filter?: DashboardContentChangelogFilterObject; + + @Type(() => DashboardContentChangelogSortObject) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'DashboardContentChangelog sort object', + required: true, + model: 'DashboardContentChangelogSortObject', + }) + sort: DashboardContentChangelogSortObject[] = [ + new DashboardContentChangelogSortObject({ field: 'create_time', order: 'ASC' }), + ]; + + @Type(() => PaginationRequest) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'Pagination object', + required: true, + model: 'PaginationRequest', + }) + pagination: PaginationRequest = new PaginationRequest({ page: 1, pagesize: 20 }); + + @IsOptional() + @Type(() => Authentication) + @ValidateNested({ each: true }) + @ApiModelProperty({ + description: 'authentication object for use with app_id / app_secret', + required: false, + model: 'Authentication', + }) + authentication?: Authentication; +} + +@ApiModel({ + description: 'DashboardContentChangelog pagination response object', + name: 'DashboardContentChangelogPaginationResponse', +}) +export class DashboardContentChangelogPaginationResponse implements PaginationResponse { + @ApiModelProperty({ + description: 'Total number results', + }) + total: number; + + @ApiModelProperty({ + description: 'Current offset of results', + }) + offset: number; + + @ApiModelProperty({ + description: 'DashboardContentChangelogs', + model: 'DashboardContentChangelog', + }) + data: DashboardContentChangelog[]; +} diff --git a/api/src/api_models/index.ts b/api/src/api_models/index.ts index 43e1ba2a4..13668f713 100644 --- a/api/src/api_models/index.ts +++ b/api/src/api_models/index.ts @@ -65,6 +65,23 @@ import { DashboardPermissionUpdateRequest, PermissionResource, } from './dashboard_permission'; +import { + DashboardContent, + DashboardContentCreateRequest, + DashboardContentListRequest, + DashboardContentFilterObject, + DashboardContentPaginationResponse, + DashboardContentSortObject, + DashboardContentIDRequest, + DashboardContentUpdateRequest, +} from './dashboard_content'; +import { + DashboardContentChangelog, + DashboardContentChangelogFilterObject, + DashboardContentChangelogListRequest, + DashboardContentChangelogPaginationResponse, + DashboardContentChangelogSortObject, +} from './dashboard_content_changelog'; import { ApiError, Authentication, FilterObject } from './base'; export default { @@ -144,4 +161,19 @@ export default { DashboardOwnerUpdateRequest, DashboardPermissionUpdateRequest, PermissionResource, + + DashboardContent, + DashboardContentCreateRequest, + DashboardContentListRequest, + DashboardContentFilterObject, + DashboardContentPaginationResponse, + DashboardContentSortObject, + DashboardContentIDRequest, + DashboardContentUpdateRequest, + + DashboardContentChangelog, + DashboardContentChangelogFilterObject, + DashboardContentChangelogListRequest, + DashboardContentChangelogPaginationResponse, + DashboardContentChangelogSortObject, }; diff --git a/api/src/api_models/job.ts b/api/src/api_models/job.ts index a17968ed3..e47f1efab 100644 --- a/api/src/api_models/job.ts +++ b/api/src/api_models/job.ts @@ -176,13 +176,13 @@ export class JobPaginationResponse implements PaginationResponse { name: 'JobRunRequest', }) export class JobRunRequest { - @IsIn(['RENAME_DATASOURCE']) + @IsIn(['RENAME_DATASOURCE', 'FIX_DASHBOARD_PERMISSION']) @ApiModelProperty({ description: 'Type of job', required: true, - enum: ['RENAME_DATASOURCE'], + enum: ['RENAME_DATASOURCE', 'FIX_DASHBOARD_PERMISSION'], }) - type: 'RENAME_DATASOURCE'; + type: 'RENAME_DATASOURCE' | 'FIX_DASHBOARD_PERMISSION'; @IsOptional() @Type(() => Authentication) diff --git a/api/src/controller/account.controller.ts b/api/src/controller/account.controller.ts index 12f811562..675d3388b 100644 --- a/api/src/controller/account.controller.ts +++ b/api/src/controller/account.controller.ts @@ -55,10 +55,10 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/login', ensureAuthEnabled) + @httpPost('/login', ensureAuthEnabled, validate(AccountLoginRequest)) public async login(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { name, password } = validate(AccountLoginRequest, req.body); + const { name, password } = req.body as AccountLoginRequest; const result = await this.accountService.login(name, password, req.locale); res.json(result); } catch (err) { @@ -81,10 +81,10 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/list', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR)) + @httpPost('/list', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR), validate(AccountListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(AccountListRequest, req.body); + const { filter, sort, pagination } = req.body as AccountListRequest; const result = await this.accountService.list(filter, sort, pagination); res.json(result); } catch (err) { @@ -122,11 +122,17 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/create', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.ADMIN)) + @httpPost( + '/create', + ensureAuthEnabled, + ensureAuthIsAccount, + permission(ROLE_TYPES.ADMIN), + validate(AccountCreateRequest), + ) public async create(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const account: Account = req.body.auth; - const { name, email, password, role_id } = validate(AccountCreateRequest, req.body); + const { name, email, password, role_id } = req.body as AccountCreateRequest; if (account.role_id <= role_id) { throw new ApiError(UNAUTHORIZED, { message: translate('ACCOUNT_NO_ADD_SIMILAR_OR_HIGHER_PRIVILEGES', req.locale), @@ -150,11 +156,17 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPut('/update', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.READER)) + @httpPut( + '/update', + ensureAuthEnabled, + ensureAuthIsAccount, + permission(ROLE_TYPES.READER), + validate(AccountUpdateRequest), + ) public async update(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const account: Account = req.body.auth; - const { name, email } = validate(AccountUpdateRequest, req.body); + const { name, email } = req.body as AccountUpdateRequest; const result = await this.accountService.update(account.id, name, email, req.locale); res.json(result); } catch (err) { @@ -173,11 +185,11 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPut('/edit', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.ADMIN)) + @httpPut('/edit', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.ADMIN), validate(AccountEditRequest)) public async edit(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const account: Account = req.body.auth; - const { id, name, email, role_id, reset_password, new_password } = validate(AccountEditRequest, req.body); + const { id, name, email, role_id, reset_password, new_password } = req.body as AccountEditRequest; if (id === account.id) { throw new ApiError(BAD_REQUEST, { message: translate('ACCOUNT_NO_EDIT_SELF', req.locale) }); } @@ -208,11 +220,17 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/changepassword', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.READER)) + @httpPost( + '/changepassword', + ensureAuthEnabled, + ensureAuthIsAccount, + permission(ROLE_TYPES.READER), + validate(AccountChangePasswordRequest), + ) public async changePassword(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const account: Account = req.body.auth; - const { old_password, new_password } = validate(AccountChangePasswordRequest, req.body); + const { old_password, new_password } = req.body as AccountChangePasswordRequest; if (new_password.length < 8) { throw new ApiError(BAD_REQUEST, { message: translate('ACCOUNT_PWD_LENGTH_SHOULD_BE_GRATER_THAN_8', req.locale), @@ -236,11 +254,11 @@ export class AccountController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/delete', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.ADMIN)) + @httpPost('/delete', ensureAuthEnabled, ensureAuthIsAccount, permission(ROLE_TYPES.ADMIN), validate(AccountIDRequest)) public async delete(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const account: Account = req.body.auth; - const { id } = validate(AccountIDRequest, req.body); + const { id } = req.body as AccountIDRequest; if (id === account.id) { throw new ApiError(BAD_REQUEST, { message: translate('ACCOUNT_NO_DELETE_SELF', req.locale) }); } diff --git a/api/src/controller/api.controller.ts b/api/src/controller/api.controller.ts index 3c392d158..bf779a62b 100644 --- a/api/src/controller/api.controller.ts +++ b/api/src/controller/api.controller.ts @@ -37,10 +37,10 @@ export class APIController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/key/list', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN)) + @httpPost('/key/list', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN), validate(ApiKeyListRequest)) public async listKeys(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(ApiKeyListRequest, req.body); + const { filter, sort, pagination } = req.body as ApiKeyListRequest; const result = await this.apiService.listKeys(filter, sort, pagination); res.json(result); } catch (err) { @@ -59,10 +59,10 @@ export class APIController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/key/create', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN)) + @httpPost('/key/create', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN), validate(ApiKeyCreateRequest)) public async createKey(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { name, role_id } = validate(ApiKeyCreateRequest, req.body); + const { name, role_id } = req.body as ApiKeyCreateRequest; const result = await this.apiService.createKey(name, role_id, req.locale); res.json(result); } catch (err) { @@ -81,10 +81,10 @@ export class APIController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/key/delete', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN)) + @httpPost('/key/delete', ensureAuthEnabled, permission(ROLE_TYPES.ADMIN), validate(ApiKeyIDRequest)) public async deleteKey(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id } = validate(ApiKeyIDRequest, req.body); + const { id } = req.body as ApiKeyIDRequest; await this.apiService.deleteKey(id, req.locale); res.json({ id }); } catch (err) { diff --git a/api/src/controller/config.controller.ts b/api/src/controller/config.controller.ts index 45b573eae..1002bae52 100644 --- a/api/src/controller/config.controller.ts +++ b/api/src/controller/config.controller.ts @@ -30,10 +30,10 @@ export class ConfigController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/get') + @httpPost('/get', validate(ConfigGetRequest)) public async get(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { key } = validate(ConfigGetRequest, req.body); + const { key } = req.body as ConfigGetRequest; const result = await this.configService.get(key, req.body.auth); res.json(result); } catch (err) { @@ -52,10 +52,10 @@ export class ConfigController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/update') + @httpPost('/update', validate(ConfigUpdateRequest)) public async update(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { key, value } = validate(ConfigUpdateRequest, req.body); + const { key, value } = req.body as ConfigUpdateRequest; const result = await this.configService.update(key, value, req.body.auth, req.locale); res.json(result); } catch (err) { diff --git a/api/src/controller/dashboard.controller.ts b/api/src/controller/dashboard.controller.ts index 38e1a9150..262d856a4 100644 --- a/api/src/controller/dashboard.controller.ts +++ b/api/src/controller/dashboard.controller.ts @@ -46,10 +46,10 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/list', permission(ROLE_TYPES.READER)) + @httpPost('/list', permission(ROLE_TYPES.READER), validate(DashboardListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(DashboardListRequest, req.body); + const { filter, sort, pagination } = req.body as DashboardListRequest; const result = await this.dashboardService.list(filter, sort, pagination, req.body.auth); res.json(result); } catch (err) { @@ -68,11 +68,11 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/create', permission(ROLE_TYPES.AUTHOR)) + @httpPost('/create', permission(ROLE_TYPES.AUTHOR), validate(DashboardCreateRequest)) public async create(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { name, content, group } = validate(DashboardCreateRequest, req.body); - const result = await this.dashboardService.create(name, content, group, req.locale, req.body.auth); + const { name, group } = req.body as DashboardCreateRequest; + const result = await this.dashboardService.create(name, group, req.locale, req.body.auth); res.json(result); } catch (err) { next(err); @@ -91,10 +91,10 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/details', permission(ROLE_TYPES.READER)) + @httpPost('/details', permission(ROLE_TYPES.READER), validate(DashboardIDRequest)) public async details(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id } = validate(DashboardIDRequest, req.body); + const { id } = req.body as DashboardIDRequest; await DashboardPermissionService.checkPermission( id, 'VIEW', @@ -122,10 +122,10 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/detailsByName', permission(ROLE_TYPES.READER)) + @httpPost('/detailsByName', permission(ROLE_TYPES.READER), validate(DashboardNameRequest)) public async detailsByName(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { name, is_preset } = validate(DashboardNameRequest, req.body); + const { name, is_preset } = req.body as DashboardNameRequest; const result = await this.dashboardService.getByName(name, is_preset); await DashboardPermissionService.checkPermission( result.id, @@ -153,11 +153,11 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPut('/update', permission(ROLE_TYPES.AUTHOR)) + @httpPut('/update', permission(ROLE_TYPES.AUTHOR), validate(DashboardUpdateRequest)) public async update(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const auth: Account | ApiKey | null = req.body.auth; - const { id, name, content, is_removed, group } = validate(DashboardUpdateRequest, req.body); + const { id, name, content_id, is_removed, group } = req.body as DashboardUpdateRequest; await DashboardPermissionService.checkPermission( id, 'EDIT', @@ -169,7 +169,7 @@ export class DashboardController implements interfaces.Controller { const result = await this.dashboardService.update( id, name, - content, + content_id, is_removed, group, req.locale, @@ -199,11 +199,11 @@ export class DashboardController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/delete', permission(ROLE_TYPES.AUTHOR)) + @httpPost('/delete', permission(ROLE_TYPES.AUTHOR), validate(DashboardIDRequest)) public async delete(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const auth: Account | ApiKey | null = req.body.auth; - const { id } = validate(DashboardIDRequest, req.body); + const { id } = req.body as DashboardIDRequest; await DashboardPermissionService.checkPermission( id, 'EDIT', diff --git a/api/src/controller/dashboard_changelog.controller.ts b/api/src/controller/dashboard_changelog.controller.ts index f7c868b2a..1627a397a 100644 --- a/api/src/controller/dashboard_changelog.controller.ts +++ b/api/src/controller/dashboard_changelog.controller.ts @@ -1,6 +1,6 @@ import * as express from 'express'; import { inject, interfaces as inverfaces } from 'inversify'; -import { controller, httpPost, httpPut, interfaces } from 'inversify-express-utils'; +import { controller, httpPost, interfaces } from 'inversify-express-utils'; import { ApiOperationPost, ApiPath, SwaggerDefinitionConstant } from 'swagger-express-ts'; import { DashboardChangelogListRequest } from '../api_models/dashboard_changelog'; import { ROLE_TYPES } from '../api_models/role'; @@ -39,10 +39,10 @@ export class DashboardChangelogController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/list', permission(ROLE_TYPES.READER)) + @httpPost('/list', permission(ROLE_TYPES.READER), validate(DashboardChangelogListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(DashboardChangelogListRequest, req.body); + const { filter, sort, pagination } = req.body as DashboardChangelogListRequest; const result = await this.dashboardChangelogService.list(filter, sort, pagination); res.json(result); } catch (err) { diff --git a/api/src/controller/dashboard_content.controller.ts b/api/src/controller/dashboard_content.controller.ts new file mode 100644 index 000000000..14e05fe06 --- /dev/null +++ b/api/src/controller/dashboard_content.controller.ts @@ -0,0 +1,193 @@ +import * as express from 'express'; +import { inject, interfaces as inverfaces } from 'inversify'; +import { controller, httpPost, httpPut, interfaces } from 'inversify-express-utils'; +import { ApiOperationPost, ApiOperationPut, ApiPath, SwaggerDefinitionConstant } from 'swagger-express-ts'; +import { validate } from '../middleware/validation'; +import { ROLE_TYPES } from '../api_models/role'; +import permission from '../middleware/permission'; +import ApiKey from '../models/apiKey'; +import Account from '../models/account'; +import { DashboardPermissionService } from '../services/dashboard_permission.service'; +import { channelBuilder, SERVER_CHANNELS, socketEmit } from '../utils/websocket'; +import { + DashboardContentCreateRequest, + DashboardContentIDRequest, + DashboardContentListRequest, + DashboardContentUpdateRequest, +} from '../api_models/dashboard_content'; +import { DashboardContentService } from '../services/dashboard_content.service'; + +@ApiPath({ + path: '/dashboard_content', + name: 'DashboardContent', +}) +@controller('/dashboard_content') +export class DashboardContentController implements interfaces.Controller { + public static TARGET_NAME = 'DashboardContent'; + private dashboardContentService: DashboardContentService; + + public constructor( + @inject('Newable') DashboardContentService: inverfaces.Newable, + ) { + this.dashboardContentService = new DashboardContentService(); + } + + @ApiOperationPost({ + path: '/list', + description: 'List saved dashboard content', + parameters: { + body: { description: 'dashboard content list request', required: true, model: 'DashboardContentListRequest' }, + }, + responses: { + 200: { + description: 'SUCCESS', + type: SwaggerDefinitionConstant.Response.Type.OBJECT, + model: 'DashboardContentPaginationResponse', + }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPost('/list', permission(ROLE_TYPES.READER), validate(DashboardContentListRequest)) + public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const { dashboard_id, filter, sort, pagination } = req.body as DashboardContentListRequest; + await DashboardPermissionService.checkPermission( + dashboard_id, + 'VIEW', + req.locale, + req.body.auth?.id, + req.body.auth ? (req.body.auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT') : undefined, + req.body.auth?.role_id, + ); + const result = await this.dashboardContentService.list(dashboard_id, filter, sort, pagination); + res.json(result); + } catch (err) { + next(err); + } + } + + @ApiOperationPost({ + path: '/create', + description: 'Create a new dashboard content', + parameters: { + body: { description: 'new dashboard content request', required: true, model: 'DashboardContentCreateRequest' }, + }, + responses: { + 200: { description: 'SUCCESS', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'DashboardContent' }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPost('/create', permission(ROLE_TYPES.AUTHOR), validate(DashboardContentCreateRequest)) + public async create(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const { dashboard_id, name, content } = req.body as DashboardContentCreateRequest; + await DashboardPermissionService.checkPermission( + dashboard_id, + 'EDIT', + req.locale, + req.body.auth?.id, + req.body.auth ? (req.body.auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT') : undefined, + req.body.auth?.role_id, + ); + const result = await this.dashboardContentService.create(dashboard_id, name, content, req.locale); + res.json(result); + } catch (err) { + next(err); + } + } + + @ApiOperationPost({ + path: '/details', + description: 'Show dashboard content', + parameters: { + body: { description: 'get dashboard content request', required: true, model: 'DashboardContentIDRequest' }, + }, + responses: { + 200: { description: 'SUCCESS', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'DashboardContent' }, + 404: { description: 'NOT FOUND', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPost('/details', permission(ROLE_TYPES.READER), validate(DashboardContentIDRequest)) + public async details(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const { id } = req.body as DashboardContentIDRequest; + const result = await this.dashboardContentService.get(id); + await DashboardPermissionService.checkPermission( + result.dashboard_id, + 'VIEW', + req.locale, + req.body.auth?.id, + req.body.auth ? (req.body.auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT') : undefined, + req.body.auth?.role_id, + ); + res.json(result); + } catch (err) { + next(err); + } + } + + @ApiOperationPut({ + path: '/update', + description: 'Update dashboard content', + parameters: { + body: { description: 'update dashboard content request', required: true, model: 'DashboardContentUpdateRequest' }, + }, + responses: { + 200: { description: 'SUCCESS', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'DashboardContent' }, + 404: { description: 'NOT FOUND', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPut('/update', permission(ROLE_TYPES.AUTHOR), validate(DashboardContentUpdateRequest)) + public async update(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const auth: Account | ApiKey | null = req.body.auth; + const { id, name, content } = req.body as DashboardContentUpdateRequest; + const result = await this.dashboardContentService.update(id, name, content, req.locale, auth); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT, [id]), { + update_time: result.update_time, + message: 'UPDATED', + auth_id: auth?.id ?? null, + auth_type: !auth ? null : auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT', + }); + res.json(result); + } catch (err) { + next(err); + } + } + + @ApiOperationPost({ + path: '/delete', + description: 'Remove dashboard content', + parameters: { + body: { description: 'delete dashboard content request', required: true, model: 'DashboardContentIDRequest' }, + }, + responses: { + 200: { + description: 'SUCCESS', + type: SwaggerDefinitionConstant.Response.Type.OBJECT, + model: 'DashboardContentIDRequest', + }, + 404: { description: 'NOT FOUND', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPost('/delete', permission(ROLE_TYPES.AUTHOR), validate(DashboardContentIDRequest)) + public async delete(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const auth: Account | ApiKey | null = req.body.auth; + const { id } = req.body as DashboardContentIDRequest; + await this.dashboardContentService.delete(id, req.locale, auth); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT, [id]), { + update_time: new Date(), + message: 'DELETED', + auth_id: auth?.id ?? null, + auth_type: !auth ? null : auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT', + }); + res.json({ id }); + } catch (err) { + next(err); + } + } +} diff --git a/api/src/controller/dashboard_content_changelog.controller.ts b/api/src/controller/dashboard_content_changelog.controller.ts new file mode 100644 index 000000000..58f466da9 --- /dev/null +++ b/api/src/controller/dashboard_content_changelog.controller.ts @@ -0,0 +1,56 @@ +import * as express from 'express'; +import { inject, interfaces as inverfaces } from 'inversify'; +import { controller, httpPost, interfaces } from 'inversify-express-utils'; +import { ApiOperationPost, ApiPath, SwaggerDefinitionConstant } from 'swagger-express-ts'; +import { DashboardContentChangelogListRequest } from '../api_models/dashboard_content_changelog'; +import { ROLE_TYPES } from '../api_models/role'; +import permission from '../middleware/permission'; +import { validate } from '../middleware/validation'; +import { DashboardContentChangelogService } from '../services/dashboard_content_changelog.service'; + +@ApiPath({ + path: '/dashboard_content_changelog', + name: 'DashboardContentChangelog', +}) +@controller('/dashboard_content_changelog') +export class DashboardContentChangelogController implements interfaces.Controller { + public static TARGET_NAME = 'DashboardContentChangelog'; + private dashboardContentChangelogService: DashboardContentChangelogService; + + public constructor( + @inject('Newable') + DashboardContentChangelogService: inverfaces.Newable, + ) { + this.dashboardContentChangelogService = new DashboardContentChangelogService(); + } + + @ApiOperationPost({ + path: '/list', + description: 'List dashboard content changelogs', + parameters: { + body: { + description: 'dashboard content changelog list request', + required: true, + model: 'DashboardContentChangelogListRequest', + }, + }, + responses: { + 200: { + description: 'SUCCESS', + type: SwaggerDefinitionConstant.Response.Type.OBJECT, + model: 'DashboardContentChangelogPaginationResponse', + }, + 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, + }, + }) + @httpPost('/list', permission(ROLE_TYPES.READER), validate(DashboardContentChangelogListRequest)) + public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { + try { + const { filter, sort, pagination } = req.body as DashboardContentChangelogListRequest; + const result = await this.dashboardContentChangelogService.list(filter, sort, pagination); + res.json(result); + } catch (err) { + next(err); + } + } +} diff --git a/api/src/controller/dashboard_permission.controller.ts b/api/src/controller/dashboard_permission.controller.ts index efa664200..0c36e10e7 100644 --- a/api/src/controller/dashboard_permission.controller.ts +++ b/api/src/controller/dashboard_permission.controller.ts @@ -49,10 +49,10 @@ export class DashboardPermissionController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/list', ensureAuthEnabled, permission(ROLE_TYPES.READER)) + @httpPost('/list', ensureAuthEnabled, permission(ROLE_TYPES.READER), validate(DashboardPermissionListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(DashboardPermissionListRequest, req.body); + const { filter, sort, pagination } = req.body as DashboardPermissionListRequest; const result = await this.dashboardPermissionService.list(filter, sort, pagination); res.json(result); } catch (err) { @@ -79,10 +79,10 @@ export class DashboardPermissionController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/get', ensureAuthEnabled, permission(ROLE_TYPES.READER)) + @httpPost('/get', ensureAuthEnabled, permission(ROLE_TYPES.READER), validate(DashboardPermissionGetRequest)) public async get(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id } = validate(DashboardPermissionGetRequest, req.body); + const { id } = req.body as DashboardPermissionGetRequest; const result = await this.dashboardPermissionService.get(id); res.json(result); } catch (err) { @@ -109,10 +109,10 @@ export class DashboardPermissionController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/updateOwner', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR)) + @httpPost('/updateOwner', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR), validate(DashboardOwnerUpdateRequest)) public async updateOwner(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id, owner_id, owner_type } = validate(DashboardOwnerUpdateRequest, req.body); + const { id, owner_id, owner_type } = req.body as DashboardOwnerUpdateRequest; const result = await this.dashboardPermissionService.updateOwner( id, owner_id, @@ -145,10 +145,10 @@ export class DashboardPermissionController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/update', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR)) + @httpPost('/update', ensureAuthEnabled, permission(ROLE_TYPES.AUTHOR), validate(DashboardPermissionUpdateRequest)) public async update(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id, access } = validate(DashboardPermissionUpdateRequest, req.body); + const { id, access } = req.body as DashboardPermissionUpdateRequest; const result = await this.dashboardPermissionService.update(id, access, req.body.auth, req.locale); res.json(result); } catch (err) { diff --git a/api/src/controller/datasource.controller.ts b/api/src/controller/datasource.controller.ts index daee56b40..eef096805 100644 --- a/api/src/controller/datasource.controller.ts +++ b/api/src/controller/datasource.controller.ts @@ -47,10 +47,10 @@ export class DataSourceController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/list', permission(ROLE_TYPES.READER)) + @httpPost('/list', permission(ROLE_TYPES.READER), validate(DataSourceListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(DataSourceListRequest, req.body); + const { filter, sort, pagination } = req.body as DataSourceListRequest; const result = await this.dataSourceService.list(filter, sort, pagination); res.json(result); } catch (err) { @@ -69,12 +69,11 @@ export class DataSourceController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/create', permission(ROLE_TYPES.ADMIN)) + @httpPost('/create', permission(ROLE_TYPES.ADMIN), validate(DataSourceCreateRequest)) public async create(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - // eslint-disable-next-line prefer-const - let { type, key, config } = validate(DataSourceCreateRequest, req.body); - config = this.validateConfig(type, config, req.locale); + const { type, key, config } = req.body as DataSourceCreateRequest; + this.validateConfig(type, config, req.locale); const result = await this.dataSourceService.create(type, key, config, req.locale); res.json(result); } catch (err) { @@ -93,11 +92,11 @@ export class DataSourceController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPut('/rename', permission(ROLE_TYPES.ADMIN)) + @httpPut('/rename', permission(ROLE_TYPES.ADMIN), validate(DataSourceRenameRequest)) public async rename(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { const auth: Account | ApiKey | null = req.body.auth; - const { id, key } = validate(DataSourceRenameRequest, req.body); + const { id, key } = req.body as DataSourceRenameRequest; const result = await this.dataSourceService.rename( id, key, @@ -122,10 +121,10 @@ export class DataSourceController implements interfaces.Controller { 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, }, }) - @httpPost('/delete', permission(ROLE_TYPES.ADMIN)) + @httpPost('/delete', permission(ROLE_TYPES.ADMIN), validate(DataSourceIDRequest)) public async delete(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { id } = validate(DataSourceIDRequest, req.body); + const { id } = req.body as DataSourceIDRequest; await this.dataSourceService.delete(id, req.locale); res.json(); } catch (err) { @@ -133,16 +132,12 @@ export class DataSourceController implements interfaces.Controller { } } - private validateConfig( - type: 'mysql' | 'postgresql' | 'http', - config: DataSourceConfig, - locale: string, - ): DataSourceConfig { + private validateConfig(type: 'mysql' | 'postgresql' | 'http', config: DataSourceConfig, locale: string): void { switch (type) { case 'http': if (!_.has(config, 'processing') || !_.has(config, 'processing.pre') || !_.has(config, 'processing.post')) throw new ApiError(BAD_REQUEST, { message: translate('DATASOURCE_HTTP_REQUIRED_FIELDS', locale) }); - return config; + break; default: if ( @@ -152,7 +147,7 @@ export class DataSourceController implements interfaces.Controller { !_.has(config, 'database') ) throw new ApiError(BAD_REQUEST, { message: translate('DATASOURCE_DB_REQUIRED_FIELDS', locale) }); - return config; + break; } } } diff --git a/api/src/controller/index.ts b/api/src/controller/index.ts index 1e0ff4747..24552822d 100644 --- a/api/src/controller/index.ts +++ b/api/src/controller/index.ts @@ -10,6 +10,8 @@ import { JobController } from './job.controller'; import { DashboardChangelogController } from './dashboard_changelog.controller'; import { ConfigController } from './config.controller'; import { DashboardPermissionController } from './dashboard_permission.controller'; +import { DashboardContentController } from './dashboard_content.controller'; +import { DashboardContentChangelogController } from './dashboard_content_changelog.controller'; export function bindControllers(container: Container) { container @@ -62,4 +64,14 @@ export function bindControllers(container: Container) { .to(DashboardPermissionController) .inSingletonScope() .whenTargetNamed(DashboardPermissionController.TARGET_NAME); + container + .bind(TYPE.Controller) + .to(DashboardContentController) + .inSingletonScope() + .whenTargetNamed(DashboardContentController.TARGET_NAME); + container + .bind(TYPE.Controller) + .to(DashboardContentChangelogController) + .inSingletonScope() + .whenTargetNamed(DashboardContentChangelogController.TARGET_NAME); } diff --git a/api/src/controller/job.controller.ts b/api/src/controller/job.controller.ts index 945f5b820..caa937aac 100644 --- a/api/src/controller/job.controller.ts +++ b/api/src/controller/job.controller.ts @@ -10,16 +10,14 @@ import { JobService } from '../services/job.service'; @ApiPath({ path: '/job', - name: 'Job' + name: 'Job', }) @controller('/job') export class JobController implements interfaces.Controller { public static TARGET_NAME = 'Job'; private jobService: JobService; - public constructor( - @inject('Newable') JobService: inversaces.Newable - ) { + public constructor(@inject('Newable') JobService: inversaces.Newable) { this.jobService = new JobService(); } @@ -27,17 +25,21 @@ export class JobController implements interfaces.Controller { path: '/list', description: 'List of jobs', parameters: { - body: { description: 'job list request', required: true, model: 'JobListRequest' } + body: { description: 'job list request', required: true, model: 'JobListRequest' }, }, responses: { - 200: { description: 'SUCCESS', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'JobPaginationResponse' }, + 200: { + description: 'SUCCESS', + type: SwaggerDefinitionConstant.Response.Type.OBJECT, + model: 'JobPaginationResponse', + }, 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, - } + }, }) - @httpPost('/list', permission(ROLE_TYPES.ADMIN)) + @httpPost('/list', permission(ROLE_TYPES.ADMIN), validate(JobListRequest)) public async list(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { filter, sort, pagination } = validate(JobListRequest, req.body); + const { filter, sort, pagination } = req.body as JobListRequest; const result = await this.jobService.list(filter, sort, pagination); res.json(result); } catch (err) { @@ -49,19 +51,20 @@ export class JobController implements interfaces.Controller { path: '/run', description: 'Run jobs', parameters: { - body: { description: 'job run request', required: true, model: 'JobRunRequest' } + body: { description: 'job run request', required: true, model: 'JobRunRequest' }, }, responses: { 200: { description: 'SUCCESS' }, 500: { description: 'SERVER ERROR', type: SwaggerDefinitionConstant.Response.Type.OBJECT, model: 'ApiError' }, - } + }, }) - @httpPost('/run', permission(ROLE_TYPES.ADMIN)) + @httpPost('/run', permission(ROLE_TYPES.ADMIN), validate(JobRunRequest)) public async run(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { type } = validate(JobRunRequest, req.body); + const { type } = req.body as JobRunRequest; const functionMapper = { - RENAME_DATASOURCE: JobService.processDataSourceRename + RENAME_DATASOURCE: JobService.processDataSourceRename, + FIX_DASHBOARD_PERMISSION: JobService.processFixDashboardPermission, }; functionMapper[type](); res.json(); @@ -69,4 +72,4 @@ export class JobController implements interfaces.Controller { next(err); } } -} \ No newline at end of file +} diff --git a/api/src/controller/query.controller.ts b/api/src/controller/query.controller.ts index 283921bef..9341f488b 100644 --- a/api/src/controller/query.controller.ts +++ b/api/src/controller/query.controller.ts @@ -32,10 +32,10 @@ export class QueryController implements interfaces.Controller { 500: { description: 'ApiError', model: 'ApiError' }, }, }) - @httpPost('/', permission(ROLE_TYPES.READER)) + @httpPost('/', permission(ROLE_TYPES.READER), validate(QueryRequest)) public async query(req: express.Request, res: express.Response, next: express.NextFunction): Promise { try { - const { type, key, query } = validate(QueryRequest, req.body); + const { type, key, query } = req.body as QueryRequest; const result = await this.queryService.query(type, key, query); res.json(result); } catch (error) { diff --git a/api/src/dashboard_migration/index.ts b/api/src/dashboard_migration/index.ts index 9f3b4979f..50015b61f 100644 --- a/api/src/dashboard_migration/index.ts +++ b/api/src/dashboard_migration/index.ts @@ -1,10 +1,10 @@ import { dashboardDataSource } from '../data_sources/dashboard'; -import Dashboard from '../models/dashboard'; import logger from 'npmlog'; -import { DashboardChangelogService } from '../services/dashboard_changelog.service'; import _ from 'lodash'; -import DashboardChangelog from '../models/dashboard_changelog'; import { Repository } from 'typeorm'; +import DashboardContent from '../models/dashboard_content'; +import DashboardContentChangelog from '../models/dashboard_content_changelog'; +import { DashboardContentChangelogService } from '../services/dashboard_content_changelog.service'; // NOTE: Keep versions in order const versions = [ @@ -46,60 +46,65 @@ async function findHandler(currentVersion: string | undefined) { return import(`./handlers/${nextVersion}`); } -async function migrateOneDashboard( - dashboard: Dashboard, - dashboardChangelogRepo: Repository, - dashboardRepo: Repository, +async function migrateOneDashboardContent( + dashboardContent: DashboardContent, + dashboardContentChangelogRepo: Repository, + dashboardContentRepo: Repository, ) { try { - const version = dashboard.content.version as string; + const version = dashboardContent.content.version as string; if (version && !versions.includes(version)) { - throw new Error(`MIGRATION FAILED, dashboard [${dashboard.name}]'s version [${version}] is not migratable`); + throw new Error( + `MIGRATION FAILED, dashboard content [${dashboardContent.name}]'s version [${version}] is not migratable`, + ); } let handler = await findHandler(version); while (handler) { - const originalDashboard = _.cloneDeep(dashboard); - dashboard.content = handler.main(dashboard.content); - const updatedDashboard = await dashboardRepo.save(dashboard); - const diff = await DashboardChangelogService.createChangelog(originalDashboard, _.cloneDeep(updatedDashboard)); + const originalDashboardContent = _.cloneDeep(dashboardContent); + dashboardContent.content = handler.main(dashboardContent.content); + const updatedDashboardContent = await dashboardContentRepo.save(dashboardContent); + const diff = await DashboardContentChangelogService.createChangelog( + originalDashboardContent, + _.cloneDeep(updatedDashboardContent), + ); if (diff) { - const changelog = new DashboardChangelog(); - changelog.dashboard_id = dashboard.id; + const changelog = new DashboardContentChangelog(); + changelog.dashboard_content_id = dashboardContent.id; changelog.diff = diff; - await dashboardChangelogRepo.save(changelog); + await dashboardContentChangelogRepo.save(changelog); } - logger.info(`MIGRATED ${dashboard.id} TO VERSION ${dashboard.content.version}`); - handler = await findHandler(dashboard.content.version as string); + logger.info(`MIGRATED ${dashboardContent.id} TO VERSION ${dashboardContent.content.version}`); + handler = await findHandler(dashboardContent.content.version as string); } } catch (error) { /** * NOTE(LETO): happens when dashboard's content is not migratable * Skip to provide a chance to fix it */ - logger.error(`error migrating dashboard. ID: ${dashboard.id} name: ${dashboard.name}`); + logger.error(`error migrating dashboard content. ID: ${dashboardContent.id} name: ${dashboardContent.name}`); logger.error(error.message); } } async function main() { - logger.info('STARTING MIGRATION OF DASHBOARDS'); + logger.info('STARTING MIGRATION OF DASHBOARD CONTENTS'); try { if (!dashboardDataSource.isInitialized) { await dashboardDataSource.initialize(); } - const dashboardChangelogRepo = dashboardDataSource.getRepository(DashboardChangelog); - const dashboardRepo = dashboardDataSource.getRepository(Dashboard); - const dashboards = await dashboardRepo.find(); + const dashboardContentChangelogRepo = dashboardDataSource.getRepository(DashboardContentChangelog); + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); + const dashboardContents = await dashboardContentRepo.find(); - for (let i = 0; i < dashboards.length; i += 1) { - migrateOneDashboard(dashboards[i], dashboardChangelogRepo, dashboardRepo); + for (let i = 0; i < dashboardContents.length; i += 1) { + migrateOneDashboardContent(dashboardContents[i], dashboardContentChangelogRepo, dashboardContentRepo); } } catch (error) { - logger.error('error migrating dashboards'); + logger.error('error migrating dashboard contents'); logger.error(error.message); process.exit(1); } - logger.info('MIGRATION OF DASHBOARDS FINISHED'); + logger.info('MIGRATION OF DASHBOARD CONTENTS FINISHED'); } main(); diff --git a/api/src/data_sources/migrations/1680511303370-create-dashboard_content-table.ts b/api/src/data_sources/migrations/1680511303370-create-dashboard_content-table.ts new file mode 100644 index 000000000..6a4cdf907 --- /dev/null +++ b/api/src/data_sources/migrations/1680511303370-create-dashboard_content-table.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createDashboardContentTable1680511303370 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE dashboard_content ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + dashboard_id uuid NOT NULL, + name VARCHAR NOT NULL, + content jsonb NOT NULL DEFAULT '{}' ::jsonb, + create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_dashboard + FOREIGN KEY (dashboard_id) + REFERENCES dashboard(id) + ON DELETE CASCADE + ) + `); + + await queryRunner.query( + `CREATE UNIQUE INDEX dashboard_content_dashboard_id_name ON dashboard_content (dashboard_id, name)`, + ); + + await queryRunner.query( + `CREATE TRIGGER on_update_dashboard_content BEFORE UPDATE ON dashboard_content + FOR EACH ROW EXECUTE PROCEDURE trigger_set_update_time()`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TRIGGER on_update_dashboard_content ON dashboard_content`); + + await queryRunner.query(`DROP INDEX dashboard_content_dashboard_id_name`); + + await queryRunner.query(`DROP TABLE dashboard_content`); + } +} diff --git a/api/src/data_sources/migrations/1680595338529-add-dashboard-content_id-field.ts b/api/src/data_sources/migrations/1680595338529-add-dashboard-content_id-field.ts new file mode 100644 index 000000000..f4c0b472d --- /dev/null +++ b/api/src/data_sources/migrations/1680595338529-add-dashboard-content_id-field.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addDashboardContentIdField1680595338529 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE dashboard + ADD COLUMN content_id uuid + `); + + await queryRunner.query(` + ALTER TABLE dashboard + ADD CONSTRAINT fk_dashboard_content FOREIGN KEY (content_id) REFERENCES dashboard_content(id) ON DELETE SET NULL + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE dashboard + DROP CONSTRAINT fk_dashboard_content + `); + + await queryRunner.query(` + ALTER TABLE dashboard + DROP COLUMN content_id + `); + } +} diff --git a/api/src/data_sources/migrations/1680595965759-migrate-dashboard-content-to-dashboard_content.ts b/api/src/data_sources/migrations/1680595965759-migrate-dashboard-content-to-dashboard_content.ts new file mode 100644 index 000000000..a9cf28981 --- /dev/null +++ b/api/src/data_sources/migrations/1680595965759-migrate-dashboard-content-to-dashboard_content.ts @@ -0,0 +1,42 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class migrateDashboardContentToDashboardContent1680595965759 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO dashboard_content (dashboard_id, name, content) + SELECT id, name, content + FROM dashboard + `); + + await queryRunner.query(` + UPDATE dashboard + SET content_id = dashboard_content.id + FROM dashboard_content + WHERE dashboard.id = dashboard_content.dashboard_id + `); + + await queryRunner.query(` + ALTER TABLE dashboard + DROP COLUMN content + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE dashboard + ADD COLUMN content jsonb NOT NULL DEFAULT '{}' ::jsonb + `); + + await queryRunner.query(` + UPDATE dashboard + SET content = dashboard_content.content + FROM dashboard_content + WHERE dashboard.content_id IS NOT NULL + AND dashboard.content_id = dashboard_content.id + `); + + await queryRunner.query(` + DELETE FROM dashboard_content + `); + } +} diff --git a/api/src/data_sources/migrations/1681089840575-create-dashboard_content_changelog-table.ts b/api/src/data_sources/migrations/1681089840575-create-dashboard_content_changelog-table.ts new file mode 100644 index 000000000..768e88c0c --- /dev/null +++ b/api/src/data_sources/migrations/1681089840575-create-dashboard_content_changelog-table.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class createDashboardContentChangelogTable1681089840575 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE dashboard_content_changelog + ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + dashboard_content_id uuid NOT NULL, + diff text, + create_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT fk_dashboard_content + FOREIGN KEY (dashboard_content_id) + REFERENCES dashboard_content(id) + ON DELETE CASCADE + )`, + ); + + await queryRunner.query( + `CREATE TRIGGER on_update_dashboard_content_changelog BEFORE UPDATE ON dashboard_content_changelog + FOR EACH ROW EXECUTE PROCEDURE trigger_set_update_time()`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TRIGGER on_update_dashboard_content_changelog ON dashboard_content_changelog`); + + await queryRunner.query(`DROP TABLE dashboard_content_changelog`); + } +} diff --git a/api/src/locales/en.json b/api/src/locales/en.json index c2a8dfca8..8423e3af2 100644 --- a/api/src/locales/en.json +++ b/api/src/locales/en.json @@ -33,5 +33,9 @@ "DASHBOARD_PERMISSION_FORBIDDEN": "Insufficient privileges for this dashboard", "DASHBOARD_OWNER_INSUFFICIENT_PRIVILEGES": "Resource has insufficient privileges to take dashboard ownership", "DASHBOARD_PERMISSION_MISSING_APIKEY": "Several apikeys were not found", - "DASHBOARD_PERMISSION_MISSING_ACCOUNT": "Several accounts were not found" + "DASHBOARD_PERMISSION_MISSING_ACCOUNT": "Several accounts were not found", + "DASHBOARD_CONTENT_NAME_ALREADY_EXISTS": "A dashboard content with that name already exists", + "DASHBOARD_CONTENT_EDIT_REQUIRES_SUPERADMIN": "Only superadmin can edit preset dashboard contents", + "DASHBOARD_CONTENT_DELETE_REQUIRES_SUPERADMIN": "Only superadmin can delete preset dashboard contents", + "DASHBOARD_CONTENT_DOES_NOT_EXIST": "That dashboard content does not exist" } diff --git a/api/src/locales/zh.json b/api/src/locales/zh.json index b7f234e7a..7b86c9383 100644 --- a/api/src/locales/zh.json +++ b/api/src/locales/zh.json @@ -33,5 +33,9 @@ "DASHBOARD_PERMISSION_FORBIDDEN": "报表的权限不足", "DASHBOARD_OWNER_INSUFFICIENT_PRIVILEGES": "资源没有足够的权限获得报表所有权", "DASHBOARD_PERMISSION_MISSING_APIKEY": "有一些apikey找不到", - "DASHBOARD_PERMISSION_MISSING_ACCOUNT": "有一些账户找不到" + "DASHBOARD_PERMISSION_MISSING_ACCOUNT": "有一些账户找不到", + "DASHBOARD_CONTENT_NAME_ALREADY_EXISTS": "具有该名称的报表内容已存在", + "DASHBOARD_CONTENT_EDIT_REQUIRES_SUPERADMIN": "只有超级管理员才能编辑预设报表内容", + "DASHBOARD_CONTENT_DELETE_REQUIRES_SUPERADMIN": "只有超级管理员才能删除预设报表内容", + "DASHBOARD_CONTENT_DOES_NOT_EXIST": "目标报表内容不存在" } diff --git a/api/src/middleware/authorization.ts b/api/src/middleware/authorization.ts index 4b732e9e3..0a2522f56 100644 --- a/api/src/middleware/authorization.ts +++ b/api/src/middleware/authorization.ts @@ -2,7 +2,7 @@ import * as express from 'express'; import { Authentication } from '../api_models/base'; import { AccountService } from '../services/account.service'; import { ApiService } from '../services/api.service'; -import { validate } from './validation'; +import { validateClass } from './validation'; export default async function authorization(req: express.Request, res: express.Response, next: express.NextFunction) { // Bearer @@ -16,7 +16,7 @@ export default async function authorization(req: express.Request, res: express.R const { authentication, ...rest } = req.body; try { if (authentication !== undefined) { - const keyData = validate(Authentication, authentication); + const keyData = validateClass(Authentication, authentication); const apiKey = await ApiService.verifyApiKey(keyData, rest); req.body.auth = apiKey; } diff --git a/api/src/middleware/validation.ts b/api/src/middleware/validation.ts index 0ff184a15..263525396 100644 --- a/api/src/middleware/validation.ts +++ b/api/src/middleware/validation.ts @@ -1,10 +1,11 @@ +import * as express from 'express'; import { Validator } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { ApiError, VALIDATION_FAILED } from '../utils/errors'; type Constructor = { new (): T }; -export function validate(type: Constructor, body: any): T { +export function validateClass(type: Constructor, body: any): T { const validator = new Validator(); const input = plainToClass(type, body); const errors = validator.validateSync(input); @@ -13,3 +14,10 @@ export function validate(type: Constructor, body: any): T { } return input; } + +export function validate(type: Constructor) { + return (req: express.Request, res: express.Response, next: express.NextFunction) => { + req.body = validateClass(type, req.body); + next(); + }; +} diff --git a/api/src/models/dashboard.ts b/api/src/models/dashboard.ts index ba9a2b8b3..1f73ddf56 100644 --- a/api/src/models/dashboard.ts +++ b/api/src/models/dashboard.ts @@ -10,8 +10,13 @@ export default class Dashboard extends BaseModel { }) name: string; - @Column('jsonb', { name: 'content' }) - content: Record; + @Column('uuid', { + nullable: true, + default: null, + name: 'content_id', + comment: '报表内容ID', + }) + content_id: string | null; @Column('boolean', { default: false, diff --git a/api/src/models/dashboard_content.ts b/api/src/models/dashboard_content.ts new file mode 100644 index 000000000..395ba3160 --- /dev/null +++ b/api/src/models/dashboard_content.ts @@ -0,0 +1,24 @@ +import { Entity, Column } from 'typeorm'; +import { BaseModel } from './base'; + +@Entity() +export default class DashboardContent extends BaseModel { + @Column('uuid', { + nullable: false, + name: 'dashboard_id', + comment: '报表ID', + }) + dashboard_id: string; + + @Column('character varying', { + nullable: false, + name: 'name', + }) + name: string; + + @Column('jsonb', { + nullable: false, + name: 'content', + }) + content: Record; +} diff --git a/api/src/models/dashboard_content_changelog.ts b/api/src/models/dashboard_content_changelog.ts new file mode 100644 index 000000000..45a1d8f31 --- /dev/null +++ b/api/src/models/dashboard_content_changelog.ts @@ -0,0 +1,14 @@ +import { Entity, Column } from 'typeorm'; +import { BaseModel } from './base'; + +@Entity() +export default class DashboardContentChangelog extends BaseModel { + @Column('uuid', { + name: 'dashboard_content_id', + comment: '报表内容ID', + }) + dashboard_content_id: string; + + @Column('text', { name: 'diff' }) + diff: string; +} diff --git a/api/src/preset/scripts/configure_dashboards.ts b/api/src/preset/scripts/configure_dashboards.ts index be638ac4d..5979d7f14 100644 --- a/api/src/preset/scripts/configure_dashboards.ts +++ b/api/src/preset/scripts/configure_dashboards.ts @@ -9,6 +9,9 @@ import DashboardChangelog from '../../models/dashboard_changelog'; import Account from '../../models/account'; import { ROLE_TYPES } from '../../api_models/role'; import DashboardPermission from '../../models/dashboard_permission'; +import DashboardContentChangelog from '../../models/dashboard_content_changelog'; +import DashboardContent from '../../models/dashboard_content'; +import { DashboardContentChangelogService } from '../../services/dashboard_content_changelog.service'; type Source = { type: string; @@ -29,6 +32,8 @@ async function upsert() { .findOneByOrFail({ role_id: ROLE_TYPES.SUPERADMIN }); const dashboardChangelogRepo = queryRunner.manager.getRepository(DashboardChangelog); const dashboardRepo = queryRunner.manager.getRepository(Dashboard); + const dashboardContentChangelogRepo = queryRunner.manager.getRepository(DashboardContentChangelog); + const dashboardContentRepo = queryRunner.manager.getRepository(DashboardContent); const dashboardPermissionRepo = queryRunner.manager.getRepository(DashboardPermission); const datasourceRepo = queryRunner.manager.getRepository(DataSource); @@ -79,15 +84,25 @@ async function upsert() { dashboard.is_preset = true; isNew = true; } - dashboard.content = config; - const createdDashboard = await dashboardRepo.save(dashboard); + dashboard = await dashboardRepo.save(dashboard); if (isNew) { const dashboardPermission = new DashboardPermission(); - dashboardPermission.id = createdDashboard.id; + dashboardPermission.id = dashboard.id; dashboardPermission.owner_id = superadmin.id; dashboardPermission.owner_type = 'ACCOUNT'; await dashboardPermissionRepo.save(dashboardPermission); } + let dashboardContent = await dashboardContentRepo.findOneBy({ dashboard_id: dashboard.id }); + const originalDashboardContent: DashboardContent | null = _.cloneDeep(dashboardContent); + if (!dashboardContent) { + dashboardContent = new DashboardContent(); + dashboardContent.name = name; + dashboardContent.dashboard_id = dashboard.id; + } + dashboardContent.content = config; + dashboardContent = await dashboardContentRepo.save(dashboardContent); + dashboard.content_id = dashboardContent.id; + dashboard = await dashboardRepo.save(dashboard); if (originalDashboard) { const diff = await DashboardChangelogService.createChangelog(originalDashboard, _.cloneDeep(dashboard)); if (diff) { @@ -97,6 +112,18 @@ async function upsert() { await dashboardChangelogRepo.save(changelog); } } + if (originalDashboardContent) { + const diff = await DashboardContentChangelogService.createChangelog( + originalDashboardContent, + _.cloneDeep(dashboardContent), + ); + if (diff) { + const changelog = new DashboardContentChangelog(); + changelog.dashboard_content_id = originalDashboardContent.id; + changelog.diff = diff; + await dashboardContentChangelogRepo.save(changelog); + } + } } if (!_.isEmpty(errors)) { throw { message: `Missing preset datasources: ${JSON.stringify(errors)}` }; diff --git a/api/src/preset/scripts/configure_datasources.ts b/api/src/preset/scripts/configure_datasources.ts index 1578d8f00..db9e4bfa2 100644 --- a/api/src/preset/scripts/configure_datasources.ts +++ b/api/src/preset/scripts/configure_datasources.ts @@ -6,7 +6,7 @@ import { dashboardDataSource } from '../../data_sources/dashboard'; import DataSource from '../../models/datasource'; import { configureDatabaseSource } from '../../utils/helpers'; import { maybeEncryptPassword } from '../../utils/encryption'; -import { validate } from '../../middleware/validation'; +import { validateClass } from '../../middleware/validation'; class DatabaseSource { @IsString() @@ -59,14 +59,14 @@ async function upsert() { await queryRunner.startTransaction(); try { // eslint-disable-next-line @typescript-eslint/no-var-requires - const data: PresetDatasources = validate(PresetDatasources, require('../data_sources/config.json')); + const data: PresetDatasources = validateClass(PresetDatasources, require('../data_sources/config.json')); const datasourceRepo = queryRunner.manager.getRepository(DataSource); await datasourceRepo.delete({ is_preset: true }); for (const source of data.postgresql) { - const { key } = validate(DatabaseSource, source); - const config = validate(DatabaseConfig, source.config); + const { key } = validateClass(DatabaseSource, source); + const config = validateClass(DatabaseConfig, source.config); await testDatabaseConfiguration(key, 'postgresql', config); maybeEncryptPassword(config); let dataSource: DataSource | null; @@ -82,8 +82,8 @@ async function upsert() { } for (const source of data.mysql) { - const { key } = validate(DatabaseSource, source); - const config = validate(DatabaseConfig, source.config); + const { key } = validateClass(DatabaseSource, source); + const config = validateClass(DatabaseConfig, source.config); await testDatabaseConfiguration(key, 'mysql', config); maybeEncryptPassword(config); let dataSource: DataSource | null; @@ -99,8 +99,8 @@ async function upsert() { } for (const source of data.http) { - const { key } = validate(DatabaseSource, source); - const config = validate(BaseConfig, source.config); + const { key } = validateClass(DatabaseSource, source); + const config = validateClass(BaseConfig, source.config); let dataSource: DataSource | null; dataSource = await datasourceRepo.findOneBy({ type: 'http', key }); if (!dataSource) { diff --git a/api/src/services/dashboard.service.ts b/api/src/services/dashboard.service.ts index 905b81ab0..3eb762508 100644 --- a/api/src/services/dashboard.service.ts +++ b/api/src/services/dashboard.service.ts @@ -15,6 +15,7 @@ import Account from '../models/account'; import ApiKey from '../models/apiKey'; import DashboardPermission from '../models/dashboard_permission'; import { PermissionResource } from '../api_models/dashboard_permission'; +import DashboardContent from '../models/dashboard_content'; export class DashboardService { async list( @@ -77,20 +78,13 @@ export class DashboardService { }; } - async create( - name: string, - content: Record, - group: string, - locale: string, - auth?: Account | ApiKey, - ): Promise { + async create(name: string, group: string, locale: string, auth?: Account | ApiKey): Promise { const dashboardRepo = dashboardDataSource.getRepository(Dashboard); if (await dashboardRepo.exist({ where: { name, is_preset: false, is_removed: false } })) { throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_NAME_ALREADY_EXISTS', locale) }); } const dashboard = new Dashboard(); dashboard.name = name; - dashboard.content = content; dashboard.group = group; const result = await dashboardRepo.save(dashboard); @@ -115,15 +109,16 @@ export class DashboardService { async update( id: string, name: string | undefined, - content: Record | undefined, + content_id: string | null | undefined, is_removed: boolean | undefined, group: string | undefined, locale: string, role_id?: ROLE_TYPES, ): Promise { const dashboardRepo = dashboardDataSource.getRepository(Dashboard); + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); const dashboard = await dashboardRepo.findOneByOrFail({ id }); - if (name === undefined && content === undefined && is_removed === undefined && group === undefined) { + if (name === undefined && content_id === undefined && is_removed === undefined && group === undefined) { return dashboard; } if (name !== undefined && dashboard.name !== name) { @@ -131,12 +126,17 @@ export class DashboardService { throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_NAME_ALREADY_EXISTS', locale) }); } } + if (content_id !== undefined && content_id !== null) { + if (!(await dashboardContentRepo.exist({ where: { id: content_id, dashboard_id: id } }))) { + throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_CONTENT_DOES_NOT_EXIST', locale) }); + } + } const originalDashboard = _.cloneDeep(dashboard); if (AUTH_ENABLED && dashboard.is_preset && (!role_id || role_id < ROLE_TYPES.SUPERADMIN)) { throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_EDIT_REQUIRES_SUPERADMIN', locale) }); } dashboard.name = name === undefined ? dashboard.name : name; - dashboard.content = content === undefined ? dashboard.content : content; + dashboard.content_id = content_id === undefined ? dashboard.content_id : content_id; dashboard.is_removed = is_removed === undefined ? dashboard.is_removed : is_removed; dashboard.group = group === undefined ? dashboard.group : group; await dashboardRepo.save(dashboard); diff --git a/api/src/services/dashboard_changelog.service.ts b/api/src/services/dashboard_changelog.service.ts index bdb51dced..f5148bddc 100644 --- a/api/src/services/dashboard_changelog.service.ts +++ b/api/src/services/dashboard_changelog.service.ts @@ -1,38 +1,19 @@ import { dashboardDataSource } from '../data_sources/dashboard'; import Dashboard from '../models/dashboard'; import DashboardChangelog from '../models/dashboard_changelog'; -import fs from 'fs-extra'; -import path from 'path'; -import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git'; import { DashboardChangelogFilterObject, DashboardChangelogPaginationResponse, DashboardChangelogSortObject, } from '../api_models/dashboard_changelog'; import { PaginationRequest } from '../api_models/base'; +import { getDiff, omitTime } from '../utils/helpers'; export class DashboardChangelogService { static async createChangelog(oldDashboard: Dashboard, newDashboard: Dashboard): Promise { - const time = new Date().getTime(); - const dir = path.join(__dirname, `${time}_${oldDashboard.id}`); - await fs.ensureDir(dir); - - const options: Partial = { - baseDir: dir, - binary: 'git', - }; - const git: SimpleGit = simpleGit(options); - await git.init(); - await git.addConfig('user.name', 'Devtable'); - await git.addConfig('user.email', 'Devtable@merico.dev'); - const filename = path.join(dir, 'data.json'); - await fs.writeJson(filename, oldDashboard, { spaces: '\t' }); - await git.add(filename); - await git.commit('First'); - await fs.writeJson(filename, newDashboard, { spaces: '\t' }); - const diff = await git.diff(); - await fs.rm(dir, { recursive: true, force: true }); - return diff; + const oldData = omitTime(oldDashboard); + const newData = omitTime(newDashboard); + return await getDiff(oldData, newData); } async list( diff --git a/api/src/services/dashboard_content.service.ts b/api/src/services/dashboard_content.service.ts new file mode 100644 index 000000000..a37f7df72 --- /dev/null +++ b/api/src/services/dashboard_content.service.ts @@ -0,0 +1,151 @@ +import _ from 'lodash'; +import { PaginationRequest } from '../api_models/base'; +import { + DashboardContentFilterObject, + DashboardContentPaginationResponse, + DashboardContentSortObject, +} from '../api_models/dashboard_content'; +import { ROLE_TYPES } from '../api_models/role'; +import { dashboardDataSource } from '../data_sources/dashboard'; +import Dashboard from '../models/dashboard'; +import { AUTH_ENABLED } from '../utils/constants'; +import { ApiError, BAD_REQUEST } from '../utils/errors'; +import { escapeLikePattern } from '../utils/helpers'; +import { translate } from '../utils/i18n'; +import { DashboardPermissionService } from './dashboard_permission.service'; +import Account from '../models/account'; +import ApiKey from '../models/apiKey'; +import DashboardContent from '../models/dashboard_content'; +import { DashboardContentChangelogService } from './dashboard_content_changelog.service'; +import DashboardContentChangelog from '../models/dashboard_content_changelog'; + +export class DashboardContentService { + async list( + dashboard_id: string, + filter: DashboardContentFilterObject | undefined, + sort: DashboardContentSortObject[], + pagination: PaginationRequest, + ): Promise { + const offset = pagination.pagesize * (pagination.page - 1); + const qb = dashboardDataSource.manager + .createQueryBuilder() + .from(DashboardContent, 'dashboard_content') + .where('dashboard_content.dashboard_id = :dashboard_id', { dashboard_id }) + .orderBy(sort[0].field, sort[0].order) + .offset(offset) + .limit(pagination.pagesize); + + if (filter !== undefined) { + if (filter.name) { + filter.name.isFuzzy + ? qb.andWhere('dashboard_content.name ilike :name', { name: `%${escapeLikePattern(filter.name.value)}%` }) + : qb.andWhere('dashboard_content.name = :name', { name: filter.name.value }); + } + } + + sort.slice(1).forEach((s) => { + qb.addOrderBy(s.field, s.order); + }); + + const dashboardContents = await qb.getRawMany(); + const total = await qb.getCount(); + + return { + total, + offset, + data: dashboardContents, + }; + } + + async create( + dashboard_id: string, + name: string, + content: Record, + locale: string, + ): Promise { + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); + if (await dashboardContentRepo.exist({ where: { dashboard_id, name } })) { + throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_CONTENT_NAME_ALREADY_EXISTS', locale) }); + } + const dashboardContent = new DashboardContent(); + dashboardContent.dashboard_id = dashboard_id; + dashboardContent.name = name; + dashboardContent.content = content; + const result = await dashboardContentRepo.save(dashboardContent); + return result; + } + + async get(id: string): Promise { + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); + return await dashboardContentRepo.findOneByOrFail({ id }); + } + + async update( + id: string, + name: string | undefined, + content: Record | undefined, + locale: string, + auth: Account | ApiKey | null, + ): Promise { + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); + const dashboardContent = await dashboardContentRepo.findOneByOrFail({ id }); + const dashboard = await dashboardDataSource + .getRepository(Dashboard) + .findOneByOrFail({ id: dashboardContent.dashboard_id }); + await DashboardPermissionService.checkPermission( + dashboard.id, + 'EDIT', + locale, + auth?.id, + auth ? (auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT') : undefined, + auth?.role_id, + ); + if (AUTH_ENABLED && dashboard.is_preset && (!auth?.role_id || auth.role_id < ROLE_TYPES.SUPERADMIN)) { + throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_CONTENT_EDIT_REQUIRES_SUPERADMIN', locale) }); + } + if (name === undefined && content === undefined) { + return dashboardContent; + } + if (name !== undefined && dashboardContent.name !== name) { + if (await dashboardContentRepo.exist({ where: { name, dashboard_id: dashboardContent.dashboard_id } })) { + throw new ApiError(BAD_REQUEST, { message: translate('DASHBOARD_CONTENT_NAME_ALREADY_EXISTS', locale) }); + } + } + const originalDashboardContent = _.cloneDeep(dashboardContent); + dashboardContent.name = name === undefined ? dashboardContent.name : name; + dashboardContent.content = content === undefined ? dashboardContent.content : content; + await dashboardContentRepo.save(dashboardContent); + const result = await dashboardContentRepo.findOneByOrFail({ id }); + const diff = await DashboardContentChangelogService.createChangelog(originalDashboardContent, _.cloneDeep(result)); + if (diff) { + const dashboardContentChangelogRepo = dashboardDataSource.getRepository(DashboardContentChangelog); + const changelog = new DashboardContentChangelog(); + changelog.dashboard_content_id = dashboardContent.id; + changelog.diff = diff; + await dashboardContentChangelogRepo.save(changelog); + } + return result; + } + + async delete(id: string, locale: string, auth: Account | ApiKey | null): Promise { + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); + const dashboardContent = await dashboardContentRepo.findOneByOrFail({ id }); + const dashboard = await dashboardDataSource + .getRepository(Dashboard) + .findOneByOrFail({ id: dashboardContent.dashboard_id }); + await DashboardPermissionService.checkPermission( + dashboard.id, + 'EDIT', + locale, + auth?.id, + auth ? (auth instanceof ApiKey ? 'APIKEY' : 'ACCOUNT') : undefined, + auth?.role_id, + ); + if (AUTH_ENABLED && dashboard.is_preset && (!auth?.role_id || auth.role_id < ROLE_TYPES.SUPERADMIN)) { + throw new ApiError(BAD_REQUEST, { + message: translate('DASHBOARD_CONTENT_DELETE_REQUIRES_SUPERADMIN', locale), + }); + } + await dashboardContentRepo.delete(dashboardContent.id); + } +} diff --git a/api/src/services/dashboard_content_changelog.service.ts b/api/src/services/dashboard_content_changelog.service.ts new file mode 100644 index 000000000..c32b53883 --- /dev/null +++ b/api/src/services/dashboard_content_changelog.service.ts @@ -0,0 +1,59 @@ +import { dashboardDataSource } from '../data_sources/dashboard'; +import DashboardContent from '../models/dashboard_content'; +import DashboardContentChangelog from '../models/dashboard_content_changelog'; +import { + DashboardContentChangelogFilterObject, + DashboardContentChangelogPaginationResponse, + DashboardContentChangelogSortObject, +} from '../api_models/dashboard_content_changelog'; +import { PaginationRequest } from '../api_models/base'; +import { getDiff, omitTime } from '../utils/helpers'; + +export class DashboardContentChangelogService { + static async createChangelog( + oldDashboardContent: DashboardContent, + newDashboardContent: DashboardContent, + ): Promise { + const oldData = omitTime(oldDashboardContent); + const newData = omitTime(newDashboardContent); + return await getDiff(oldData, newData); + } + + async list( + filter: DashboardContentChangelogFilterObject | undefined, + sort: DashboardContentChangelogSortObject[], + pagination: PaginationRequest, + ): Promise { + const offset = pagination.pagesize * (pagination.page - 1); + const qb = dashboardDataSource.manager + .createQueryBuilder() + .from(DashboardContentChangelog, 'dcc') + .select('id', 'id') + .addSelect('dashboard_content_id', 'dashboard_content_id') + .addSelect('diff', 'diff') + .addSelect('create_time', 'create_time') + .where('true') + .orderBy(sort[0].field, sort[0].order) + .offset(offset) + .limit(pagination.pagesize); + + if (filter?.dashboard_content_id) { + qb.andWhere('dashboard_content_id = :dashboard_content_id', { + dashboard_content_id: filter.dashboard_content_id.value, + }); + } + + sort.slice(1).forEach((s) => { + qb.addOrderBy(s.field, s.order); + }); + + const dashboardContentChangelogs = await qb.getRawMany(); + const total = await qb.getCount(); + + return { + total, + offset, + data: dashboardContentChangelogs, + }; + } +} diff --git a/api/src/services/index.ts b/api/src/services/index.ts index bfe5cfca4..46b86433f 100644 --- a/api/src/services/index.ts +++ b/api/src/services/index.ts @@ -9,6 +9,8 @@ import { JobService } from './job.service'; import { DashboardChangelogService } from './dashboard_changelog.service'; import { ConfigService } from './config.service'; import { DashboardPermissionService } from './dashboard_permission.service'; +import { DashboardContentService } from './dashboard_content.service'; +import { DashboardContentChangelogService } from './dashboard_content_changelog.service'; export function bindServices(container: Container) { container.bind>('Newable').toConstructor(JobService); @@ -33,4 +35,10 @@ export function bindServices(container: Container) { container .bind>('Newable') .toConstructor(DashboardPermissionService); + container + .bind>('Newable') + .toConstructor(DashboardContentService); + container + .bind>('Newable') + .toConstructor(DashboardContentChangelogService); } diff --git a/api/src/services/job.service.ts b/api/src/services/job.service.ts index 567e4f664..62fb24fd2 100644 --- a/api/src/services/job.service.ts +++ b/api/src/services/job.service.ts @@ -3,13 +3,14 @@ import { PaginationRequest } from '../api_models/base'; import { JobFilterObject, JobPaginationResponse, JobSortObject } from '../api_models/job'; import { dashboardDataSource } from '../data_sources/dashboard'; import Dashboard from '../models/dashboard'; -import DashboardChangelog from '../models/dashboard_changelog'; +import DashboardContent from '../models/dashboard_content'; +import DashboardContentChangelog from '../models/dashboard_content_changelog'; import DashboardPermission from '../models/dashboard_permission'; import DataSource from '../models/datasource'; import Job from '../models/job'; import { escapeLikePattern } from '../utils/helpers'; import { channelBuilder, SERVER_CHANNELS, socketEmit } from '../utils/websocket'; -import { DashboardChangelogService } from './dashboard_changelog.service'; +import { DashboardContentChangelogService } from './dashboard_content_changelog.service'; import { QueryService } from './query.service'; enum JobType { @@ -71,9 +72,9 @@ export class JobService { const runner = dashboardDataSource.createQueryRunner(); await runner.connect(); - const dashboardChangelogRepo = runner.manager.getRepository(DashboardChangelog); + const dashboardContentChangelogRepo = runner.manager.getRepository(DashboardContentChangelog); const datasourceRepo = runner.manager.getRepository(DataSource); - const dashboardRepo = runner.manager.getRepository(Dashboard); + const dashboardContentRepo = runner.manager.getRepository(Dashboard); const jobRepo = runner.manager.getRepository(Job); let jobs = await jobRepo @@ -95,49 +96,52 @@ export class JobService { datasource.key = params.new_key; await datasourceRepo.save(datasource); - const result: { affected_dashboards: { dashboardId: string; queries: string[] }[] } = { - affected_dashboards: [], + const result: { affected_dashboard_contents: { contentId: string; queries: string[] }[] } = { + affected_dashboard_contents: [], }; - const dashboards = await runner.manager + const dashboardContents = await runner.manager .createQueryBuilder() - .from(Dashboard, 'dashboard') + .from(DashboardContent, 'dashboard_content') .where(`content @> '{"definition":{"queries":[{"type": "${params.type}", "key": "${params.old_key}"}]}}' `) - .getRawMany(); + .getRawMany(); - const updatedDashboardIds: string[] = []; - for (const dashboard of dashboards) { + const updatedDashboardContentIds: string[] = []; + for (const dashboardContent of dashboardContents) { let updated = false; - const originalDashboard = _.cloneDeep(dashboard); + const originalDashboardContent = _.cloneDeep(dashboardContent); const queries: string[] = []; - for (let i = 0; i < dashboard.content.definition.queries.length; i++) { - const query = dashboard.content.definition.queries[i]; + for (let i = 0; i < dashboardContent.content.definition.queries.length; i++) { + const query = dashboardContent.content.definition.queries[i]; if (query.type !== params.type || query.key !== params.old_key) continue; query.key = params.new_key; queries.push(query.id); updated = true; } if (updated) { - await dashboardRepo.save(dashboard); - updatedDashboardIds.push(dashboard.id); - const diff = await DashboardChangelogService.createChangelog(originalDashboard, _.cloneDeep(dashboard)); + await dashboardContentRepo.save(dashboardContent); + updatedDashboardContentIds.push(dashboardContent.id); + const diff = await DashboardContentChangelogService.createChangelog( + originalDashboardContent, + _.cloneDeep(dashboardContent), + ); if (diff) { - const changelog = new DashboardChangelog(); - changelog.dashboard_id = dashboard.id; + const changelog = new DashboardContentChangelog(); + changelog.dashboard_content_id = dashboardContent.id; changelog.diff = diff; - await dashboardChangelogRepo.save(changelog); + await dashboardContentChangelogRepo.save(changelog); } } - result.affected_dashboards.push({ dashboardId: dashboard.id, queries }); + result.affected_dashboard_contents.push({ contentId: dashboardContent.id, queries }); } job.status = JobStatus.SUCCESS; job.result = result; await jobRepo.save(job); await runner.commitTransaction(); - updatedDashboardIds.forEach(async (id) => { - const dashboard = await dashboardRepo.findOneByOrFail({ id }); - socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD, [id]), { - update_time: dashboard.update_time, + updatedDashboardContentIds.forEach(async (id) => { + const data = await dashboardContentRepo.findOneByOrFail({ id }); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT, [id]), { + update_time: data.update_time, message: 'UPDATED', auth_id: params.auth_id, auth_type: params.auth_type, diff --git a/api/src/services/query.service.ts b/api/src/services/query.service.ts index 152f4d3dc..7e522af79 100644 --- a/api/src/services/query.service.ts +++ b/api/src/services/query.service.ts @@ -2,7 +2,7 @@ import { APIClient } from '../utils/api_client'; import { DataSourceService } from './datasource.service'; import { DataSource } from 'typeorm'; import { configureDatabaseSource } from '../utils/helpers'; -import { validate } from '../middleware/validation'; +import { validateClass } from '../middleware/validation'; import { HttpParams } from '../api_models/query'; export class QueryService { @@ -75,7 +75,7 @@ export class QueryService { } private async httpQuery(key: string, query: string): Promise { - const options = validate(HttpParams, JSON.parse(query)); + const options = validateClass(HttpParams, JSON.parse(query)); const sourceConfig = await DataSourceService.getByTypeKey('http', key); const { host } = sourceConfig.config; return await APIClient.request(host)(options); diff --git a/api/src/utils/helpers.ts b/api/src/utils/helpers.ts index e35da66e1..5a4ae6644 100644 --- a/api/src/utils/helpers.ts +++ b/api/src/utils/helpers.ts @@ -1,7 +1,12 @@ import { DataSourceOptions } from 'typeorm'; import { DataSourceConfig } from '../api_models/datasource'; import crypto from 'crypto'; +import fs from 'fs-extra'; +import path from 'path'; +import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git'; import { DATABASE_CONNECTION_TIMEOUT_MS, DATABASE_POOL_SIZE } from './constants'; +import logger from 'npmlog'; +import { omit } from 'lodash'; export function configureDatabaseSource(type: 'mysql' | 'postgresql', config: DataSourceConfig): DataSourceOptions { const commonConfig = { @@ -58,3 +63,35 @@ export const cryptSign = (params: { [propName: string]: any }, appsecret: string crypt.update(buffer); return crypt.digest('hex').toUpperCase(); }; + +export const getDiff = async (oldData: any, newData: any): Promise => { + const time = new Date().getTime(); + const dir = path.join(__dirname, `${time}_${oldData.id}`); + await fs.ensureDir(dir); + let diff: string | undefined; + try { + const options: Partial = { + baseDir: dir, + binary: 'git', + }; + const git: SimpleGit = simpleGit(options); + await git.init(); + await git.addConfig('user.name', 'Devtable'); + await git.addConfig('user.email', 'Devtable@merico.dev'); + const filename = path.join(dir, 'data.json'); + await fs.writeJson(filename, oldData, { spaces: '\t' }); + await git.add(filename); + await git.commit('First'); + await fs.writeJson(filename, newData, { spaces: '\t' }); + diff = await git.diff(); + } catch (e) { + logger.warn('get diff failed'); + logger.warn(e); + } + await fs.rm(dir, { recursive: true, force: true }); + return diff; +}; + +export const omitTime = (obj: any): any => { + return omit(obj, ['create_time', 'update_time']); +}; diff --git a/api/src/utils/i18n.ts b/api/src/utils/i18n.ts index 37942750b..6d350e1fa 100644 --- a/api/src/utils/i18n.ts +++ b/api/src/utils/i18n.ts @@ -50,6 +50,12 @@ type DASHBOARD_PERMISSION_KEYS = | 'DASHBOARD_PERMISSION_MISSING_APIKEY' | 'DASHBOARD_PERMISSION_MISSING_ACCOUNT'; +type DASHBOARD_CONTENT_KEYS = + | 'DASHBOARD_CONTENT_NAME_ALREADY_EXISTS' + | 'DASHBOARD_CONTENT_EDIT_REQUIRES_SUPERADMIN' + | 'DASHBOARD_CONTENT_DELETE_REQUIRES_SUPERADMIN' + | 'DASHBOARD_CONTENT_DOES_NOT_EXIST'; + type LANGUAGE_KEYS = | ACCOUNT_KEYS | DATASOURCE_KEYS @@ -58,7 +64,8 @@ type LANGUAGE_KEYS = | CONFIG_KEYS | DASHBOARD_KEYS | PERMISSION_KEYS - | DASHBOARD_PERMISSION_KEYS; + | DASHBOARD_PERMISSION_KEYS + | DASHBOARD_CONTENT_KEYS; export function translate(key: LANGUAGE_KEYS, locale: string): string { return i18n.__({ phrase: key, locale }); diff --git a/api/src/utils/websocket.ts b/api/src/utils/websocket.ts index 7ceb0f40a..c0ba28c76 100644 --- a/api/src/utils/websocket.ts +++ b/api/src/utils/websocket.ts @@ -10,14 +10,24 @@ import { translate } from './i18n'; export enum SERVER_CHANNELS { DASHBOARD = 'DASHBOARD', DASHBOARD_EDIT_PRESENCE = 'DASHBOARD_EDIT_PRESENCE', + DASHBOARD_CONTENT = 'DASHBOARD_CONTENT', + DASHBOARD_CONTENT_EDIT_PRESENCE = 'DASHBOARD_CONTENT_EDIT_PRESENCE', } enum CLIENT_CHANNELS { DASHBOARD_GET_EDIT_PRESENCE = 'DASHBOARD_GET_EDIT_PRESENCE', DASHBOARD_START_EDIT = 'DASHBOARD_START_EDIT', DASHBOARD_END_EDIT = 'DASHBOARD_END_EDIT', + DASHBOARD_CONTENT_GET_EDIT_PRESENCE = 'DASHBOARD_CONTENT_GET_EDIT_PRESENCE', + DASHBOARD_CONTENT_START_EDIT = 'DASHBOARD_CONTENT_START_EDIT', + DASHBOARD_CONTENT_END_EDIT = 'DASHBOARD_CONTENT_END_EDIT', } +type SOCKET_AUTH = { auth_id: string; auth_name: string; auth_type: 'NO_AUTH' | 'ACCOUNT' | 'APIKEY' }; +type PRESENCE = Map }>>; +const dashboardEditPresence: PRESENCE = new Map(); +const dashboardContentEditPresence: PRESENCE = new Map(); + let socket: Server; export function initWebsocket(server: http.Server, origin: string[]) { @@ -32,14 +42,18 @@ export function initWebsocket(server: http.Server, origin: string[]) { let authenticated = false; if (!AUTH_ENABLED) { authenticated = true; - socket.handshake.auth.auth = { id: '0', name: 'NO_AUTH', type: 'NO_AUTH' }; + socket.handshake.auth.auth = { auth_id: '0', auth_name: 'NO_AUTH', auth_type: 'NO_AUTH' } as SOCKET_AUTH; } if (!authenticated) { const account = await AccountService.getByToken(socket.handshake.auth.account); if (account) { authenticated = true; - socket.handshake.auth.auth = { id: account.id, name: account.name, type: 'ACCOUNT' }; + socket.handshake.auth.auth = { + auth_id: account.id, + auth_name: account.name, + auth_type: 'ACCOUNT', + } as SOCKET_AUTH; } } @@ -47,7 +61,7 @@ export function initWebsocket(server: http.Server, origin: string[]) { const apiKey = await ApiService.verifyApiKey(socket.handshake.auth.apikey, {}); if (apiKey) { authenticated = true; - socket.handshake.auth.auth = { id: apiKey.id, name: apiKey.name, type: 'APIKEY' }; + socket.handshake.auth.auth = { auth_id: apiKey.id, auth_name: apiKey.name, auth_type: 'APIKEY' } as SOCKET_AUTH; } } @@ -69,39 +83,79 @@ export function initWebsocket(server: http.Server, origin: string[]) { client.on('disconnect', () => { logger.info(`user disconnected from websocket with id: ${client.id}`); - removeDashboardEditPresence( - client.handshake.auth.auth.id, - client.handshake.auth.auth.type, - client.handshake.auth.auth.name, + const dashboardEditInfos = removePresence( + dashboardEditPresence, + client.handshake.auth.auth as SOCKET_AUTH, + client.id, + ); + const dashboardContentEditInfos = removePresence( + dashboardContentEditPresence, + client.handshake.auth.auth as SOCKET_AUTH, client.id, ); + for (const data of dashboardEditInfos) { + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [data.id]), parsePresence(data.info)); + } + for (const data of dashboardContentEditInfos) { + socketEmit( + channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT_EDIT_PRESENCE, [data.id]), + parsePresence(data.info), + ); + } }); client.on(CLIENT_CHANNELS.DASHBOARD_GET_EDIT_PRESENCE, (data: { id: string }) => { const result = dashboardEditPresence.get(data.id) ?? {}; - client.emit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [data.id]), parseDashboardPresence(result)); + client.emit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [data.id]), parsePresence(result)); }); client.on(CLIENT_CHANNELS.DASHBOARD_START_EDIT, (data: { id: string }) => { - updateDashboardEditPresence( + const { id, info } = updatePresence( + dashboardEditPresence, data.id, - client.handshake.auth.auth.id, - client.handshake.auth.auth.type, - client.handshake.auth.auth.name, + client.handshake.auth.auth as SOCKET_AUTH, client.id, 'ADD', ); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [id]), parsePresence(info)); }); client.on(CLIENT_CHANNELS.DASHBOARD_END_EDIT, (data: { id: string }) => { - updateDashboardEditPresence( + const { id, info } = updatePresence( + dashboardEditPresence, + data.id, + client.handshake.auth.auth as SOCKET_AUTH, + client.id, + 'REMOVE', + ); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [id]), parsePresence(info)); + }); + + client.on(CLIENT_CHANNELS.DASHBOARD_CONTENT_GET_EDIT_PRESENCE, (data: { id: string }) => { + const result = dashboardContentEditPresence.get(data.id) ?? {}; + client.emit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT_EDIT_PRESENCE, [data.id]), parsePresence(result)); + }); + + client.on(CLIENT_CHANNELS.DASHBOARD_CONTENT_START_EDIT, (data: { id: string }) => { + const { id, info } = updatePresence( + dashboardContentEditPresence, + data.id, + client.handshake.auth.auth as SOCKET_AUTH, + client.id, + 'ADD', + ); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT_EDIT_PRESENCE, [id]), parsePresence(info)); + }); + + client.on(CLIENT_CHANNELS.DASHBOARD_CONTENT_END_EDIT, (data: { id: string }) => { + const { id, info } = updatePresence( + dashboardContentEditPresence, data.id, - client.handshake.auth.auth.id, - client.handshake.auth.auth.type, - client.handshake.auth.auth.name, + client.handshake.auth.auth as SOCKET_AUTH, client.id, 'REMOVE', ); + socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_CONTENT_EDIT_PRESENCE, [id]), parsePresence(info)); }); }); } @@ -120,16 +174,22 @@ function getPresenceAuthKey(auth_id: string, auth_type: string) { return `${auth_id}:${auth_type}`; } -const dashboardEditPresence = new Map }>>(); -function updateDashboardEditPresence( +function parsePresence(presence: Record }>) { + const result: Record = {}; + for (const [key, value] of Object.entries(presence)) { + result[key] = { name: value.name, count: value.connections.size }; + } + return result; +} + +function updatePresence( + presence: PRESENCE, id: string, - auth_id: string, - auth_type: string, - auth_name: string, + { auth_id, auth_type, auth_name }: SOCKET_AUTH, client_id: string, type: 'ADD' | 'REMOVE', -) { - const info = dashboardEditPresence.get(id) ?? {}; +): { id: string; info: Record }> } { + const info = presence.get(id) ?? {}; const authKey = getPresenceAuthKey(auth_id, auth_type); if (type === 'ADD') { info[authKey] = info[authKey] ?? { name: auth_name, connections: new Set() }; @@ -142,18 +202,18 @@ function updateDashboardEditPresence( } } } - dashboardEditPresence.set(id, info); - socketEmit(channelBuilder(SERVER_CHANNELS.DASHBOARD_EDIT_PRESENCE, [id]), parseDashboardPresence(info)); + presence.set(id, info); + return { id, info }; } -function removeDashboardEditPresence(auth_id: string, auth_type: string, auth_name: string, client_id: string) { - for (const [id, _info] of dashboardEditPresence) { - updateDashboardEditPresence(id, auth_id, auth_type, auth_name, client_id, 'REMOVE'); - } -} -function parseDashboardPresence(presence: Record }>) { - const result: Record = {}; - for (const [key, value] of Object.entries(presence)) { - result[key] = { name: value.name, count: value.connections.size }; + +function removePresence( + presence: PRESENCE, + { auth_id, auth_type, auth_name }: SOCKET_AUTH, + client_id: string, +): { id: string; info: Record }> }[] { + const infos: { id: string; info: Record }> }[] = []; + for (const [id, _info] of presence) { + infos.push(updatePresence(presence, id, { auth_id, auth_type, auth_name }, client_id, 'REMOVE')); } - return result; + return infos; } diff --git a/api/tests/e2e/01_account.test.ts b/api/tests/e2e/01_account.test.ts index 5bd128bf8..e98aa7fb6 100644 --- a/api/tests/e2e/01_account.test.ts +++ b/api/tests/e2e/01_account.test.ts @@ -14,9 +14,9 @@ import { import { ROLE_TYPES } from '~/api_models/role'; import { AccountLoginResponse } from '~/api_models/account'; import { omit } from 'lodash'; -import * as validation from '~/middleware/validation'; import request from 'supertest'; import { app } from '~/server'; +import { omitTime } from '~/utils/helpers'; describe('AccountController', () => { connectionHook(); @@ -28,35 +28,25 @@ describe('AccountController', () => { let account2Login: AccountLoginResponse; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { superadmin = await dashboardDataSource.manager.findOne(Account, { where: { name: 'superadmin' } }); }); - beforeEach(() => { - validate.mockReset(); - }); - describe('login', () => { it('should be successfull', async () => { const query: AccountLoginRequest = { name: superadmin.name, password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); + response.body.account = omitTime(response.body.account); superadminLogin = response.body; - superadminLogin.account.create_time = new Date(superadminLogin.account.create_time); - superadminLogin.account.update_time = new Date(superadminLogin.account.update_time); expect(superadminLogin).toMatchObject({ token: superadminLogin.token, account: { id: superadmin.id, - create_time: superadmin.create_time, - update_time: superadmin.update_time, name: superadmin.name, email: superadmin.email, role_id: superadmin.role_id, @@ -69,7 +59,6 @@ describe('AccountController', () => { name: superadmin.name, password: 'incorrect password', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); @@ -88,22 +77,18 @@ describe('AccountController', () => { email: 'account1@test.com', role_id: ROLE_TYPES.ADMIN, }; - validate.mockReturnValueOnce(createQuery1); const createResponse1 = await server .post('/account/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(createQuery1); - createResponse1.body.create_time = new Date(createResponse1.body.create_time); - createResponse1.body.update_time = new Date(createResponse1.body.update_time); + createResponse1.body = omitTime(createResponse1.body); expect(createResponse1.body).toMatchObject({ name: 'account1', email: 'account1@test.com', role_id: ROLE_TYPES.ADMIN, id: createResponse1.body.id, - create_time: createResponse1.body.create_time, - update_time: createResponse1.body.update_time, }); account1 = createResponse1.body; @@ -111,19 +96,15 @@ describe('AccountController', () => { name: account1.name, password: 'account1', }; - validate.mockReturnValueOnce(loginQuery1); const loginResponse1 = await server.post('/account/login').send(loginQuery1); + loginResponse1.body.account = omitTime(loginResponse1.body.account); account1Login = loginResponse1.body; - account1Login.account.create_time = new Date(account1Login.account.create_time); - account1Login.account.update_time = new Date(account1Login.account.update_time); expect(account1Login).toMatchObject({ token: account1Login.token, account: { id: account1.id, - create_time: account1.create_time, - update_time: account1.update_time, name: account1.name, email: account1.email, role_id: account1.role_id, @@ -136,22 +117,18 @@ describe('AccountController', () => { email: 'account2@test.com', role_id: ROLE_TYPES.ADMIN, }; - validate.mockReturnValueOnce(createQuery2); const createReponse2 = await server .post('/account/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(createQuery2); - createReponse2.body.create_time = new Date(createReponse2.body.create_time); - createReponse2.body.update_time = new Date(createReponse2.body.update_time); + createReponse2.body = omitTime(createReponse2.body); expect(createReponse2.body).toMatchObject({ name: 'account2', email: 'account2@test.com', role_id: ROLE_TYPES.ADMIN, id: createReponse2.body.id, - create_time: createReponse2.body.create_time, - update_time: createReponse2.body.update_time, }); account2 = createReponse2.body; @@ -159,19 +136,15 @@ describe('AccountController', () => { name: account2.name, password: 'account2', }; - validate.mockReturnValueOnce(loginQuery2); const loginResponse2 = await server.post('/account/login').send(loginQuery2); + loginResponse2.body.account = omitTime(loginResponse2.body.account); account2Login = loginResponse2.body; - account2Login.account.create_time = new Date(account2Login.account.create_time); - account2Login.account.update_time = new Date(account2Login.account.update_time); expect(account2Login).toMatchObject({ token: account2Login.token, account: { id: account2.id, - create_time: account2.create_time, - update_time: account2.update_time, name: account2.name, email: account2.email, role_id: account2.role_id, @@ -186,7 +159,6 @@ describe('AccountController', () => { email: 'account1@test.com', role_id: ROLE_TYPES.ADMIN, }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/create') @@ -208,7 +180,6 @@ describe('AccountController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/list') @@ -247,7 +218,6 @@ describe('AccountController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/list') @@ -284,19 +254,15 @@ describe('AccountController', () => { role_id: ROLE_TYPES.AUTHOR, reset_password: false, }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ id: account1.id, - create_time: response.body.create_time, - update_time: response.body.update_time, name: 'account1_edited', email: 'account1_edited@test.com', role_id: ROLE_TYPES.AUTHOR, @@ -312,7 +278,6 @@ describe('AccountController', () => { role_id: ROLE_TYPES.ADMIN, reset_password: false, }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') @@ -335,7 +300,6 @@ describe('AccountController', () => { role_id: ROLE_TYPES.ADMIN, reset_password: false, }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') @@ -358,7 +322,6 @@ describe('AccountController', () => { role_id: ROLE_TYPES.ADMIN, reset_password: false, }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') @@ -381,7 +344,6 @@ describe('AccountController', () => { role_id: ROLE_TYPES.AUTHOR, reset_password: true, }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') @@ -403,19 +365,15 @@ describe('AccountController', () => { reset_password: true, new_password: 'account1_edited_password', }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/edit') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ id: account1.id, - create_time: response.body.create_time, - update_time: response.body.update_time, name: 'account1_edited_password', email: 'account1_edited_password@test.com', role_id: ROLE_TYPES.ADMIN, @@ -426,18 +384,14 @@ describe('AccountController', () => { name: 'account1_edited_password', password: 'account1_edited_password', }; - validate.mockReturnValueOnce(loginQuery); const loginResponse = await server.post('/account/login').send(loginQuery); - loginResponse.body.account.create_time = new Date(loginResponse.body.account.create_time); - loginResponse.body.account.update_time = new Date(loginResponse.body.account.update_time); + loginResponse.body.account = omitTime(loginResponse.body.account); expect(loginResponse.body).toMatchObject({ token: loginResponse.body.token, account: { id: account1.id, - create_time: account1.create_time, - update_time: loginResponse.body.account.update_time, name: account1.name, email: account1.email, role_id: account1.role_id, @@ -451,7 +405,6 @@ describe('AccountController', () => { const query: AccountIDRequest = { id: account1.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/delete') @@ -470,7 +423,6 @@ describe('AccountController', () => { const query: AccountIDRequest = { id: account1.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/delete') @@ -484,7 +436,6 @@ describe('AccountController', () => { const query: AccountIDRequest = { id: account1.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/delete') @@ -503,19 +454,15 @@ describe('AccountController', () => { name: 'account2_updated', email: 'account2_updated@test.com', }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/update') .set('Authorization', `Bearer ${account2Login.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ id: account2.id, - create_time: account2.create_time, - update_time: response.body.update_time, name: 'account2_updated', email: 'account2_updated@test.com', role_id: ROLE_TYPES.ADMIN, @@ -528,7 +475,6 @@ describe('AccountController', () => { name: 'account1_updated', email: 'account1_updated@test.com', }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/update') @@ -543,7 +489,6 @@ describe('AccountController', () => { name: 'superadmin_updated', email: 'superadmin_updated@test.com', }; - validate.mockReturnValueOnce(query); const response = await server .put('/account/update') @@ -560,19 +505,8 @@ describe('AccountController', () => { describe('get', () => { it('should return successfully', async () => { const response = await server.get('/account/get').set('Authorization', `Bearer ${superadminLogin.token}`).send(); - - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); - expect(response.body).toMatchObject( - omit( - { - ...superadmin, - create_time: superadmin.create_time, - update_time: superadmin.update_time, - }, - 'password', - ), - ); + response.body = omitTime(response.body); + expect(response.body).toMatchObject(omit(superadmin, ['password', 'create_time', 'update_time'])); }); it('should fail if not found', async () => { @@ -588,7 +522,6 @@ describe('AccountController', () => { old_password: 'account2_old', new_password: 'account2_new', }; - validate.mockReturnValueOnce(query); const response = await server .post('/account/changepassword') @@ -606,17 +539,14 @@ describe('AccountController', () => { name: account2.name, password: 'account2', }; - validate.mockReturnValueOnce(loginQuery1); const loginResponse1 = await server.post('/account/login').send(loginQuery1); - loginResponse1.body.account.create_time = new Date(loginResponse1.body.account.create_time); + loginResponse1.body.account = omitTime(loginResponse1.body.account); expect(loginResponse1.body).toMatchObject({ token: loginResponse1.body.token, account: { id: account2.id, - create_time: account2.create_time, - update_time: loginResponse1.body.account.update_time, name: account2.name, email: account2.email, role_id: account2.role_id, @@ -627,18 +557,15 @@ describe('AccountController', () => { old_password: 'account2', new_password: 'account2_new', }; - validate.mockReturnValueOnce(changeQuery); const changeResponse = await server .post('/account/changepassword') .set('Authorization', `Bearer ${account2Login.token}`) .send(changeQuery); - changeResponse.body.create_time = new Date(changeResponse.body.create_time); + changeResponse.body = omitTime(changeResponse.body); expect(changeResponse.body).toMatchObject({ id: account2.id, - create_time: account2.create_time, - update_time: changeResponse.body.update_time, name: account2.name, email: account2.email, role_id: account2.role_id, @@ -648,7 +575,6 @@ describe('AccountController', () => { name: account2.name, password: 'account2', }; - validate.mockReturnValueOnce(loginQuery2); const loginResponse2 = await server.post('/account/login').send(loginQuery2); @@ -661,17 +587,14 @@ describe('AccountController', () => { name: account2.name, password: 'account2_new', }; - validate.mockReturnValueOnce(loginQuery3); const loginResponse3 = await server.post('/account/login').send(loginQuery3); - loginResponse3.body.account.create_time = new Date(loginResponse3.body.account.create_time); + loginResponse3.body.account = omitTime(loginResponse3.body.account); expect(loginResponse3.body).toMatchObject({ token: loginResponse3.body.token, account: { id: account2.id, - create_time: account2.create_time, - update_time: loginResponse3.body.account.update_time, name: account2.name, email: account2.email, role_id: account2.role_id, diff --git a/api/tests/e2e/02_apikey.test.ts b/api/tests/e2e/02_apikey.test.ts index cb5d808f0..1be8566dc 100644 --- a/api/tests/e2e/02_apikey.test.ts +++ b/api/tests/e2e/02_apikey.test.ts @@ -5,7 +5,6 @@ import crypto from 'crypto'; import { dashboardDataSource } from '~/data_sources/dashboard'; import { app } from '~/server'; import request from 'supertest'; -import * as validation from '~/middleware/validation'; import { ApiKeyCreateRequest, ApiKeyIDRequest, ApiKeyListRequest } from '~/api_models/api'; import { has } from 'lodash'; @@ -15,8 +14,6 @@ describe('APIController', () => { let deletedKeyId: string; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const apiKey = new ApiKey(); apiKey.name = 'preset'; @@ -27,21 +24,15 @@ describe('APIController', () => { presetKey = await dashboardDataSource.getRepository(ApiKey).save(apiKey); }); - beforeEach(() => { - validate.mockReset(); - }); - describe('createKey', () => { it('should create successfully', async () => { const authentication1 = createAuthStruct(presetKey, { name: 'key1', role_id: ROLE_TYPES.AUTHOR }); - validate.mockReturnValueOnce(authentication1); const createRequest1: ApiKeyCreateRequest = { name: 'key1', role_id: ROLE_TYPES.AUTHOR, authentication: authentication1, }; - validate.mockReturnValueOnce(createRequest1); const response1 = await server.post('/api/key/create').send(createRequest1); @@ -49,14 +40,12 @@ describe('APIController', () => { expect(has(response1.body, 'app_secret')).toBe(true); const authentication2 = createAuthStruct(presetKey, { name: 'key2', role_id: ROLE_TYPES.ADMIN }); - validate.mockReturnValueOnce(authentication2); const createRequest2: ApiKeyCreateRequest = { name: 'key2', role_id: ROLE_TYPES.ADMIN, authentication: authentication2, }; - validate.mockReturnValueOnce(createRequest2); const response2 = await server.post('/api/key/create').send(createRequest2); @@ -66,14 +55,12 @@ describe('APIController', () => { it('should fail with duplicate key', async () => { const authentication = createAuthStruct(presetKey, { name: 'key1', role_id: ROLE_TYPES.AUTHOR }); - validate.mockReturnValueOnce(authentication); const createRequest: ApiKeyCreateRequest = { name: 'key1', role_id: ROLE_TYPES.AUTHOR, authentication, }; - validate.mockReturnValueOnce(createRequest); const response = await server.post('/api/key/create').send(createRequest); @@ -92,14 +79,12 @@ describe('APIController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }); - validate.mockReturnValueOnce(authentication); const query: ApiKeyListRequest = { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], authentication, }; - validate.mockReturnValueOnce(query); const response = await server.post('/api/key/list').send(query); @@ -142,7 +127,6 @@ describe('APIController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }); - validate.mockReturnValueOnce(authentication); const query: ApiKeyListRequest = { filter: { name: { value: 'preset', isFuzzy: true } }, @@ -150,7 +134,6 @@ describe('APIController', () => { sort: [{ field: 'name', order: 'ASC' }], authentication, }; - validate.mockReturnValueOnce(query); const response = await server.post('/api/key/list').send(query); @@ -174,13 +157,11 @@ describe('APIController', () => { describe('deleteKey', () => { it('should delete successfully', async () => { const authentication1 = createAuthStruct(presetKey, { id: deletedKeyId }); - validate.mockReturnValueOnce(authentication1); const deleteQuery: ApiKeyIDRequest = { id: deletedKeyId, authentication: authentication1, }; - validate.mockReturnValueOnce(deleteQuery); const deleteResponse = await server.post('/api/key/delete').send(deleteQuery); @@ -192,14 +173,12 @@ describe('APIController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }); - validate.mockReturnValueOnce(authentication2); const listQuery: ApiKeyListRequest = { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], authentication: authentication2, }; - validate.mockReturnValueOnce(listQuery); const listResponse = await server.post('/api/key/list').send(listQuery); @@ -229,13 +208,11 @@ describe('APIController', () => { it('should fail if not found', async () => { const authentication = createAuthStruct(presetKey, { id: deletedKeyId }); - validate.mockReturnValueOnce(authentication); const query: ApiKeyIDRequest = { id: deletedKeyId, authentication, }; - validate.mockReturnValueOnce(query); const response = await server.post('/api/key/delete').send(query); @@ -246,13 +223,11 @@ describe('APIController', () => { it('should fail if key is preset', async () => { const authentication = createAuthStruct(presetKey, { id: presetKey.id }); - validate.mockReturnValueOnce(authentication); const query: ApiKeyIDRequest = { id: presetKey.id, authentication, }; - validate.mockReturnValueOnce(query); const response = await server.post('/api/key/delete').send(query); diff --git a/api/tests/e2e/04_dashboard.test.ts b/api/tests/e2e/04_dashboard.test.ts index 17fdd5e75..1315899f8 100644 --- a/api/tests/e2e/04_dashboard.test.ts +++ b/api/tests/e2e/04_dashboard.test.ts @@ -1,7 +1,6 @@ import { connectionHook, createAuthStruct } from './jest.util'; import Dashboard from '~/models/dashboard'; import { dashboardDataSource } from '~/data_sources/dashboard'; -import * as validation from '~/middleware/validation'; import request from 'supertest'; import { app } from '~/server'; import { @@ -15,6 +14,7 @@ import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account' import { notFoundId } from './constants'; import ApiKey from '~/models/apiKey'; import DashboardPermission from '~/models/dashboard_permission'; +import { omitTime } from '~/utils/helpers'; describe('DashboardController', () => { connectionHook(); @@ -25,14 +25,11 @@ describe('DashboardController', () => { let apiKey: ApiKey; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); @@ -40,7 +37,6 @@ describe('DashboardController', () => { const presetData = new Dashboard(); presetData.name = 'preset'; - presetData.content = {}; presetData.is_preset = true; presetData.is_removed = true; presetDashboard = await dashboardDataSource.getRepository(Dashboard).save(presetData); @@ -54,33 +50,23 @@ describe('DashboardController', () => { apiKey = await dashboardDataSource.getRepository(ApiKey).findOneBy({ name: 'key1' }); }); - beforeEach(() => { - validate.mockReset(); - }); - describe('create', () => { it('should create successfully', async () => { const request1: DashboardCreateRequest = { name: 'dashboard1', - content: {}, group: '1', }; - validate.mockReturnValueOnce(request1); const response1 = await server .post('/dashboard/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(request1); - response1.body.create_time = new Date(response1.body.create_time); - response1.body.update_time = new Date(response1.body.update_time); + response1.body = omitTime(response1.body); dashboard1 = response1.body; expect(response1.body).toMatchObject({ name: 'dashboard1', - content: {}, id: response1.body.id, - create_time: response1.body.create_time, - update_time: response1.body.update_time, is_removed: false, is_preset: false, group: '1', @@ -88,25 +74,19 @@ describe('DashboardController', () => { const request2: DashboardCreateRequest = { name: 'dashboard2', - content: {}, group: '2', }; - validate.mockReturnValueOnce(request2); const response2 = await server .post('/dashboard/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(request2); - response2.body.create_time = new Date(response2.body.create_time); - response2.body.update_time = new Date(response2.body.update_time); + response2.body = omitTime(response2.body); dashboard2 = response2.body; expect(response2.body).toMatchObject({ name: 'dashboard2', - content: {}, id: response2.body.id, - create_time: response2.body.create_time, - update_time: response2.body.update_time, is_removed: false, is_preset: false, group: '2', @@ -116,10 +96,8 @@ describe('DashboardController', () => { it('should fail if duplicate name', async () => { const request: DashboardCreateRequest = { name: 'dashboard1', - content: {}, group: '1', }; - validate.mockReturnValueOnce(request); const response = await server .post('/dashboard/create') @@ -141,13 +119,13 @@ describe('DashboardController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 3, offset: 0, @@ -155,9 +133,6 @@ describe('DashboardController', () => { { id: response.body.data[0].id, name: 'dashboard1', - content: {}, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, is_removed: false, is_preset: false, group: '1', @@ -165,9 +140,6 @@ describe('DashboardController', () => { { id: response.body.data[1].id, name: 'dashboard2', - content: {}, - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, is_removed: false, is_preset: false, group: '2', @@ -175,9 +147,6 @@ describe('DashboardController', () => { { id: presetDashboard.id, name: 'preset', - content: {}, - create_time: response.body.data[2].create_time, - update_time: response.body.data[2].update_time, is_removed: true, is_preset: true, group: '', @@ -192,13 +161,13 @@ describe('DashboardController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 2, offset: 0, @@ -206,9 +175,6 @@ describe('DashboardController', () => { { id: response.body.data[0].id, name: 'dashboard1', - content: {}, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, is_removed: false, is_preset: false, group: '1', @@ -216,9 +182,6 @@ describe('DashboardController', () => { { id: response.body.data[1].id, name: 'dashboard2', - content: {}, - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, is_removed: false, is_preset: false, group: '2', @@ -233,13 +196,13 @@ describe('DashboardController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 2, offset: 0, @@ -247,9 +210,6 @@ describe('DashboardController', () => { { id: response.body.data[0].id, name: 'dashboard1', - content: {}, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, is_removed: false, is_preset: false, group: '1', @@ -257,9 +217,6 @@ describe('DashboardController', () => { { id: response.body.data[1].id, name: 'dashboard2', - content: {}, - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, is_removed: false, is_preset: false, group: '2', @@ -274,13 +231,13 @@ describe('DashboardController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'name', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 1, offset: 0, @@ -288,9 +245,6 @@ describe('DashboardController', () => { { id: presetDashboard.id, name: 'preset', - content: {}, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, is_removed: true, is_preset: true, group: '', @@ -305,23 +259,20 @@ describe('DashboardController', () => { const query: DashboardIDRequest = { id: dashboard1.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/details') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); - expect(response.body).toMatchObject(dashboard1); + response.body = omitTime(response.body); + expect(response.body).toMatchObject(omitTime(dashboard1)); }); it('should fail', async () => { const query: DashboardIDRequest = { id: notFoundId, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/details') @@ -329,7 +280,9 @@ describe('DashboardController', () => { .send(query); expect(response.body.code).toEqual('NOT_FOUND'); - expect(response.body.detail.message).toContain('Could not find any entity of type "Dashboard" matching'); + expect(response.body.detail.message).toContain( + 'Could not find any entity of type "DashboardPermission" matching', + ); expect(response.body.detail.message).toContain(notFoundId); }); }); @@ -340,16 +293,14 @@ describe('DashboardController', () => { name: dashboard1.name, is_preset: dashboard1.is_preset, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/detailsByName') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); - response.body.update_time = new Date(response.body.update_time); - expect(response.body).toMatchObject(dashboard1); + response.body = omitTime(response.body); + expect(response.body).toMatchObject(omitTime(dashboard1)); }); it('should fail', async () => { @@ -357,7 +308,6 @@ describe('DashboardController', () => { name: dashboard1.name, is_preset: !dashboard1.is_preset, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/detailsByName') @@ -377,23 +327,19 @@ describe('DashboardController', () => { id: dashboard2.id, name: 'dashboard2_updated', is_removed: true, - content: { tmp: 'tmp' }, group: '2_updated', }; - validate.mockReturnValueOnce(query); const response = await server .put('/dashboard/update') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ ...dashboard2, name: 'dashboard2_updated', is_removed: true, - content: { tmp: 'tmp' }, - update_time: response.body.update_time, group: '2_updated', }); }); @@ -403,7 +349,6 @@ describe('DashboardController', () => { id: notFoundId, name: 'not_found', }; - validate.mockReturnValueOnce(query); const response = await server .put('/dashboard/update') @@ -411,7 +356,9 @@ describe('DashboardController', () => { .send(query); expect(response.body.code).toEqual('NOT_FOUND'); - expect(response.body.detail.message).toContain('Could not find any entity of type "Dashboard" matching'); + expect(response.body.detail.message).toContain( + 'Could not find any entity of type "DashboardPermission" matching', + ); expect(response.body.detail.message).toContain(notFoundId); }); @@ -420,23 +367,19 @@ describe('DashboardController', () => { id: presetDashboard.id, name: 'preset_updated', is_removed: false, - content: { tmp: 'tmp' }, group: 'preset', }; - validate.mockReturnValueOnce(query); const response = await server .put('/dashboard/update') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ - ...presetDashboard, + ...omitTime(presetDashboard), name: 'preset_updated', is_removed: false, - content: { tmp: 'tmp' }, - update_time: response.body.update_time, group: 'preset', }); }); @@ -446,18 +389,14 @@ describe('DashboardController', () => { id: presetDashboard.id, name: 'preset_updated', is_removed: false, - content: { tmp: 'tmp' }, }); - validate.mockReturnValueOnce(authentication); const query: DashboardUpdateRequest = { id: presetDashboard.id, name: 'preset_updated', is_removed: false, - content: { tmp: 'tmp' }, authentication, }; - validate.mockReturnValueOnce(query); const response = await server.put('/dashboard/update').send(query); @@ -473,18 +412,16 @@ describe('DashboardController', () => { const query: DashboardIDRequest = { id: dashboard1.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/delete') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ - ...dashboard1, + ...omitTime(dashboard1), is_removed: true, - update_time: response.body.update_time, }); }); @@ -492,7 +429,6 @@ describe('DashboardController', () => { const query: DashboardIDRequest = { id: notFoundId, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/delete') @@ -500,7 +436,9 @@ describe('DashboardController', () => { .send(query); expect(response.body.code).toEqual('NOT_FOUND'); - expect(response.body.detail.message).toContain('Could not find any entity of type "Dashboard" matching'); + expect(response.body.detail.message).toContain( + 'Could not find any entity of type "DashboardPermission" matching', + ); expect(response.body.detail.message).toContain(notFoundId); }); @@ -508,32 +446,28 @@ describe('DashboardController', () => { const query: DashboardIDRequest = { id: presetDashboard.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard/delete') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - response.body.create_time = new Date(response.body.create_time); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ - ...presetDashboard, + ...omitTime(presetDashboard), name: 'preset_updated', is_removed: true, group: 'preset', - update_time: response.body.update_time, }); }); it('should fail to delete preset dashboard if not SUPERADMIN', async () => { const authentication = createAuthStruct(apiKey, { id: presetDashboard.id }); - validate.mockReturnValueOnce(authentication); - const query: DashboardUpdateRequest = { + const query: DashboardIDRequest = { id: presetDashboard.id, authentication, }; - validate.mockReturnValueOnce(query); const response = await server.post('/dashboard/delete').send(query); diff --git a/api/tests/e2e/05_datasource.test.ts b/api/tests/e2e/05_datasource.test.ts index 41da32bbf..764ccc66e 100644 --- a/api/tests/e2e/05_datasource.test.ts +++ b/api/tests/e2e/05_datasource.test.ts @@ -10,11 +10,11 @@ import { } from '~/api_models/datasource'; import { maybeDecryptPassword, maybeEncryptPassword } from '~/utils/encryption'; import { parseDBUrl } from '../utils'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; import { notFoundId } from './constants'; +import { omitTime } from '~/utils/helpers'; describe('DataSourceController', () => { connectionHook(); @@ -25,14 +25,11 @@ describe('DataSourceController', () => { let httpDatasource: DataSource; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); @@ -57,10 +54,6 @@ describe('DataSourceController', () => { postgresConfig = { ...presetData.config, password }; }); - beforeEach(() => { - validate.mockReset(); - }); - describe('create', () => { it('should create successfully', async () => { const pgQuery: DataSourceCreateRequest = { @@ -74,13 +67,14 @@ describe('DataSourceController', () => { port: postgresConfig.port, }, }; - validate.mockReturnValueOnce(pgQuery); const pgResponse = await server .post('/datasource/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(pgQuery); + pgResponse.body = omitTime(pgResponse.body); + pgDatasource = pgResponse.body; expect(pgResponse.body).toMatchObject({ type: 'postgresql', @@ -93,8 +87,6 @@ describe('DataSourceController', () => { port: 5432, }, id: pgDatasource.id, - create_time: pgDatasource.create_time, - update_time: pgDatasource.update_time, is_preset: false, }); @@ -109,12 +101,12 @@ describe('DataSourceController', () => { }, }, }; - validate.mockReturnValueOnce(httpQuery); const httpResponse = await server .post('/datasource/create') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(httpQuery); + httpResponse.body = omitTime(httpResponse.body); httpDatasource = httpResponse.body; expect(httpResponse.body).toMatchObject({ @@ -122,8 +114,6 @@ describe('DataSourceController', () => { key: 'jsonplaceholder', config: { host: 'http://jsonplaceholder.typicode.com', processing: { pre: '', post: '' } }, id: httpDatasource.id, - create_time: httpDatasource.create_time, - update_time: httpDatasource.update_time, is_preset: false, }); }); @@ -140,7 +130,6 @@ describe('DataSourceController', () => { port: postgresConfig.port, }, }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/create') @@ -167,7 +156,6 @@ describe('DataSourceController', () => { port: 22, }, }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/create') @@ -187,7 +175,6 @@ describe('DataSourceController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'key', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/list') @@ -226,7 +213,6 @@ describe('DataSourceController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'key', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/list') @@ -254,7 +240,6 @@ describe('DataSourceController', () => { id: notFoundId, key: 'not_found', }; - validate.mockReturnValueOnce(query); const response = await server .put('/datasource/rename') @@ -271,7 +256,6 @@ describe('DataSourceController', () => { id: httpDatasource.id, key: httpDatasource.key, }; - validate.mockReturnValueOnce(query); const response = await server .put('/datasource/rename') @@ -289,13 +273,12 @@ describe('DataSourceController', () => { id: httpDatasource.id, key: 'jsonplaceholder_renamed', }; - validate.mockReturnValueOnce(query); const response = await server .put('/datasource/rename') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - + response.body = omitTime(response.body); expect(response.body).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', @@ -305,8 +288,6 @@ describe('DataSourceController', () => { new_key: 'jsonplaceholder_renamed', }, id: response.body.id, - create_time: response.body.create_time, - update_time: response.body.update_time, }); }); }); @@ -316,7 +297,6 @@ describe('DataSourceController', () => { const deleteQuery: DataSourceIDRequest = { id: pgDatasource.id, }; - validate.mockReturnValueOnce(deleteQuery); await server.post('/datasource/delete').set('Authorization', `Bearer ${superadminLogin.token}`).send(deleteQuery); @@ -324,7 +304,6 @@ describe('DataSourceController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'key', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/list') @@ -355,7 +334,6 @@ describe('DataSourceController', () => { const query: DataSourceIDRequest = { id: pgDatasource.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/delete') @@ -371,7 +349,6 @@ describe('DataSourceController', () => { const query: DataSourceIDRequest = { id: presetDatasource.id, }; - validate.mockReturnValueOnce(query); const response = await server .post('/datasource/delete') diff --git a/api/tests/e2e/06_query.test.ts b/api/tests/e2e/06_query.test.ts index b7091641f..2460b597a 100644 --- a/api/tests/e2e/06_query.test.ts +++ b/api/tests/e2e/06_query.test.ts @@ -1,6 +1,5 @@ import { connectionHook } from './jest.util'; import { HttpParams, QueryRequest } from '~/api_models/query'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; @@ -10,24 +9,17 @@ describe('QueryController', () => { let superadminLogin: AccountLoginResponse; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); superadminLogin = response.body; }); - beforeEach(() => { - validate.mockReset(); - }); - describe('query', () => { it('should query pg successfully', async () => { const query: QueryRequest = { @@ -35,7 +27,6 @@ describe('QueryController', () => { key: 'preset', query: 'SELECT * FROM role ORDER BY id ASC', }; - validate.mockReturnValueOnce(query); const response = await server.post('/query').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); @@ -82,8 +73,6 @@ describe('QueryController', () => { key: 'jsonplaceholder_renamed', query: JSON.stringify(httpParams), }; - validate.mockReturnValueOnce(query); - validate.mockReturnValueOnce(httpParams); const response = await server.post('/query').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); @@ -112,8 +101,6 @@ describe('QueryController', () => { key: 'jsonplaceholder_renamed', query: JSON.stringify(httpParams), }; - validate.mockReturnValueOnce(query); - validate.mockReturnValueOnce(httpParams); const response = await server.post('/query').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); @@ -133,8 +120,6 @@ describe('QueryController', () => { key: 'jsonplaceholder_renamed', query: JSON.stringify(httpParams), }; - validate.mockReturnValueOnce(query); - validate.mockReturnValueOnce(httpParams); const response = await server.post('/query').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); @@ -154,8 +139,6 @@ describe('QueryController', () => { key: 'jsonplaceholder_renamed', query: JSON.stringify(httpParams), }; - validate.mockReturnValueOnce(query); - validate.mockReturnValueOnce(httpParams); const response = await server.post('/query').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); diff --git a/api/tests/e2e/07_job.test.ts b/api/tests/e2e/07_job.test.ts index d4a74f615..7bfa66c43 100644 --- a/api/tests/e2e/07_job.test.ts +++ b/api/tests/e2e/07_job.test.ts @@ -1,34 +1,29 @@ import { connectionHook, sleep } from './jest.util'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; -import Dashboard from '~/models/dashboard'; import DataSource from '~/models/datasource'; import { dashboardDataSource } from '~/data_sources/dashboard'; import { parseDBUrl } from '../utils'; import { DataSourceCreateRequest, DataSourceRenameRequest } from '~/api_models/datasource'; -import { DashboardCreateRequest, DashboardListRequest } from '~/api_models/dashboard'; +import { DashboardCreateRequest } from '~/api_models/dashboard'; import { JobListRequest, JobRunRequest } from '~/api_models/job'; import Job from '~/models/job'; +import { omitTime } from '~/utils/helpers'; describe('JobController', () => { connectionHook(); let superadminLogin: AccountLoginResponse; - let dashboard: Dashboard; let pgDatasource: DataSource; let httpDatasource: DataSource; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); @@ -36,32 +31,10 @@ describe('JobController', () => { const dashboardQuery: DashboardCreateRequest = { name: 'jobDashboard', - content: { - definition: { - queries: [ - { - id: 'pgQuery', - type: 'postgresql', - key: 'jobPG', - }, - { - id: 'httpQuery', - type: 'http', - key: 'jobHTTP', - }, - ], - }, - }, group: 'job', }; - validate.mockReturnValueOnce(dashboardQuery); - - const dashboardResponse = await server - .post('/dashboard/create') - .set('Authorization', `Bearer ${superadminLogin.token}`) - .send(dashboardQuery); - dashboard = dashboardResponse.body; + await server.post('/dashboard/create').set('Authorization', `Bearer ${superadminLogin.token}`).send(dashboardQuery); const connectionString = process.env.END_2_END_TEST_PG_URL; const { username, password, host, port, database } = parseDBUrl(connectionString); @@ -76,7 +49,6 @@ describe('JobController', () => { port, }, }; - validate.mockReturnValueOnce(pgQuery); const pgResponse = await server .post('/datasource/create') @@ -96,7 +68,6 @@ describe('JobController', () => { }, }, }; - validate.mockReturnValueOnce(httpQuery); const httpResponse = await server .post('/datasource/create') @@ -106,23 +77,18 @@ describe('JobController', () => { httpDatasource = httpResponse.body; }); - beforeEach(() => { - validate.mockReset(); - }); - describe('rename', () => { it('rename jobPG', async () => { const query: DataSourceRenameRequest = { id: pgDatasource.id, key: pgDatasource.key + '_renamed', }; - validate.mockReturnValueOnce(query); const response = await server .put('/datasource/rename') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); - + response.body = omitTime(response.body); expect(response.body).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', @@ -132,8 +98,6 @@ describe('JobController', () => { new_key: pgDatasource.key + '_renamed', }, id: response.body.id, - create_time: response.body.create_time, - update_time: response.body.update_time, }); pgDatasource.key = pgDatasource.key + '_renamed'; @@ -145,13 +109,13 @@ describe('JobController', () => { id: httpDatasource.id, key: httpDatasource.key + '_renamed', }; - validate.mockReturnValueOnce(query); const response = await server .put('/datasource/rename') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body = omitTime(response.body); expect(response.body).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', @@ -161,8 +125,6 @@ describe('JobController', () => { new_key: httpDatasource.key + '_renamed', }, id: response.body.id, - create_time: response.body.create_time, - update_time: response.body.update_time, }); httpDatasource.key = httpDatasource.key + '_renamed'; await sleep(2000); @@ -175,12 +137,13 @@ describe('JobController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/job/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 5, offset: 0, @@ -194,8 +157,6 @@ describe('JobController', () => { auth_type: 'ACCOUNT', }, result: { affected_dashboard_permissions: [] }, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, }, { id: response.body.data[1].id, @@ -206,8 +167,6 @@ describe('JobController', () => { auth_type: 'APIKEY', }, result: { affected_dashboard_permissions: [] }, - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, }, { id: response.body.data[2].id, @@ -218,9 +177,7 @@ describe('JobController', () => { new_key: 'jsonplaceholder_renamed', old_key: 'jsonplaceholder', }, - result: { affected_dashboards: [] }, - create_time: response.body.data[2].create_time, - update_time: response.body.data[2].update_time, + result: { affected_dashboard_contents: [] }, }, { id: response.body.data[3].id, @@ -232,15 +189,8 @@ describe('JobController', () => { old_key: 'jobPG', }, result: { - affected_dashboards: [ - { - queries: ['pgQuery'], - dashboardId: dashboard.id, - }, - ], + affected_dashboard_contents: [], }, - create_time: response.body.data[3].create_time, - update_time: response.body.data[3].update_time, }, { id: response.body.data[4].id, @@ -252,67 +202,8 @@ describe('JobController', () => { old_key: 'jobHTTP', }, result: { - affected_dashboards: [ - { - queries: ['httpQuery'], - dashboardId: dashboard.id, - }, - ], - }, - create_time: response.body.data[4].create_time, - update_time: response.body.data[4].update_time, - }, - ], - }); - }); - }); - - describe('check Dashboard', () => { - it('dashboard content queries should be updated', async () => { - const query: DashboardListRequest = { - filter: { - name: { value: 'jobDashboard', isFuzzy: true }, - group: { value: '', isFuzzy: true }, - is_removed: false, - }, - pagination: { page: 1, pagesize: 20 }, - sort: [{ field: 'name', order: 'ASC' }], - }; - validate.mockReturnValueOnce(query); - - const response = await server - .post('/dashboard/list') - .set('Authorization', `Bearer ${superadminLogin.token}`) - .send(query); - - expect(response.body).toMatchObject({ - total: 1, - offset: 0, - data: [ - { - id: response.body.data[0].id, - name: 'jobDashboard', - content: { - definition: { - queries: [ - { - id: 'pgQuery', - type: 'postgresql', - key: 'jobPG_renamed', - }, - { - id: 'httpQuery', - type: 'http', - key: 'jobHTTP_renamed', - }, - ], - }, + affected_dashboard_contents: [], }, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, - is_removed: false, - is_preset: false, - group: 'job', }, ], }); @@ -347,7 +238,6 @@ describe('JobController', () => { const query: JobRunRequest = { type: 'RENAME_DATASOURCE', }; - validate.mockReturnValueOnce(query); await server.post('/job/run').set('Authorization', `Bearer ${superadminLogin.token}`).send(query); @@ -359,13 +249,13 @@ describe('JobController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/job/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 7, offset: 0, @@ -379,8 +269,6 @@ describe('JobController', () => { auth_type: 'ACCOUNT', }, result: { affected_dashboard_permissions: [] }, - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, }, { id: response.body.data[1].id, @@ -391,8 +279,6 @@ describe('JobController', () => { auth_type: 'APIKEY', }, result: { affected_dashboard_permissions: [] }, - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, }, { id: response.body.data[2].id, @@ -403,9 +289,7 @@ describe('JobController', () => { new_key: 'jsonplaceholder_renamed', old_key: 'jsonplaceholder', }, - result: { affected_dashboards: [] }, - create_time: response.body.data[2].create_time, - update_time: response.body.data[2].update_time, + result: { affected_dashboard_contents: [] }, }, { id: response.body.data[3].id, @@ -417,15 +301,8 @@ describe('JobController', () => { old_key: 'jobPG', }, result: { - affected_dashboards: [ - { - queries: ['pgQuery'], - dashboardId: dashboard.id, - }, - ], + affected_dashboard_contents: [], }, - create_time: response.body.data[3].create_time, - update_time: response.body.data[3].update_time, }, { id: response.body.data[4].id, @@ -437,15 +314,8 @@ describe('JobController', () => { old_key: 'jobHTTP', }, result: { - affected_dashboards: [ - { - queries: ['httpQuery'], - dashboardId: dashboard.id, - }, - ], + affected_dashboard_contents: [], }, - create_time: response.body.data[4].create_time, - update_time: response.body.data[4].update_time, }, { id: response.body.data[5].id, @@ -457,8 +327,6 @@ describe('JobController', () => { old_key: 'jobPG', }, result: response.body.data[5].result, - create_time: response.body.data[5].create_time, - update_time: response.body.data[5].update_time, }, { id: response.body.data[6].id, @@ -470,15 +338,8 @@ describe('JobController', () => { old_key: 'jobPG_renamed', }, result: { - affected_dashboards: [ - { - queries: ['pgQuery'], - dashboardId: dashboard.id, - }, - ], + affected_dashboard_contents: [], }, - create_time: response.body.data[6].create_time, - update_time: response.body.data[6].update_time, }, ], }); diff --git a/api/tests/e2e/08_dashboard_changelog.test.ts b/api/tests/e2e/08_dashboard_changelog.test.ts index a7c9f88bc..44dbd18d7 100644 --- a/api/tests/e2e/08_dashboard_changelog.test.ts +++ b/api/tests/e2e/08_dashboard_changelog.test.ts @@ -1,9 +1,9 @@ import { connectionHook } from './jest.util'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; import { DashboardChangelogListRequest } from '~/api_models/dashboard_changelog'; +import { omitTime } from '~/utils/helpers'; describe('DashboardChangelogController', () => { connectionHook(); @@ -12,96 +12,64 @@ describe('DashboardChangelogController', () => { const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query); const response = await server.post('/account/login').send(query); superadminLogin = response.body; }); - beforeEach(() => { - validate.mockReset(); - }); - describe('list', () => { it('no filters', async () => { const query: DashboardChangelogListRequest = { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard_changelog/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ - total: 7, + total: 4, offset: 0, data: [ { id: response.body.data[0].id, dashboard_id: response.body.data[0].dashboard_id, diff: response.body.data[0].diff, - create_time: response.body.data[0].create_time, }, { id: response.body.data[1].id, dashboard_id: response.body.data[1].dashboard_id, diff: response.body.data[1].diff, - create_time: response.body.data[1].create_time, }, { id: response.body.data[2].id, dashboard_id: response.body.data[2].dashboard_id, diff: response.body.data[2].diff, - create_time: response.body.data[2].create_time, }, { id: response.body.data[3].id, dashboard_id: response.body.data[3].dashboard_id, diff: response.body.data[3].diff, - create_time: response.body.data[3].create_time, - }, - { - id: response.body.data[4].id, - dashboard_id: response.body.data[4].dashboard_id, - diff: response.body.data[4].diff, - create_time: response.body.data[4].create_time, - }, - { - id: response.body.data[5].id, - dashboard_id: response.body.data[5].dashboard_id, - diff: response.body.data[5].diff, - create_time: response.body.data[5].create_time, - }, - { - id: response.body.data[6].id, - dashboard_id: response.body.data[6].dashboard_id, - diff: response.body.data[6].diff, - create_time: response.body.data[6].create_time, }, ], }); expect(response.body.data[0].diff).toContain('diff --git a/data.json b/data.json'); expect(response.body.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[0].diff).toContain('@@ -2,9 +2,11 @@'); + expect(response.body.data[0].diff).toContain('@@ -1,8 +1,8 @@'); expect(response.body.data[0].diff).toContain( '-\t"name": "dashboard2",\n' + - '-\t"content": {},\n' + - '-\t"is_removed": false,\n' + '+\t"name": "dashboard2_updated",\n' + - '+\t"content": {\n' + - '+\t\t"tmp": "tmp"\n' + - '+\t},\n' + + ' \t"content_id": null,\n' + + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": false,\n' + '-\t"group": "2"\n' + @@ -111,15 +79,12 @@ describe('DashboardChangelogController', () => { expect(response.body.data[1].diff).toContain('diff --git a/data.json b/data.json'); expect(response.body.data[1].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[1].diff).toContain('@@ -2,9 +2,11 @@'); + expect(response.body.data[1].diff).toContain('@@ -1,8 +1,8 @@'); expect(response.body.data[1].diff).toContain( '-\t"name": "preset",\n' + - '-\t"content": {},\n' + - '-\t"is_removed": true,\n' + '+\t"name": "preset_updated",\n' + - '+\t"content": {\n' + - '+\t\t"tmp": "tmp"\n' + - '+\t},\n' + + ' \t"content_id": null,\n' + + '-\t"is_removed": true,\n' + '+\t"is_removed": false,\n' + ' \t"is_preset": true,\n' + '-\t"group": ""\n' + @@ -129,10 +94,10 @@ describe('DashboardChangelogController', () => { expect(response.body.data[2].diff).toContain('diff --git a/data.json b/data.json'); expect(response.body.data[2].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[2].diff).toContain('@@ -4,7 +4,7 @@'); + expect(response.body.data[2].diff).toContain('@@ -2,7 +2,7 @@'); expect(response.body.data[2].diff).toContain( ' \t"name": "dashboard1",\n' + - ' \t"content": {},\n' + + ' \t"content_id": null,\n' + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": false,\n' + @@ -142,11 +107,10 @@ describe('DashboardChangelogController', () => { expect(response.body.data[3].diff).toContain('diff --git a/data.json b/data.json'); expect(response.body.data[3].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[3].diff).toContain('@@ -6,7 +6,7 @@'); + expect(response.body.data[3].diff).toContain('@@ -2,7 +2,7 @@'); expect(response.body.data[3].diff).toContain( - ' \t"content": {\n' + - ' \t\t"tmp": "tmp"\n' + - ' \t},\n' + + ' \t"name": "preset_updated",\n' + + ' \t"content_id": null,\n' + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": true,\n' + @@ -154,48 +118,6 @@ describe('DashboardChangelogController', () => { ' }\n', ); - expect(response.body.data[4].diff).toContain('diff --git a/data.json b/data.json'); - expect(response.body.data[4].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[4].diff).toContain('@@ -6,7 +6,7 @@'); - expect(response.body.data[4].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery",\n' + - '-\t\t\t\t\t"key": "jobPG",\n' + - '+\t\t\t\t\t"key": "jobPG_renamed",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - - expect(response.body.data[5].diff).toContain('diff --git a/data.json b/data.json'); - expect(response.body.data[5].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[5].diff).toContain('@@ -11,7 +11,7 @@'); - expect(response.body.data[5].diff).toContain( - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "httpQuery",\n' + - '-\t\t\t\t\t"key": "jobHTTP",\n' + - '+\t\t\t\t\t"key": "jobHTTP_renamed",\n' + - ' \t\t\t\t\t"type": "http"\n' + - ' \t\t\t\t}\n' + - ' \t\t\t]\n', - ); - - expect(response.body.data[6].diff).toContain('diff --git a/data.json b/data.json'); - expect(response.body.data[6].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[6].diff).toContain('@@ -6,7 +6,7 @@'); - expect(response.body.data[6].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery",\n' + - '-\t\t\t\t\t"key": "jobPG_renamed",\n' + - '+\t\t\t\t\t"key": "jobPG",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - changelogDashboardId = response.body.data[0].dashboard_id; }); @@ -205,13 +127,13 @@ describe('DashboardChangelogController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard_changelog/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 1, offset: 0, @@ -220,21 +142,18 @@ describe('DashboardChangelogController', () => { id: response.body.data[0].id, dashboard_id: response.body.data[0].dashboard_id, diff: response.body.data[0].diff, - create_time: response.body.data[0].create_time, }, ], }); + expect(response.body.data[0].diff).toContain('diff --git a/data.json b/data.json'); expect(response.body.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(response.body.data[0].diff).toContain('@@ -2,9 +2,11 @@'); + expect(response.body.data[0].diff).toContain('@@ -1,8 +1,8 @@'); expect(response.body.data[0].diff).toContain( '-\t"name": "dashboard2",\n' + - '-\t"content": {},\n' + - '-\t"is_removed": false,\n' + '+\t"name": "dashboard2_updated",\n' + - '+\t"content": {\n' + - '+\t\t"tmp": "tmp"\n' + - '+\t},\n' + + ' \t"content_id": null,\n' + + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": false,\n' + '-\t"group": "2"\n' + diff --git a/api/tests/e2e/09_config.test.ts b/api/tests/e2e/09_config.test.ts index 405ceb47a..24ea02dfb 100644 --- a/api/tests/e2e/09_config.test.ts +++ b/api/tests/e2e/09_config.test.ts @@ -1,6 +1,5 @@ import bcrypt from 'bcrypt'; import { connectionHook, createAuthStruct } from './jest.util'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { dashboardDataSource } from '~/data_sources/dashboard'; @@ -20,14 +19,11 @@ describe('ConfigController', () => { let apiKey: ApiKey; const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const query1: AccountLoginRequest = { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(query1); const response1 = await server.post('/account/login').send(query1); @@ -44,7 +40,6 @@ describe('ConfigController', () => { name: account.name, password: '12345678', }; - validate.mockReturnValueOnce(query2); const response2 = await server.post('/account/login').send(query2); @@ -60,16 +55,11 @@ describe('ConfigController', () => { await dashboardDataSource.getRepository(Config).save(config); }); - beforeEach(() => { - validate.mockReset(); - }); - describe('get', () => { it('should return default locale when not authenticated or not updated', async () => { const request1: ConfigGetRequest = { key: 'lang', }; - validate.mockReturnValueOnce(request1); const response1 = await server.post('/config/get').send(request1); @@ -81,13 +71,11 @@ describe('ConfigController', () => { const authentication = createAuthStruct(apiKey, { key: 'lang', }); - validate.mockReturnValueOnce(authentication); const request2: ConfigGetRequest = { key: 'lang', authentication, }; - validate.mockReturnValueOnce(request2); const response2 = await server.post('/config/get').send(request2); @@ -99,7 +87,6 @@ describe('ConfigController', () => { const request3: ConfigGetRequest = { key: 'website_settings', }; - validate.mockReturnValueOnce(request3); const response3 = await server.post('/config/get').send(request3); @@ -114,7 +101,6 @@ describe('ConfigController', () => { const request1: ConfigGetRequest = { key: 'lang', }; - validate.mockReturnValueOnce(request1); const response1 = await server .post('/config/get') @@ -134,7 +120,6 @@ describe('ConfigController', () => { key: 'lang', value: 'en', }; - validate.mockReturnValueOnce(request1); const response1 = await server .post('/config/update') @@ -150,14 +135,12 @@ describe('ConfigController', () => { key: 'lang', value: 'zh', }); - validate.mockReturnValueOnce(authentication); const request2: ConfigUpdateRequest = { key: 'lang', value: 'zh', authentication, }; - validate.mockReturnValueOnce(request2); const response2 = await server.post('/config/update').send(request2); @@ -170,7 +153,6 @@ describe('ConfigController', () => { key: 'website_settings', value: '', }; - validate.mockReturnValueOnce(request3); const response3 = await server .post('/config/update') @@ -219,7 +201,6 @@ describe('ConfigController', () => { key: 'lang', value: 'incorrect_lang', }; - validate.mockReturnValueOnce(request); const response = await server .post('/config/update') @@ -237,7 +218,6 @@ describe('ConfigController', () => { key: 'lang', value: 'en', }; - validate.mockReturnValueOnce(request1); const response1 = await server.post('/config/update').send(request1); @@ -250,7 +230,6 @@ describe('ConfigController', () => { key: 'website_settings', value: '', }; - validate.mockReturnValueOnce(request2); const response2 = await server.post('/config/update').send(request2); @@ -265,7 +244,6 @@ describe('ConfigController', () => { key: 'website_settings', value: '', }; - validate.mockReturnValueOnce(request); const response = await server .post('/config/update') diff --git a/api/tests/e2e/10_dashboard_permission.test.ts b/api/tests/e2e/10_dashboard_permission.test.ts index 539985d9d..9f4922af1 100644 --- a/api/tests/e2e/10_dashboard_permission.test.ts +++ b/api/tests/e2e/10_dashboard_permission.test.ts @@ -1,7 +1,6 @@ import bcrypt from 'bcrypt'; import crypto from 'crypto'; import { connectionHook, createAuthStruct } from './jest.util'; -import * as validation from '~/middleware/validation'; import { app } from '~/server'; import request from 'supertest'; import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; @@ -18,6 +17,7 @@ import { SALT_ROUNDS } from '~/utils/constants'; import Dashboard from '~/models/dashboard'; import DashboardPermission from '~/models/dashboard_permission'; import { DashboardIDRequest, DashboardUpdateRequest } from '~/api_models/dashboard'; +import { omitTime } from '~/utils/helpers'; describe('DashboardPermissionController', () => { connectionHook(); @@ -38,8 +38,6 @@ describe('DashboardPermissionController', () => { const server = request(app); - const validate = jest.spyOn(validation, 'validate'); - beforeAll(async () => { const readerAccountData = new Account(); readerAccountData.name = 'reader_dashboard_permission'; @@ -71,7 +69,6 @@ describe('DashboardPermissionController', () => { const dashboardData = new Dashboard(); dashboardData.name = 'dashboard_permission'; - dashboardData.content = {}; dashboardData.group = 'dashboard_permission'; dashboardData.is_preset = false; const dashboard = await dashboardDataSource.getRepository(Dashboard).save(dashboardData); @@ -85,7 +82,6 @@ describe('DashboardPermissionController', () => { name: 'superadmin', password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', }; - validate.mockReturnValueOnce(superadminQuery); const superadminResponse = await server.post('/account/login').send(superadminQuery); superadminLogin = superadminResponse.body; @@ -94,7 +90,6 @@ describe('DashboardPermissionController', () => { name: readerAccount.name, password: readerAccount.name, }; - validate.mockReturnValueOnce(readerQuery); const readerResponse = await server.post('/account/login').send(readerQuery); readerLogin = readerResponse.body; @@ -103,29 +98,24 @@ describe('DashboardPermissionController', () => { name: authorAccount.name, password: authorAccount.name, }; - validate.mockReturnValueOnce(authorQuery); const authorResponse = await server.post('/account/login').send(authorQuery); authorLogin = authorResponse.body; }); - beforeEach(() => { - validate.mockReset(); - }); - describe('list', () => { it('no filters', async () => { const query: DashboardPermissionListRequest = { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query); const response = await server .post('/dashboard_permission/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query); + response.body.data = response.body.data.map(omitTime); expect(response.body).toMatchObject({ total: 5, offset: 0, @@ -135,40 +125,30 @@ describe('DashboardPermissionController', () => { owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response.body.data[0].create_time, - update_time: response.body.data[0].update_time, }, { id: response.body.data[1].id, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response.body.data[1].create_time, - update_time: response.body.data[1].update_time, }, { id: response.body.data[2].id, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response.body.data[2].create_time, - update_time: response.body.data[2].update_time, }, { id: response.body.data[3].id, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response.body.data[3].create_time, - update_time: response.body.data[3].update_time, }, { id: response.body.data[4].id, owner_id: null, owner_type: null, access: [], - create_time: response.body.data[4].create_time, - update_time: response.body.data[4].update_time, }, ], }); @@ -185,13 +165,13 @@ describe('DashboardPermissionController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard_permission/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query1); + response1.body.data = response1.body.data.map(omitTime); expect(response1.body).toMatchObject({ total: 1, offset: 0, @@ -201,8 +181,6 @@ describe('DashboardPermissionController', () => { owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response1.body.data[0].create_time, - update_time: response1.body.data[0].update_time, }, ], }); @@ -212,13 +190,13 @@ describe('DashboardPermissionController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query2); const response2 = await server .post('/dashboard_permission/list') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query2); + response2.body.data = response2.body.data.map(omitTime); expect(response2.body).toMatchObject({ total: 4, offset: 0, @@ -228,32 +206,24 @@ describe('DashboardPermissionController', () => { owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response2.body.data[0].create_time, - update_time: response2.body.data[0].update_time, }, { id: dashboardId2, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response2.body.data[1].create_time, - update_time: response2.body.data[1].update_time, }, { id: dashboardId3, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response2.body.data[2].create_time, - update_time: response2.body.data[2].update_time, }, { id: dashboardId4, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [], - create_time: response2.body.data[3].create_time, - update_time: response2.body.data[3].update_time, }, ], }); @@ -263,7 +233,6 @@ describe('DashboardPermissionController', () => { pagination: { page: 1, pagesize: 20 }, sort: [{ field: 'create_time', order: 'ASC' }], }; - validate.mockReturnValueOnce(query3); const response3 = await server .post('/dashboard_permission/list') @@ -283,27 +252,25 @@ describe('DashboardPermissionController', () => { const query1: DashboardPermissionUpdateRequest = { id: dashboardId1, access: [ - { type: 'ACCOUNT', id: readerAccount.id, permission: 'EDIT' }, + { type: 'ACCOUNT', id: readerAccount.id, permission: 'VIEW' }, { type: 'APIKEY', id: authorApiKey.id, permission: 'EDIT' }, { type: 'ACCOUNT', id: authorAccount.id, permission: 'VIEW' }, { type: 'APIKEY', id: readerApiKey.id, permission: 'VIEW' }, ], }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard_permission/update') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query1); + response1.body = omitTime(response1.body); expect(response1.body).toMatchObject({ id: dashboardId1, - create_time: response1.body.create_time, - update_time: response1.body.update_time, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [ - { type: 'ACCOUNT', id: readerAccount.id, permission: 'EDIT' }, + { type: 'ACCOUNT', id: readerAccount.id, permission: 'VIEW' }, { type: 'APIKEY', id: authorApiKey.id, permission: 'EDIT' }, { type: 'ACCOUNT', id: authorAccount.id, permission: 'VIEW' }, { type: 'APIKEY', id: readerApiKey.id, permission: 'VIEW' }, @@ -317,21 +284,19 @@ describe('DashboardPermissionController', () => { { type: 'APIKEY', id: readerApiKey.id, permission: 'REMOVE' }, ], }; - validate.mockReturnValueOnce(query2); const response2 = await server .post('/dashboard_permission/update') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query2); + response2.body = omitTime(response2.body); expect(response2.body).toMatchObject({ id: dashboardId1, - create_time: response2.body.create_time, - update_time: response2.body.update_time, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [ - { type: 'ACCOUNT', id: readerAccount.id, permission: 'EDIT' }, + { type: 'ACCOUNT', id: readerAccount.id, permission: 'VIEW' }, { type: 'APIKEY', id: authorApiKey.id, permission: 'EDIT' }, ], }); @@ -340,17 +305,15 @@ describe('DashboardPermissionController', () => { id: dashboardId2, access: [{ type: 'ACCOUNT', id: authorAccount.id, permission: 'VIEW' }], }; - validate.mockReturnValueOnce(query3); const response3 = await server .post('/dashboard_permission/update') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query3); + response3.body = omitTime(response3.body); expect(response3.body).toMatchObject({ id: dashboardId2, - create_time: response3.body.create_time, - update_time: response3.body.update_time, owner_id: superadminLogin.account.id, owner_type: 'ACCOUNT', access: [{ type: 'ACCOUNT', id: authorAccount.id, permission: 'VIEW' }], @@ -362,7 +325,6 @@ describe('DashboardPermissionController', () => { id: dashboardId2, access: [], }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard_permission/update') @@ -378,7 +340,6 @@ describe('DashboardPermissionController', () => { id: dashboardId5, access: [], }; - validate.mockReturnValueOnce(query2); const response2 = await server .post('/dashboard_permission/update') @@ -397,7 +358,6 @@ describe('DashboardPermissionController', () => { const query1: DashboardIDRequest = { id: dashboardId1, }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard/details') .set('Authorization', `Bearer ${readerLogin.token}`) @@ -407,7 +367,6 @@ describe('DashboardPermissionController', () => { const query2: DashboardIDRequest = { id: dashboardId2, }; - validate.mockReturnValueOnce(query2); const response2 = await server .post('/dashboard/details') .set('Authorization', `Bearer ${authorLogin.token}`) @@ -417,7 +376,6 @@ describe('DashboardPermissionController', () => { const query3: DashboardIDRequest = { id: dashboardId1, }; - validate.mockReturnValueOnce(query3); const response3 = await server .post('/dashboard/details') .set('Authorization', `Bearer ${superadminLogin.token}`) @@ -427,12 +385,10 @@ describe('DashboardPermissionController', () => { const authentication = createAuthStruct(authorApiKey, { id: dashboardId1, }); - validate.mockReturnValueOnce(authentication); const query4: DashboardUpdateRequest = { id: dashboardId1, authentication, }; - validate.mockReturnValueOnce(query4); const response4 = await server.put('/dashboard/update').send(query4); expect(response4.body.id).toEqual(dashboardId1); }); @@ -441,8 +397,6 @@ describe('DashboardPermissionController', () => { const query1: DashboardIDRequest = { id: dashboardId1, }; - validate.mockReturnValueOnce(query1); - const response1 = await server .post('/dashboard/details') .set('Authorization', `Bearer ${authorLogin.token}`) @@ -451,12 +405,9 @@ describe('DashboardPermissionController', () => { code: 'FORBIDDEN', detail: { message: 'Insufficient privileges for this dashboard' }, }); - const query2: DashboardIDRequest = { id: dashboardId2, }; - validate.mockReturnValueOnce(query2); - const response2 = await server .put('/dashboard/update') .set('Authorization', `Bearer ${authorLogin.token}`) @@ -475,18 +426,17 @@ describe('DashboardPermissionController', () => { owner_id: authorApiKey.id, owner_type: 'APIKEY', }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard_permission/updateOwner') .set('Authorization', `Bearer ${superadminLogin.token}`) .send(query1); + + response1.body = omitTime(response1.body); expect(response1.body).toMatchObject({ id: dashboardId1, - create_time: response1.body.create_time, - update_time: response1.body.update_time, owner_id: authorApiKey.id, owner_type: 'APIKEY', - access: [{ id: readerAccount.id, type: 'ACCOUNT', permission: 'EDIT' }], + access: [{ id: readerAccount.id, type: 'ACCOUNT', permission: 'VIEW' }], }); }); @@ -496,7 +446,6 @@ describe('DashboardPermissionController', () => { owner_id: readerAccount.id, owner_type: 'ACCOUNT', }; - validate.mockReturnValueOnce(query1); const response1 = await server .post('/dashboard_permission/updateOwner') .set('Authorization', `Bearer ${superadminLogin.token}`) diff --git a/api/tests/e2e/11_dashboard_content.test.ts b/api/tests/e2e/11_dashboard_content.test.ts new file mode 100644 index 000000000..111c595ab --- /dev/null +++ b/api/tests/e2e/11_dashboard_content.test.ts @@ -0,0 +1,623 @@ +import { connectionHook, createAuthStruct } from './jest.util'; +import Dashboard from '~/models/dashboard'; +import DashboardContent from '~/models/dashboard_content'; +import { dashboardDataSource } from '~/data_sources/dashboard'; +import request from 'supertest'; +import { app } from '~/server'; +import { DashboardIDRequest, DashboardUpdateRequest } from '~/api_models/dashboard'; +import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; +import { notFoundId } from './constants'; +import ApiKey from '~/models/apiKey'; +import DashboardPermission from '~/models/dashboard_permission'; +import { + DashboardContentCreateRequest, + DashboardContentListRequest, + DashboardContentIDRequest, + DashboardContentUpdateRequest, +} from '~/api_models/dashboard_content'; +import { omitTime } from '~/utils/helpers'; + +describe('DashboardContentController', () => { + connectionHook(); + let presetDashboard: Dashboard; + let presetDashboardContent1: DashboardContent; + let presetDashboardContent2: DashboardContent; + let superadminLogin: AccountLoginResponse; + let dashboard1: Dashboard; + let dashboard1Content1: DashboardContent; + let dashboard1Content2: DashboardContent; + let dashboard2: Dashboard; + let dashboard2Content1: DashboardContent; + let dashboard2Content2: DashboardContent; + let apiKey: ApiKey; + const server = request(app); + + beforeAll(async () => { + const query: AccountLoginRequest = { + name: 'superadmin', + password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', + }; + + const response = await server.post('/account/login').send(query); + + superadminLogin = response.body; + + const presetData = new Dashboard(); + presetData.name = 'preset'; + presetData.is_preset = true; + presetData.is_removed = true; + presetData.group = 'dashboard_content'; + presetDashboard = await dashboardDataSource.getRepository(Dashboard).save(presetData); + + const presetDashboardPermission = new DashboardPermission(); + presetDashboardPermission.id = presetDashboard.id; + presetDashboardPermission.owner_id = superadminLogin.account.id; + presetDashboardPermission.owner_type = 'ACCOUNT'; + await dashboardDataSource.getRepository(DashboardPermission).save(presetDashboardPermission); + + const presetContent1Data = new DashboardContent(); + presetContent1Data.dashboard_id = presetDashboard.id; + presetContent1Data.name = 'presetContent1'; + presetContent1Data.content = {}; + presetDashboardContent1 = await dashboardDataSource.getRepository(DashboardContent).save(presetContent1Data); + + const presetContent2Data = new DashboardContent(); + presetContent2Data.dashboard_id = presetDashboard.id; + presetContent2Data.name = 'presetContent2'; + presetContent2Data.content = {}; + presetDashboardContent2 = await dashboardDataSource.getRepository(DashboardContent).save(presetContent2Data); + + const dashboard1Data = new Dashboard(); + dashboard1Data.name = 'content_dashboard1'; + dashboard1Data.group = 'dashboard_content'; + dashboard1 = await dashboardDataSource.getRepository(Dashboard).save(dashboard1Data); + + const dashboard1Permission = new DashboardPermission(); + dashboard1Permission.id = dashboard1Data.id; + dashboard1Permission.owner_id = superadminLogin.account.id; + dashboard1Permission.owner_type = 'ACCOUNT'; + await dashboardDataSource.getRepository(DashboardPermission).save(dashboard1Permission); + + const dashboard2Data = new Dashboard(); + dashboard2Data.name = 'content_dashboard2'; + dashboard2Data.group = 'dashboard_content'; + dashboard2 = await dashboardDataSource.getRepository(Dashboard).save(dashboard2Data); + + const dashboard2Permission = new DashboardPermission(); + dashboard2Permission.id = dashboard2Data.id; + dashboard2Permission.owner_id = superadminLogin.account.id; + dashboard2Permission.owner_type = 'ACCOUNT'; + await dashboardDataSource.getRepository(DashboardPermission).save(dashboard2Permission); + + apiKey = await dashboardDataSource.getRepository(ApiKey).findOneBy({ name: 'key1' }); + }); + + describe('create', () => { + it('should create successfully', async () => { + const request1: DashboardContentCreateRequest = { + dashboard_id: dashboard1.id, + name: 'dashboard1_content1', + content: {}, + }; + + const response1 = await server + .post('/dashboard_content/create') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(request1); + + response1.body = omitTime(response1.body); + dashboard1Content1 = response1.body; + expect(response1.body).toMatchObject({ + id: response1.body.id, + dashboard_id: dashboard1.id, + name: 'dashboard1_content1', + content: {}, + }); + + const request2: DashboardContentCreateRequest = { + dashboard_id: dashboard1.id, + name: 'dashboard1_content2', + content: {}, + }; + + const response2 = await server + .post('/dashboard_content/create') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(request2); + + response2.body = omitTime(response2.body); + dashboard1Content2 = response2.body; + expect(response2.body).toMatchObject({ + id: response2.body.id, + dashboard_id: dashboard1.id, + name: 'dashboard1_content2', + content: {}, + }); + + const request3: DashboardContentCreateRequest = { + dashboard_id: dashboard2.id, + name: 'dashboard2_content1', + content: {}, + }; + + const response3 = await server + .post('/dashboard_content/create') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(request3); + + response3.body = omitTime(response3.body); + dashboard2Content1 = response3.body; + expect(response3.body).toMatchObject({ + id: response3.body.id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content1', + content: {}, + }); + + const request4: DashboardContentCreateRequest = { + dashboard_id: dashboard2.id, + name: 'dashboard2_content2', + content: {}, + }; + + const response4 = await server + .post('/dashboard_content/create') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(request4); + + response4.body = omitTime(response4.body); + dashboard2Content2 = response4.body; + expect(response4.body).toMatchObject({ + id: response4.body.id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content2', + content: {}, + }); + }); + + it('should fail if duplicate name', async () => { + const request: DashboardContentCreateRequest = { + dashboard_id: dashboard1.id, + name: 'dashboard1_content1', + content: {}, + }; + + const response = await server + .post('/dashboard_content/create') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(request); + + expect(response.body).toMatchObject({ + code: 'BAD_REQUEST', + detail: { + message: 'A dashboard content with that name already exists', + }, + }); + }); + }); + + describe('list', () => { + it('no filters', async () => { + const query1: DashboardContentListRequest = { + dashboard_id: dashboard1.id, + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'name', order: 'ASC' }], + }; + + const response1 = await server + .post('/dashboard_content/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + response1.body.data = response1.body.data.map(omitTime); + expect(response1.body).toMatchObject({ + total: 2, + offset: 0, + data: [ + { + id: response1.body.data[0].id, + dashboard_id: dashboard1.id, + name: 'dashboard1_content1', + content: {}, + }, + { + id: response1.body.data[1].id, + dashboard_id: dashboard1.id, + name: 'dashboard1_content2', + content: {}, + }, + ], + }); + + const query2: DashboardContentListRequest = { + dashboard_id: dashboard2.id, + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'name', order: 'ASC' }], + }; + + const response2 = await server + .post('/dashboard_content/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query2); + + response2.body.data = response2.body.data.map(omitTime); + expect(response2.body).toMatchObject({ + total: 2, + offset: 0, + data: [ + { + id: response2.body.data[0].id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content1', + content: {}, + }, + { + id: response2.body.data[1].id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content2', + content: {}, + }, + ], + }); + }); + + it('with filters', async () => { + const query: DashboardContentListRequest = { + dashboard_id: dashboard1.id, + filter: { name: { value: 'dashboard1_content1', isFuzzy: true } }, + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'name', order: 'ASC' }], + }; + + const response = await server + .post('/dashboard_content/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + response.body.data = response.body.data.map(omitTime); + expect(response.body).toMatchObject({ + total: 1, + offset: 0, + data: [ + { + id: response.body.data[0].id, + dashboard_id: dashboard1.id, + name: 'dashboard1_content1', + content: {}, + }, + ], + }); + }); + }); + + describe('details', () => { + it('should return successfully', async () => { + const query: DashboardContentIDRequest = { + id: dashboard1Content1.id, + }; + + const response = await server + .post('/dashboard_content/details') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + response.body = omitTime(response.body); + expect(response.body).toMatchObject(dashboard1Content1); + }); + + it('should fail', async () => { + const query: DashboardIDRequest = { + id: notFoundId, + }; + + const response = await server + .post('/dashboard_content/details') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + expect(response.body.code).toEqual('NOT_FOUND'); + expect(response.body.detail.message).toContain('Could not find any entity of type "DashboardContent" matching'); + expect(response.body.detail.message).toContain(notFoundId); + }); + }); + + describe('update', () => { + it('should update successfully', async () => { + const query1: DashboardContentUpdateRequest = { + id: dashboard1Content1.id, + name: 'dashboard1_content1_updated', + content: { tmp: 'tmp' }, + }; + + const response1 = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + response1.body = omitTime(response1.body); + expect(response1.body).toMatchObject({ + ...dashboard1Content1, + name: 'dashboard1_content1_updated', + }); + + const query2: DashboardContentUpdateRequest = { + id: dashboard2Content1.id, + name: 'dashboard2_content1_updated', + content: { tmp: 'tmp' }, + }; + + const response2 = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query2); + + response2.body = omitTime(response2.body); + expect(response2.body).toMatchObject({ + ...dashboard2Content1, + name: 'dashboard2_content1_updated', + }); + + const query3: DashboardContentUpdateRequest = { + id: dashboard2Content2.id, + name: 'dashboard2_content2_updated', + content: { tmp: 'tmp' }, + }; + + const response3 = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query3); + + response3.body = omitTime(response3.body); + expect(response3.body).toMatchObject({ + ...dashboard2Content2, + name: 'dashboard2_content2_updated', + }); + + const query4: DashboardContentUpdateRequest = { + id: dashboard1Content2.id, + name: 'dashboard1_content2_updated', + content: { tmp: 'tmp1' }, + }; + + const response4 = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query4); + + response4.body = omitTime(response4.body); + expect(response4.body).toMatchObject({ + ...dashboard1Content2, + name: 'dashboard1_content2_updated', + }); + }); + + it('should fail if not found', async () => { + const query: DashboardContentUpdateRequest = { + id: notFoundId, + name: 'not_found', + }; + + const response = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + expect(response.body.code).toEqual('NOT_FOUND'); + expect(response.body.detail.message).toContain('Could not find any entity of type "DashboardContent" matching'); + expect(response.body.detail.message).toContain(notFoundId); + }); + + it('should update preset dashboard successfully', async () => { + const query: DashboardContentUpdateRequest = { + id: presetDashboardContent1.id, + name: 'presetContent1_updated', + content: { tmp: 'tmp' }, + }; + + const response = await server + .put('/dashboard_content/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + response.body = omitTime(response.body); + expect(response.body).toMatchObject({ + ...omitTime(presetDashboardContent1), + name: 'presetContent1_updated', + content: { tmp: 'tmp' }, + }); + }); + + it('should fail if not SUPERADMIN', async () => { + const authentication = createAuthStruct(apiKey, { + id: presetDashboardContent1.id, + name: 'presetContent1_updated', + }); + + const query: DashboardUpdateRequest = { + id: presetDashboardContent1.id, + name: 'presetContent1_updated', + authentication, + }; + + const response = await server.put('/dashboard_content/update').send(query); + + expect(response.body).toMatchObject({ + code: 'BAD_REQUEST', + detail: { message: '只有超级管理员才能编辑预设报表内容' }, + }); + }); + }); + + describe('Dashboard update content_id', () => { + it('should update successfully', async () => { + const query1: DashboardUpdateRequest = { + id: dashboard1.id, + content_id: dashboard1Content1.id, + }; + + const response1 = await server + .put('/dashboard/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + response1.body = omitTime(response1.body); + expect(response1.body).toMatchObject({ + ...omitTime(dashboard1), + content_id: dashboard1Content1.id, + }); + + const query2: DashboardUpdateRequest = { + id: dashboard2.id, + content_id: dashboard2Content1.id, + }; + + const response2 = await server + .put('/dashboard/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query2); + + response2.body = omitTime(response2.body); + expect(response2.body).toMatchObject({ + ...omitTime(dashboard2), + content_id: dashboard2Content1.id, + }); + }); + + it('should fail if not found', async () => { + const query: DashboardUpdateRequest = { + id: dashboard1.id, + content_id: notFoundId, + }; + + const response = await server + .put('/dashboard/update') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + expect(response.body).toMatchObject({ + code: 'BAD_REQUEST', + detail: { message: 'That dashboard content does not exist' }, + }); + }); + }); + + describe('delete', () => { + it('should delete successfully', async () => { + const query1: DashboardContentIDRequest = { + id: dashboard1Content1.id, + }; + + const response1 = await server + .post('/dashboard_content/delete') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + expect(response1.body).toMatchObject({ id: dashboard1Content1.id }); + + const query2: DashboardIDRequest = { + id: dashboard1.id, + }; + + const response2 = await server + .post('/dashboard/details') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query2); + + response2.body = omitTime(response2.body); + expect(response2.body).toMatchObject({ + ...omitTime(dashboard1), + content_id: null, + }); + }); + + it('deleting dashboard should also delete all content', async () => { + const query1: DashboardContentListRequest = { + dashboard_id: dashboard2.id, + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'name', order: 'ASC' }], + }; + + const response1 = await server + .post('/dashboard_content/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + response1.body.data = response1.body.data.map(omitTime); + expect(response1.body).toMatchObject({ + total: 2, + offset: 0, + data: [ + { + id: response1.body.data[0].id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content1_updated', + content: {}, + }, + { + id: response1.body.data[1].id, + dashboard_id: dashboard2.id, + name: 'dashboard2_content2_updated', + content: {}, + }, + ], + }); + + await dashboardDataSource.manager.getRepository(Dashboard).delete(dashboard2.id); + + const response2 = await server + .post('/dashboard_content/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query1); + + expect(response2.body.code).toEqual('NOT_FOUND'); + expect(response2.body.detail.message).toContain( + 'Could not find any entity of type "DashboardPermission" matching', + ); + }); + + it('should fail if not found', async () => { + const query: DashboardContentIDRequest = { + id: notFoundId, + }; + + const response = await server + .post('/dashboard_content/delete') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + expect(response.body.code).toEqual('NOT_FOUND'); + expect(response.body.detail.message).toContain('Could not find any entity of type "DashboardContent" matching'); + expect(response.body.detail.message).toContain(notFoundId); + }); + + it('should delete preset dashboard content successfully if SUPERADMIN', async () => { + const query: DashboardContentIDRequest = { + id: presetDashboardContent1.id, + }; + + const response = await server + .post('/dashboard_content/delete') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + expect(response.body).toMatchObject({ id: presetDashboardContent1.id }); + }); + + it('should fail to delete preset dashboard content if not SUPERADMIN', async () => { + const authentication = createAuthStruct(apiKey, { id: presetDashboardContent2.id }); + + const query: DashboardContentIDRequest = { + id: presetDashboardContent2.id, + authentication, + }; + + const response = await server.post('/dashboard_content/delete').send(query); + + expect(response.body).toMatchObject({ + code: 'BAD_REQUEST', + detail: { message: '只有超级管理员才能删除预设报表内容' }, + }); + }); + }); +}); diff --git a/api/tests/e2e/12_dashboard_content_changelog.test.ts b/api/tests/e2e/12_dashboard_content_changelog.test.ts new file mode 100644 index 000000000..c9e0de8af --- /dev/null +++ b/api/tests/e2e/12_dashboard_content_changelog.test.ts @@ -0,0 +1,105 @@ +import { connectionHook } from './jest.util'; +import { app } from '~/server'; +import request from 'supertest'; +import { AccountLoginRequest, AccountLoginResponse } from '~/api_models/account'; +import { DashboardContentChangelogListRequest } from '~/api_models/dashboard_content_changelog'; +import { omitTime } from '~/utils/helpers'; + +describe('DashboardChangelogController', () => { + connectionHook(); + let superadminLogin: AccountLoginResponse; + let changelogDashboardContentId: string; + + const server = request(app); + + beforeAll(async () => { + const query: AccountLoginRequest = { + name: 'superadmin', + password: process.env.SUPER_ADMIN_PASSWORD ?? 'secret', + }; + + const response = await server.post('/account/login').send(query); + superadminLogin = response.body; + }); + + describe('list', () => { + it('no filters', async () => { + const query: DashboardContentChangelogListRequest = { + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'create_time', order: 'ASC' }], + }; + + const response = await server + .post('/dashboard_content_changelog/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + response.body.data = response.body.data.map(omitTime); + expect(response.body).toMatchObject({ + total: 1, + offset: 0, + data: [ + { + id: response.body.data[0].id, + dashboard_content_id: response.body.data[0].dashboard_content_id, + diff: response.body.data[0].diff, + }, + ], + }); + + expect(response.body.data[0].diff).toContain('diff --git a/data.json b/data.json'); + expect(response.body.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(response.body.data[0].diff).toContain('@@ -1,6 +1,8 @@'); + expect(response.body.data[0].diff).toContain( + '-\t"name": "dashboard1_content2",\n' + + '-\t"content": {}\n' + + '+\t"name": "dashboard1_content2_updated",\n' + + '+\t"content": {\n' + + '+\t\t"tmp": "tmp1"\n' + + '+\t}\n' + + ' }\n', + ); + + changelogDashboardContentId = response.body.data[0].dashboard_content_id; + }); + + it('with filters', async () => { + const query: DashboardContentChangelogListRequest = { + filter: { dashboard_content_id: { value: changelogDashboardContentId, isFuzzy: false } }, + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'create_time', order: 'ASC' }], + }; + + const response = await server + .post('/dashboard_content_changelog/list') + .set('Authorization', `Bearer ${superadminLogin.token}`) + .send(query); + + response.body.data = response.body.data.map(omitTime); + expect(response.body).toMatchObject({ + total: 1, + offset: 0, + data: [ + { + id: response.body.data[0].id, + dashboard_content_id: response.body.data[0].dashboard_content_id, + diff: response.body.data[0].diff, + }, + ], + }); + + expect(response.body.data[0].diff).toContain('diff --git a/data.json b/data.json'); + expect(response.body.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(response.body.data[0].diff).toContain('@@ -1,6 +1,8 @@'); + expect(response.body.data[0].diff).toContain( + '-\t"name": "dashboard1_content2",\n' + + '-\t"content": {}\n' + + '+\t"name": "dashboard1_content2_updated",\n' + + '+\t"content": {\n' + + '+\t\t"tmp": "tmp1"\n' + + '+\t}\n' + + ' }\n', + ); + }); + }); +}); diff --git a/api/tests/e2e/jest.mock.ts b/api/tests/e2e/jest.mock.ts new file mode 100644 index 000000000..06ced08b7 --- /dev/null +++ b/api/tests/e2e/jest.mock.ts @@ -0,0 +1,6 @@ +import { NextFunction, Request, Response } from 'express'; + +jest.mock('~/middleware/validation', () => ({ + validate: () => (req: Request, res: Response, next: NextFunction) => next(), + validateClass: (type, data) => data, +})); diff --git a/api/tests/integration/01_account.test.ts b/api/tests/integration/01_account.test.ts index 4f681d5f1..c63b5cd72 100644 --- a/api/tests/integration/01_account.test.ts +++ b/api/tests/integration/01_account.test.ts @@ -1,13 +1,14 @@ -import { connectionHook } from './jest.util'; +import { connectionHook, sleep } from './jest.util'; import { AccountService } from '~/services/account.service'; import { notFoundId } from './constants'; import { ROLE_TYPES } from '~/api_models/role'; -import { EntityNotFoundError, QueryFailedError } from 'typeorm'; +import { EntityNotFoundError } from 'typeorm'; import { Account as AccountApiModel, AccountLoginResponse } from '~/api_models/account'; import { ApiError, BAD_REQUEST, INVALID_CREDENTIALS, PASSWORD_MISMATCH } from '~/utils/errors'; import { dashboardDataSource } from '~/data_sources/dashboard'; import Account from '~/models/account'; import { DEFAULT_LANGUAGE } from '~/utils/constants'; +import { omitTime } from '~/utils/helpers'; describe('AccountService', () => { connectionHook(); @@ -30,13 +31,11 @@ describe('AccountService', () => { ROLE_TYPES.ADMIN, DEFAULT_LANGUAGE, ); - expect(account5).toMatchObject({ + expect(omitTime(account5)).toMatchObject({ id: account5.id, name: 'account5', email: 'account5@test.com', role_id: ROLE_TYPES.ADMIN, - create_time: account5.create_time, - update_time: account5.update_time, }); }); @@ -55,12 +54,7 @@ describe('AccountService', () => { expect(account5Login).toMatchObject({ token: account5Login.token, account: { - id: account5.id, - name: account5.name, - email: account5.email, - role_id: account5.role_id, - create_time: account5.create_time, - update_time: account5.update_time, + ...omitTime(account5), }, }); }); @@ -75,10 +69,8 @@ describe('AccountService', () => { describe('getByToken', () => { it('should return account', async () => { const account = await AccountService.getByToken(account5Login.token); - expect(account).toMatchObject({ + expect(omitTime(account)).toMatchObject({ id: account5.id, - create_time: account5.create_time, - update_time: account5.update_time, name: 'account5', email: 'account5@test.com', role_id: ROLE_TYPES.ADMIN, @@ -110,10 +102,8 @@ describe('AccountService', () => { 'account5_updated@test.test', DEFAULT_LANGUAGE, ); - expect(account5).toMatchObject({ + expect(omitTime(account5)).toMatchObject({ id: account5.id, - create_time: account5.create_time, - update_time: account5.update_time, name: 'account5_updated', email: 'account5_updated@test.test', role_id: ROLE_TYPES.ADMIN, @@ -145,10 +135,8 @@ describe('AccountService', () => { ROLE_TYPES.SUPERADMIN, DEFAULT_LANGUAGE, ); - expect(account5).toMatchObject({ + expect(omitTime(account5)).toMatchObject({ id: account5.id, - create_time: account5.create_time, - update_time: account5.update_time, name: 'account5_updated_again', email: 'account5_updated_again@test.test', role_id: ROLE_TYPES.READER, @@ -217,12 +205,7 @@ describe('AccountService', () => { expect(login).toMatchObject({ token: login.token, account: { - id: account5.id, - create_time: account5.create_time, - update_time: login.account.update_time, - name: 'account5_updated_again', - email: 'account5_updated_again@test.test', - role_id: ROLE_TYPES.READER, + ...omitTime(account5), }, }); @@ -236,12 +219,7 @@ describe('AccountService', () => { expect(login).toMatchObject({ token: login.token, account: { - id: account5.id, - create_time: account5.create_time, - update_time: login.account.update_time, - name: 'account5_updated_again', - email: 'account5_updated_again@test.test', - role_id: ROLE_TYPES.READER, + ...omitTime(account5), }, }); }); @@ -338,6 +316,8 @@ describe('AccountService', () => { }, ], }); + + await sleep(5000); }); it('should fail because not found', async () => { diff --git a/api/tests/integration/02_apikey.test.ts b/api/tests/integration/02_apikey.test.ts index f30b662d9..2fd0581ca 100644 --- a/api/tests/integration/02_apikey.test.ts +++ b/api/tests/integration/02_apikey.test.ts @@ -2,7 +2,7 @@ import { connectionHook, sleep } from './jest.util'; import { ApiService } from '~/services/api.service'; import { notFoundId } from './constants'; import { ROLE_TYPES } from '~/api_models/role'; -import { EntityNotFoundError, QueryFailedError } from 'typeorm'; +import { EntityNotFoundError } from 'typeorm'; import { ApiError, BAD_REQUEST } from '~/utils/errors'; import { cryptSign } from '~/utils/helpers'; import ApiKey from '~/models/apiKey'; diff --git a/api/tests/integration/04_dashboard.test.ts b/api/tests/integration/04_dashboard.test.ts index c0666add9..153c21c26 100644 --- a/api/tests/integration/04_dashboard.test.ts +++ b/api/tests/integration/04_dashboard.test.ts @@ -1,12 +1,13 @@ import { connectionHook } from './jest.util'; import { DashboardService } from '~/services/dashboard.service'; import Dashboard from '~/models/dashboard'; -import { EntityNotFoundError, QueryFailedError } from 'typeorm'; +import { EntityNotFoundError } from 'typeorm'; import { ROLE_TYPES } from '~/api_models/role'; import { ApiError, BAD_REQUEST } from '~/utils/errors'; import { notFoundId } from './constants'; import { dashboardDataSource } from '~/data_sources/dashboard'; import { DEFAULT_LANGUAGE } from '~/utils/constants'; +import { omitTime } from '~/utils/helpers'; describe('DashboardService', () => { connectionHook(); @@ -21,11 +22,11 @@ describe('DashboardService', () => { describe('create', () => { it('should create successfully', async () => { - dashboard3 = await dashboardService.create('dashboard3', {}, '2', DEFAULT_LANGUAGE); + dashboard3 = await dashboardService.create('dashboard3', '2', DEFAULT_LANGUAGE); }); it('should fail if duplicate name', async () => { - await expect(dashboardService.create('dashboard3', {}, '2', DEFAULT_LANGUAGE)).rejects.toThrowError( + await expect(dashboardService.create('dashboard3', '2', DEFAULT_LANGUAGE)).rejects.toThrowError( new ApiError(BAD_REQUEST, { message: 'A dashboard with that name already exists' }), ); }); @@ -37,6 +38,7 @@ describe('DashboardService', () => { page: 1, pagesize: 20, }); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ total: 3, offset: 0, @@ -44,24 +46,7 @@ describe('DashboardService', () => { { id: dashboards[0].id, name: 'dashboard1', - content: { - definition: { - queries: [ - { - id: 'pgQuery1', - type: 'postgresql', - key: 'pg', - }, - { - id: 'httpQuery1', - type: 'http', - key: 'jsonplaceholder', - }, - ], - }, - }, - create_time: dashboards[0].create_time, - update_time: dashboards[0].update_time, + content_id: '9afa4842-77ef-4b19-8a53-034cb41ee7f6', is_removed: true, is_preset: false, group: '1', @@ -69,24 +54,7 @@ describe('DashboardService', () => { { id: dashboards[1].id, name: 'dashboard2', - content: { - definition: { - queries: [ - { - id: 'pgQuery2', - type: 'postgresql', - key: 'pg', - }, - { - id: 'httpQuery2', - type: 'http', - key: 'jsonplaceholder', - }, - ], - }, - }, - create_time: dashboards[1].create_time, - update_time: dashboards[1].update_time, + content_id: '5959a66b-5b6b-4509-9d87-bb8b96100658', is_removed: false, is_preset: true, group: '1', @@ -94,9 +62,7 @@ describe('DashboardService', () => { { id: dashboard3.id, name: 'dashboard3', - content: {}, - create_time: dashboard3.create_time, - update_time: dashboard3.update_time, + content_id: null, is_removed: false, is_preset: false, group: '2', @@ -111,6 +77,7 @@ describe('DashboardService', () => { [{ field: 'create_time', order: 'ASC' }], { page: 1, pagesize: 20 }, ); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ total: 1, offset: 0, @@ -118,9 +85,7 @@ describe('DashboardService', () => { { id: dashboard3.id, name: 'dashboard3', - content: {}, - create_time: dashboard3.create_time, - update_time: dashboard3.update_time, + content_id: null, is_removed: false, is_preset: false, group: '2', @@ -165,18 +130,25 @@ describe('DashboardService', () => { DEFAULT_LANGUAGE, ROLE_TYPES.SUPERADMIN, ); - expect(updatedDashboard).toMatchObject({ - ...dashboard3, + expect(omitTime(updatedDashboard)).toMatchObject({ + ...omitTime(dashboard3), name: 'dashboard3_updated', is_removed: true, group: '2_updated', - update_time: updatedDashboard.update_time, }); }); it('should fail if not found', async () => { await expect( - dashboardService.update(notFoundId, 'xxxx', {}, false, '2_updated', DEFAULT_LANGUAGE, ROLE_TYPES.SUPERADMIN), + dashboardService.update( + notFoundId, + 'xxxx', + undefined, + false, + '2_updated', + DEFAULT_LANGUAGE, + ROLE_TYPES.SUPERADMIN, + ), ).rejects.toThrowError(EntityNotFoundError); }); @@ -190,12 +162,11 @@ describe('DashboardService', () => { DEFAULT_LANGUAGE, ROLE_TYPES.SUPERADMIN, ); - expect(updatedDashboard).toMatchObject({ - ...dashboards[1], + expect(omitTime(updatedDashboard)).toMatchObject({ + ...omitTime(dashboards[1]), name: 'dashboard2_updated', is_removed: false, group: '1_updated', - update_time: updatedDashboard.update_time, }); }); @@ -204,7 +175,7 @@ describe('DashboardService', () => { dashboardService.update( dashboards[1].id, 'dashboard2_updated', - {}, + undefined, false, '1_updated', DEFAULT_LANGUAGE, @@ -217,12 +188,11 @@ describe('DashboardService', () => { describe('delete', () => { it('should delete successfully', async () => { const deletedDashboard = await dashboardService.delete(dashboard3.id, DEFAULT_LANGUAGE, ROLE_TYPES.SUPERADMIN); - expect(deletedDashboard).toMatchObject({ - ...dashboard3, + expect(omitTime(deletedDashboard)).toMatchObject({ + ...omitTime(dashboard3), name: 'dashboard3_updated', is_removed: true, group: '2_updated', - update_time: deletedDashboard.update_time, }); }); @@ -234,12 +204,11 @@ describe('DashboardService', () => { it('should delete preset dashboard successfully if SUPERADMIN', async () => { const deletedDashboard = await dashboardService.delete(dashboards[1].id, DEFAULT_LANGUAGE, ROLE_TYPES.SUPERADMIN); - expect(deletedDashboard).toMatchObject({ - ...dashboards[1], + expect(omitTime(deletedDashboard)).toMatchObject({ + ...omitTime(dashboards[1]), name: 'dashboard2_updated', is_removed: true, group: '1_updated', - update_time: deletedDashboard.update_time, }); }); diff --git a/api/tests/integration/05_datasource.test.ts b/api/tests/integration/05_datasource.test.ts index dd0639e6f..77db74f78 100644 --- a/api/tests/integration/05_datasource.test.ts +++ b/api/tests/integration/05_datasource.test.ts @@ -2,11 +2,12 @@ import { connectionHook, sleep } from './jest.util'; import { DataSourceService } from '~/services/datasource.service'; import DataSource from '~/models/datasource'; import { dashboardDataSource } from '~/data_sources/dashboard'; -import { EntityNotFoundError, QueryFailedError } from 'typeorm'; +import { EntityNotFoundError } from 'typeorm'; import { ApiError, BAD_REQUEST } from '~/utils/errors'; import { notFoundId, pgSourceConfig } from './constants'; import { maybeDecryptPassword } from '~/utils/encryption'; import { DEFAULT_LANGUAGE } from '~/utils/constants'; +import { omitTime } from '~/utils/helpers'; describe('DataSourceService', () => { connectionHook(); @@ -23,13 +24,11 @@ describe('DataSourceService', () => { describe('create', () => { it('should create successfully', async () => { pgDatasource = await datasourceService.create('postgresql', 'pg_2', pgSourceConfig, DEFAULT_LANGUAGE); - expect(pgDatasource).toMatchObject({ + expect(omitTime(pgDatasource)).toMatchObject({ type: 'postgresql', key: 'pg_2', config: pgSourceConfig, id: pgDatasource.id, - create_time: pgDatasource.create_time, - update_time: pgDatasource.update_time, is_preset: false, }); @@ -45,13 +44,11 @@ describe('DataSourceService', () => { }, DEFAULT_LANGUAGE, ); - expect(httpDatasource).toMatchObject({ + expect(omitTime(httpDatasource)).toMatchObject({ type: 'http', key: 'jsonplaceholder_2', config: dataSources[0].config, id: httpDatasource.id, - create_time: httpDatasource.create_time, - update_time: httpDatasource.update_time, is_preset: false, }); }); @@ -152,38 +149,34 @@ describe('DataSourceService', () => { describe('rename', () => { it('should fail if new key is same as old key', async () => { - await expect(datasourceService.rename(pgDatasource.id, pgDatasource.key, DEFAULT_LANGUAGE)).rejects.toThrowError( - new ApiError(BAD_REQUEST, { message: 'New key is the same as the old one' }), - ); + await expect( + datasourceService.rename(pgDatasource.id, pgDatasource.key, DEFAULT_LANGUAGE, null, null), + ).rejects.toThrowError(new ApiError(BAD_REQUEST, { message: 'New key is the same as the old one' })); }); it('should fail if entity not found', async () => { - await expect(datasourceService.rename(notFoundId, '', DEFAULT_LANGUAGE)).rejects.toThrowError( + await expect(datasourceService.rename(notFoundId, '', DEFAULT_LANGUAGE, null, null)).rejects.toThrowError( EntityNotFoundError, ); }); it('should rename successfully', async () => { const newPGKey = pgDatasource.key + '_renamed'; - const pgResult = await datasourceService.rename(pgDatasource.id, newPGKey, DEFAULT_LANGUAGE); - expect(pgResult).toMatchObject({ + const pgResult = await datasourceService.rename(pgDatasource.id, newPGKey, DEFAULT_LANGUAGE, null, null); + expect(omitTime(pgResult)).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', params: { type: pgDatasource.type, old_key: pgDatasource.key, new_key: newPGKey }, id: pgResult.id, - create_time: pgResult.create_time, - update_time: pgResult.update_time, }); const newHTTPKey = httpDatasource.key + '_renamed'; - const httpResult = await datasourceService.rename(httpDatasource.id, newHTTPKey, DEFAULT_LANGUAGE); - expect(httpResult).toMatchObject({ + const httpResult = await datasourceService.rename(httpDatasource.id, newHTTPKey, DEFAULT_LANGUAGE, null, null); + expect(omitTime(httpResult)).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', params: { type: httpDatasource.type, old_key: httpDatasource.key, new_key: newHTTPKey }, id: httpResult.id, - create_time: httpResult.create_time, - update_time: httpResult.update_time, }); await sleep(3000); diff --git a/api/tests/integration/06_query.test.ts b/api/tests/integration/06_query.test.ts index 81a97e120..a073972df 100644 --- a/api/tests/integration/06_query.test.ts +++ b/api/tests/integration/06_query.test.ts @@ -52,8 +52,8 @@ describe('QueryService', () => { headers: { 'Content-Type': 'application/json' }, url: '/posts/1', }; - const validate = jest.spyOn(validation, 'validate'); - validate.mockReturnValueOnce(query); + const validateClass = jest.spyOn(validation, 'validateClass'); + validateClass.mockReturnValueOnce(query); const results = await queryService.query('http', 'jsonplaceholder', JSON.stringify(query)); expect(results).toMatchObject({ userId: 1, @@ -75,8 +75,8 @@ describe('QueryService', () => { headers: { 'Content-Type': 'application/json' }, url: '/posts', }; - const validate = jest.spyOn(validation, 'validate'); - validate.mockReturnValueOnce(query); + const validateClass = jest.spyOn(validation, 'validateClass'); + validateClass.mockReturnValueOnce(query); const results = await queryService.query('http', 'jsonplaceholder', JSON.stringify(query)); expect(results).toMatchObject({ title: 'foo', body: 'bar', userId: 1, id: 101 }); }); @@ -89,8 +89,8 @@ describe('QueryService', () => { headers: { 'Content-Type': 'application/json' }, url: '/posts/1', }; - const validate = jest.spyOn(validation, 'validate'); - validate.mockReturnValueOnce(query); + const validateClass = jest.spyOn(validation, 'validateClass'); + validateClass.mockReturnValueOnce(query); const results = await queryService.query('http', 'jsonplaceholder', JSON.stringify(query)); expect(results).toMatchObject({ title: 'foo', body: 'bar', userId: 1, id: 1 }); }); @@ -103,8 +103,8 @@ describe('QueryService', () => { headers: { 'Content-Type': 'application/json' }, url: '/posts/1', }; - const validate = jest.spyOn(validation, 'validate'); - validate.mockReturnValueOnce(query); + const validateClass = jest.spyOn(validation, 'validateClass'); + validateClass.mockReturnValueOnce(query); const results = await queryService.query('http', 'jsonplaceholder', JSON.stringify(query)); expect(results).toMatchObject({}); }); diff --git a/api/tests/integration/07_job.test.ts b/api/tests/integration/07_job.test.ts index c09cfc3b2..a5303b25a 100644 --- a/api/tests/integration/07_job.test.ts +++ b/api/tests/integration/07_job.test.ts @@ -2,10 +2,16 @@ import { connectionHook, sleep } from './jest.util'; import { JobService } from '~/services/job.service'; import Job from '~/models/job'; import { dashboardDataSource } from '~/data_sources/dashboard'; +import * as crypto from 'crypto'; +import { omitTime } from '~/utils/helpers'; describe('JobService', () => { connectionHook(); let jobService: JobService; + const jobAuthId1 = crypto.randomUUID(); + const jobAuthId2 = crypto.randomUUID(); + const jobAuthId3 = crypto.randomUUID(); + const jobAuthId4 = crypto.randomUUID(); beforeAll(async () => { jobService = new JobService(); @@ -17,48 +23,54 @@ describe('JobService', () => { type: 'postgresql', old_key: 'pg', new_key: 'pg_renamed', + auth_id: null, + auth_type: null, }); - expect(job1).toMatchObject({ + expect(omitTime(job1)).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', - params: { type: 'postgresql', old_key: 'pg', new_key: 'pg_renamed' }, + params: { type: 'postgresql', old_key: 'pg', new_key: 'pg_renamed', auth_id: null, auth_type: null }, id: job1.id, - create_time: job1.create_time, - update_time: job1.update_time, }); const job2 = await JobService.addRenameDataSourceJob({ type: 'http', old_key: 'jsonplaceholder', new_key: 'jsonplaceholder_renamed', + auth_id: null, + auth_type: null, }); - expect(job2).toMatchObject({ + expect(omitTime(job2)).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', - params: { type: 'http', old_key: 'jsonplaceholder', new_key: 'jsonplaceholder_renamed' }, + params: { + type: 'http', + old_key: 'jsonplaceholder', + new_key: 'jsonplaceholder_renamed', + auth_id: null, + auth_type: null, + }, id: job2.id, - create_time: job2.create_time, - update_time: job2.update_time, }); const job3 = await JobService.addRenameDataSourceJob({ type: 'non_existent', old_key: 'old_key', new_key: 'new_key', + auth_id: null, + auth_type: null, }); - expect(job3).toMatchObject({ + expect(omitTime(job3)).toMatchObject({ type: 'RENAME_DATASOURCE', status: 'INIT', - params: { type: 'non_existent', old_key: 'old_key', new_key: 'new_key' }, + params: { type: 'non_existent', old_key: 'old_key', new_key: 'new_key', auth_id: null, auth_type: null }, id: job3.id, - create_time: job3.create_time, - update_time: job3.update_time, }); }); }); describe('processDataSourceRename', () => { - it('manually insert jobs and run', async () => { + it('manually insert jobs', async () => { const jobRepo = dashboardDataSource.getRepository(Job); const job1 = new Job(); job1.type = 'RENAME_DATASOURCE'; @@ -67,6 +79,8 @@ describe('JobService', () => { type: 'postgresql', old_key: 'pg_renamed', new_key: 'pg', + auth_id: null, + auth_type: null, }; await jobRepo.save(job1); @@ -77,6 +91,8 @@ describe('JobService', () => { type: 'http', old_key: 'jsonplaceholder_renamed', new_key: 'jsonplaceholder', + auth_id: null, + auth_type: null, }; await jobRepo.save(job2); @@ -87,6 +103,8 @@ describe('JobService', () => { type: 'non_existent', old_key: 'new_key', new_key: 'old_key', + auth_id: null, + auth_type: null, }; await jobRepo.save(job3); @@ -95,149 +113,258 @@ describe('JobService', () => { }); }); + describe('addFixDashboardPermissionJob', () => { + it('add several jobs', async () => { + const job1 = await JobService.addFixDashboardPermissionJob({ + auth_id: jobAuthId1, + auth_type: 'ACCOUNT', + }); + expect(omitTime(job1)).toMatchObject({ + type: 'FIX_DASHBOARD_PERMISSION', + status: 'INIT', + params: { auth_id: jobAuthId1, auth_type: 'ACCOUNT' }, + id: job1.id, + }); + + const job2 = await JobService.addFixDashboardPermissionJob({ + auth_id: jobAuthId2, + auth_type: 'APIKEY', + }); + expect(omitTime(job2)).toMatchObject({ + type: 'FIX_DASHBOARD_PERMISSION', + status: 'INIT', + params: { auth_id: jobAuthId2, auth_type: 'APIKEY' }, + id: job2.id, + }); + }); + }); + + describe('processFixDashboardPermission', () => { + it('manually insert jobs', async () => { + const jobRepo = dashboardDataSource.getRepository(Job); + const job1 = new Job(); + job1.type = 'FIX_DASHBOARD_PERMISSION'; + job1.status = 'INIT'; + job1.params = { + auth_id: jobAuthId3, + auth_type: 'ACCOUNT', + }; + await jobRepo.save(job1); + + const job2 = new Job(); + job2.type = 'FIX_DASHBOARD_PERMISSION'; + job2.status = 'INIT'; + job2.params = { + auth_id: jobAuthId4, + auth_type: 'APIKEY', + }; + await jobRepo.save(job2); + + await JobService.processFixDashboardPermission(); + await sleep(10000); + }); + }); + describe('list', () => { it('no filters', async () => { const results = await jobService.list(undefined, [{ field: 'create_time', order: 'ASC' }], { page: 1, pagesize: 20, }); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ - total: 10, + total: 14, offset: 0, data: [ { id: results.data[0].id, type: 'FIX_DASHBOARD_PERMISSION', status: 'SUCCESS', - params: {}, + params: { + auth_id: results.data[0].params['auth_id'], + auth_type: 'ACCOUNT', + }, result: { affected_dashboard_permissions: [] }, - create_time: results.data[0].create_time, - update_time: results.data[0].update_time, }, { id: results.data[1].id, type: 'FIX_DASHBOARD_PERMISSION', status: 'SUCCESS', - params: {}, + params: { + auth_id: results.data[1].params['auth_id'], + auth_type: 'APIKEY', + }, result: { affected_dashboard_permissions: [] }, - create_time: results.data[1].create_time, - update_time: results.data[1].update_time, }, { id: results.data[2].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg_2_renamed', old_key: 'pg_2' }, - result: { affected_dashboards: [] }, - create_time: results.data[2].create_time, - update_time: results.data[2].update_time, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg_2_renamed', + old_key: 'pg_2', + auth_type: null, + }, + result: { affected_dashboard_contents: [] }, }, { id: results.data[3].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder_2_renamed', old_key: 'jsonplaceholder_2' }, - result: { affected_dashboards: [] }, - create_time: results.data[3].create_time, - update_time: results.data[3].update_time, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder_2_renamed', + old_key: 'jsonplaceholder_2', + auth_type: null, + }, + result: { affected_dashboard_contents: [] }, }, { id: results.data[4].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg_renamed', old_key: 'pg' }, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg_renamed', + old_key: 'pg', + auth_type: null, + }, result: { - affected_dashboards: [ + affected_dashboard_contents: [ { queries: ['pgQuery1'], - dashboardId: results.data[4].result['affected_dashboards'][0].dashboardId, + contentId: results.data[4].result['affected_dashboard_contents'][0].contentId, }, { queries: ['pgQuery2'], - dashboardId: results.data[4].result['affected_dashboards'][1].dashboardId, + contentId: results.data[4].result['affected_dashboard_contents'][1].contentId, }, ], }, - create_time: results.data[4].create_time, - update_time: results.data[4].update_time, }, { id: results.data[5].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder_renamed', old_key: 'jsonplaceholder' }, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder_renamed', + old_key: 'jsonplaceholder', + auth_type: null, + }, result: { - affected_dashboards: [ + affected_dashboard_contents: [ { queries: ['httpQuery1'], - dashboardId: results.data[5].result['affected_dashboards'][0].dashboardId, + contentId: results.data[5].result['affected_dashboard_contents'][0].contentId, }, { queries: ['httpQuery2'], - dashboardId: results.data[5].result['affected_dashboards'][1].dashboardId, + contentId: results.data[5].result['affected_dashboard_contents'][1].contentId, }, ], }, - create_time: results.data[5].create_time, - update_time: results.data[5].update_time, }, { id: results.data[6].id, type: 'RENAME_DATASOURCE', status: 'FAILED', - params: { type: 'non_existent', new_key: 'new_key', old_key: 'old_key' }, + params: { + type: 'non_existent', + auth_id: null, + new_key: 'new_key', + old_key: 'old_key', + auth_type: null, + }, result: results.data[6].result, - create_time: results.data[6].create_time, - update_time: results.data[6].update_time, }, { id: results.data[7].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg', old_key: 'pg_renamed' }, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg', + old_key: 'pg_renamed', + auth_type: null, + }, result: { - affected_dashboards: [ - { - queries: ['pgQuery1'], - dashboardId: results.data[7].result['affected_dashboards'][0].dashboardId, - }, - { - queries: ['pgQuery2'], - dashboardId: results.data[7].result['affected_dashboards'][1].dashboardId, - }, - ], + affected_dashboard_contents: [], }, - create_time: results.data[7].create_time, - update_time: results.data[7].update_time, }, { id: results.data[8].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder', old_key: 'jsonplaceholder_renamed' }, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder', + old_key: 'jsonplaceholder_renamed', + auth_type: null, + }, result: { - affected_dashboards: [ - { - queries: ['httpQuery1'], - dashboardId: results.data[8].result['affected_dashboards'][0].dashboardId, - }, - { - queries: ['httpQuery2'], - dashboardId: results.data[8].result['affected_dashboards'][1].dashboardId, - }, - ], + affected_dashboard_contents: [], }, - create_time: results.data[8].create_time, - update_time: results.data[8].update_time, }, { id: results.data[9].id, type: 'RENAME_DATASOURCE', status: 'FAILED', - params: { type: 'non_existent', new_key: 'old_key', old_key: 'new_key' }, + params: { + type: 'non_existent', + auth_id: null, + new_key: 'old_key', + old_key: 'new_key', + auth_type: null, + }, result: results.data[9].result, - create_time: results.data[9].create_time, - update_time: results.data[9].update_time, + }, + { + id: results.data[10].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId1, + auth_type: 'ACCOUNT', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[11].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId2, + auth_type: 'APIKEY', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[12].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId3, + auth_type: 'ACCOUNT', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[13].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId4, + auth_type: 'APIKEY', + }, + result: { affected_dashboard_permissions: [] }, }, ], }); @@ -260,125 +387,174 @@ describe('JobService', () => { [{ field: 'create_time', order: 'ASC' }], { page: 1, pagesize: 20 }, ); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ - total: 8, + total: 12, offset: 0, data: [ { id: results.data[0].id, type: 'FIX_DASHBOARD_PERMISSION', status: 'SUCCESS', - params: {}, + params: { + auth_id: results.data[0].params['auth_id'], + auth_type: 'ACCOUNT', + }, result: { affected_dashboard_permissions: [] }, - create_time: results.data[0].create_time, - update_time: results.data[0].update_time, }, { id: results.data[1].id, type: 'FIX_DASHBOARD_PERMISSION', status: 'SUCCESS', - params: {}, + params: { + auth_id: results.data[1].params['auth_id'], + auth_type: 'APIKEY', + }, result: { affected_dashboard_permissions: [] }, - create_time: results.data[1].create_time, - update_time: results.data[1].update_time, }, { id: results.data[2].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg_2_renamed', old_key: 'pg_2' }, - result: { affected_dashboards: [] }, - create_time: results.data[2].create_time, - update_time: results.data[2].update_time, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg_2_renamed', + old_key: 'pg_2', + auth_type: null, + }, + result: { affected_dashboard_contents: [] }, }, { id: results.data[3].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder_2_renamed', old_key: 'jsonplaceholder_2' }, - result: { affected_dashboards: [] }, - create_time: results.data[3].create_time, - update_time: results.data[3].update_time, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder_2_renamed', + old_key: 'jsonplaceholder_2', + auth_type: null, + }, + result: { affected_dashboard_contents: [] }, }, { id: results.data[4].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg_renamed', old_key: 'pg' }, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg_renamed', + old_key: 'pg', + auth_type: null, + }, result: { - affected_dashboards: [ + affected_dashboard_contents: [ { queries: ['pgQuery1'], - dashboardId: results.data[4].result['affected_dashboards'][0].dashboardId, + contentId: results.data[4].result['affected_dashboard_contents'][0].contentId, }, { queries: ['pgQuery2'], - dashboardId: results.data[4].result['affected_dashboards'][1].dashboardId, + contentId: results.data[4].result['affected_dashboard_contents'][1].contentId, }, ], }, - create_time: results.data[4].create_time, - update_time: results.data[4].update_time, }, { id: results.data[5].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder_renamed', old_key: 'jsonplaceholder' }, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder_renamed', + old_key: 'jsonplaceholder', + auth_type: null, + }, result: { - affected_dashboards: [ + affected_dashboard_contents: [ { queries: ['httpQuery1'], - dashboardId: results.data[5].result['affected_dashboards'][0].dashboardId, + contentId: results.data[5].result['affected_dashboard_contents'][0].contentId, }, { queries: ['httpQuery2'], - dashboardId: results.data[5].result['affected_dashboards'][1].dashboardId, + contentId: results.data[5].result['affected_dashboard_contents'][1].contentId, }, ], }, - create_time: results.data[5].create_time, - update_time: results.data[5].update_time, }, { id: results.data[6].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'postgresql', new_key: 'pg', old_key: 'pg_renamed' }, + params: { + type: 'postgresql', + auth_id: null, + new_key: 'pg', + old_key: 'pg_renamed', + auth_type: null, + }, result: { - affected_dashboards: [ - { - queries: ['pgQuery1'], - dashboardId: results.data[6].result['affected_dashboards'][0].dashboardId, - }, - { - queries: ['pgQuery2'], - dashboardId: results.data[6].result['affected_dashboards'][1].dashboardId, - }, - ], + affected_dashboard_contents: [], }, - create_time: results.data[6].create_time, - update_time: results.data[6].update_time, }, { id: results.data[7].id, type: 'RENAME_DATASOURCE', status: 'SUCCESS', - params: { type: 'http', new_key: 'jsonplaceholder', old_key: 'jsonplaceholder_renamed' }, + params: { + type: 'http', + auth_id: null, + new_key: 'jsonplaceholder', + old_key: 'jsonplaceholder_renamed', + auth_type: null, + }, result: { - affected_dashboards: [ - { - queries: ['httpQuery1'], - dashboardId: results.data[7].result['affected_dashboards'][0].dashboardId, - }, - { - queries: ['httpQuery2'], - dashboardId: results.data[7].result['affected_dashboards'][1].dashboardId, - }, - ], + affected_dashboard_contents: [], + }, + }, + { + id: results.data[8].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId1, + auth_type: 'ACCOUNT', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[9].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId2, + auth_type: 'APIKEY', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[10].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId3, + auth_type: 'ACCOUNT', + }, + result: { affected_dashboard_permissions: [] }, + }, + { + id: results.data[11].id, + type: 'FIX_DASHBOARD_PERMISSION', + status: 'SUCCESS', + params: { + auth_id: jobAuthId4, + auth_type: 'APIKEY', }, - create_time: results.data[7].create_time, - update_time: results.data[7].update_time, + result: { affected_dashboard_permissions: [] }, }, ], }); @@ -390,6 +566,7 @@ describe('JobService', () => { [{ field: 'create_time', order: 'ASC' }], { page: 1, pagesize: 20 }, ); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ total: 2, offset: 0, @@ -400,8 +577,6 @@ describe('JobService', () => { status: 'FAILED', params: { type: 'non_existent', new_key: 'new_key', old_key: 'old_key' }, result: results.data[0].result, - create_time: results.data[0].create_time, - update_time: results.data[0].update_time, }, { id: results.data[1].id, @@ -409,8 +584,6 @@ describe('JobService', () => { status: 'FAILED', params: { type: 'non_existent', new_key: 'old_key', old_key: 'new_key' }, result: results.data[1].result, - create_time: results.data[1].create_time, - update_time: results.data[1].update_time, }, ], }); diff --git a/api/tests/integration/08_dashboard_changelog.test.ts b/api/tests/integration/08_dashboard_changelog.test.ts index bcb8be04b..f6d1b4d7d 100644 --- a/api/tests/integration/08_dashboard_changelog.test.ts +++ b/api/tests/integration/08_dashboard_changelog.test.ts @@ -16,87 +16,16 @@ describe('DashboardChangelogService', () => { page: 1, pagesize: 20, }); - - expect(results).toMatchObject({ - total: 11, - offset: 0, - data: [ - { - id: results.data[0].id, - dashboard_id: results.data[0].dashboard_id, - diff: results.data[0].diff, - create_time: results.data[0].create_time, - }, - { - id: results.data[1].id, - dashboard_id: results.data[1].dashboard_id, - diff: results.data[1].diff, - create_time: results.data[1].create_time, - }, - { - id: results.data[2].id, - dashboard_id: results.data[2].dashboard_id, - diff: results.data[2].diff, - create_time: results.data[2].create_time, - }, - { - id: results.data[3].id, - dashboard_id: results.data[3].dashboard_id, - diff: results.data[3].diff, - create_time: results.data[3].create_time, - }, - { - id: results.data[4].id, - dashboard_id: results.data[4].dashboard_id, - diff: results.data[4].diff, - create_time: results.data[4].create_time, - }, - { - id: results.data[5].id, - dashboard_id: results.data[5].dashboard_id, - diff: results.data[5].diff, - create_time: results.data[5].create_time, - }, - { - id: results.data[6].id, - dashboard_id: results.data[6].dashboard_id, - diff: results.data[6].diff, - create_time: results.data[6].create_time, - }, - { - id: results.data[7].id, - dashboard_id: results.data[7].dashboard_id, - diff: results.data[7].diff, - create_time: results.data[7].create_time, - }, - { - id: results.data[8].id, - dashboard_id: results.data[8].dashboard_id, - diff: results.data[8].diff, - create_time: results.data[8].create_time, - }, - { - id: results.data[9].id, - dashboard_id: results.data[9].dashboard_id, - diff: results.data[9].diff, - create_time: results.data[9].create_time, - }, - { - id: results.data[10].id, - dashboard_id: results.data[10].dashboard_id, - diff: results.data[10].diff, - create_time: results.data[10].create_time, - }, - ], - }); + expect(results.total).toEqual(3); + expect(results.offset).toEqual(0); expect(results.data[0].diff).toContain('diff --git a/data.json b/data.json'); expect(results.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[0].diff).toContain('@@ -2,9 +2,9 @@'); + expect(results.data[0].diff).toContain('@@ -1,8 +1,8 @@'); expect(results.data[0].diff).toContain( '-\t"name": "dashboard3",\n' + '+\t"name": "dashboard3_updated",\n' + - ' \t"content": {},\n' + + ' \t"content_id": null,\n' + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": false,\n' + @@ -107,141 +36,29 @@ describe('DashboardChangelogService', () => { expect(results.data[1].diff).toContain('diff --git a/data.json b/data.json'); expect(results.data[1].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[1].diff).toContain('@@ -2,7 +2,7 @@'); + expect(results.data[1].diff).toContain('@@ -1,8 +1,8 @@'); expect(results.data[1].diff).toContain( '-\t"name": "dashboard2",\n' + '+\t"name": "dashboard2_updated",\n' + - ' \t"content": {\n' + - ' \t\t"definition": {\n' + - ' \t\t\t"queries": [\n', + ' \t"content_id": "5959a66b-5b6b-4509-9d87-bb8b96100658",\n' + + ' \t"is_removed": false,\n' + + ' \t"is_preset": true,\n' + + '-\t"group": "1"\n' + + '+\t"group": "1_updated"\n' + + ' }\n', ); expect(results.data[2].diff).toContain('diff --git a/data.json b/data.json'); expect(results.data[2].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[2].diff).toContain('@@ -19,7 +19,7 @@'); + expect(results.data[2].diff).toContain('@@ -2,7 +2,7 @@'); expect(results.data[2].diff).toContain( - ' \t\t\t]\n' + - ' \t\t}\n' + - ' \t},\n' + - '-\t"is_removed": false,\n' + + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": true,\n' + ' \t"group": "1_updated"\n' + ' }\n', ); - expect(results.data[3].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[3].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[3].diff).toContain('@@ -6,7 +6,7 @@'); - expect(results.data[3].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery1",\n' + - '-\t\t\t\t\t"key": "pg",\n' + - '+\t\t\t\t\t"key": "pg_renamed",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - - expect(results.data[4].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[4].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[4].diff).toContain('@@ -6,7 +6,7 @@'); - expect(results.data[4].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery2",\n' + - '-\t\t\t\t\t"key": "pg",\n' + - '+\t\t\t\t\t"key": "pg_renamed",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - - expect(results.data[5].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[5].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[5].diff).toContain('@@ -11,7 +11,7 @@'); - expect(results.data[5].diff).toContain( - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "httpQuery1",\n' + - '-\t\t\t\t\t"key": "jsonplaceholder",\n' + - '+\t\t\t\t\t"key": "jsonplaceholder_renamed",\n' + - ' \t\t\t\t\t"type": "http"\n' + - ' \t\t\t\t}\n' + - ' \t\t\t]\n', - ); - - expect(results.data[6].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[6].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[6].diff).toContain('@@ -11,7 +11,7 @@'); - expect(results.data[6].diff).toContain( - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "httpQuery2",\n' + - '-\t\t\t\t\t"key": "jsonplaceholder",\n' + - '+\t\t\t\t\t"key": "jsonplaceholder_renamed",\n' + - ' \t\t\t\t\t"type": "http"\n' + - ' \t\t\t\t}\n' + - ' \t\t\t]\n', - ); - - expect(results.data[7].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[7].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[7].diff).toContain('@@ -6,7 +6,7 @@'); - expect(results.data[7].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery1",\n' + - '-\t\t\t\t\t"key": "pg_renamed",\n' + - '+\t\t\t\t\t"key": "pg",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - - expect(results.data[8].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[8].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[8].diff).toContain('@@ -6,7 +6,7 @@'); - expect(results.data[8].diff).toContain( - ' \t\t\t"queries": [\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "pgQuery2",\n' + - '-\t\t\t\t\t"key": "pg_renamed",\n' + - '+\t\t\t\t\t"key": "pg",\n' + - ' \t\t\t\t\t"type": "postgresql"\n' + - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n', - ); - - expect(results.data[9].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[9].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[9].diff).toContain('@@ -11,7 +11,7 @@'); - expect(results.data[9].diff).toContain( - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "httpQuery1",\n' + - '-\t\t\t\t\t"key": "jsonplaceholder_renamed",\n' + - '+\t\t\t\t\t"key": "jsonplaceholder",\n' + - ' \t\t\t\t\t"type": "http"\n' + - ' \t\t\t\t}\n' + - ' \t\t\t]\n', - ); - - expect(results.data[10].diff).toContain('diff --git a/data.json b/data.json'); - expect(results.data[10].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[10].diff).toContain('@@ -11,7 +11,7 @@'); - expect(results.data[10].diff).toContain( - ' \t\t\t\t},\n' + - ' \t\t\t\t{\n' + - ' \t\t\t\t\t"id": "httpQuery2",\n' + - '-\t\t\t\t\t"key": "jsonplaceholder_renamed",\n' + - '+\t\t\t\t\t"key": "jsonplaceholder",\n' + - ' \t\t\t\t\t"type": "http"\n' + - ' \t\t\t\t}\n' + - ' \t\t\t]\n', - ); - changelogDashboardId = results.data[0].dashboard_id; }); @@ -252,26 +69,16 @@ describe('DashboardChangelogService', () => { { page: 1, pagesize: 20 }, ); - expect(results).toMatchObject({ - total: 1, - offset: 0, - data: [ - { - id: results.data[0].id, - dashboard_id: results.data[0].dashboard_id, - diff: results.data[0].diff, - create_time: results.data[0].create_time, - }, - ], - }); + expect(results.total).toEqual(1); + expect(results.offset).toEqual(0); expect(results.data[0].diff).toContain('diff --git a/data.json b/data.json'); expect(results.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); - expect(results.data[0].diff).toContain('@@ -2,9 +2,9 @@'); + expect(results.data[0].diff).toContain('@@ -1,8 +1,8 @@'); expect(results.data[0].diff).toContain( '-\t"name": "dashboard3",\n' + '+\t"name": "dashboard3_updated",\n' + - ' \t"content": {},\n' + + ' \t"content_id": null,\n' + '-\t"is_removed": false,\n' + '+\t"is_removed": true,\n' + ' \t"is_preset": false,\n' + diff --git a/api/tests/integration/10_dashboard_permission.test.ts b/api/tests/integration/10_dashboard_permission.test.ts index 7004c8392..4a2545be1 100644 --- a/api/tests/integration/10_dashboard_permission.test.ts +++ b/api/tests/integration/10_dashboard_permission.test.ts @@ -13,6 +13,7 @@ import { dashboardDataSource } from '~/data_sources/dashboard'; import ApiKey from '~/models/apiKey'; import { ApiError, BAD_REQUEST, FORBIDDEN } from '~/utils/errors'; import { translate } from '~/utils/i18n'; +import { omitTime } from '~/utils/helpers'; describe('DashboardPermissionService', () => { connectionHook(); @@ -66,7 +67,6 @@ describe('DashboardPermissionService', () => { accountDashboard = await dashboardService.create( 'accountDashboard', - {}, 'dashboard_permission', DEFAULT_LANGUAGE, account, @@ -74,7 +74,6 @@ describe('DashboardPermissionService', () => { apiKeyDashboard = await dashboardService.create( 'apiKeyDashboard', - {}, 'dashboard_permission', DEFAULT_LANGUAGE, apiKey, @@ -87,6 +86,7 @@ describe('DashboardPermissionService', () => { page: 1, pagesize: 20, }); + results.data = results.data.map(omitTime); expect(results).toMatchObject({ total: 5, offset: 0, @@ -96,40 +96,30 @@ describe('DashboardPermissionService', () => { owner_id: results.data[0].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: results.data[0].create_time, - update_time: results.data[0].update_time, }, { id: results.data[1].id, owner_id: results.data[1].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: results.data[1].create_time, - update_time: results.data[1].update_time, }, { id: results.data[2].id, owner_id: null, owner_type: null, access: [], - create_time: results.data[2].create_time, - update_time: results.data[2].update_time, }, { id: results.data[3].id, owner_id: account.id, owner_type: 'ACCOUNT', access: [], - create_time: results.data[3].create_time, - update_time: results.data[3].update_time, }, { id: results.data[4].id, owner_id: apiKey.id, owner_type: 'APIKEY', access: [], - create_time: results.data[4].create_time, - update_time: results.data[4].update_time, }, ], }); @@ -146,6 +136,7 @@ describe('DashboardPermissionService', () => { pagesize: 20, }, ); + results1.data = results1.data.map(omitTime); expect(results1).toMatchObject({ total: 1, offset: 0, @@ -155,8 +146,6 @@ describe('DashboardPermissionService', () => { owner_id: null, owner_type: null, access: [], - create_time: results1.data[0].create_time, - update_time: results1.data[0].update_time, }, ], }); @@ -170,6 +159,7 @@ describe('DashboardPermissionService', () => { }, ); + results2.data = results2.data.map(omitTime); expect(results2).toMatchObject({ total: 1, offset: 0, @@ -179,52 +169,43 @@ describe('DashboardPermissionService', () => { owner_id: apiKey.id, owner_type: 'APIKEY', access: [], - create_time: results2.data[0].create_time, - update_time: results2.data[0].update_time, }, ], }); - const result3 = await dashboardPermissionService.list( + const results3 = await dashboardPermissionService.list( { owner_type: { isFuzzy: true, value: 'A' } }, [{ field: 'create_time', order: 'ASC' }], { page: 1, pagesize: 20 }, ); - expect(result3).toMatchObject({ + results3.data = results3.data.map(omitTime); + expect(results3).toMatchObject({ total: 4, offset: 0, data: [ { - id: result3.data[0].id, - owner_id: result3.data[0].owner_id, + id: results3.data[0].id, + owner_id: results3.data[0].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result3.data[0].create_time, - update_time: result3.data[0].update_time, }, { - id: result3.data[1].id, - owner_id: result3.data[1].owner_id, + id: results3.data[1].id, + owner_id: results3.data[1].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result3.data[1].create_time, - update_time: result3.data[1].update_time, }, { id: accountDashboard.id, owner_id: account.id, owner_type: 'ACCOUNT', access: [], - create_time: result3.data[2].create_time, - update_time: result3.data[2].update_time, }, { id: apiKeyDashboard.id, owner_id: apiKey.id, owner_type: 'APIKEY', access: [], - create_time: result3.data[3].create_time, - update_time: result3.data[3].update_time, }, ], }); @@ -239,10 +220,8 @@ describe('DashboardPermissionService', () => { account, DEFAULT_LANGUAGE, ); - expect(result1).toMatchObject({ + expect(omitTime(result1)).toMatchObject({ id: accountDashboard.id, - create_time: result1.create_time, - update_time: result1.update_time, owner_id: account.id, owner_type: 'ACCOUNT', access: [{ id: apiKey.id, type: 'APIKEY', permission: 'VIEW' }], @@ -254,10 +233,8 @@ describe('DashboardPermissionService', () => { account, DEFAULT_LANGUAGE, ); - expect(result2).toMatchObject({ + expect(omitTime(result2)).toMatchObject({ id: accountDashboard.id, - create_time: result2.create_time, - update_time: result2.update_time, owner_id: account.id, owner_type: 'ACCOUNT', access: [], @@ -271,10 +248,8 @@ describe('DashboardPermissionService', () => { apiKey, DEFAULT_LANGUAGE, ); - expect(result).toMatchObject({ + expect(omitTime(result)).toMatchObject({ id: apiKeyDashboard.id, - create_time: result.create_time, - update_time: result.update_time, owner_id: apiKey.id, owner_type: 'APIKEY', access: [], @@ -299,10 +274,8 @@ describe('DashboardPermissionService', () => { adminAccount, DEFAULT_LANGUAGE, ); - expect(result).toMatchObject({ + expect(omitTime(result)).toMatchObject({ id: accountDashboard.id, - create_time: result.create_time, - update_time: result.update_time, owner_id: account.id, owner_type: 'ACCOUNT', access: [{ id: adminAccount.id, type: 'ACCOUNT', permission: 'EDIT' }], @@ -327,10 +300,8 @@ describe('DashboardPermissionService', () => { adminAccount, DEFAULT_LANGUAGE, ); - expect(result1).toMatchObject({ + expect(omitTime(result1)).toMatchObject({ id: noOwnerDashboardId, - create_time: result1.create_time, - update_time: result1.update_time, owner_id: account.id, owner_type: 'ACCOUNT', access: [], @@ -342,10 +313,8 @@ describe('DashboardPermissionService', () => { adminAccount, DEFAULT_LANGUAGE, ); - expect(result2).toMatchObject({ + expect(omitTime(result2)).toMatchObject({ id: accountDashboard.id, - create_time: result2.create_time, - update_time: result2.update_time, owner_id: adminAccount.id, owner_type: 'ACCOUNT', access: [], @@ -388,6 +357,7 @@ describe('DashboardPermissionService', () => { page: 1, pagesize: 20, }); + result.data = result.data.map(omitTime); expect(result).toMatchObject({ total: 5, offset: 0, @@ -397,40 +367,30 @@ describe('DashboardPermissionService', () => { owner_id: result.data[0].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[0].create_time, - update_time: result.data[0].update_time, }, { id: result.data[1].id, owner_id: result.data[1].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[1].create_time, - update_time: result.data[1].update_time, }, { id: result.data[2].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[2].create_time, - update_time: result.data[2].update_time, }, { id: result.data[3].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[3].create_time, - update_time: result.data[3].update_time, }, { id: result.data[4].id, owner_id: result.data[4].owner_id, owner_type: 'APIKEY', access: [], - create_time: result.data[4].create_time, - update_time: result.data[4].update_time, }, ], }); @@ -444,6 +404,7 @@ describe('DashboardPermissionService', () => { page: 1, pagesize: 20, }); + result.data = result.data.map(omitTime); expect(result).toMatchObject({ total: 5, offset: 0, @@ -453,40 +414,30 @@ describe('DashboardPermissionService', () => { owner_id: result.data[0].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[0].create_time, - update_time: result.data[0].update_time, }, { id: result.data[1].id, owner_id: result.data[1].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[1].create_time, - update_time: result.data[1].update_time, }, { id: result.data[2].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[2].create_time, - update_time: result.data[2].update_time, }, { id: result.data[3].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[3].create_time, - update_time: result.data[3].update_time, }, { id: result.data[4].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[4].create_time, - update_time: result.data[4].update_time, }, ], }); @@ -500,6 +451,7 @@ describe('DashboardPermissionService', () => { page: 1, pagesize: 20, }); + result.data = result.data.map(omitTime); expect(result).toMatchObject({ total: 3, offset: 0, @@ -509,24 +461,18 @@ describe('DashboardPermissionService', () => { owner_id: result.data[0].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[0].create_time, - update_time: result.data[0].update_time, }, { id: result.data[1].id, owner_id: result.data[1].owner_id, owner_type: 'ACCOUNT', access: [], - create_time: result.data[1].create_time, - update_time: result.data[1].update_time, }, { id: result.data[2].id, owner_id: null, owner_type: null, access: [], - create_time: result.data[2].create_time, - update_time: result.data[2].update_time, }, ], }); diff --git a/api/tests/integration/11_dashboard_content.test.ts b/api/tests/integration/11_dashboard_content.test.ts new file mode 100644 index 000000000..4143e1fac --- /dev/null +++ b/api/tests/integration/11_dashboard_content.test.ts @@ -0,0 +1,217 @@ +import { connectionHook } from './jest.util'; +import { DashboardService } from '~/services/dashboard.service'; +import { DashboardContentService } from '~/services/dashboard_content.service'; +import Dashboard from '~/models/dashboard'; +import DashboardContent from '~/models/dashboard_content'; +import DashboardPermission from '~/models/dashboard_permission'; +import Account from '~/models/account'; +import { EntityNotFoundError } from 'typeorm'; +import { ROLE_TYPES } from '~/api_models/role'; +import { ApiError, BAD_REQUEST } from '~/utils/errors'; +import { notFoundId } from './constants'; +import { dashboardDataSource } from '~/data_sources/dashboard'; +import { DEFAULT_LANGUAGE } from '~/utils/constants'; +import { omitTime } from '~/utils/helpers'; + +describe('DashboardContentService', () => { + connectionHook(); + let dashboardService: DashboardService; + let dashboardContentService: DashboardContentService; + let dashboardContent1: DashboardContent; + let dashboardContent2: DashboardContent; + let dashboardContent3: DashboardContent; + let tempDashboard: Dashboard; + let tempPresetDashboardContent: DashboardContent; + let superadmin: Account; + let authorAccount: Account; + + beforeAll(async () => { + dashboardService = new DashboardService(); + dashboardContentService = new DashboardContentService(); + superadmin = await dashboardDataSource.manager.findOne(Account, { where: { role_id: ROLE_TYPES.SUPERADMIN } }); + authorAccount = await dashboardDataSource.manager.findOne(Account, { where: { name: 'account3' } }); + + tempDashboard = await dashboardService.create('tempDashboard', 'dashboard_content', DEFAULT_LANGUAGE, superadmin); + + let tempPresetDashboard = new Dashboard(); + tempPresetDashboard.name = 'tempPresetDashboard'; + tempPresetDashboard.is_preset = true; + tempPresetDashboard = await dashboardDataSource.manager.save(tempPresetDashboard); + + const tempPresetDashboardPermission: DashboardPermission = new DashboardPermission(); + tempPresetDashboardPermission.owner_id = superadmin.id; + tempPresetDashboardPermission.owner_type = 'ACCOUNT'; + tempPresetDashboardPermission.id = tempPresetDashboard.id; + await dashboardDataSource.manager.save(tempPresetDashboardPermission); + + tempPresetDashboardContent = new DashboardContent(); + tempPresetDashboardContent.dashboard_id = tempPresetDashboard.id; + tempPresetDashboardContent.name = 'tempPresetDashboardContent'; + tempPresetDashboardContent = await dashboardDataSource.manager.save(tempPresetDashboardContent); + }); + describe('create', () => { + it('should create successfully', async () => { + dashboardContent1 = await dashboardContentService.create( + tempDashboard.id, + 'dashboardContent1', + {}, + DEFAULT_LANGUAGE, + ); + dashboardContent2 = await dashboardContentService.create( + tempDashboard.id, + 'dashboardContent2', + {}, + DEFAULT_LANGUAGE, + ); + dashboardContent3 = await dashboardContentService.create( + tempDashboard.id, + 'dashboardContent3', + {}, + DEFAULT_LANGUAGE, + ); + }); + it('should fail if duplicate name', async () => { + await expect( + dashboardContentService.create(tempDashboard.id, 'dashboardContent1', {}, DEFAULT_LANGUAGE), + ).rejects.toThrowError( + new ApiError(BAD_REQUEST, { message: 'A dashboard content with that name already exists' }), + ); + }); + }); + describe('list', () => { + it('no filters', async () => { + const results = await dashboardContentService.list( + tempDashboard.id, + undefined, + [{ field: 'name', order: 'ASC' }], + { + page: 1, + pagesize: 20, + }, + ); + results.data = results.data.map(omitTime); + expect(results).toMatchObject({ + total: 3, + offset: 0, + data: [ + { + id: dashboardContent1.id, + dashboard_id: tempDashboard.id, + name: 'dashboardContent1', + content: {}, + }, + { + id: dashboardContent2.id, + dashboard_id: tempDashboard.id, + name: 'dashboardContent2', + content: {}, + }, + { + id: dashboardContent3.id, + dashboard_id: tempDashboard.id, + name: 'dashboardContent3', + content: {}, + }, + ], + }); + }); + it('with filter', async () => { + const results = await dashboardContentService.list( + tempDashboard.id, + { name: { value: '3', isFuzzy: true } }, + [{ field: 'create_time', order: 'ASC' }], + { page: 1, pagesize: 20 }, + ); + results.data = results.data.map(omitTime); + expect(results).toMatchObject({ + total: 1, + offset: 0, + data: [ + { + id: dashboardContent3.id, + dashboard_id: tempDashboard.id, + name: 'dashboardContent3', + content: {}, + }, + ], + }); + }); + }); + describe('get', () => { + it('should return successfully', async () => { + const dashboardContent = await dashboardContentService.get(dashboardContent1.id); + expect(dashboardContent).toMatchObject(dashboardContent1); + }); + it('should fail', async () => { + await expect(dashboardContentService.get(notFoundId)).rejects.toThrowError(EntityNotFoundError); + }); + }); + describe('update', () => { + it('should update successfully', async () => { + const updatedDashboardContent = await dashboardContentService.update( + dashboardContent1.id, + 'dashboardContent1_updated', + undefined, + DEFAULT_LANGUAGE, + superadmin, + ); + expect(omitTime(updatedDashboardContent)).toMatchObject({ + ...omitTime(dashboardContent1), + name: 'dashboardContent1_updated', + }); + }); + it('should fail if not found', async () => { + await expect( + dashboardContentService.update(notFoundId, 'xxxx', undefined, DEFAULT_LANGUAGE, superadmin), + ).rejects.toThrowError(EntityNotFoundError); + }); + it('should update preset dashboard content successfully', async () => { + const updatedDashboardContent = await dashboardContentService.update( + tempPresetDashboardContent.id, + 'tempPresetDashboardContent_updated', + undefined, + DEFAULT_LANGUAGE, + superadmin, + ); + expect(omitTime(updatedDashboardContent)).toMatchObject({ + ...omitTime(tempPresetDashboardContent), + name: 'tempPresetDashboardContent_updated', + }); + }); + it('should fail if not SUPERADMIN', async () => { + await expect( + dashboardContentService.update( + tempPresetDashboardContent.id, + 'tempPresetDashboardContent_updated', + undefined, + DEFAULT_LANGUAGE, + authorAccount, + ), + ).rejects.toThrowError( + new ApiError(BAD_REQUEST, { message: 'Only superadmin can edit preset dashboard contents' }), + ); + }); + }); + describe('delete', () => { + it('should delete successfully', async () => { + await dashboardContentService.delete(dashboardContent1.id, DEFAULT_LANGUAGE, superadmin); + await dashboardContentService.delete(dashboardContent2.id, DEFAULT_LANGUAGE, superadmin); + await dashboardContentService.delete(dashboardContent3.id, DEFAULT_LANGUAGE, superadmin); + }); + it('should fail if not found', async () => { + await expect(dashboardContentService.delete(notFoundId, DEFAULT_LANGUAGE, superadmin)).rejects.toThrowError( + EntityNotFoundError, + ); + }); + it('should fail to delete preset dashboard contents if not SUPERADMIN', async () => { + await expect( + dashboardContentService.delete(tempPresetDashboardContent.id, DEFAULT_LANGUAGE, authorAccount), + ).rejects.toThrowError( + new ApiError(BAD_REQUEST, { message: 'Only superadmin can delete preset dashboard contents' }), + ); + }); + it('should delete preset dashboard contents successfully if SUPERADMIN', async () => { + await dashboardContentService.delete(tempPresetDashboardContent.id, DEFAULT_LANGUAGE, superadmin); + }); + }); +}); diff --git a/api/tests/integration/12_dashboard_content_changelog.test.ts b/api/tests/integration/12_dashboard_content_changelog.test.ts new file mode 100644 index 000000000..644c5c8df --- /dev/null +++ b/api/tests/integration/12_dashboard_content_changelog.test.ts @@ -0,0 +1,102 @@ +import { connectionHook } from './jest.util'; +import { DashboardContentChangelogService } from '~/services/dashboard_content_changelog.service'; + +describe('DashboardContentChangelogService', () => { + connectionHook(); + let dashboardContentChangelogService: DashboardContentChangelogService; + let changelogDashboardContentId: string; + + beforeAll(async () => { + dashboardContentChangelogService = new DashboardContentChangelogService(); + }); + + describe('list', () => { + it('no filters', async () => { + const results = await dashboardContentChangelogService.list(undefined, [{ field: 'create_time', order: 'ASC' }], { + page: 1, + pagesize: 20, + }); + expect(results.total).toEqual(4); + expect(results.offset).toEqual(0); + + expect(results.data[0].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[0].diff).toContain('@@ -7,7 +7,7 @@'); + expect(results.data[0].diff).toContain('-\t\t\t\t\t"key": "pg",\n' + '+\t\t\t\t\t"key": "pg_renamed",\n'); + expect(results.data[0].diff).toContain('@@ -17,5 +17,9 @@'); + expect(results.data[0].diff).toContain( + '-\t}\n' + + '+\t},\n' + + '+\t"content_id": null,\n' + + '+\t"is_removed": false,\n' + + '+\t"is_preset": false,\n' + + '+\t"group": ""\n' + + ' }\n', + ); + + expect(results.data[1].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[1].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[1].diff).toContain('@@ -7,7 +7,7 @@'); + expect(results.data[1].diff).toContain('-\t\t\t\t\t"key": "pg",\n' + '+\t\t\t\t\t"key": "pg_renamed",\n'); + expect(results.data[1].diff).toContain('@@ -17,5 +17,9 @@'); + expect(results.data[1].diff).toContain( + '-\t}\n' + + '+\t},\n' + + '+\t"content_id": null,\n' + + '+\t"is_removed": false,\n' + + '+\t"is_preset": false,\n' + + '+\t"group": ""\n' + + ' }\n', + ); + + expect(results.data[2].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[2].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[2].diff).toContain('@@ -12,7 +12,7 @@'); + expect(results.data[2].diff).toContain( + '-\t\t\t\t\t"key": "jsonplaceholder",\n' + '+\t\t\t\t\t"key": "jsonplaceholder_renamed",\n', + ); + + expect(results.data[3].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[3].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[3].diff).toContain('@@ -12,7 +12,7 @@'); + expect(results.data[3].diff).toContain( + '-\t\t\t\t\t"key": "jsonplaceholder",\n' + '+\t\t\t\t\t"key": "jsonplaceholder_renamed",\n', + ); + + changelogDashboardContentId = results.data[0].dashboard_content_id; + }); + + it('with search filter', async () => { + const results = await dashboardContentChangelogService.list( + { dashboard_content_id: { value: changelogDashboardContentId, isFuzzy: true } }, + [{ field: 'create_time', order: 'ASC' }], + { page: 1, pagesize: 20 }, + ); + + expect(results.total).toEqual(2); + expect(results.offset).toEqual(0); + + expect(results.data[0].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[0].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[0].diff).toContain('@@ -7,7 +7,7 @@'); + expect(results.data[0].diff).toContain('-\t\t\t\t\t"key": "pg",\n' + '+\t\t\t\t\t"key": "pg_renamed",\n'); + expect(results.data[0].diff).toContain('@@ -17,5 +17,9 @@'); + expect(results.data[0].diff).toContain( + '-\t}\n' + + '+\t},\n' + + '+\t"content_id": null,\n' + + '+\t"is_removed": false,\n' + + '+\t"is_preset": false,\n' + + '+\t"group": ""\n' + + ' }\n', + ); + + expect(results.data[1].diff).toContain('diff --git a/data.json b/data.json'); + expect(results.data[1].diff).toContain('--- a/data.json\n' + '+++ b/data.json'); + expect(results.data[1].diff).toContain('@@ -12,7 +12,7 @@'); + expect(results.data[1].diff).toContain( + '-\t\t\t\t\t"key": "jsonplaceholder",\n' + '+\t\t\t\t\t"key": "jsonplaceholder_renamed",\n', + ); + }); + }); +}); diff --git a/api/tests/integration/constants.ts b/api/tests/integration/constants.ts index 5f8bebd18..050a09455 100644 --- a/api/tests/integration/constants.ts +++ b/api/tests/integration/constants.ts @@ -4,6 +4,7 @@ import ApiKey from '~/models/apiKey'; import DataSource from '~/models/datasource'; import Dashboard from '~/models/dashboard'; import { parseDBUrl } from '../utils'; +import DashboardContent from '~/models/dashboard_content'; export const accounts: Account[] = [ { @@ -146,6 +147,28 @@ export const dashboards: Dashboard[] = [ name: 'dashboard1', is_preset: false, is_removed: true, + content_id: null, + group: '1', + create_time: new Date(), + update_time: new Date(), + }, + { + id: '173b84d2-7ed9-4d1a-a386-e68a6cce192b', + name: 'dashboard2', + is_preset: true, + is_removed: false, + content_id: null, + group: '1', + create_time: new Date(), + update_time: new Date(), + }, +]; + +export const dashboardContents: DashboardContent[] = [ + { + id: '9afa4842-77ef-4b19-8a53-034cb41ee7f6', + dashboard_id: '63c52cf7-0783-40fb-803a-68abc6564de0', + name: 'dashboard1', content: { definition: { queries: [ @@ -162,15 +185,13 @@ export const dashboards: Dashboard[] = [ ], }, }, - group: '1', create_time: new Date(), update_time: new Date(), }, { - id: '173b84d2-7ed9-4d1a-a386-e68a6cce192b', + id: '5959a66b-5b6b-4509-9d87-bb8b96100658', + dashboard_id: '173b84d2-7ed9-4d1a-a386-e68a6cce192b', name: 'dashboard2', - is_preset: true, - is_removed: false, content: { definition: { queries: [ @@ -187,7 +208,6 @@ export const dashboards: Dashboard[] = [ ], }, }, - group: '1', create_time: new Date(), update_time: new Date(), }, diff --git a/api/tests/integration/jest.util.ts b/api/tests/integration/jest.util.ts index 7626ff683..419994e4e 100644 --- a/api/tests/integration/jest.util.ts +++ b/api/tests/integration/jest.util.ts @@ -1,4 +1,3 @@ -import { omit } from 'lodash'; import { dashboardDataSource } from '~/data_sources/dashboard'; export function connectionHook(): void { @@ -11,12 +10,6 @@ export function connectionHook(): void { }); } -export function omitTime(data: any[]): any[] { - return data.map((x) => { - return omit(x, ['create_time', 'update_time']); - }); -} - function timeout(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/api/tests/integration/seed/seed.ts b/api/tests/integration/seed/seed.ts index 5c93d767a..346661138 100644 --- a/api/tests/integration/seed/seed.ts +++ b/api/tests/integration/seed/seed.ts @@ -1,13 +1,14 @@ import bcrypt from 'bcrypt'; import { dashboardDataSource } from '~/data_sources/dashboard'; import { SALT_ROUNDS } from '~/utils/constants'; -import { accounts, apiKeys, dataSources, dashboards } from '../constants'; +import { accounts, apiKeys, dataSources, dashboards, dashboardContents } from '../constants'; import Account from '~/models/account'; import ApiKey from '~/models/apiKey'; import DataSource from '~/models/datasource'; import Dashboard from '~/models/dashboard'; import { maybeEncryptPassword } from '~/utils/encryption'; import DashboardPermission from '~/models/dashboard_permission'; +import DashboardContent from '~/models/dashboard_content'; export async function seed() { if (!dashboardDataSource.isInitialized) { @@ -17,7 +18,7 @@ export async function seed() { await addAccounts(); await addApiKeys(); await addDataSources(); - await addDashboards(); + await addDashboardsAndContent(); } async function addAccounts() { @@ -47,14 +48,19 @@ async function addDataSources() { } } -async function addDashboards() { +async function addDashboardsAndContent() { const superadmin = accounts[4]; const author = accounts[2]; const dashboardRepo = dashboardDataSource.getRepository(Dashboard); + const dashboardContentRepo = dashboardDataSource.getRepository(DashboardContent); const dashboardPermissionRepo = dashboardDataSource.getRepository(DashboardPermission); for (let i = 0; i < dashboards.length; i++) { const dashboard = dashboards[i]; await dashboardRepo.save(dashboard); + const dashboardContent = dashboardContents[i]; + await dashboardContentRepo.save(dashboardContent); + dashboard.content_id = dashboardContent.id; + await dashboardRepo.save(dashboard); const dashboardPermission = new DashboardPermission(); dashboardPermission.id = dashboard.id; dashboardPermission.owner_id = dashboard.is_preset ? superadmin.id : author.id; diff --git a/api/tests/unit/validation.test.ts b/api/tests/unit/validation.test.ts index e9a260c4a..b74d0f86e 100644 --- a/api/tests/unit/validation.test.ts +++ b/api/tests/unit/validation.test.ts @@ -31,8 +31,15 @@ import { DashboardOwnerUpdateRequest, DashboardPermissionUpdateRequest, } from '~/api_models/dashboard_permission'; +import { DashboardContentChangelogListRequest } from '~/api_models/dashboard_content_changelog'; +import { + DashboardContentListRequest, + DashboardContentCreateRequest, + DashboardContentIDRequest, + DashboardContentUpdateRequest, +} from '~/api_models/dashboard_content'; import { ApiError } from '~/utils/errors'; -import { validate } from '~/middleware/validation'; +import { validateClass } from '~/middleware/validation'; import { VALIDATION_FAILED } from '~/utils/errors'; import { DEFAULT_LANGUAGE } from '~/utils/constants'; import * as crypto from 'crypto'; @@ -46,17 +53,17 @@ describe('validation', () => { password: 'test', }; - const result = validate(AccountLoginRequest, data); + const result = validateClass(AccountLoginRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(AccountLoginRequest, data)).toThrow( + expect(() => validateClass(AccountLoginRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountLoginRequest, data); + validateClass(AccountLoginRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -86,13 +93,13 @@ describe('validation', () => { filter: { name: { value: '', isFuzzy: true }, email: { value: '', isFuzzy: true } }, }; - const result = validate(AccountListRequest, data); + const result = validateClass(AccountListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(AccountListRequest, data); + const result = validateClass(AccountListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -103,11 +110,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(AccountListRequest, data)).toThrow( + expect(() => validateClass(AccountListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountListRequest, data); + validateClass(AccountListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -147,17 +154,17 @@ describe('validation', () => { role_id: ROLE_TYPES.AUTHOR, }; - const result = validate(AccountCreateRequest, data); + const result = validateClass(AccountCreateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(AccountCreateRequest, data)).toThrow( + expect(() => validateClass(AccountCreateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountCreateRequest, data); + validateClass(AccountCreateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -199,7 +206,7 @@ describe('validation', () => { it('Should have no validation errors', () => { const data: AccountUpdateRequest = {}; - const result = validate(AccountUpdateRequest, data); + const result = validateClass(AccountUpdateRequest, data); expect(result).toMatchObject(data); }); @@ -208,11 +215,11 @@ describe('validation', () => { name: '', email: '', }; - expect(() => validate(AccountUpdateRequest, data)).toThrow( + expect(() => validateClass(AccountUpdateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountUpdateRequest, data); + validateClass(AccountUpdateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -250,7 +257,7 @@ describe('validation', () => { id: crypto.randomUUID(), }; - const result = validate(AccountEditRequest, data); + const result = validateClass(AccountEditRequest, data); expect(result).toMatchObject(data); }); @@ -260,11 +267,11 @@ describe('validation', () => { name: '', role_id: 0, }; - expect(() => validate(AccountEditRequest, data)).toThrow( + expect(() => validateClass(AccountEditRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountEditRequest, data); + validateClass(AccountEditRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -316,17 +323,17 @@ describe('validation', () => { old_password: 'test1234', }; - const result = validate(AccountChangePasswordRequest, data); + const result = validateClass(AccountChangePasswordRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(AccountChangePasswordRequest, data)).toThrow( + expect(() => validateClass(AccountChangePasswordRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountChangePasswordRequest, data); + validateClass(AccountChangePasswordRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -358,17 +365,17 @@ describe('validation', () => { id: crypto.randomUUID(), }; - const result = validate(AccountIDRequest, data); + const result = validateClass(AccountIDRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(AccountIDRequest, data)).toThrow( + expect(() => validateClass(AccountIDRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(AccountIDRequest, data); + validateClass(AccountIDRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -393,13 +400,13 @@ describe('validation', () => { filter: { name: { value: '', isFuzzy: true } }, }; - const result = validate(ApiKeyListRequest, data); + const result = validateClass(ApiKeyListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(ApiKeyListRequest, data); + const result = validateClass(ApiKeyListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -410,11 +417,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(ApiKeyListRequest, data)).toThrow( + expect(() => validateClass(ApiKeyListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(ApiKeyListRequest, data); + validateClass(ApiKeyListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -453,17 +460,17 @@ describe('validation', () => { role_id: ROLE_TYPES.AUTHOR, }; - const result = validate(ApiKeyCreateRequest, data); + const result = validateClass(ApiKeyCreateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(ApiKeyCreateRequest, data)).toThrow( + expect(() => validateClass(ApiKeyCreateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(ApiKeyCreateRequest, data); + validateClass(ApiKeyCreateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -497,17 +504,17 @@ describe('validation', () => { id: crypto.randomUUID(), }; - const result = validate(ApiKeyIDRequest, data); + const result = validateClass(ApiKeyIDRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(ApiKeyIDRequest, data)).toThrow( + expect(() => validateClass(ApiKeyIDRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(ApiKeyIDRequest, data); + validateClass(ApiKeyIDRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -532,13 +539,13 @@ describe('validation', () => { filter: { group: { value: '', isFuzzy: true }, name: { value: '', isFuzzy: true }, is_removed: false }, }; - const result = validate(DashboardListRequest, data); + const result = validateClass(DashboardListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(DashboardListRequest, data); + const result = validateClass(DashboardListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -549,11 +556,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(DashboardListRequest, data)).toThrow( + expect(() => validateClass(DashboardListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardListRequest, data); + validateClass(DashboardListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -589,21 +596,20 @@ describe('validation', () => { it('Should have no validation errors', () => { const data: DashboardCreateRequest = { name: 'test', - content: {}, group: '', }; - const result = validate(DashboardCreateRequest, data); + const result = validateClass(DashboardCreateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DashboardCreateRequest, data)).toThrow( + expect(() => validateClass(DashboardCreateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardCreateRequest, data); + validateClass(DashboardCreateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -616,13 +622,6 @@ describe('validation', () => { isString: 'name must be a string', }, }, - { - target: {}, - value: undefined, - property: 'content', - children: [], - constraints: { isObject: 'content must be an object' }, - }, { target: {}, value: undefined, @@ -641,17 +640,17 @@ describe('validation', () => { id: crypto.randomUUID(), }; - const result = validate(DashboardIDRequest, data); + const result = validateClass(DashboardIDRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DashboardIDRequest, data)).toThrow( + expect(() => validateClass(DashboardIDRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardIDRequest, data); + validateClass(DashboardIDRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -673,17 +672,17 @@ describe('validation', () => { is_preset: false, }; - const result = validate(DashboardNameRequest, data); + const result = validateClass(DashboardNameRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DashboardNameRequest, data)).toThrow( + expect(() => validateClass(DashboardNameRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardNameRequest, data); + validateClass(DashboardNameRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -712,22 +711,22 @@ describe('validation', () => { it('Should have no validation errors', () => { const data: DashboardUpdateRequest = { id: crypto.randomUUID(), - content: {}, + content_id: crypto.randomUUID(), is_removed: true, name: 'test', }; - const result = validate(DashboardUpdateRequest, data); + const result = validateClass(DashboardUpdateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DashboardUpdateRequest, data)).toThrow( + expect(() => validateClass(DashboardUpdateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardUpdateRequest, data); + validateClass(DashboardUpdateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -752,13 +751,13 @@ describe('validation', () => { filter: { key: { value: '', isFuzzy: true }, type: { value: '', isFuzzy: true } }, }; - const result = validate(DataSourceListRequest, data); + const result = validateClass(DataSourceListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(DataSourceListRequest, data); + const result = validateClass(DataSourceListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -769,11 +768,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(DataSourceListRequest, data)).toThrow( + expect(() => validateClass(DataSourceListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DataSourceListRequest, data); + validateClass(DataSourceListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -823,17 +822,17 @@ describe('validation', () => { type: 'postgresql', }; - const result = validate(DataSourceCreateRequest, data); + const result = validateClass(DataSourceCreateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DataSourceCreateRequest, data)).toThrow( + expect(() => validateClass(DataSourceCreateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DataSourceCreateRequest, data); + validateClass(DataSourceCreateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -875,17 +874,17 @@ describe('validation', () => { key: 'test_new', }; - const result = validate(DataSourceRenameRequest, data); + const result = validateClass(DataSourceRenameRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DataSourceRenameRequest, data)).toThrow( + expect(() => validateClass(DataSourceRenameRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DataSourceRenameRequest, data); + validateClass(DataSourceRenameRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -916,17 +915,17 @@ describe('validation', () => { id: crypto.randomUUID(), }; - const result = validate(DataSourceIDRequest, data); + const result = validateClass(DataSourceIDRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DataSourceIDRequest, data)).toThrow( + expect(() => validateClass(DataSourceIDRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DataSourceIDRequest, data); + validateClass(DataSourceIDRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -951,13 +950,13 @@ describe('validation', () => { filter: { status: { value: '', isFuzzy: true }, type: { value: '', isFuzzy: true } }, }; - const result = validate(JobListRequest, data); + const result = validateClass(JobListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(JobListRequest, data); + const result = validateClass(JobListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -968,11 +967,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(JobListRequest, data)).toThrow( + expect(() => validateClass(JobListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(JobListRequest, data); + validateClass(JobListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1010,17 +1009,17 @@ describe('validation', () => { type: 'RENAME_DATASOURCE', }; - const result = validate(JobRunRequest, data); + const result = validateClass(JobRunRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(JobRunRequest, data)).toThrow( + expect(() => validateClass(JobRunRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(JobRunRequest, data); + validateClass(JobRunRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1029,7 +1028,7 @@ describe('validation', () => { property: 'type', children: [], constraints: { - isIn: 'type must be one of the following values: RENAME_DATASOURCE', + isIn: 'type must be one of the following values: RENAME_DATASOURCE, FIX_DASHBOARD_PERMISSION', }, }, ]); @@ -1047,17 +1046,17 @@ describe('validation', () => { query: '', }; - const result = validate(QueryRequest, data); + const result = validateClass(QueryRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(QueryRequest, data)).toThrow( + expect(() => validateClass(QueryRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(QueryRequest, data); + validateClass(QueryRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1095,17 +1094,17 @@ describe('validation', () => { const data: ConfigGetRequest = { key: 'lang', }; - const result = validate(ConfigGetRequest, data); + const result = validateClass(ConfigGetRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(ConfigGetRequest, data)).toThrow( + expect(() => validateClass(ConfigGetRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(ConfigGetRequest, data); + validateClass(ConfigGetRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1126,17 +1125,17 @@ describe('validation', () => { key: 'lang', value: DEFAULT_LANGUAGE, }; - const result = validate(ConfigUpdateRequest, data); + const result = validateClass(ConfigUpdateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(ConfigUpdateRequest, data)).toThrow( + expect(() => validateClass(ConfigUpdateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(ConfigUpdateRequest, data); + validateClass(ConfigUpdateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1168,13 +1167,13 @@ describe('validation', () => { filter: { dashboard_id: { value: '', isFuzzy: true } }, }; - const result = validate(DashboardChangelogListRequest, data); + const result = validateClass(DashboardChangelogListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(DashboardChangelogListRequest, data); + const result = validateClass(DashboardChangelogListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -1185,11 +1184,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(DashboardChangelogListRequest, data)).toThrow( + expect(() => validateClass(DashboardChangelogListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardChangelogListRequest, data); + validateClass(DashboardChangelogListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1231,13 +1230,13 @@ describe('validation', () => { filter: { id: { value: '', isFuzzy: true } }, }; - const result = validate(DashboardPermissionListRequest, data); + const result = validateClass(DashboardPermissionListRequest, data); expect(result).toMatchObject(data); }); it('Empty request should also have no validation errors', () => { const data = {}; - const result = validate(DashboardPermissionListRequest, data); + const result = validateClass(DashboardPermissionListRequest, data); expect(result).toMatchObject({ sort: [{ field: 'create_time', order: 'ASC' }], pagination: { page: 1, pagesize: 20 }, @@ -1248,11 +1247,11 @@ describe('validation', () => { const data = { pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, }; - expect(() => validate(DashboardPermissionListRequest, data)).toThrow( + expect(() => validateClass(DashboardPermissionListRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardPermissionListRequest, data); + validateClass(DashboardPermissionListRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1291,17 +1290,17 @@ describe('validation', () => { owner_id: crypto.randomUUID(), owner_type: 'ACCOUNT', }; - const result = validate(DashboardOwnerUpdateRequest, data); + const result = validateClass(DashboardOwnerUpdateRequest, data); expect(result).toMatchObject(data); }); it('Should have validation errors', () => { const data = {}; - expect(() => validate(DashboardOwnerUpdateRequest, data)).toThrow( + expect(() => validateClass(DashboardOwnerUpdateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardOwnerUpdateRequest, data); + validateClass(DashboardOwnerUpdateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1336,7 +1335,7 @@ describe('validation', () => { id: crypto.randomUUID(), access: [{ id: crypto.randomUUID(), type: 'ACCOUNT', permission: 'VIEW' }], }; - const result = validate(DashboardPermissionUpdateRequest, data); + const result = validateClass(DashboardPermissionUpdateRequest, data); expect(result).toMatchObject(data); }); @@ -1344,11 +1343,11 @@ describe('validation', () => { const data = { access: [{ id: '', type: 'INCORRECT', permission: 'INCORRECT' }], }; - expect(() => validate(DashboardPermissionUpdateRequest, data)).toThrow( + expect(() => validateClass(DashboardPermissionUpdateRequest, data)).toThrow( new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), ); try { - validate(DashboardPermissionUpdateRequest, data); + validateClass(DashboardPermissionUpdateRequest, data); } catch (error) { expect(error.detail.errors).toMatchObject([ { @@ -1398,4 +1397,254 @@ describe('validation', () => { }); }); }); + + describe('DashboardContentController', () => { + describe('DashboardContentChangelogListRequest', () => { + it('Should have no validation errors', () => { + const data: DashboardContentChangelogListRequest = { + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'create_time', order: 'ASC' }], + filter: { dashboard_content_id: { value: '', isFuzzy: true } }, + }; + + const result = validateClass(DashboardContentChangelogListRequest, data); + expect(result).toMatchObject(data); + }); + + it('Empty request should also have no validation errors', () => { + const data = {}; + const result = validateClass(DashboardContentChangelogListRequest, data); + expect(result).toMatchObject({ + sort: [{ field: 'create_time', order: 'ASC' }], + pagination: { page: 1, pagesize: 20 }, + }); + }); + + it('Should have validation errors', () => { + const data = { + pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, + }; + expect(() => validateClass(DashboardContentChangelogListRequest, data)).toThrow( + new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), + ); + try { + validateClass(DashboardContentChangelogListRequest, data); + } catch (error) { + expect(error.detail.errors).toMatchObject([ + { + target: { + sort: [{ field: 'create_time', order: 'ASC' }], + pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, + }, + value: { incorrect_page: 1, incorrect_pageSize: 20 }, + property: 'pagination', + children: [ + { + target: { incorrect_page: 1, incorrect_pageSize: 20 }, + value: undefined, + property: 'page', + children: [], + constraints: { isInt: 'page must be an integer number' }, + }, + { + target: { incorrect_page: 1, incorrect_pageSize: 20 }, + value: undefined, + property: 'pagesize', + children: [], + constraints: { isInt: 'pagesize must be an integer number' }, + }, + ], + }, + ]); + } + }); + }); + }); + + describe('DashboardContentChangelogController', () => { + describe('DashboardContentListRequest', () => { + it('should have no validation errors', () => { + const data: DashboardContentListRequest = { + dashboard_id: crypto.randomUUID(), + pagination: { page: 1, pagesize: 20 }, + sort: [{ field: 'create_time', order: 'ASC' }], + filter: { name: { value: '', isFuzzy: true } }, + }; + const result = validateClass(DashboardContentListRequest, data); + expect(result).toMatchObject(data); + }); + + it('Empty request should also have no validation errors', () => { + const data = { + dashboard_id: crypto.randomUUID(), + }; + const result = validateClass(DashboardContentListRequest, data); + expect(result).toMatchObject({ + dashboard_id: data.dashboard_id, + sort: [{ field: 'create_time', order: 'ASC' }], + pagination: { page: 1, pagesize: 20 }, + }); + }); + + it('Should have validation errors', () => { + const data = { + pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, + }; + expect(() => validateClass(DashboardContentListRequest, data)).toThrow( + new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), + ); + try { + validateClass(DashboardContentListRequest, data); + } catch (error) { + expect(error.detail.errors).toMatchObject([ + { + target: { + sort: [{ field: 'create_time', order: 'ASC' }], + pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, + }, + value: undefined, + property: 'dashboard_id', + children: [], + constraints: { isUuid: 'dashboard_id must be a UUID' }, + }, + { + target: { + sort: [{ field: 'create_time', order: 'ASC' }], + pagination: { incorrect_page: 1, incorrect_pageSize: 20 }, + }, + value: { incorrect_page: 1, incorrect_pageSize: 20 }, + property: 'pagination', + children: [ + { + target: { incorrect_page: 1, incorrect_pageSize: 20 }, + value: undefined, + property: 'page', + children: [], + constraints: { isInt: 'page must be an integer number' }, + }, + { + target: { incorrect_page: 1, incorrect_pageSize: 20 }, + value: undefined, + property: 'pagesize', + children: [], + constraints: { isInt: 'pagesize must be an integer number' }, + }, + ], + }, + ]); + } + }); + }); + + describe('DashboardContentCreateRequest', () => { + it('should have no validation errors', () => { + const data: DashboardContentCreateRequest = { + dashboard_id: crypto.randomUUID(), + name: 'test', + content: {}, + }; + const result = validateClass(DashboardContentCreateRequest, data); + expect(result).toMatchObject(data); + }); + + it('should have validation errors', () => { + const data = {}; + expect(() => validateClass(DashboardContentCreateRequest, data)).toThrow( + new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), + ); + try { + validateClass(DashboardContentCreateRequest, data); + } catch (error) { + expect(error.detail.errors).toMatchObject([ + { + target: {}, + value: undefined, + property: 'dashboard_id', + children: [], + constraints: { isUuid: 'dashboard_id must be a UUID' }, + }, + { + target: {}, + value: undefined, + property: 'name', + children: [], + constraints: { + isLength: 'name must be longer than or equal to 1 characters', + isString: 'name must be a string', + }, + }, + { + target: {}, + value: undefined, + property: 'content', + children: [], + constraints: { isObject: 'content must be an object' }, + }, + ]); + } + }); + }); + + describe('DashboardContentIDRequest', () => { + it('should have no validation errors', () => { + const data: DashboardContentIDRequest = { + id: crypto.randomUUID(), + }; + const result = validateClass(DashboardContentIDRequest, data); + expect(result).toMatchObject(data); + }); + + it('should have validation errors', () => { + const data = {}; + expect(() => validateClass(DashboardContentIDRequest, data)).toThrow( + new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), + ); + try { + validateClass(DashboardContentIDRequest, data); + } catch (error) { + expect(error.detail.errors).toMatchObject([ + { + target: {}, + value: undefined, + property: 'id', + children: [], + constraints: { isUuid: 'id must be a UUID' }, + }, + ]); + } + }); + }); + + describe('DashboardContentUpdateRequest', () => { + it('should have no validation errors', () => { + const data: DashboardContentUpdateRequest = { + id: crypto.randomUUID(), + name: 'test', + content: {}, + }; + const result = validateClass(DashboardContentUpdateRequest, data); + expect(result).toMatchObject(data); + }); + + it('should have validation errors', () => { + const data = {}; + expect(() => validateClass(DashboardContentUpdateRequest, data)).toThrow( + new ApiError(VALIDATION_FAILED, { message: `request body is incorrect` }), + ); + try { + validateClass(DashboardContentUpdateRequest, data); + } catch (error) { + expect(error.detail.errors).toMatchObject([ + { + target: {}, + value: undefined, + property: 'id', + children: [], + constraints: { isUuid: 'id must be a UUID' }, + }, + ]); + } + }); + }); + }); }); diff --git a/dashboard/package.json b/dashboard/package.json index 69ef06935..ad803b329 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@devtable/dashboard", - "version": "8.61.2", + "version": "8.62.2", "license": "Apache-2.0", "publishConfig": { "access": "public", diff --git a/dashboard/src/contexts/content-model-context.ts b/dashboard/src/contexts/content-model-context.ts new file mode 100644 index 000000000..120f65591 --- /dev/null +++ b/dashboard/src/contexts/content-model-context.ts @@ -0,0 +1,14 @@ +import React from 'react'; +import { ContentModelInstance } from '~/model/content'; + +const ContentModelContext = React.createContext(null); + +export const ContentModelContextProvider = ContentModelContext.Provider; + +export function useContentModelContext() { + const model = React.useContext(ContentModelContext); + if (!model) { + throw new Error('Please use ContentModelContextProvider'); + } + return model; +} diff --git a/dashboard/src/contexts/index.ts b/dashboard/src/contexts/index.ts index 791b0ddaf..b937e4664 100644 --- a/dashboard/src/contexts/index.ts +++ b/dashboard/src/contexts/index.ts @@ -1,4 +1,5 @@ export * from './model-context'; +export * from './content-model-context'; export * from './layout-state-context'; export * from './panel-context'; export * from './full-screen-panel-context'; diff --git a/dashboard/src/filter/filter-multi-select/render.tsx b/dashboard/src/filter/filter-multi-select/render.tsx index ad778b538..2057468a8 100644 --- a/dashboard/src/filter/filter-multi-select/render.tsx +++ b/dashboard/src/filter/filter-multi-select/render.tsx @@ -1,6 +1,6 @@ import { MultiSelect } from '@mantine/core'; import { observer } from 'mobx-react-lite'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { FilterModelInstance } from '../../model'; import { IFilterConfig_MultiSelect } from '../../model/filters/filter/multi-select'; import { FilterSelectItem } from '../select-item'; @@ -12,7 +12,7 @@ interface IFilterMultiSelect extends Omit { - const model = useModelContext(); + const model = useContentModelContext(); const usingRemoteOptions = !!config.options_query_id; const { state } = model.getDataStuffByID(config.options_query_id); const loading = state === 'loading'; diff --git a/dashboard/src/filter/filter-select/render.tsx b/dashboard/src/filter/filter-select/render.tsx index 9a4925ba0..952768ca8 100644 --- a/dashboard/src/filter/filter-select/render.tsx +++ b/dashboard/src/filter/filter-select/render.tsx @@ -1,7 +1,7 @@ import { Select } from '@mantine/core'; import { observer } from 'mobx-react-lite'; import { useEffect } from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { FilterModelInstance } from '../../model'; import { IFilterConfig_Select } from '../../model/filters/filter/select'; import { FilterSelectItem } from '../select-item'; @@ -13,7 +13,7 @@ interface IFilterSelect extends Omit { - const model = useModelContext(); + const model = useContentModelContext(); const usingRemoteOptions = !!config.options_query_id; const { state } = model.getDataStuffByID(config.options_query_id); const loading = state === 'loading'; diff --git a/dashboard/src/filter/filter-settings/filter-setting.tsx b/dashboard/src/filter/filter-settings/filter-setting.tsx index 244aba8d6..c6281f0a8 100644 --- a/dashboard/src/filter/filter-settings/filter-setting.tsx +++ b/dashboard/src/filter/filter-settings/filter-setting.tsx @@ -1,7 +1,7 @@ import { Box, Checkbox, Group, MultiSelect, NumberInput, Select, Stack, Text, TextInput } from '@mantine/core'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { FilterModelInstance } from '../../model'; import { FilterEditorCheckbox } from '../filter-checkbox/editor'; import { FilterEditorDateRange } from '../filter-date-range/editor'; @@ -34,7 +34,7 @@ interface IFilterSetting { } export const FilterSetting = observer(function _FilterSetting({ filter }: IFilterSetting) { - const model = useModelContext(); + const model = useContentModelContext(); const FilterEditor = React.useMemo(() => { return editors[filter.type]; }, [filter.type]); diff --git a/dashboard/src/filter/filter-settings/filter-settings.tsx b/dashboard/src/filter/filter-settings/filter-settings.tsx index a6e4f8d77..a05d035cf 100644 --- a/dashboard/src/filter/filter-settings/filter-settings.tsx +++ b/dashboard/src/filter/filter-settings/filter-settings.tsx @@ -3,7 +3,7 @@ import { randomId } from '@mantine/hooks'; import { useModals } from '@mantine/modals'; import { observer } from 'mobx-react-lite'; import { PlaylistAdd, Recycle, Trash } from 'tabler-icons-react'; -import { useModelContext } from '../../contexts'; +import { useContentModelContext } from '../../contexts'; import { FilterModelInstance } from '../../model'; import { DashboardFilterType } from '../../model/filters/filter/common'; import { createFilterConfig_TextInput } from '../../model/filters/filter/text-input'; @@ -11,7 +11,7 @@ import { FilterSetting } from './filter-setting'; import './filter-settings.css'; export const FilterSettings = observer(function _FilterSettings() { - const model = useModelContext(); + const model = useContentModelContext(); const filters = model.filters.current; const addFilter = () => { diff --git a/dashboard/src/filter/filter-tree-select/render/index.tsx b/dashboard/src/filter/filter-tree-select/render/index.tsx index 51225dffc..9619d77fa 100644 --- a/dashboard/src/filter/filter-tree-select/render/index.tsx +++ b/dashboard/src/filter/filter-tree-select/render/index.tsx @@ -2,7 +2,7 @@ import { Text } from '@mantine/core'; import { cloneDeep } from 'lodash'; import { observer } from 'mobx-react-lite'; import { useEffect, useMemo } from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { FilterModelInstance } from '../../../model'; import { IFilterConfig_TreeSelect } from '../../../model/filters/filter/tree-select'; import { ITreeDataQueryOption, ITreeDataRenderItem } from '../types'; @@ -39,7 +39,7 @@ interface IFilterTreeSelect extends Omit { - const model = useModelContext(); + const model = useContentModelContext(); const { state, dataProxy, len } = model.getDataStuffByID(config.options_query_id); const loading = state === 'loading'; diff --git a/dashboard/src/filter/index.tsx b/dashboard/src/filter/index.tsx index 0246d4742..42c252aa8 100644 --- a/dashboard/src/filter/index.tsx +++ b/dashboard/src/filter/index.tsx @@ -4,11 +4,11 @@ import { observer } from 'mobx-react-lite'; import { useEffect, useMemo } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; import { FilterModelInstance, ViewModelInstance } from '..'; -import { useModelContext } from '../contexts/model-context'; +import { useContentModelContext } from '~/contexts'; import { Filter } from './filter'; export const Filters = observer(function _Filters({ view }: { view: ViewModelInstance }) { - const model = useModelContext(); + const model = useContentModelContext(); const { control, handleSubmit, reset } = useForm({ defaultValues: model.filters.values, diff --git a/dashboard/src/filter/pick-query-for-filter/index.tsx b/dashboard/src/filter/pick-query-for-filter/index.tsx index e1d3911c3..90458ad20 100644 --- a/dashboard/src/filter/pick-query-for-filter/index.tsx +++ b/dashboard/src/filter/pick-query-for-filter/index.tsx @@ -1,10 +1,10 @@ import { Select } from '@mantine/core'; import { observer } from 'mobx-react-lite'; import React from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; export const PickQueryForFilter = observer(({ value, onChange }: { value: string; onChange: (v: string) => void }) => { - const model = useModelContext(); + const model = useContentModelContext(); const options = React.useMemo(() => { return model.queries.options; }, [model.queries.current]); diff --git a/dashboard/src/interactions/interactions-viewer/data/edges.ts b/dashboard/src/interactions/interactions-viewer/data/edges.ts index b04233930..4772ada54 100644 --- a/dashboard/src/interactions/interactions-viewer/data/edges.ts +++ b/dashboard/src/interactions/interactions-viewer/data/edges.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { Edge } from 'reactflow'; -import { DashboardModelInstance } from '~/model'; +import { ContentModelInstance } from '~/model/content'; import { PanelModelInstance } from '~/model/panels'; import { AnyObject } from '~/types'; import { TFlowNode } from './types'; @@ -68,7 +68,7 @@ function makeEdgesFromPanels( return edges; } -export function makeEdges(model: DashboardModelInstance, staticNodeMap: _.Dictionary) { +export function makeEdges(model: ContentModelInstance, staticNodeMap: _.Dictionary) { const filterLabelMap = model.filters.keyLabelMap; const edges = makeEdgesFromPanels(model.panels.list, staticNodeMap, filterLabelMap); return { edges, edgeNodes: [] }; diff --git a/dashboard/src/interactions/interactions-viewer/data/index.ts b/dashboard/src/interactions/interactions-viewer/data/index.ts index f278de08b..248c048ac 100644 --- a/dashboard/src/interactions/interactions-viewer/data/index.ts +++ b/dashboard/src/interactions/interactions-viewer/data/index.ts @@ -1,10 +1,10 @@ import _ from 'lodash'; -import { DashboardModelInstance } from '~/model'; import { makeEdges } from './edges'; import { makeNodes } from './nodes'; import { reposition } from './position'; +import { ContentModelInstance } from '~/model'; -export function makeNodesAndEdges(model: DashboardModelInstance) { +export function makeNodesAndEdges(model: ContentModelInstance) { const staticNodes = makeNodes(model); const staticNodeMap = _.keyBy(staticNodes, (n) => n.id); const { edges, edgeNodes } = makeEdges(model, staticNodeMap); diff --git a/dashboard/src/interactions/interactions-viewer/data/nodes.ts b/dashboard/src/interactions/interactions-viewer/data/nodes.ts index a34661f0e..952bfd900 100644 --- a/dashboard/src/interactions/interactions-viewer/data/nodes.ts +++ b/dashboard/src/interactions/interactions-viewer/data/nodes.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { Position } from 'reactflow'; -import { DashboardModelInstance, FiltersModelInstance, PanelModelInstance, ViewsModelInstance } from '~/model'; +import { ContentModelInstance, FiltersModelInstance, PanelModelInstance, ViewsModelInstance } from '~/model'; import { IViewConfigModel_Tabs, ViewConfigModel_Tabs_Tab_Instance } from '~/model/views/view/tabs'; import { EViewComponentType, ViewComponentTypeBackground } from '~/types'; import { @@ -112,7 +112,7 @@ function addParentToTabView(viewNodes: TFlowNode[]) { }); } -export function makeNodes(model: DashboardModelInstance) { +export function makeNodes(model: ContentModelInstance) { const viewNodes = makeViewNodes(model.views); addParentToTabView(viewNodes); const panelNodes = makePanelNodes(model.views, model.panels.list); diff --git a/dashboard/src/interactions/interactions-viewer/viewer.tsx b/dashboard/src/interactions/interactions-viewer/viewer.tsx index 8a7f73517..6fd03e6ee 100644 --- a/dashboard/src/interactions/interactions-viewer/viewer.tsx +++ b/dashboard/src/interactions/interactions-viewer/viewer.tsx @@ -2,7 +2,7 @@ import _ from 'lodash'; import { observer } from 'mobx-react-lite'; import ReactFlow, { Background, Controls, MiniMap } from 'reactflow'; import 'reactflow/dist/style.css'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { makeNodesAndEdges } from './data'; import { InteractionNode } from './node-with-interactions'; import './viewer.css'; @@ -12,7 +12,7 @@ const nodeTypes = { }; export const InteractionsViewer = observer(() => { - const model = useModelContext(); + const model = useContentModelContext(); const { edges, nodes } = makeNodesAndEdges(model); return ( { - const model = useModelContext(); + const model = useContentModelContext(); const { value = defaultValue, set } = useStorageData( props.operation.operationData, 'config', diff --git a/dashboard/src/interactions/operation/operations/open-view.tsx b/dashboard/src/interactions/operation/operations/open-view.tsx index 61fd2c4e0..cf7c82544 100644 --- a/dashboard/src/interactions/operation/operations/open-view.tsx +++ b/dashboard/src/interactions/operation/operations/open-view.tsx @@ -1,6 +1,6 @@ import { Select } from '@mantine/core'; import { observer } from 'mobx-react-lite'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { useStorageData } from '~/plugins'; import { IDashboardOperation, IDashboardOperationSchema, IOperationConfigProps } from '~/types/plugin'; @@ -9,7 +9,7 @@ export interface IOpenViewOperationConfig { } const OpenViewOperationSettings = observer((props: IOperationConfigProps) => { - const model = useModelContext(); + const model = useContentModelContext(); const { value, set } = useStorageData(props.operation.operationData, 'config'); console.log({ value, viewID: value?.viewID }); diff --git a/dashboard/src/interactions/operation/operations/set-filter-values.tsx b/dashboard/src/interactions/operation/operations/set-filter-values.tsx index a3b3a64fd..3fe3c7265 100644 --- a/dashboard/src/interactions/operation/operations/set-filter-values.tsx +++ b/dashboard/src/interactions/operation/operations/set-filter-values.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Button, Flex, Stack, Text, TextInput } from '@mantine/core'; import { observer } from 'mobx-react-lite'; import { Trash } from 'tabler-icons-react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { DataFieldSelector } from '~/panel/settings/common/data-field-selector'; import { useStorageData } from '~/plugins'; import { IDashboardOperation, IDashboardOperationSchema, IOperationConfigProps } from '~/types/plugin'; @@ -13,7 +13,7 @@ export interface ISetFilterValuesOperationConfig { const defaultValue: ISetFilterValuesOperationConfig = { dictionary: {} }; const SetFilterValuesOperationSettings = observer((props: IOperationConfigProps) => { - const model = useModelContext(); + const model = useContentModelContext(); const { value = defaultValue, set } = useStorageData( props.operation.operationData, 'config', diff --git a/dashboard/src/interactions/temp-hack.ts b/dashboard/src/interactions/temp-hack.ts index 7bbd6a93d..c3d8ae19d 100644 --- a/dashboard/src/interactions/temp-hack.ts +++ b/dashboard/src/interactions/temp-hack.ts @@ -1,8 +1,8 @@ import { useEffect } from 'react'; -import { AnyObject, DashboardModelInstance } from '..'; +import { AnyObject, ContentModelInstance } from '..'; import _, { cloneDeepWith, template } from 'lodash'; -export function useInteractionOperationHacks(model: DashboardModelInstance, inEditMode: boolean) { +export function useInteractionOperationHacks(model: ContentModelInstance, inEditMode: boolean) { useEffect(() => { const handler = (e: $TSFixMe) => { console.log(e); @@ -100,8 +100,8 @@ export function useInteractionOperationHacks(model: DashboardModelInstance, inEd const url = compiled( urlEncodeFields({ ...payload, - filters: model.filters.values, - context: model.context.current, + filters: model.payloadForSQL.filterValues, + context: model.payloadForSQL.context, }), ); diff --git a/dashboard/src/main/dashboard-editor/header/header-menu.tsx b/dashboard/src/main/dashboard-editor/header/header-menu.tsx new file mode 100644 index 000000000..6df56d597 --- /dev/null +++ b/dashboard/src/main/dashboard-editor/header/header-menu.tsx @@ -0,0 +1,38 @@ +import { ActionIcon, Menu } from '@mantine/core'; +import { IconCode, IconDownload, IconMenu2 } from '@tabler/icons'; +import { observer } from 'mobx-react-lite'; +import { ReactNode } from 'react'; +import { useContentModelContext } from '~/contexts'; +import { downloadJSON } from '~/utils/download'; + +interface IProps { + headerMenuItems?: ReactNode; +} +export const HeaderMenu = observer(({ headerMenuItems = null }: IProps) => { + const model = useContentModelContext(); + + const downloadSchema = () => { + const schema = JSON.stringify(model.json, null, 2); + downloadJSON(model.name, schema); + }; + + return ( + + + + + + + + + } onClick={model.queries.downloadAllData}> + Download Data + + } onClick={downloadSchema}> + Download Schema + + {headerMenuItems} + + + ); +}); diff --git a/dashboard/src/main/dashboard-editor/header/index.tsx b/dashboard/src/main/dashboard-editor/header/index.tsx index 1aab888b0..fd72b209a 100644 --- a/dashboard/src/main/dashboard-editor/header/index.tsx +++ b/dashboard/src/main/dashboard-editor/header/index.tsx @@ -1,136 +1,16 @@ -import { ActionIcon, Button, Group, Header as MantineHeader, Text, Tooltip } from '@mantine/core'; -import { useModals } from '@mantine/modals'; -import { - IconAlertTriangle, - IconArrowLeft, - IconCode, - IconDeviceFloppy, - IconDownload, - IconPlaylistAdd, - IconRecycle, -} from '@tabler/icons'; import { observer } from 'mobx-react-lite'; -import { ReactNode } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { useModelContext } from '~/contexts'; -import { ActionIconGroupStyle } from '~/styles/action-icon-group-style'; -import { downloadJSON } from '~/utils/download'; +import { IDashbaordEditorHeaderMain, MainHeader } from './main-header'; +import { SubHeader } from './sub-header'; -interface IDashboardEditorHeader { - saveDashboardChanges: () => void; - headerSlot?: ReactNode; +interface IDashboardEditorHeader extends IDashbaordEditorHeaderMain { + [key: string]: any; } -export const DashboardEditorHeader = observer(({ saveDashboardChanges, headerSlot = null }: IDashboardEditorHeader) => { - const navigate = useNavigate(); - const model = useModelContext(); - - const downloadSchema = () => { - const schema = JSON.stringify(model.json, null, 2); - downloadJSON(model.name, schema); - }; - - const goBack = () => { - navigate(`/dashboard/${model.id}`); - }; - const modals = useModals(); - - const goBackWithConfirmation = () => { - modals.openConfirmModal({ - title: ( - - - There are unsaved changes - - ), - labels: { confirm: 'Discard', cancel: 'Cancel' }, - confirmProps: { color: 'red' }, - onCancel: () => console.log('Cancel'), - onConfirm: goBack, - zIndex: 320, - withCloseButton: false, - }); - }; - - const revertWithConfirmation = () => { - modals.openConfirmModal({ - title: ( - - - You are reverting changes - - ), - labels: { confirm: 'Confirm', cancel: 'Cancel' }, - confirmProps: { color: 'red' }, - onCancel: () => console.log('Cancel'), - onConfirm: () => model.reset(), - zIndex: 320, - withCloseButton: false, - }); - }; - - const hasChanges = model.changed; - +export const DashboardEditorHeader = observer((props: IDashboardEditorHeader) => { return ( - - - - - - {headerSlot} - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + + ); }); diff --git a/dashboard/src/main/dashboard-editor/header/main-header.tsx b/dashboard/src/main/dashboard-editor/header/main-header.tsx new file mode 100644 index 000000000..121702b88 --- /dev/null +++ b/dashboard/src/main/dashboard-editor/header/main-header.tsx @@ -0,0 +1,71 @@ +import { Button, Group, Header as MantineHeader, Text } from '@mantine/core'; +import { useModals } from '@mantine/modals'; +import { IconAlertTriangle, IconArrowLeft } from '@tabler/icons'; +import { observer } from 'mobx-react-lite'; +import { ReactNode } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useModelContext } from '~/contexts'; +import { HeaderMenu } from './header-menu'; +import { ISaveChangesOrMore, SaveChangesOrMore } from './save-changes-or-more'; + +export interface IDashbaordEditorHeaderMain extends ISaveChangesOrMore { + headerSlot?: ReactNode; + headerMenuItems?: ReactNode; +} + +export const MainHeader = observer( + ({ saveDashboardChanges, headerSlot = null, headerMenuItems }: IDashbaordEditorHeaderMain) => { + const navigate = useNavigate(); + const model = useModelContext(); + + const goBack = () => { + navigate(`/dashboard/${model.id}`); + }; + const modals = useModals(); + + const goBackWithConfirmation = () => { + modals.openConfirmModal({ + title: ( + + + There are unsaved changes + + ), + labels: { confirm: 'Discard', cancel: 'Cancel' }, + confirmProps: { color: 'red' }, + onCancel: () => console.log('Cancel'), + onConfirm: goBack, + zIndex: 320, + withCloseButton: false, + }); + }; + + const hasChanges = model.content.changed; + + return ( + + + + + + + + + {headerSlot} + + + + + ); + }, +); diff --git a/dashboard/src/main/dashboard-editor/header/save-changes-or-more.tsx b/dashboard/src/main/dashboard-editor/header/save-changes-or-more.tsx new file mode 100644 index 000000000..2afb8379a --- /dev/null +++ b/dashboard/src/main/dashboard-editor/header/save-changes-or-more.tsx @@ -0,0 +1,82 @@ +import { ActionIcon, Button, Group, Menu, Text } from '@mantine/core'; +import { useModals } from '@mantine/modals'; +import { IconAlertTriangle, IconCaretDown, IconDeviceFloppy, IconRecycle } from '@tabler/icons'; +import { observer } from 'mobx-react-lite'; +import { useContentModelContext } from '~/contexts'; + +export interface ISaveChangesOrMore { + saveDashboardChanges: () => void; +} + +export const SaveChangesOrMore = observer(({ saveDashboardChanges }: ISaveChangesOrMore) => { + const modals = useModals(); + const model = useContentModelContext(); + + const revertWithConfirmation = () => { + modals.openConfirmModal({ + title: ( + + + You are reverting changes + + ), + labels: { confirm: 'Confirm', cancel: 'Cancel' }, + confirmProps: { color: 'red' }, + onCancel: () => console.log('Cancel'), + onConfirm: () => model.reset(), + zIndex: 320, + withCloseButton: false, + }); + }; + + const hasChanges = model.changed; + return ( + + + + + + + + + + + } + disabled={!hasChanges} + onClick={revertWithConfirmation} + > + Revert Changes + + + + + ); +}); diff --git a/dashboard/src/main/dashboard-editor/header/sub-header.tsx b/dashboard/src/main/dashboard-editor/header/sub-header.tsx new file mode 100644 index 000000000..1cb11fbff --- /dev/null +++ b/dashboard/src/main/dashboard-editor/header/sub-header.tsx @@ -0,0 +1,46 @@ +import { Box, Button, Group, Sx } from '@mantine/core'; +import { IconPlaylistAdd } from '@tabler/icons'; +import { observer } from 'mobx-react-lite'; +import { useContentModelContext } from '~/contexts'; + +const SubHeaderSx: Sx = { + position: 'fixed', + top: 60, // height of mantine-header + left: 0, + right: 0, + height: 30, + zIndex: 299, + borderBottom: '1px solid #e9ecef', + background: 'rgba(233,236,239, 0.15)', +}; + +export const SubHeader = observer(() => { + const model = useContentModelContext(); + return ( + + + + + + + + ); +}); diff --git a/dashboard/src/main/dashboard-editor/index.tsx b/dashboard/src/main/dashboard-editor/index.tsx index 607bcee96..08aa1a078 100644 --- a/dashboard/src/main/dashboard-editor/index.tsx +++ b/dashboard/src/main/dashboard-editor/index.tsx @@ -12,7 +12,7 @@ import { ModelContextProvider } from '~/contexts/model-context'; import { ContextInfoType, createDashboardModel } from '~/model'; import { useTopLevelServices } from '../use-top-level-services'; import { createPluginContext, PluginContext } from '~/plugins'; -import { IDashboard } from '../../types/dashboard'; +import { DashboardContentDBType, IDashboard } from '../../types/dashboard'; import { listDataSources } from '~/api-caller'; import './index.css'; import { DashboardEditorHeader } from './header'; @@ -21,6 +21,7 @@ import { Settings } from './settings'; import { useLoadMonacoEditor } from './utils/load-monaco-editor'; import { reaction, toJS } from 'mobx'; import { registerThemes } from '~/styles/register-themes'; +import { ContentModelContextProvider } from '~/contexts/content-model-context'; registerThemes(); @@ -38,7 +39,7 @@ const AppShellStyles = { flexGrow: 1, display: 'flex', flexDirection: 'column', - paddingTop: 60, + paddingTop: 60 + 30, // main header & sub header height: '100vh', }, } as const; @@ -46,60 +47,76 @@ const AppShellStyles = { interface IDashboardProps { context: ContextInfoType; dashboard: IDashboard; + content: DashboardContentDBType; className?: string; - update: (dashboard: IDashboard) => Promise; + update: (d: IDashboard, c: DashboardContentDBType) => Promise; config: IDashboardConfig; onChange?: (dashboard: IDashboard) => void; headerSlot?: ReactNode; + headerMenuItems?: ReactNode; } export interface IDashboardModel { readonly json: IDashboard; - updateCurrent: (dashboard: IDashboard) => void; + updateCurrent: (dashboard: IDashboard, content: DashboardContentDBType) => void; + updateCurrentContent: (content: DashboardContentDBType) => void; } -export const Dashboard = observer( - forwardRef(function _Dashboard( - { context, dashboard, update, className = 'dashboard', config, onChange, headerSlot }: IDashboardProps, - ref: ForwardedRef, - ) { - useLoadMonacoEditor(config.monacoPath); - configureAPIClient(config); +const _DashboardEditor = ( + { + context, + dashboard, + content, + update, + className = 'dashboard', + config, + onChange, + headerSlot, + headerMenuItems, + }: IDashboardProps, + ref: ForwardedRef, +) => { + useLoadMonacoEditor(config.monacoPath); + configureAPIClient(config); - const { data: datasources = [] } = useRequest(listDataSources); + const { data: datasources = [] } = useRequest(listDataSources); - const [layoutFrozen, freezeLayout] = React.useState(false); + const [layoutFrozen, freezeLayout] = React.useState(false); - const model = React.useMemo(() => createDashboardModel(dashboard, datasources, context), [dashboard]); - React.useImperativeHandle(ref, () => model, [model]); - useInteractionOperationHacks(model, true); + const model = React.useMemo( + () => createDashboardModel(dashboard, content, datasources, context), + [dashboard, content], + ); + React.useImperativeHandle(ref, () => model, [model]); + useInteractionOperationHacks(model.content, true); - React.useEffect(() => { - model.context.replace(context); - }, [context]); + React.useEffect(() => { + model.context.replace(context); + }, [context]); - React.useEffect(() => { - model.datasources.replace(datasources); - }, [datasources]); + React.useEffect(() => { + model.datasources.replace(datasources); + }, [datasources]); - React.useEffect(() => { - return reaction( - () => toJS(model.json), - (json) => { - onChange?.(json); - }, - ); - }, [model]); + React.useEffect(() => { + return reaction( + () => toJS(model.json), + (json) => { + onChange?.(json); + }, + ); + }, [model]); - const saveDashboardChanges = async () => { - await update(model.json); - }; + const saveDashboardChanges = async () => { + await update(model.json, model.content.json); + }; - const pluginContext = useCreation(createPluginContext, []); - const configureServices = useTopLevelServices(pluginContext); - return ( - - + const pluginContext = useCreation(createPluginContext, []); + const configureServices = useTopLevelServices(pluginContext); + return ( + + + } + header={ + + } navbar={} styles={AppShellStyles} > @@ -121,7 +144,7 @@ export const Dashboard = observer( position: 'relative', }} > - {model.views.visibleViews.map((view) => ( + {model.content.views.visibleViews.map((view) => ( ))} @@ -130,8 +153,10 @@ export const Dashboard = observer( - - - ); - }), -); + + + + ); +}; + +export const DashboardEditor = observer(forwardRef(_DashboardEditor)); diff --git a/dashboard/src/main/dashboard-editor/navbar/index.tsx b/dashboard/src/main/dashboard-editor/navbar/index.tsx index 26b3e7618..dcf37765f 100644 --- a/dashboard/src/main/dashboard-editor/navbar/index.tsx +++ b/dashboard/src/main/dashboard-editor/navbar/index.tsx @@ -2,26 +2,27 @@ import { ActionIcon, Button, Group, Navbar as MantineNavbar, Text, Tooltip } fro import { IconDatabase, IconFilter, IconLink, IconSettings } from '@tabler/icons'; import { observer } from 'mobx-react-lite'; import { useState } from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext, useModelContext } from '~/contexts'; import { InteractionsViewerModal } from '~/interactions/interactions-viewer'; import { ActionIconGroupStyle } from '~/styles/action-icon-group-style'; import { ViewLinks } from './view-links'; export const DashboardEditorNavbar = observer(() => { const model = useModelContext(); + const content = useContentModelContext(); const openQueries = () => { - if (!model.queries.firstID) { + if (!content.queries.firstID) { model.editor.open(['_QUERIES_', '']); return; } - model.editor.open(['_QUERIES_', model.queries.firstID]); + model.editor.open(['_QUERIES_', content.queries.firstID]); }; const openFilters = () => { - if (!model.filters.firstID) { + if (!content.filters.firstID) { model.editor.open(['_FILTERS_', '']); return; } - model.editor.open(['_FILTERS_', model.filters.firstID]); + model.editor.open(['_FILTERS_', content.filters.firstID]); }; const [interactionsOpened, setInteractionsOpened] = useState(false); @@ -29,25 +30,28 @@ export const DashboardEditorNavbar = observer(() => { const closeInteractions = () => setInteractionsOpened(false); return ( - + - + - + - + diff --git a/dashboard/src/main/dashboard-editor/navbar/view-links.tsx b/dashboard/src/main/dashboard-editor/navbar/view-links.tsx index b2b41d0cc..d15f5502f 100644 --- a/dashboard/src/main/dashboard-editor/navbar/view-links.tsx +++ b/dashboard/src/main/dashboard-editor/navbar/view-links.tsx @@ -2,7 +2,7 @@ import { ActionIcon, Box, Button, Divider, Group, Text, Tooltip, UnstyledButton import { IconAdjustments, IconPlus } from '@tabler/icons'; import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext, useModelContext } from '~/contexts'; interface IViewLink { onClick: () => void; @@ -52,16 +52,17 @@ function ViewLink({ onClick, name, active, openSettings }: IViewLink) { export const ViewLinks = observer(() => { const model = useModelContext(); - const getClickHandler = useCallback((id: string) => () => model.views.setIDOfVIE(id), [model]); + const content = useContentModelContext(); + const getClickHandler = useCallback((id: string) => () => content.views.setIDOfVIE(id), [content]); const openSettings = (id: string) => { model.editor.open(['_VIEWS_', id]); }; return ( - {model.views.options.map((v) => ( + {content.views.options.map((v) => ( openSettings(v.value)} @@ -74,7 +75,7 @@ export const ViewLinks = observer(() => { size="sm" px="xs" color="blue" - onClick={model.views.addARandomNewView} + onClick={content.views.addARandomNewView} sx={{ width: '100%', borderRadius: 0 }} styles={{ inner: { diff --git a/dashboard/src/main/dashboard-editor/settings/content/data-preview/index.tsx b/dashboard/src/main/dashboard-editor/settings/content/data-preview/index.tsx index 6ca616e4f..c6ca72eca 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/data-preview/index.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/data-preview/index.tsx @@ -2,19 +2,19 @@ import { ActionIcon, Box, Group, LoadingOverlay, Stack, Text } from '@mantine/co import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import { Download, Refresh } from 'tabler-icons-react'; -import { useModelContext } from '../../../../../contexts'; +import { useContentModelContext } from '../../../../../contexts'; import { QueryStateMessage } from './query-state-message'; import { DataTable } from './data-table'; export const DataPreview = observer(function _DataPreview({ id }: { id: string }) { - const model = useModelContext(); - const { data, state } = model.getDataStuffByID(id); + const content = useContentModelContext(); + const { data, state } = content.getDataStuffByID(id); const loading = state === 'loading'; const refresh = () => { - model.queries.refetchDataByQueryID(id); + content.queries.refetchDataByQueryID(id); }; const download = () => { - model.queries.downloadDataByQueryID(id); + content.queries.downloadDataByQueryID(id); }; const firstTenRows = useMemo(() => { if (!Array.isArray(data)) { diff --git a/dashboard/src/main/dashboard-editor/settings/content/data-preview/query-state-message.tsx b/dashboard/src/main/dashboard-editor/settings/content/data-preview/query-state-message.tsx index 3d681fb9b..7f433e546 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/data-preview/query-state-message.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/data-preview/query-state-message.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Text } from '@mantine/core'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; interface IQueryStateMessage { queryID: string; } export const QueryStateMessage = ({ queryID }: IQueryStateMessage) => { - const model = useModelContext(); + const model = useContentModelContext(); const { state, error } = model.getDataStuffByID(queryID); const query = React.useMemo(() => model.queries.findByID(queryID), [model, queryID]); if (state === 'loading') { diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-filter/index.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-filter/index.tsx index d9823e3d5..e81255031 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-filter/index.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-filter/index.tsx @@ -2,17 +2,18 @@ import { Box, Button, Group, Stack, Text } from '@mantine/core'; import { useModals } from '@mantine/modals'; import { observer } from 'mobx-react-lite'; import { Trash } from 'tabler-icons-react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext, useModelContext } from '~/contexts'; import { FilterSetting } from '~/filter/filter-settings/filter-setting'; export const EditFilter = observer(({ id }: { id: string }) => { const modals = useModals(); const model = useModelContext(); + const content = useContentModelContext(); if (id === '') { return null; } - const filter = model.filters.findByID(id); + const filter = content.filters.findByID(id); if (!filter) { return Filter by ID[{id}] is not found; } @@ -27,7 +28,7 @@ export const EditFilter = observer(({ id }: { id: string }) => { labels: { confirm: 'Confirm', cancel: 'Cancel' }, onCancel: () => console.log('Cancel'), onConfirm: () => { - model.filters.removeByID(id); + content.filters.removeByID(id); resetEditorPath(); }, zIndex: 320, diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-mock-context/index.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-mock-context/index.tsx index 910c019a3..3d2f386be 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-mock-context/index.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-mock-context/index.tsx @@ -3,14 +3,14 @@ import { showNotification } from '@mantine/notifications'; import { observer } from 'mobx-react-lite'; import { useMemo, useState } from 'react'; import { DeviceFloppy } from 'tabler-icons-react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; export const EditMockContext = observer(() => { - const model = useModelContext(); - const [v, setV] = useState(() => JSON.stringify(model.mock_context.current, null, 4)); + const content = useContentModelContext(); + const [v, setV] = useState(() => JSON.stringify(content.mock_context.current, null, 4)); const submit = () => { try { - model.mock_context.replace(JSON.parse(v)); + content.mock_context.replace(JSON.parse(v)); } catch (error) { showNotification({ title: 'Failed', @@ -23,11 +23,11 @@ export const EditMockContext = observer(() => { const changed = useMemo(() => { try { - return JSON.stringify(JSON.parse(v)) !== JSON.stringify(model.mock_context.current); + return JSON.stringify(JSON.parse(v)) !== JSON.stringify(content.mock_context.current); } catch (error) { return false; } - }, [v, model.mock_context.current]); + }, [v, content.mock_context.current]); return ( diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/change-view-of-panel.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/change-view-of-panel.tsx index 6dd8876c7..0634598bb 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/change-view-of-panel.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/change-view-of-panel.tsx @@ -3,7 +3,7 @@ import { useDisclosure } from '@mantine/hooks'; import { IconBoxMultiple, IconDeviceFloppy, IconX } from '@tabler/icons'; import { observer } from 'mobx-react-lite'; import { useEffect, useState } from 'react'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { PanelModelInstance } from '~/model/panels'; interface IChangeViewOfPanel { @@ -11,7 +11,7 @@ interface IChangeViewOfPanel { sourceViewID: string; } export const ChangeViewOfPanel = observer(({ panel, sourceViewID }: IChangeViewOfPanel) => { - const model = useModelContext(); + const content = useContentModelContext(); const [targetViewID, setTargetViewID] = useState(sourceViewID); useEffect(() => { setTargetViewID(sourceViewID); @@ -37,7 +37,7 @@ export const ChangeViewOfPanel = observer(({ panel, sourceViewID }: IChangeViewO sx={{ flexGrow: 1, maxHeight: 'calc(100vh - 185px - 30px)', overflow: 'auto' }} > - {model.views.options.map((o) => ( + {content.views.options.map((o) => ( ))} diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/index.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/index.tsx index d1119e4dd..0511111b8 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/index.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/index.tsx @@ -1,15 +1,15 @@ import { Text } from '@mantine/core'; import { observer } from 'mobx-react-lite'; -import { useModelContext } from '~/contexts'; +import { useContentModelContext } from '~/contexts'; import { PanelEditor } from './panel-editor'; export const EditPanel = observer(({ viewID, panelID }: { viewID: string; panelID: string }) => { - const model = useModelContext(); - const view = model.views.findByID(viewID); + const content = useContentModelContext(); + const view = content.views.findByID(viewID); if (!view) { return View by ID[{viewID}] is not found; } - const panel = model.panels.findByID(panelID); + const panel = content.panels.findByID(panelID); if (!panel) { return Panel by ID[{panelID}] is not found; } diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/panel-editor.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/panel-editor.tsx index e914bfdc1..f0bae8d3a 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/panel-editor.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/panel-editor.tsx @@ -3,7 +3,7 @@ import { useModals } from '@mantine/modals'; import { IconTrash } from '@tabler/icons'; import { observer } from 'mobx-react-lite'; import { ReactNode, useEffect, useState } from 'react'; -import { PanelContextProvider, useModelContext } from '~/contexts'; +import { PanelContextProvider, useContentModelContext, useModelContext } from '~/contexts'; import { InteractionSettingsPanel } from '~/interactions'; import { PanelConfig } from '~/main/dashboard-editor/settings/content/edit-panel/panel-config'; import { PickQuery } from '~/main/dashboard-editor/settings/content/edit-panel/pick-query'; @@ -52,9 +52,10 @@ function doesVizRequiresData(type: string) { export const PanelEditor = observer(({ panel }: { panel: PanelModelInstance }) => { const modals = useModals(); const model = useModelContext(); + const content = useContentModelContext(); const [tab, setTab] = useState('Data'); - const { data, state, error } = model.getDataStuffByID(panel.queryID); - const query = model.queries.findByID(panel.queryID); + const { data, state, error } = content.getDataStuffByID(panel.queryID); + const query = content.queries.findByID(panel.queryID); const panelNeedData = doesVizRequiresData(panel.viz.type); const loading = panelNeedData && state === 'loading'; @@ -80,7 +81,7 @@ export const PanelEditor = observer(({ panel }: { panel: PanelModelInstance }) = labels: { confirm: 'Confirm', cancel: 'Cancel' }, onCancel: () => console.log('Cancel'), onConfirm: () => { - model.removePanelByID(panel.id, viewID); + content.removePanelByID(panel.id, viewID); resetEditorPath(); }, confirmProps: { color: 'red' }, diff --git a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/pick-query/index.tsx b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/pick-query/index.tsx index ce80d2396..0aa949ac4 100644 --- a/dashboard/src/main/dashboard-editor/settings/content/edit-panel/pick-query/index.tsx +++ b/dashboard/src/main/dashboard-editor/settings/content/edit-panel/pick-query/index.tsx @@ -1,12 +1,12 @@ import { ActionIcon, Group, Select, Stack, Text, Tooltip } from '@mantine/core'; import { IconArrowCurveRight } from '@tabler/icons'; import { observer } from 'mobx-react-lite'; -import React from 'react'; -import { useModelContext, usePanelContext } from '../../../../../../contexts'; +import { useContentModelContext, useModelContext, usePanelContext } from '../../../../../../contexts'; import { DataPreview } from '../../data-preview'; export const PickQuery = observer(function _PickQuery() { const model = useModelContext(); + const content = useContentModelContext(); const { panel: { queryID, setQueryID }, } = usePanelContext(); @@ -20,7 +20,7 @@ export const PickQuery = observer(function _PickQuery() { Use query