Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dashboard #860

Merged
merged 32 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
525f7a2
feat: plugins
boris-w Aug 26, 2024
d6c9e6b
feat: dashboard panel and plugin publish procedure
boris-w Aug 28, 2024
b453daa
chore: rename dashboard db name
boris-w Aug 28, 2024
bd61e4b
feat: base query add cell format params
boris-w Sep 10, 2024
70818de
feat: dashboard and plugin render
boris-w Sep 10, 2024
a2bed49
feat: dashboard permission controll
boris-w Sep 11, 2024
2870bda
chore: remove chart page
boris-w Sep 11, 2024
050bcb8
feat: add isExpand status
boris-w Sep 11, 2024
ac5dc0e
feat: auth plugin render
boris-w Sep 12, 2024
4080dfb
feat: chart plugin
boris-w Sep 12, 2024
9eb90dd
chore: add plugin chart scripts
boris-w Sep 13, 2024
cddc897
chore: remove dist
boris-w Sep 13, 2024
2d89b1b
feat: plugin docker build and chart plugin init
boris-w Sep 14, 2024
99c18cc
chore: plugin chart build
boris-w Sep 14, 2024
eaef4bb
chore: plugin chart lint
boris-w Sep 14, 2024
caf404e
fix: base query e2e
boris-w Sep 14, 2024
09be84c
fix: markdown preview theme
boris-w Sep 14, 2024
a149025
fix: plugin e2e
boris-w Sep 14, 2024
b19e8fe
fix: first admin user
boris-w Sep 14, 2024
5e484bd
fix: insert env in nextjs-app/.env
boris-w Sep 14, 2024
5187407
fix: e2e error
boris-w Sep 14, 2024
2dac412
fix: plugin rows
boris-w Sep 14, 2024
e9608bb
fix: plugin and dashboard service spec
boris-w Sep 14, 2024
b3a0e9b
fix: init official plugin lock attachments database table
boris-w Sep 14, 2024
85e2c88
fix: test error
boris-w Sep 14, 2024
7a5403e
fix: init plugin conflict on e2e
boris-w Sep 14, 2024
8e7b4d8
fix: init plugin conflict on e2e
boris-w Sep 14, 2024
e35e526
fix: init plugin conflict on e2e
boris-w Sep 14, 2024
966f83a
fix: init plugin conflict on e2e
boris-w Sep 14, 2024
9a7c3ab
chore: better message
boris-w Sep 14, 2024
151c139
fix: init plugin conflict on e2e
boris-w Sep 14, 2024
d1222be
chore: remove lock
boris-w Sep 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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