Skip to content

Commit

Permalink
feat: dashboard (#860)
Browse files Browse the repository at this point in the history
* feat: plugins

* feat: dashboard panel and plugin publish procedure

* chore: rename dashboard db name

* feat: base query add cell format params

* feat: dashboard and plugin render

* feat: dashboard permission controll

* chore: remove chart page

* feat: add isExpand status

* feat: auth plugin render

* feat: chart plugin

* chore: add plugin chart scripts

* chore: remove dist

* feat: plugin docker build and chart plugin init

* chore: plugin chart build

* chore: plugin chart lint

* fix: base query e2e

* fix: markdown preview theme

* fix: plugin e2e

* fix: first admin user

* fix: insert env in nextjs-app/.env

* fix: e2e error

* fix: plugin rows

* fix: plugin and dashboard service spec

* fix: init official plugin lock attachments database table

* fix: test error

* fix: init plugin conflict on e2e

* fix: init plugin conflict on e2e

* fix: init plugin conflict on e2e

* fix: init plugin conflict on e2e

* chore: better message

* fix: init plugin conflict on e2e

* chore: remove lock
  • Loading branch information
boris-w authored Sep 18, 2024
1 parent a184b5a commit a3171ae
Show file tree
Hide file tree
Showing 241 changed files with 10,091 additions and 1,111 deletions.
1 change: 1 addition & 0 deletions apps/nestjs-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"fs-extra": "11.2.0",
"handlebars": "4.7.8",
"helmet": "7.1.0",
"http-proxy-middleware": "3.0.2",
"ioredis": "5.4.1",
"is-port-reachable": "3.1.0",
"joi": "17.12.2",
Expand Down
4 changes: 4 additions & 0 deletions apps/nestjs-backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AuthModule } from './features/auth/auth.module';
import { BaseModule } from './features/base/base.module';
import { ChatModule } from './features/chat/chat.module';
import { CollaboratorModule } from './features/collaborator/collaborator.module';
import { DashboardModule } from './features/dashboard/dashboard.module';
import { ExportOpenApiModule } from './features/export/open-api/export-open-api.module';
import { FieldOpenApiModule } from './features/field/open-api/field-open-api.module';
import { HealthModule } from './features/health/health.module';
Expand All @@ -16,6 +17,7 @@ import { NextModule } from './features/next/next.module';
import { NotificationModule } from './features/notification/notification.module';
import { OAuthModule } from './features/oauth/oauth.module';
import { PinModule } from './features/pin/pin.module';
import { PluginModule } from './features/plugin/plugin.module';
import { SelectionModule } from './features/selection/selection.module';
import { SettingModule } from './features/setting/setting.module';
import { ShareModule } from './features/share/share.module';
Expand Down Expand Up @@ -55,6 +57,8 @@ export const appModules = {
SettingModule,
OAuthModule,
TrashModule,
PluginModule,
DashboardModule,
],
providers: [InitBootstrapProvider],
};
Expand Down
5 changes: 5 additions & 0 deletions apps/nestjs-backend/src/cache/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ICacheStore {
// userId:tableId:windowId
[key: `operations:undo:${string}:${string}:${string}`]: IUndoRedoOperation[];
[key: `operations:redo:${string}:${string}:${string}`]: IUndoRedoOperation[];
[key: `plugin:auth-code:${string}`]: IPluginAuthStore;
}

