Browse Source

feat: add tables apis

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 2 years ago
parent
commit
8ef95f63d1
  1. 128
      packages/nocodb-nest/src/modules/tables/tables.controller.ts
  2. 485
      packages/nocodb-nest/src/modules/tables/tables.service.ts
  3. 9
      packages/nocodb-nest/src/utils/extractRolesObj.ts

128
packages/nocodb-nest/src/modules/tables/tables.controller.ts

@ -1,7 +1,131 @@
import { Controller } from '@nestjs/common';
import {
Controller,
Get,
Param,
Query,
UseGuards,
Request,
Post,
Body,
Patch,
Delete,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { TableReqType } from 'nocodb-sdk';
import extractRolesObj from 'src/utils/extractRolesObj';
import { PagedResponseImpl } from '../../helpers/PagedResponse';
import {
ExtractProjectIdMiddleware,
UseAclMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware';
import { TablesService } from './tables.service';
@Controller('tables')
@Controller()
@UseGuards(ExtractProjectIdMiddleware, AuthGuard('jwt'))
export class TablesController {
constructor(private readonly tablesService: TablesService) {}
@Get([
'/api/v1/db/meta/projects/:projectId/tables',
'/api/v1/db/meta/projects/:projectId/:baseId/tables',
])
@UseAclMiddleware({
permissionName: 'tableList',
})
async tableList(
@Param('projectId') projectId: string,
@Param('baseId') baseId: string,
@Query('includeM2M') includeM2M: string,
@Request() req,
) {
return new PagedResponseImpl(
await this.tablesService.getAccessibleTables({
projectId,
baseId,
includeM2M: includeM2M === 'true',
roles: extractRolesObj(req.user.roles),
}),
);
}
@Post([
'/api/v1/db/meta/projects/:projectId/tables',
'/api/v1/db/meta/projects/:projectId/:baseId/tables',
])
@UseAclMiddleware({
permissionName: 'tableCreate',
})
async tableCreate(
@Param('projectId') projectId: string,
@Param('baseId') baseId: string,
@Body() body: TableReqType,
@Request() req,
) {
const result = await this.tablesService.tableCreate({
projectId: projectId,
baseId: baseId,
table: body,
user: req.user,
});
return result;
}
@Get('/api/v1/db/meta/tables/:tableId')
@UseAclMiddleware({
permissionName: 'tableGet',
})
async tableGet(@Param('tableId') tableId: string, @Request() req) {
const table = await this.tablesService.getTableWithAccessibleViews({
tableId: req.params.tableId,
user: req.user,
});
return table;
}
@Patch('/api/v1/db/meta/tables/:tableId')
@UseAclMiddleware({
permissionName: 'tableUpdate',
})
async tableUpdate(
@Param('tableId') tableId: string,
@Body() body: TableReqType,
@Request() req,
) {
await this.tablesService.tableUpdate({
tableId: tableId,
table: body,
projectId: req.user,
});
return { msg: 'The table has been updated successfully' };
}
@Delete('/api/v1/db/meta/tables/:tableId')
@UseAclMiddleware({
permissionName: 'tableDelete',
})
async tableDelete(@Param('tableId') tableId: string, @Request() req) {
const result = await this.tablesService.tableDelete({
tableId: req.params.tableId,
user: (req as any).user,
req,
});
return result;
}
@Post('/api/v1/db/meta/tables/:tableId/reorder')
@UseAclMiddleware({
permissionName: 'tableReorder',
})
async tableReorder(
@Param('tableId') tableId: string,
@Body() body: { order: number },
) {
return this.tablesService.reorderTable({
tableId,
order: body.order,
});
}
}

485
packages/nocodb-nest/src/modules/tables/tables.service.ts

@ -1,4 +1,487 @@
import { Injectable } from '@nestjs/common';
import DOMPurify from 'isomorphic-dompurify';
import {
AuditOperationSubTypes,
AuditOperationTypes,
ColumnType,
isVirtualCol,
ModelTypes,
NormalColumnRequestType,
TableReqType,
UITypes,
} from 'nocodb-sdk';
import ProjectMgrv2 from '../../db/sql-mgr/v2/ProjectMgrv2';
import { NcError } from '../../helpers/catchError';
import getColumnPropsFromUIDT from '../../helpers/getColumnPropsFromUIDT';
import getColumnUiType from '../../helpers/getColumnUiType';
import getTableNameAlias, {
getColumnNameAlias,
} from '../../helpers/getTableName';
import mapDefaultDisplayValue from '../../helpers/mapDefaultDisplayValue';
import {
Audit,
Column,
LinkToAnotherRecordColumn,
Model,
ModelRoleVisibility,
Project,
User,
View,
} from '../../models';
import NcConnectionMgrv2 from '../../utils/common/NcConnectionMgrv2';
import { T } from 'nc-help';
import { validatePayload } from 'src/helpers';
@Injectable()
export class TablesService {}
export class TablesService {
async tableUpdate(param: {
tableId: any;
table: TableReqType & { project_id?: string };
projectId?: string;
}) {
const model = await Model.get(param.tableId);
const project = await Project.getWithInfo(
param.table.project_id || param.projectId,
);
const base = project.bases.find((b) => b.id === model.base_id);
if (model.project_id !== project.id) {
NcError.badRequest('Model does not belong to project');
}
// if meta present update meta and return
// todo: allow user to update meta and other prop in single api call
if ('meta' in param.table) {
await Model.updateMeta(param.tableId, param.table.meta);
return true;
}
if (!param.table.table_name) {
NcError.badRequest(
'Missing table name `table_name` property in request body',
);
}
if (base.is_meta && project.prefix) {
if (!param.table.table_name.startsWith(project.prefix)) {
param.table.table_name = `${project.prefix}${param.table.table_name}`;
}
}
param.table.table_name = DOMPurify.sanitize(param.table.table_name);
// validate table name
if (/^\s+|\s+$/.test(param.table.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names',
);
}
if (
!(await Model.checkTitleAvailable({
table_name: param.table.table_name,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
}
if (!param.table.title) {
param.table.title = getTableNameAlias(
param.table.table_name,
project.prefix,
base,
);
}
if (
!(await Model.checkAliasAvailable({
title: param.table.title,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table alias');
}
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.knex.clientType();
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (param.table.table_name.length > tableNameLengthLimit) {
NcError.badRequest(
`Table name exceeds ${tableNameLengthLimit} characters`,
);
}
await Model.updateAliasAndTableName(
param.tableId,
param.table.title,
param.table.table_name,
);
await sqlMgr.sqlOpPlus(base, 'tableRename', {
...param.table,
tn: param.table.table_name,
tn_old: model.table_name,
});
T.emit('evt', { evt_type: 'table:updated' });
return true;
}
reorderTable(param: { tableId: string; order: any }) {
return Model.updateOrder(param.tableId, param.order);
}
async tableDelete(param: { tableId: string; user: User; req?: any }) {
const table = await Model.getByIdOrName({ id: param.tableId });
await table.getColumns();
const relationColumns = table.columns.filter(
(c) => c.uidt === UITypes.LinkToAnotherRecord,
);
if (relationColumns?.length) {
const referredTables = await Promise.all(
relationColumns.map(async (c) =>
c
.getColOptions<LinkToAnotherRecordColumn>()
.then((opt) => opt.getRelatedTable())
.then(),
),
);
NcError.badRequest(
`Table can't be deleted since Table is being referred in following tables : ${referredTables.join(
', ',
)}. Delete LinkToAnotherRecord columns and try again.`,
);
}
const project = await Project.getWithInfo(table.project_id);
const base = project.bases.find((b) => b.id === table.base_id);
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
(table as any).tn = table.table_name;
table.columns = table.columns.filter((c) => !isVirtualCol(c));
table.columns.forEach((c) => {
(c as any).cn = c.column_name;
});
if (table.type === ModelTypes.TABLE) {
await sqlMgr.sqlOpPlus(base, 'tableDelete', table);
} else if (table.type === ModelTypes.VIEW) {
await sqlMgr.sqlOpPlus(base, 'viewDelete', {
...table,
view_name: table.table_name,
});
}
await Audit.insert({
project_id: project.id,
base_id: base.id,
op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.DELETED,
user: param.user?.email,
description: `Deleted ${table.type} ${table.table_name} with alias ${table.title} `,
ip: param.req?.clientIp,
}).then(() => {});
T.emit('evt', { evt_type: 'table:deleted' });
return table.delete();
}
async getTableWithAccessibleViews(param: { tableId: string; user: User }) {
const table = await Model.getWithInfo({
id: param.tableId,
});
// todo: optimise
const viewList = <View[]>(
await this.xcVisibilityMetaGet(table.project_id, [table])
);
//await View.list(param.tableId)
table.views = viewList.filter((table: any) => {
return Object.keys(param.user?.roles).some(
(role) => param.user?.roles[role] && !table.disabled[role],
);
});
return table;
}
async xcVisibilityMetaGet(
projectId,
_models: Model[] = null,
includeM2M = true,
// type: 'table' | 'tableAndViews' | 'views' = 'table'
) {
// todo: move to
const roles = [
'owner',
'creator',
'viewer',
'editor',
'commenter',
'guest',
];
const defaultDisabled = roles.reduce((o, r) => ({ ...o, [r]: false }), {});
let models =
_models ||
(await Model.list({
project_id: projectId,
base_id: undefined,
}));
models = includeM2M ? models : (models.filter((t) => !t.mm) as Model[]);
const result = await models.reduce(async (_obj, model) => {
const obj = await _obj;
const views = await model.getViews();
for (const view of views) {
obj[view.id] = {
ptn: model.table_name,
_ptn: model.title,
ptype: model.type,
tn: view.title,
_tn: view.title,
table_meta: model.meta,
...view,
disabled: { ...defaultDisabled },
};
}
return obj;
}, Promise.resolve({}));
const disabledList = await ModelRoleVisibility.list(projectId);
for (const d of disabledList) {
if (result[d.fk_view_id])
result[d.fk_view_id].disabled[d.role] = !!d.disabled;
}
return Object.values(result);
}
async getAccessibleTables(param: {
projectId: string;
baseId: string;
includeM2M?: boolean;
roles: Record<string, boolean>;
}) {
const viewList = await this.xcVisibilityMetaGet(param.projectId);
// todo: optimise
const tableViewMapping = viewList.reduce((o, view: any) => {
o[view.fk_model_id] = o[view.fk_model_id] || 0;
if (
Object.keys(param.roles).some(
(role) => param.roles[role] && !view.disabled[role],
)
) {
o[view.fk_model_id]++;
}
return o;
}, {});
const tableList = (
await Model.list({
project_id: param.projectId,
base_id: param.baseId,
})
).filter((t) => tableViewMapping[t.id]);
return param.includeM2M
? tableList
: (tableList.filter((t) => !t.mm) as Model[]);
}
async tableCreate(param: {
projectId: string;
baseId?: string;
table: TableReqType;
user: User;
req?: any;
}) {
validatePayload('swagger.json#/components/schemas/TableReq', param.table);
const tableCreatePayLoad: Omit<TableReqType, 'columns'> & {
columns: (Omit<ColumnType, 'column_name' | 'title'> & { cn?: string })[];
} = {
...param.table,
};
const project = await Project.getWithInfo(param.projectId);
let base = project.bases[0];
if (param.baseId) {
base = project.bases.find((b) => b.id === param.baseId);
}
if (
!tableCreatePayLoad.table_name ||
(project.prefix && project.prefix === tableCreatePayLoad.table_name)
) {
NcError.badRequest(
'Missing table name `table_name` property in request body',
);
}
if (base.is_meta && project.prefix) {
if (!tableCreatePayLoad.table_name.startsWith(project.prefix)) {
tableCreatePayLoad.table_name = `${project.prefix}_${tableCreatePayLoad.table_name}`;
}
}
tableCreatePayLoad.table_name = DOMPurify.sanitize(
tableCreatePayLoad.table_name,
);
// validate table name
if (/^\s+|\s+$/.test(tableCreatePayLoad.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names',
);
}
if (
!(await Model.checkTitleAvailable({
table_name: tableCreatePayLoad.table_name,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
}
if (!tableCreatePayLoad.title) {
tableCreatePayLoad.title = getTableNameAlias(
tableCreatePayLoad.table_name,
project.prefix,
base,
);
}
if (
!(await Model.checkAliasAvailable({
title: tableCreatePayLoad.title,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table alias');
}
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.knex.clientType();
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (tableCreatePayLoad.table_name.length > tableNameLengthLimit) {
NcError.badRequest(
`Table name exceeds ${tableNameLengthLimit} characters`,
);
}
const mxColumnLength = Column.getMaxColumnNameLength(sqlClientType);
for (const column of param.table.columns) {
if (column.column_name.length > mxColumnLength) {
NcError.badRequest(
`Column name ${column.column_name} exceeds ${mxColumnLength} characters`,
);
}
}
tableCreatePayLoad.columns = await Promise.all(
param.table.columns?.map(async (c) => ({
...(await getColumnPropsFromUIDT(c as any, base)),
cn: c.column_name,
column_name: c.column_name,
})),
);
await sqlMgr.sqlOpPlus(base, 'tableCreate', {
...tableCreatePayLoad,
tn: tableCreatePayLoad.table_name,
});
const columns: Array<
Omit<Column, 'column_name' | 'title'> & {
cn: string;
system?: boolean;
}
> = (await sqlClient.columnList({ tn: tableCreatePayLoad.table_name }))
?.data?.list;
const tables = await Model.list({
project_id: project.id,
base_id: base.id,
});
await Audit.insert({
project_id: project.id,
base_id: base.id,
op_type: AuditOperationTypes.TABLE,
op_sub_type: AuditOperationSubTypes.CREATED,
user: param.user?.email,
description: `created table ${tableCreatePayLoad.table_name} with alias ${tableCreatePayLoad.title} `,
ip: param.req?.clientIp,
}).then(() => {});
mapDefaultDisplayValue(param.table.columns);
T.emit('evt', { evt_type: 'table:created' });
// todo: type correction
const result = await Model.insert(project.id, base.id, {
...tableCreatePayLoad,
columns: columns.map((c, i) => {
const colMetaFromReq = param.table?.columns?.find(
(c1) => c.cn === c1.column_name,
);
return {
...colMetaFromReq,
uidt: colMetaFromReq?.uidt || c.uidt || getColumnUiType(base, c),
...c,
dtxp: [UITypes.MultiSelect, UITypes.SingleSelect].includes(
colMetaFromReq.uidt as any,
)
? colMetaFromReq.dtxp
: c.dtxp,
title: colMetaFromReq?.title || getColumnNameAlias(c.cn, base),
column_name: c.cn,
order: i + 1,
} as NormalColumnRequestType;
}),
order: +(tables?.pop()?.order ?? 0) + 1,
} as any);
return result;
}
}

9
packages/nocodb-nest/src/utils/extractRolesObj.ts

@ -0,0 +1,9 @@
export default (roles: string | string[]) => {
if (typeof roles === 'string') {
roles = roles.split(',');
}
return roles.reduce((acc, role) => {
acc[role] = true;
return acc;
}, {});
};
Loading…
Cancel
Save