export interface IAttachmentSignatureCache {
Expand Down Expand Up @@ -242,3 +243,7 @@ export type IUndoRedoOperation =
| ICreateViewOperation
| IDeleteViewOperation
| IUpdateViewOperation;
export interface IPluginAuthStore {
baseId: string;
pluginId: string;
}
1 change: 1 addition & 0 deletions apps/nestjs-backend/src/configs/base.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const baseConfig = registerAs('base', () => ({
defaultMaxBaseDBConnections: Number(process.env.DEFAULT_MAX_BASE_DB_CONNECTIONS ?? 20),
templateSpaceId: process.env.TEMPLATE_SPACE_ID,
recordHistoryDisabled: process.env.RECORD_HISTORY_DISABLED === 'true',
pluginServerPort: process.env.PLUGIN_SERVER_PORT || '3002',
}));

export const BaseConfig = () => Inject(baseConfig.KEY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ export class AccessTokenService {

async validate(splitAccessTokenObj: { accessTokenId: string; sign: string }) {
const { accessTokenId, sign } = splitAccessTokenObj;
const accessTokenEntity = await this.prismaService.accessToken.findUniqueOrThrow({
where: { id: accessTokenId },
select: {
userId: true,
id: true,
sign: true,
expiredTime: true,
},
});
const accessTokenEntity = await this.prismaService.accessToken
.findUniqueOrThrow({
where: { id: accessTokenId },
select: {
userId: true,
id: true,
sign: true,
expiredTime: true,
},
})
.catch(() => {
throw new UnauthorizedException('token not found');
});
if (sign !== accessTokenEntity.sign) {
throw new UnauthorizedException('sign error');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class AttachmentsController {
headers['Content-Disposition'] = responseContentDisposition;
}
}
headers['Cross-Origin-Resource-Policy'] = 'unsafe-none';
res.set(headers);
return new StreamableFile(fileStream);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default abstract class StorageAdapter {
case UploadType.Avatar:
case UploadType.OAuth:
case UploadType.Form:
case UploadType.Plugin:
return storageConfig().publicBucket;
default:
throw new BadRequestException('Invalid upload type');
Expand All @@ -31,6 +32,8 @@ export default abstract class StorageAdapter {
return 'oauth';
case UploadType.Import:
return 'import';
case UploadType.Plugin:
return 'plugin';
default:
throw new BadRequestException('Invalid upload type');
}
Expand Down
8 changes: 5 additions & 3 deletions apps/nestjs-backend/src/features/attachments/plugins/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,13 @@ export class LocalStorage implements StorageAdapter {
});
}

async save(filePath: string, rename: string) {
async save(filePath: string, rename: string, isDelete: boolean = true) {
const distPath = resolve(this.storageDir);
const newFilePath = resolve(distPath, rename);
await fse.copy(filePath, newFilePath);
await fse.remove(filePath);
if (isDelete) {
await fse.remove(filePath);
}
return join(this.path, rename);
}

Expand Down Expand Up @@ -239,7 +241,7 @@ export class LocalStorage implements StorageAdapter {
_metadata: Record<string, unknown>
) {
const hash = await FileUtils.getHash(filePath);
await this.save(filePath, join(bucket, path));
await this.save(filePath, join(bucket, path), false);
return {
hash,
path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class PermissionService {
select: { scopes: true, spaceIds: true, baseIds: true, clientId: true, userId: true },
});
const scopes = JSON.parse(stringifyScopes) as Action[];
if (clientId) {
if (clientId && clientId.startsWith(IdPrefix.OAuthClient)) {
const { spaceIds: spaceIdsByOAuth, baseIds: baseIdsByOAuth } =
await this.getOAuthAccessBy(userId);
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class AccessTokenStrategy extends PassportStrategy(PassportAccessTokenStr

const user = await this.userService.getUserById(userId);
if (!user) {
throw new UnauthorizedException();
throw new UnauthorizedException('User not found');
}
if (user.deactivatedTime) {
throw new UnauthorizedException('Your account has been deactivated by the administrator');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common';
import type { IAttachmentCellValue } from '@teable/core';
import { CellFormat, FieldType } from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import { BaseQueryColumnType, BaseQueryJoinType } from '@teable/openapi';
import type { IBaseQueryJoin, IBaseQuery, IBaseQueryVo, IBaseQueryColumn } from '@teable/openapi';
Expand All @@ -14,6 +16,7 @@ import {
createFieldInstanceByVo,
type IFieldInstance,
} from '../../field/model/factory';
import { RecordService } from '../../record/record.service';
import { QueryAggregation } from './parse/aggregation';
import { QueryFilter } from './parse/filter';
import { QueryGroup } from './parse/group';
Expand All @@ -31,7 +34,8 @@ export class BaseQueryService {

private readonly fieldService: FieldService,
private readonly prismaService: PrismaService,
private readonly cls: ClsService<IClsStore>
private readonly cls: ClsService<IClsStore>,
private readonly recordService: RecordService
) {}

private convertFieldMapToColumn(fieldMap: Record<string, IFieldInstance>): IBaseQueryColumn[] {
Expand All @@ -48,23 +52,56 @@ export class BaseQueryService {
});
}

private handleBigIntRows(rows: { [key in string]: unknown }[]) {
return rows.map((row) => {
return Object.entries(row).reduce(
(acc, [key, value]) => {
// eslint-disable-next-line sonarjs/cognitive-complexity
private async dbRows2Rows(
rows: Record<string, unknown>[],
columns: IBaseQueryColumn[],
cellFormat: CellFormat
) {
const resRows: Record<string, unknown>[] = [];
for (const row of rows) {
const resRow: Record<string, unknown> = {};
for (const field of columns) {
if (!field.fieldSource) {
const value = row[field.column];
resRow[field.column] = row[field.column];
// handle bigint
if (typeof value === 'bigint') {
acc[key] = Number(value);
resRow[field.column] = Number(value);
} else {
acc[key] = value;
resRow[field.column] = value;
}
return acc;
},
{} as { [key in string]: unknown }
);
});
continue;
}
const dbCellValue = row[field.column];
const fieldInstance = createFieldInstanceByVo(field.fieldSource);
const cellValue = fieldInstance.convertDBValue2CellValue(dbCellValue);

// number no need to convert string
if (typeof cellValue === 'number') {
resRow[field.column] = cellValue;
continue;
}
if (cellValue != null) {
resRow[field.column] =
cellFormat === CellFormat.Text ? fieldInstance.cellValue2String(cellValue) : cellValue;
}
if (fieldInstance.type === FieldType.Attachment) {
resRow[field.column] = await this.recordService.getAttachmentPresignedCellValue(
cellValue as IAttachmentCellValue
);
}
}
resRows.push(resRow);
}
return resRows;
}

async baseQuery(baseId: string, baseQuery: IBaseQuery): Promise<IBaseQueryVo> {
async baseQuery(
baseId: string,
baseQuery: IBaseQuery,
cellFormat: CellFormat = CellFormat.Json
): Promise<IBaseQueryVo> {
const { queryBuilder, fieldMap } = await this.parseBaseQuery(baseId, baseQuery, 0);
const query = queryBuilder.toQuery();
this.logger.log('baseQuery SQL: ', query);
Expand All @@ -74,10 +111,11 @@ export class BaseQueryService {
this.logger.error(e);
throw new BadRequestException(`Query failed: ${query}, ${e.message}`);
});
const columns = this.convertFieldMapToColumn(fieldMap);

return {
rows: this.handleBigIntRows(rows),
columns: this.convertFieldMapToColumn(fieldMap),
rows: await this.dbRows2Rows(rows, columns, cellFormat),
columns,
};
}

Expand Down
3 changes: 2 additions & 1 deletion apps/nestjs-backend/src/features/base/base.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,12 @@ export class BaseController {
}

@Get(':baseId/query')
@Permissions('base|query_data')
async sqlQuery(
@Param('baseId') baseId: string,
@Query(new ZodValidationPipe(baseQuerySchemaRo)) query: IBaseQuerySchemaRo
) {
return this.baseQueryService.baseQuery(baseId, query.query);
return this.baseQueryService.baseQuery(baseId, query.query, query.cellFormat);
}

@Permissions('base|invite_link')
Expand Down
10 changes: 9 additions & 1 deletion apps/nestjs-backend/src/features/base/base.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DbProvider } from '../../db-provider/db.provider';
import { CollaboratorModule } from '../collaborator/collaborator.module';
import { FieldModule } from '../field/field.module';
import { InvitationModule } from '../invitation/invitation.module';
import { RecordModule } from '../record/record.module';
import { TableOpenApiModule } from '../table/open-api/table-open-api.module';
import { TableModule } from '../table/table.module';
import { BaseDuplicateService } from './base-duplicate.service';
Expand All @@ -13,7 +14,14 @@ import { DbConnectionService } from './db-connection.service';

@Module({
controllers: [BaseController],
imports: [CollaboratorModule, FieldModule, TableModule, InvitationModule, TableOpenApiModule],
imports: [
CollaboratorModule,
FieldModule,
TableModule,
InvitationModule,
TableOpenApiModule,
RecordModule,
],
providers: [DbProvider, BaseService, DbConnectionService, BaseDuplicateService, BaseQueryService],
exports: [BaseService, DbConnectionService, BaseDuplicateService],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { TestingModule } from '@nestjs/testing';
import { Test } from '@nestjs/testing';
import { DashboardController } from './dashboard.controller';

describe('DashboardController', () => {
let controller: DashboardController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DashboardController],
}).compile();

controller = module.get<DashboardController>(DashboardController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Loading

0 comments on commit a3171ae

Please sign in to comment.